Cryptography

Complete specification of every cryptographic operation in NOONE.

1. Key Derivation

Source: crates/nostrcomm-crypto/src/identity.rs

Inputs

Process

PRF OUTPUT 32 bytes, hardware-bound PIN UTF-8 bytes SALT 32 bytes, random PBKDF2-SHA256 (600K rounds) XOR master seed (32B) HKDF-SHA256 expand secp256k1 -- Nostr identity X25519 -- key agreement Ed25519 -- device certificates AES-256 -- vault encryption ML-KEM-768 -- post-quantum Neither PRF alone nor PIN alone is sufficient to derive keys

PBKDF2 iteration count follows OWASP 2024 guidelines. The XOR combination ensures two-factor derivation: hardware possession (PRF) and knowledge (PIN).

2. Sealed Sender (NIP-59 Gift Wrap)

Source: crates/nostrcomm-crypto/src/envelope.rs

Sealing (Encryption)

SEAL (SENDER) Inner Event (kind 14) BIP-340 signed by sender ECDH (X25519) shared_c = 32 bytes ML-KEM-768 Encap shared_pq = 32 bytes HKDF(c || pq) symmetric key (32B) ChaCha20-Poly1305 Encrypt Outer Event (kind 1059) pubkey: ephemeral (random) tags: [["p", recipient_pk]] content: base64(kem_ct) + hex(nonce) + base64(ciphertext) sig: BIP-340 with ephemeral key Real sender completely hidden

Opening (Decryption)

The recipient performs the reverse: parse the content format (3 parts = hybrid, 2 parts = classical), derive the same shared secret via ECDH + ML-KEM Decapsulate, and decrypt with ChaCha20-Poly1305.

1. Parse content: detect format by colon count
   - 3 parts = hybrid (kem_ct : nonce : ciphertext)
   - 2 parts = classical (nonce : ciphertext)
2. Hybrid key agreement:
   a. Classical: shared_c = ECDH(recipient_x25519_sk, sender_x25519_pk)
   b. Post-quantum: shared_pq = ML-KEM-768.Decapsulate(recipient_kem_dk, kem_ct)
   c. Combined: same HKDF as sealing
3. plaintext = ChaCha20-Poly1305.Decrypt(shared, nonce, ciphertext)
4. Parse inner Nostr event from plaintext JSON

3. Double Ratchet

Source: crates/nostrcomm-crypto/src/ratchet.rs

Signal-compatible Double Ratchet providing forward secrecy and break-in recovery.

Ratchet State

ROOT CHAIN root_key DH ratchet root_key' DH ratchet ... SENDING CHAIN ck_send KDF ck_send' ... msg_key RECEIVING CHAIN same structure: ck_recv -> KDF -> msg_keys Each DH ratchet step provides break-in recovery.

KDF Functions

kdf_rk(root_key, dh_output):
  HKDF-SHA256(salt=root_key, ikm=dh_output)
  -> expand("nostrcomm-ratchet-root-v1")  = new_root_key  [32 bytes]
  -> expand("nostrcomm-ratchet-chain-v1") = chain_key      [32 bytes]

kdf_ck(chain_key):
  HKDF-SHA256(ikm=chain_key)
  -> expand("nostrcomm-ratchet-chain-v1") = new_chain_key  [32 bytes]
  -> expand("nostrcomm-ratchet-msg-v1")   = msg_material   [44 bytes]
     msg_material[0..32] = AES key, msg_material[32..44] = nonce

Encrypt

1. (new_ck, msg_key) = kdf_ck(ck_send)
2. ck_send = new_ck
3. ciphertext = ChaCha20-Poly1305.Encrypt(msg_key.key, msg_key.nonce, plaintext)
4. Return (ciphertext, n_send++, dh_pk)

Decrypt

1. If sender's DH pk differs from stored dh_remote_pk:
   a. Store skipped message keys for current receiving chain
   b. Perform DH ratchet step:
      - dh_output = X25519(dh_sk, sender_dh_pk)
      - (root_key, ck_recv) = kdf_rk(root_key, dh_output)
      - Generate new DH keypair
      - dh_output2 = X25519(new_dh_sk, sender_dh_pk)
      - (root_key, ck_send) = kdf_rk(root_key, dh_output2)
2. Skip message keys up to received n
3. (new_ck, msg_key) = kdf_ck(ck_recv)
4. plaintext = ChaCha20-Poly1305.Decrypt(msg_key.key, msg_key.nonce, ciphertext)

4. Group Message Encryption

Source: web/src/group-crypto.js

1. session_key = random 256-bit AES key (crypto.getRandomValues)
2. iv = random 96-bit nonce
3. ciphertext = AES-256-GCM.Encrypt(session_key, iv, plaintext)
4. For each group member:
   inner_content = JSON.stringify({ g: groupId, sk: hex(session_key), ct: base64(ciphertext), iv: hex(iv) })
   sealed_event = seal_event_for_recipient(member.x25519_pk, member.kem_pk, inner_content)
5. Publish N sealed events (one per member)

Each member independently decrypts their sealed event, extracts the session key, and decrypts the message body. The session key is per-message (not reused).

5. P2P DataChannel Encryption

Source: crates/nostrcomm-crypto/src/lib.rs -- p2p_encrypt / p2p_decrypt

Encrypt:
1. shared_c = ECDH(my_x25519_sk, peer_x25519_pk)
2. If peer has KEM key:
   (kem_ct, shared_pq) = ML-KEM-768.Encapsulate(peer_kem_pk)
   shared = HKDF(shared_c || shared_pq, info="nostrcomm-hybrid-p2p-v1")
3. Else:
   shared = HKDF(shared_c, info="nostrcomm-p2p-v1")
4. nonce = random 12 bytes
5. ct = ChaCha20-Poly1305.Encrypt(shared, nonce, plaintext)
6. Return { ct, nonce, kem_ct }

6. Vault Encryption

Key: vault_key (32 bytes, derived from identity HKDF)
Algorithm: AES-256-GCM via Web Crypto API
IV: random 12 bytes per record
Each IndexedDB record encrypted independently

7. Device Certificates

Source: crates/nostrcomm-crypto/src/types.rs, identity.rs

Encoding: CBOR (Concise Binary Object Representation)
Signing: Ed25519 over canonical CBOR of all fields except signature

Verification (chain):
1. Chain[0] must be self-signed (tier 0) and in local trust store
2. Each cert[i] signed by cert[i-1].device_pk
3. All timestamps valid against current time
4. Chain depth <= max_tier policy
Known limitation: No certificate revocation mechanism. Compromised device certs remain valid until expiry. In-band revocation events are planned.

8. HKDF Info Strings (Complete List)

ContextInfo String
Nostr key derivationnostrcomm-nostr-v1
X25519 key derivationnostrcomm-x25519-v1
Ed25519 key derivationnostrcomm-ed25519-v1
Vault key derivationnostrcomm-vault-v1
ML-KEM seed derivationnostrcomm-mlkem-v1
Sealed sender (hybrid)nostrcomm-hybrid-seal-v1
Sealed sender (classical)nostrcomm-seal-v1
P2P encryption (hybrid)nostrcomm-hybrid-p2p-v1
P2P encryption (classical)nostrcomm-p2p-v1
Ratchet root KDFnostrcomm-ratchet-root-v1
Ratchet chain KDFnostrcomm-ratchet-chain-v1
Ratchet message KDFnostrcomm-ratchet-msg-v1
Emoji code derivationnostrcomm-emoji-v1

All cryptographic code is MIT licensed and available for independent audit.