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:
徐枫
2026-03-17 14:56:01 +08:00
parent c9f6e4d08b
commit afbaca1112
24 changed files with 3847 additions and 0 deletions

View 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);
});
});