Infrastructure build to support third-party app integrations

This commit is contained in:
KS Jannette
2026-08-01 07:35:01 -04:00
parent b4666c5439
commit 15af3465e2
53 changed files with 5864 additions and 659 deletions

77
backend/lib/crypto.ts Normal file
View File

@@ -0,0 +1,77 @@
import {
createCipheriv,
createDecipheriv,
createHash,
randomBytes,
timingSafeEqual,
} from 'node:crypto';
const ALGORITHM = 'aes-256-gcm';
const IV_BYTES = 12;
const KEY_BYTES = 32;
const loadKey = (): Buffer => {
const raw = process.env.TOKEN_ENCRYPTION_KEY;
if (!raw) {
throw new Error('TOKEN_ENCRYPTION_KEY is not set');
}
const key = Buffer.from(raw, 'base64');
if (key.length !== KEY_BYTES) {
throw new Error(
`TOKEN_ENCRYPTION_KEY must decode to ${KEY_BYTES} bytes, got ${key.length}`
);
}
return key;
};
/** Serialized as base64(iv):base64(authTag):base64(ciphertext). */
export const encryptSecret = (plaintext: string): string => {
const iv = randomBytes(IV_BYTES);
const cipher = createCipheriv(ALGORITHM, loadKey(), iv);
const payload = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
return [
iv.toString('base64'),
cipher.getAuthTag().toString('base64'),
payload.toString('base64'),
].join(':');
};
export const decryptSecret = (ciphertext: string): string => {
const parts = ciphertext.split(':');
if (parts.length !== 3) {
throw new Error('Ciphertext is not in the expected iv:tag:payload form');
}
const [iv, tag, payload] = parts;
const decipher = createDecipheriv(
ALGORITHM,
loadKey(),
Buffer.from(iv, 'base64')
);
decipher.setAuthTag(Buffer.from(tag, 'base64'));
return Buffer.concat([
decipher.update(Buffer.from(payload, 'base64')),
decipher.final(),
]).toString('utf8');
};
export const hashApiKey = (key: string): string =>
createHash('sha256').update(key, 'utf8').digest('hex');
/**
* Constant-time string comparison. Both sides are hashed first so that
* unequal lengths cannot short-circuit the comparison or throw.
*/
export const safeEquals = (a: string, b: string): boolean => {
const digestA = createHash('sha256').update(a, 'utf8').digest();
const digestB = createHash('sha256').update(b, 'utf8').digest();
return timingSafeEqual(digestA, digestB);
};