feat: design system overhaul — sidebar, AI chats, settings, brainstorm, color cleanup
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
This commit is contained in:
65
memento-note/lib/crypto.ts
Normal file
65
memento-note/lib/crypto.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import {
|
||||
createCipheriv,
|
||||
createDecipheriv,
|
||||
createHash,
|
||||
randomBytes,
|
||||
scrypt,
|
||||
} from 'crypto';
|
||||
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const KEY_LEN = 32;
|
||||
const IV_LEN = 16;
|
||||
const SALT_LEN = 16;
|
||||
const TAG_LEN = 16;
|
||||
|
||||
function getMasterPassphrase(): string {
|
||||
const master = process.env.MASTER_ENCRYPTION_KEY;
|
||||
if (!master || master.length < 32) {
|
||||
throw new Error('MASTER_ENCRYPTION_KEY must be set (minimum 32 characters)');
|
||||
}
|
||||
return master;
|
||||
}
|
||||
|
||||
async function deriveKey(salt: Buffer): Promise<Buffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
scrypt(getMasterPassphrase(), salt, KEY_LEN, (err, key) => {
|
||||
if (err) reject(err);
|
||||
else resolve(key);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function encryptApiKey(plaintext: string): Promise<string> {
|
||||
const salt = randomBytes(SALT_LEN);
|
||||
const iv = randomBytes(IV_LEN);
|
||||
const key = await deriveKey(salt);
|
||||
const cipher = createCipheriv(ALGORITHM, key, iv);
|
||||
const encrypted = Buffer.concat([
|
||||
cipher.update(plaintext, 'utf8'),
|
||||
cipher.final(),
|
||||
]);
|
||||
const tag = cipher.getAuthTag();
|
||||
return Buffer.concat([salt, iv, tag, encrypted]).toString('base64');
|
||||
}
|
||||
|
||||
export async function decryptApiKey(payload: string): Promise<string> {
|
||||
const buf = Buffer.from(payload, 'base64');
|
||||
if (buf.length < SALT_LEN + IV_LEN + TAG_LEN + 1) {
|
||||
throw new Error('Invalid encrypted key payload');
|
||||
}
|
||||
const salt = buf.subarray(0, SALT_LEN);
|
||||
const iv = buf.subarray(SALT_LEN, SALT_LEN + IV_LEN);
|
||||
const tag = buf.subarray(SALT_LEN + IV_LEN, SALT_LEN + IV_LEN + TAG_LEN);
|
||||
const ciphertext = buf.subarray(SALT_LEN + IV_LEN + TAG_LEN);
|
||||
const key = await deriveKey(salt);
|
||||
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
||||
decipher.setAuthTag(tag);
|
||||
return Buffer.concat([
|
||||
decipher.update(ciphertext),
|
||||
decipher.final(),
|
||||
]).toString('utf8');
|
||||
}
|
||||
|
||||
export function hashApiKey(plaintext: string): string {
|
||||
return createHash('sha256').update(plaintext, 'utf8').digest('hex');
|
||||
}
|
||||
Reference in New Issue
Block a user