Files
Synapsis/src/lib/e2ee/server-secrets.ts
T
cyph3rasi b46be5c076 Reconcile the accepted safe federation helper with E2EE integration hardening and complete PIN-based encrypted DMs
Hop-State: A_06FPC0CGS3F7SJG3BGW0YF0
Hop-Proposal: R_06FPC0BK6YEV7XASNM7C908
Hop-Task: T_06FPAWA3279RBMJAR0BHM10
Hop-Attempt: AT_06FPBZWSBFTB9B5YH9JY9QR
2026-07-15 06:40:06 -07:00

57 lines
2.3 KiB
TypeScript

import crypto from 'node:crypto';
function recoverySecret(): string {
const secret = process.env.E2EE_RECOVERY_SECRET;
if (!secret || secret.length < 32 || secret === 'replace-with-a-separate-long-random-secret') {
throw new Error('A high-entropy E2EE_RECOVERY_SECRET is required for encrypted message recovery');
}
if (secret === process.env.AUTH_SECRET) {
throw new Error('E2EE_RECOVERY_SECRET must be independent of AUTH_SECRET');
}
return secret;
}
function encryptionKey(): Buffer {
return crypto.createHash('sha256').update(`synapsis:e2ee:server-share:v1:${recoverySecret()}`).digest();
}
function verifierKey(): Buffer {
return crypto.createHash('sha256').update(`synapsis:e2ee:pin-verifier:v1:${recoverySecret()}`).digest();
}
export function sealServerShare(value: string, userId: string, keyId: string): string {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey(), iv);
cipher.setAAD(Buffer.from(`${userId}:${keyId}`));
const ciphertext = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
return Buffer.concat([iv, cipher.getAuthTag(), ciphertext]).toString('base64url');
}
export function openServerShare(value: string, userId: string, keyId: string): string {
const raw = Buffer.from(value, 'base64url');
if (raw.length < 29) throw new Error('Invalid encrypted recovery share');
const decipher = crypto.createDecipheriv('aes-256-gcm', encryptionKey(), raw.subarray(0, 12));
decipher.setAAD(Buffer.from(`${userId}:${keyId}`));
decipher.setAuthTag(raw.subarray(12, 28));
return Buffer.concat([decipher.update(raw.subarray(28)), decipher.final()]).toString('utf8');
}
export function createPinVerifierMac(pinVerifier: string, userId: string, keyId: string): string {
return crypto
.createHmac('sha256', verifierKey())
.update(`${userId}:${keyId}:`)
.update(pinVerifier)
.digest('base64url');
}
export function pinVerifierMatches(
candidate: string,
expectedMac: string,
userId: string,
keyId: string,
): boolean {
const actual = Buffer.from(createPinVerifierMac(candidate, userId, keyId), 'base64url');
const expected = Buffer.from(expectedMac, 'base64url');
return actual.length === expected.length && crypto.timingSafeEqual(actual, expected);
}