Files
spoon/packages/backend/convex/secretCrypto.ts
T
Gabriel Brown 2dfa97ee4f
Build and Push Next App / quality (push) Failing after 48s
Build and Push Next App / build-next (push) Has been skipped
Add agent workflows & stuff
2026-06-21 21:15:15 -05:00

57 lines
1.5 KiB
TypeScript

'use node';
import {
createCipheriv,
createDecipheriv,
createHash,
randomBytes,
} from 'node:crypto';
import { ConvexError } from 'convex/values';
const getSecret = () => {
const secret =
process.env.SPOON_ENCRYPTION_KEY?.trim() ??
process.env.INSTANCE_SECRET?.trim();
if (!secret) {
throw new ConvexError(
'SPOON_ENCRYPTION_KEY is not configured. Add it before storing user API keys.',
);
}
return secret;
};
const getKey = () => createHash('sha256').update(getSecret()).digest();
export const encryptSecret = (plaintext: string) => {
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', getKey(), iv);
const ciphertext = Buffer.concat([
cipher.update(plaintext, 'utf8'),
cipher.final(),
]);
const tag = cipher.getAuthTag();
return [
iv.toString('base64url'),
tag.toString('base64url'),
ciphertext.toString('base64url'),
].join('.');
};
export const decryptSecret = (encrypted: string) => {
const [ivRaw, tagRaw, ciphertextRaw] = encrypted.split('.');
if (!ivRaw || !tagRaw || !ciphertextRaw) {
throw new ConvexError('Stored secret has an invalid format.');
}
const decipher = createDecipheriv(
'aes-256-gcm',
getKey(),
Buffer.from(ivRaw, 'base64url'),
);
decipher.setAuthTag(Buffer.from(tagRaw, 'base64url'));
const plaintext = Buffer.concat([
decipher.update(Buffer.from(ciphertextRaw, 'base64url')),
decipher.final(),
]);
return plaintext.toString('utf8');
};