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>
This commit is contained in:
68
projects/cookiebridge/src/crypto/encryption.ts
Normal file
68
projects/cookiebridge/src/crypto/encryption.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user