Files
CookieBridge/projects/cookiebridge/src/crypto/encryption.ts
徐枫 afbaca1112 feat: implement CookieBridge M1 — core protocol & relay server
- Protocol spec: encrypted envelope format, device identity (Ed25519 + X25519),
  LWW conflict resolution with Lamport clocks
- E2E encryption: XChaCha20-Poly1305 via sodium-native, X25519 key exchange
- WebSocket relay server: stateless message forwarding, device auth via
  challenge-response, offline message queuing, ping/pong keepalive
- Device pairing: time-limited pairing codes, key exchange broker via HTTP
- Sync protocol: envelope builder/opener, conflict-resolving cookie store
- 31 tests passing (crypto, pairing, conflict resolution, full integration)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-03-17 14:56:01 +08:00

69 lines
1.8 KiB
TypeScript

import sodium from "sodium-native";
/**
* Derive a shared secret from our X25519 secret key and peer's X25519 public key.
* Uses crypto_scalarmult (raw X25519 ECDH) then hashes with crypto_generichash
* to produce a 32-byte symmetric key.
*/
export function deriveSharedKey(
ourEncSec: Buffer,
peerEncPub: Buffer,
): Buffer {
const raw = Buffer.alloc(sodium.crypto_scalarmult_BYTES);
sodium.crypto_scalarmult(raw, ourEncSec, peerEncPub);
// Hash the raw shared secret to get a uniform key
const sharedKey = Buffer.alloc(32);
sodium.crypto_generichash(sharedKey, raw);
return sharedKey;
}
/**
* Encrypt plaintext using XChaCha20-Poly1305 with the shared key.
* Returns { nonce, ciphertext } where both are Buffers.
*/
export function encrypt(
plaintext: Buffer,
sharedKey: Buffer,
): { nonce: Buffer; ciphertext: Buffer } {
const nonce = Buffer.alloc(sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES);
sodium.randombytes_buf(nonce);
const ciphertext = Buffer.alloc(
plaintext.length + sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES,
);
sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
ciphertext,
plaintext,
null, // no additional data
null, // unused nsec
nonce,
sharedKey,
);
return { nonce, ciphertext };
}
/**
* Decrypt ciphertext using XChaCha20-Poly1305 with the shared key.
* Returns the plaintext Buffer, or throws on authentication failure.
*/
export function decrypt(
ciphertext: Buffer,
nonce: Buffer,
sharedKey: Buffer,
): Buffer {
const plaintext = Buffer.alloc(
ciphertext.length - sodium.crypto_aead_xchacha20poly1305_ietf_ABYTES,
);
sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
plaintext,
null, // unused nsec
ciphertext,
null, // no additional data
nonce,
sharedKey,
);
return plaintext;
}