Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

ratchet-pair — dm-ratchet.js

The pairwise confidentiality plugin: a per-message symmetric ratchet for 1:1 threads. An ECDH-derived per-pair epoch secret seeds a per-sender HKDF ratchet, so every message uses a fresh key. Suite: enc-xchacha-v1 (XChaCha20-Poly1305, 24-byte nonce). Used by dm and super. Normative spec: ratchet-pair.

import {
  ratchetMessageKey, dmRatchetEncrypt, dmRatchetDecrypt,
} from '@enc-protocol/core/dm-ratchet.js'

Ratchet API

ratchetMessageKey(epochSecret, senderSeq)

ratchetMessageKey(epochSecret: Uint8Array, senderSeq: number) → Uint8Array(32)

Derives the message key for a sequence number by chaining HKDF advances from the epoch secret. Deterministic — the same (epochSecret, senderSeq) always yields the same key.

dmRatchetEncrypt(epochSecret, plaintext, senderSeq, epochN)

dmRatchetEncrypt(epochSecret, plaintext: string, senderSeq: number, epochN: number)
  → { epoch: number, sender_seq: number, ciphertext: string }

Encrypts plaintext under the message key for senderSeq, tagging the result with the epoch number. ciphertext is the base64 XChaCha20-Poly1305 output.

dmRatchetDecrypt(epochSecret, ciphertextB64, senderSeq)

dmRatchetDecrypt(epochSecret, ciphertextB64: string, senderSeq: number) → string

Re-derives the message key for senderSeq and decrypts.

NIP-44 v2 interop

dm-ratchet.js also ships a Nostr NIP-44 v2 code path for bridging to Nostr DMs:

nip44Encrypt(privHex, peerPubHex, plaintext) → string   // base64 NIP-44 payload
nip44Decrypt(privHex, peerPubHex, payload) → string

These are built from the lower-level primitives nip44ConversationKey(privHex, pubHex), nip44MessageKeys(ck, nonce), nip44Pad / nip44Unpad, nip44HmacAad, and nip44EncryptWithKey / nip44DecryptWithKey.

Example

import { dmRatchetEncrypt, dmRatchetDecrypt } from '@enc-protocol/core/dm-ratchet.js'
 
// both sides hold the same 32-byte per-pair epoch secret (ECDH-derived)
const env = dmRatchetEncrypt(epochSecret, 'hello', 0, 0)
// → { epoch: 0, sender_seq: 0, ciphertext: '…base64…' }
 
const plain = dmRatchetDecrypt(epochSecret, env.ciphertext, env.sender_seq)
console.log(plain) // 'hello'

Or the NIP-44 v2 path, for Nostr interop:

import { nip44Encrypt, nip44Decrypt } from '@enc-protocol/core/dm-ratchet.js'
 
const payload = nip44Encrypt(alicePrivHex, bobPubHex, 'hi bob')
const msg = nip44Decrypt(bobPrivHex, alicePubHex, payload) // 'hi bob'

See also