UNPKG

relay-companion

Version:

Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.

221 lines (207 loc) • 6.87 kB
"use strict"; const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { createHash, createPrivateKey, createPublicKey, generateKeyPairSync, sign, verify } = require("node:crypto"); const { atomicWriteJsonSync } = require("./atomic-json.cjs"); const E2EE_ENROLLMENT_CONTEXT = "relay-e2ee-device-enrollment-v1"; const E2EE_DEVICE_CROSS_SIGNATURE_CONTEXT = "relay-e2ee-device-cross-signature-v1"; const E2EE_PROTOCOL = "mls10"; const E2EE_CIPHER_SUITE = "MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519"; function identityPath({ env = process.env, homeDir = os.homedir() } = {}) { const explicitConfig = String(env.RELAY_CONFIG || "").trim(); const root = explicitConfig ? path.dirname(explicitConfig) : env.RELAY_CONFIG_DIR || path.join(homeDir, ".relay"); return path.join(root, "e2ee-device-identity.json"); } function enrollmentStatement({ pairingCode, name, platform, signaturePublicKey }) { return Buffer.from( JSON.stringify([ E2EE_ENROLLMENT_CONTEXT, String(pairingCode || "").trim().toUpperCase(), String(name || ""), String(platform || ""), E2EE_PROTOCOL, E2EE_CIPHER_SUITE, String(signaturePublicKey || ""), ]), "utf8", ); } function deviceCrossSignatureStatement({ relayUserId, signerDeviceId, signerFingerprint, subjectDeviceId, subjectFingerprint, signedAt, }) { return Buffer.from( JSON.stringify([ E2EE_DEVICE_CROSS_SIGNATURE_CONTEXT, String(relayUserId || ""), String(signerDeviceId || ""), String(signerFingerprint || ""), String(subjectDeviceId || ""), String(subjectFingerprint || ""), String(signedAt || ""), ]), "utf8", ); } /** Create the proof an already-trusted device sends when approving a new one. */ function createDeviceCrossSignature(identity, subject, { now = new Date() } = {}) { if (!identity?.userId || !identity?.deviceId || !identity?.fingerprint || !identity?.privateKeyJwk) { throw new Error("This device does not have a complete local encryption identity."); } if (!subject?.deviceId || !/^[A-Za-z0-9_-]{43}$/.test(String(subject.fingerprint || ""))) { throw new Error("The new device does not have a valid encryption identity."); } if (subject.deviceId === identity.deviceId) { throw new Error("A device cannot approve itself."); } const signedAt = now.toISOString(); const statement = deviceCrossSignatureStatement({ relayUserId: identity.userId, signerDeviceId: identity.deviceId, signerFingerprint: identity.fingerprint, subjectDeviceId: subject.deviceId, subjectFingerprint: subject.fingerprint, signedAt, }); return { subjectDeviceId: subject.deviceId, subjectFingerprint: subject.fingerprint, signedAt, signature: sign( null, statement, createPrivateKey({ key: identity.privateKeyJwk, format: "jwk" }), ).toString("base64url"), }; } /** Verify a server-returned proof before extending local trust to its subject. */ function verifyDeviceCrossSignature({ relayUserId, signer, proof }) { if ( !signer?.deviceId || !/^[A-Za-z0-9_-]{43}$/.test(String(signer.signaturePublicKey || "")) || proof?.signerDeviceId !== signer.deviceId || proof?.signerFingerprint !== signer.fingerprint ) { return false; } try { const spki = Buffer.concat([ Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(signer.signaturePublicKey, "base64url"), ]); const statement = deviceCrossSignatureStatement({ relayUserId, signerDeviceId: proof.signerDeviceId, signerFingerprint: proof.signerFingerprint, subjectDeviceId: proof.subjectDeviceId, subjectFingerprint: proof.subjectFingerprint, signedAt: proof.signedAt, }); return verify( null, statement, createPublicKey({ key: spki, format: "der", type: "spki" }), Buffer.from(String(proof.signature || ""), "base64url"), ); } catch { return false; } } /** * Generate a fresh identity for one pairing attempt. Nothing is written until * the API confirms that it stored the matching public key. */ function createPairingIdentity({ pairingCode, name, platform }) { const { publicKey, privateKey } = generateKeyPairSync("ed25519"); const publicJwk = publicKey.export({ format: "jwk" }); const privateKeyJwk = privateKey.export({ format: "jwk" }); const signaturePublicKey = String(publicJwk.x || ""); if (!/^[A-Za-z0-9_-]{43}$/.test(signaturePublicKey)) { throw new Error("Relay could not create a valid Ed25519 device identity."); } const statement = enrollmentStatement({ pairingCode, name, platform, signaturePublicKey }); const proof = sign(null, statement, privateKey).toString("base64url"); const fingerprint = createHash("sha256") .update(Buffer.from(signaturePublicKey, "base64url")) .digest("base64url"); return { request: { protocol: E2EE_PROTOCOL, cipherSuite: E2EE_CIPHER_SUITE, signaturePublicKey, proof, }, state: { version: 1, protocol: E2EE_PROTOCOL, cipherSuite: E2EE_CIPHER_SUITE, signaturePublicKey, fingerprint, privateKeyJwk, createdAt: new Date().toISOString(), }, }; } function persistPairedIdentity(state, registration, options = {}) { const confirmed = registration && registration.e2ee; if (!confirmed || confirmed.fingerprint !== state.fingerprint) { throw new Error( "Relay's API did not confirm this device's encryption identity. Pairing stopped before saving credentials.", ); } const record = { ...state, deviceId: registration.deviceId, userId: registration.user && registration.user.id, }; atomicWriteJsonSync(identityPath(options), record, { mode: 0o600 }); return record; } function readPairedIdentity(options = {}) { try { const record = JSON.parse(fs.readFileSync(identityPath(options), "utf8")); if ( record && record.version === 1 && record.protocol === E2EE_PROTOCOL && record.cipherSuite === E2EE_CIPHER_SUITE && /^[A-Za-z0-9_-]{43}$/.test(String(record.signaturePublicKey || "")) && record.privateKeyJwk && record.deviceId && record.userId ) { return record; } } catch {} return null; } function removePairedIdentity(options = {}) { try { fs.rmSync(identityPath(options), { force: true }); return true; } catch { return false; } } module.exports = { E2EE_CIPHER_SUITE, E2EE_DEVICE_CROSS_SIGNATURE_CONTEXT, E2EE_ENROLLMENT_CONTEXT, E2EE_PROTOCOL, createDeviceCrossSignature, createPairingIdentity, deviceCrossSignatureStatement, enrollmentStatement, identityPath, persistPairedIdentity, readPairedIdentity, removePairedIdentity, verifyDeviceCrossSignature, };