UNPKG

relay-companion

Version:

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

119 lines (111 loc) • 4.69 kB
import fs from "node:fs"; import path from "node:path"; import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto"; import { createRequire } from "node:module"; import atomicJson from "./atomic-json.cjs"; import e2eeIdentityModule from "./e2ee-identity.cjs"; const { atomicWriteJsonSync } = atomicJson; const { identityPath } = e2eeIdentityModule; const { writeCredential, readCredential, deleteCredential } = createRequire(import.meta.url)("./credential-store.cjs"); export const E2EE_MCP_CERTIFICATE_CREDENTIAL_SERVICE = "work.relay.companion.e2ee-mcp-certificate"; const CONTEXT = "relay-e2ee-mcp-certificate-v1"; function account(identity) { return createHash("sha256").update(`${identity.userId}\n${identity.deviceId}`).digest("base64url").slice(0, 32); } function aad(identity) { return Buffer.from(JSON.stringify([CONTEXT, identity.userId, identity.deviceId]), "utf8"); } function masterKey(identity, { credentialService = E2EE_MCP_CERTIFICATE_CREDENTIAL_SERVICE, credentials = { writeCredential, readCredential, deleteCredential }, allowLegacyMigration = false, } = {}) { const options = { service: credentialService, account: account(identity) }; const existing = credentials.readCredential({ ...options, allowLegacyMigration }); if (existing?.ok) { const key = Buffer.from(String(existing.value || ""), "base64url"); if (key.length !== 32) throw new Error("The native E2EE certificate key is invalid"); return key; } const key = randomBytes(32); const encoded = key.toString("base64url"); const written = credentials.writeCredential(encoded, options); if (!written?.ok) { throw new Error(`Relay cannot protect its Claude certificate in this device's credential store (${written?.detail || "unavailable"}).`); } const verified = credentials.readCredential(options); if (!verified?.ok || verified.value !== encoded) { credentials.deleteCredential(options); throw new Error("Relay could not verify the native key protecting its Claude certificate."); } return key; } function validate(value, identity) { if ( value?.version !== 1 || value.context !== CONTEXT || value.userId !== identity.userId || value.deviceId !== identity.deviceId || typeof value.accountKey !== "string" || typeof value.certificateKey !== "string" || typeof value.certificate !== "string" || typeof value.hostname !== "string" || typeof value.expiresAt !== "string" || typeof value.directoryUrl !== "string" ) throw new Error("Stored E2EE Claude certificate is invalid"); return value; } export function createE2eeMcpCertificateStore(identity, options = {}) { const file = options.file || path.join( path.dirname(identityPath(options)), `e2ee-mcp-certificate-${account(identity)}.json`, ); const key = masterKey(identity, { ...options, allowLegacyMigration: fs.existsSync(file) }); return { file, read() { let envelope; try { envelope = JSON.parse(fs.readFileSync(file, "utf8")); } catch (error) { if (error?.code === "ENOENT") return null; throw new Error("Relay could not read its protected Claude certificate."); } try { const iv = Buffer.from(String(envelope.iv || ""), "base64url"); const tag = Buffer.from(String(envelope.tag || ""), "base64url"); if (envelope.version !== 1 || iv.length !== 12 || tag.length !== 16 || typeof envelope.ciphertext !== "string") { throw new Error("invalid envelope"); } const decipher = createDecipheriv("aes-256-gcm", key, iv); decipher.setAAD(aad(identity)); decipher.setAuthTag(tag); return validate(JSON.parse(Buffer.concat([ decipher.update(Buffer.from(envelope.ciphertext, "base64url")), decipher.final(), ]).toString("utf8")), identity); } catch { throw new Error("Stored E2EE Claude certificate could not be authenticated"); } }, write(value) { const state = validate({ ...value, version: 1, context: CONTEXT, userId: identity.userId, deviceId: identity.deviceId, }, identity); const iv = randomBytes(12); const cipher = createCipheriv("aes-256-gcm", key, iv); cipher.setAAD(aad(identity)); const ciphertext = Buffer.concat([cipher.update(JSON.stringify(state), "utf8"), cipher.final()]); atomicWriteJsonSync(file, { version: 1, iv: iv.toString("base64url"), tag: cipher.getAuthTag().toString("base64url"), ciphertext: ciphertext.toString("base64url"), }, { mode: 0o600 }); return state; }, }; }