feat(federation): seal federation peer client keys at rest (FED-M2-05) (#495)
This commit was merged in pull request #495.
This commit is contained in:
@@ -1,62 +1,10 @@
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
||||
import { seal, unseal } from '@mosaicstack/auth';
|
||||
import type { Db } from '@mosaicstack/db';
|
||||
import { providerCredentials, eq, and } from '@mosaicstack/db';
|
||||
import { DB } from '../database/database.module.js';
|
||||
import type { ProviderCredentialSummaryDto } from './provider-credentials.dto.js';
|
||||
|
||||
const ALGORITHM = 'aes-256-gcm';
|
||||
const IV_LENGTH = 12; // 96-bit IV for GCM
|
||||
const TAG_LENGTH = 16; // 128-bit auth tag
|
||||
|
||||
/**
|
||||
* Derive a 32-byte AES-256 key from BETTER_AUTH_SECRET using SHA-256.
|
||||
* The secret is assumed to be set in the environment.
|
||||
*/
|
||||
function deriveEncryptionKey(): Buffer {
|
||||
const secret = process.env['BETTER_AUTH_SECRET'];
|
||||
if (!secret) {
|
||||
throw new Error('BETTER_AUTH_SECRET is not set — cannot derive encryption key');
|
||||
}
|
||||
return createHash('sha256').update(secret).digest();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a plain-text value using AES-256-GCM.
|
||||
* Output format: base64(iv + authTag + ciphertext)
|
||||
*/
|
||||
function encrypt(plaintext: string): string {
|
||||
const key = deriveEncryptionKey();
|
||||
const iv = randomBytes(IV_LENGTH);
|
||||
const cipher = createCipheriv(ALGORITHM, key, iv);
|
||||
|
||||
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
// Combine iv (12) + authTag (16) + ciphertext and base64-encode
|
||||
const combined = Buffer.concat([iv, authTag, encrypted]);
|
||||
return combined.toString('base64');
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a value encrypted by `encrypt()`.
|
||||
* Throws on authentication failure (tampered data).
|
||||
*/
|
||||
function decrypt(encoded: string): string {
|
||||
const key = deriveEncryptionKey();
|
||||
const combined = Buffer.from(encoded, 'base64');
|
||||
|
||||
const iv = combined.subarray(0, IV_LENGTH);
|
||||
const authTag = combined.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
||||
const ciphertext = combined.subarray(IV_LENGTH + TAG_LENGTH);
|
||||
|
||||
const decipher = createDecipheriv(ALGORITHM, key, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
||||
return decrypted.toString('utf8');
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ProviderCredentialsService {
|
||||
private readonly logger = new Logger(ProviderCredentialsService.name);
|
||||
@@ -74,7 +22,7 @@ export class ProviderCredentialsService {
|
||||
value: string,
|
||||
metadata?: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
const encryptedValue = encrypt(value);
|
||||
const encryptedValue = seal(value);
|
||||
|
||||
await this.db
|
||||
.insert(providerCredentials)
|
||||
@@ -122,7 +70,7 @@ export class ProviderCredentialsService {
|
||||
}
|
||||
|
||||
try {
|
||||
return decrypt(row.encryptedValue);
|
||||
return unseal(row.encryptedValue);
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
`Failed to decrypt credential for user=${userId} provider=${provider}`,
|
||||
|
||||
Reference in New Issue
Block a user