- 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>
69 lines
1.8 KiB
TypeScript
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;
|
|
}
|