b46be5c076
Hop-State: A_06FPC0CGS3F7SJG3BGW0YF0 Hop-Proposal: R_06FPC0BK6YEV7XASNM7C908 Hop-Task: T_06FPAWA3279RBMJAR0BHM10 Hop-Attempt: AT_06FPBZWSBFTB9B5YH9JY9QR
57 lines
2.3 KiB
TypeScript
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);
|
|
}
|