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:
124
tests/crypto.test.ts
Normal file
124
tests/crypto.test.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import {
|
||||
generateKeyPair,
|
||||
deviceIdFromKeys,
|
||||
serializeKeyPair,
|
||||
deserializeKeyPair,
|
||||
deriveSharedKey,
|
||||
encrypt,
|
||||
decrypt,
|
||||
sign,
|
||||
verify,
|
||||
buildSignablePayload,
|
||||
} from "../src/crypto/index.js";
|
||||
|
||||
describe("Key generation", () => {
|
||||
it("generates unique keypairs", () => {
|
||||
const kp1 = generateKeyPair();
|
||||
const kp2 = generateKeyPair();
|
||||
expect(kp1.signPub).not.toEqual(kp2.signPub);
|
||||
expect(kp1.encPub).not.toEqual(kp2.encPub);
|
||||
});
|
||||
|
||||
it("derives deviceId from signing public key", () => {
|
||||
const kp = generateKeyPair();
|
||||
const id = deviceIdFromKeys(kp);
|
||||
expect(id).toBe(kp.signPub.toString("hex"));
|
||||
expect(id).toHaveLength(64); // 32 bytes hex
|
||||
});
|
||||
|
||||
it("serializes and deserializes keypair", () => {
|
||||
const kp = generateKeyPair();
|
||||
const serialized = serializeKeyPair(kp);
|
||||
const restored = deserializeKeyPair(serialized);
|
||||
expect(restored.signPub).toEqual(kp.signPub);
|
||||
expect(restored.signSec).toEqual(kp.signSec);
|
||||
expect(restored.encPub).toEqual(kp.encPub);
|
||||
expect(restored.encSec).toEqual(kp.encSec);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Encryption", () => {
|
||||
it("encrypts and decrypts with shared key", () => {
|
||||
const alice = generateKeyPair();
|
||||
const bob = generateKeyPair();
|
||||
|
||||
const aliceShared = deriveSharedKey(alice.encSec, bob.encPub);
|
||||
const bobShared = deriveSharedKey(bob.encSec, alice.encPub);
|
||||
|
||||
// Both sides derive the same shared key
|
||||
expect(aliceShared).toEqual(bobShared);
|
||||
|
||||
const plaintext = Buffer.from("hello cookies");
|
||||
const { nonce, ciphertext } = encrypt(plaintext, aliceShared);
|
||||
|
||||
const decrypted = decrypt(ciphertext, nonce, bobShared);
|
||||
expect(decrypted.toString()).toBe("hello cookies");
|
||||
});
|
||||
|
||||
it("fails to decrypt with wrong key", () => {
|
||||
const alice = generateKeyPair();
|
||||
const bob = generateKeyPair();
|
||||
const eve = generateKeyPair();
|
||||
|
||||
const sharedKey = deriveSharedKey(alice.encSec, bob.encPub);
|
||||
const wrongKey = deriveSharedKey(eve.encSec, bob.encPub);
|
||||
|
||||
const plaintext = Buffer.from("secret");
|
||||
const { nonce, ciphertext } = encrypt(plaintext, sharedKey);
|
||||
|
||||
expect(() => decrypt(ciphertext, nonce, wrongKey)).toThrow();
|
||||
});
|
||||
|
||||
it("produces different ciphertexts for same plaintext (random nonce)", () => {
|
||||
const alice = generateKeyPair();
|
||||
const bob = generateKeyPair();
|
||||
const shared = deriveSharedKey(alice.encSec, bob.encPub);
|
||||
|
||||
const plaintext = Buffer.from("same message");
|
||||
const r1 = encrypt(plaintext, shared);
|
||||
const r2 = encrypt(plaintext, shared);
|
||||
|
||||
expect(r1.nonce).not.toEqual(r2.nonce);
|
||||
expect(r1.ciphertext).not.toEqual(r2.ciphertext);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Signing", () => {
|
||||
it("signs and verifies", () => {
|
||||
const kp = generateKeyPair();
|
||||
const msg = Buffer.from("test message");
|
||||
const sig = sign(msg, kp.signSec);
|
||||
expect(verify(msg, sig, kp.signPub)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects tampered message", () => {
|
||||
const kp = generateKeyPair();
|
||||
const msg = Buffer.from("original");
|
||||
const sig = sign(msg, kp.signSec);
|
||||
const tampered = Buffer.from("tampered");
|
||||
expect(verify(tampered, sig, kp.signPub)).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects wrong signer", () => {
|
||||
const alice = generateKeyPair();
|
||||
const bob = generateKeyPair();
|
||||
const msg = Buffer.from("from alice");
|
||||
const sig = sign(msg, alice.signSec);
|
||||
expect(verify(msg, sig, bob.signPub)).toBe(false);
|
||||
});
|
||||
|
||||
it("builds deterministic signable payload", () => {
|
||||
const fields = {
|
||||
type: "cookie_sync",
|
||||
from: "aaa",
|
||||
to: "bbb",
|
||||
nonce: "ccc",
|
||||
payload: "ddd",
|
||||
timestamp: "2024-01-01T00:00:00.000Z",
|
||||
};
|
||||
const p1 = buildSignablePayload(fields);
|
||||
const p2 = buildSignablePayload(fields);
|
||||
expect(p1).toEqual(p2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user