learning_ai_common_plat/packages/field-encrypt/src/key-provider-akv.ts
saravanakumardb1 bb3f5385fc feat(field-encrypt): create @bytelyst/field-encrypt package with AES-256-GCM envelope encryption
- 10 source files: types, aes-gcm, 3 key providers (memory/env/akv), envelope, key-cache, dek-store, guards, migration, factory
- 42 Vitest tests: AES-GCM roundtrips, tamper detection, unicode, 100KB payloads, key providers, DEK cache TTL/LRU, envelope lifecycle, migration (dry-run + idempotent), config validation
- AKV MEK creation script (scripts/create-encryption-keys.sh) for 10 product MEKs
- .env.example updated with FIELD_ENCRYPT_* vars
2026-03-21 09:18:10 -07:00

69 lines
2.4 KiB
TypeScript

/**
* @bytelyst/field-encrypt — Azure Key Vault key provider
*
* Production provider — uses AKV RSA keys for DEK wrapping.
* Requires @azure/keyvault-keys and @azure/identity as peer deps.
*/
import type { KeyProvider } from './types.js';
/**
* Azure Key Vault key provider.
*
* Uses RSA-OAEP wrapping — the MEK never leaves AKV.
* Requires:
* - @azure/keyvault-keys (CryptographyClient)
* - @azure/identity (DefaultAzureCredential)
* - AKV RBAC: Key Vault Crypto User role on the managed identity
*/
export class AkvKeyProvider implements KeyProvider {
private readonly vaultUrl: string;
private readonly mekName: string;
private cryptoClient: unknown | null = null;
constructor(vaultUrl: string, mekName: string) {
if (!vaultUrl) throw new Error('AkvKeyProvider: vaultUrl is required');
if (!mekName) throw new Error('AkvKeyProvider: mekName is required');
this.vaultUrl = vaultUrl;
this.mekName = mekName;
}
private async getClient(): Promise<{
wrapKey(alg: string, key: Uint8Array): Promise<{ result: Uint8Array }>;
unwrapKey(alg: string, key: Uint8Array): Promise<{ result: Uint8Array }>;
}> {
if (this.cryptoClient) return this.cryptoClient as never;
// Dynamic import to keep peer deps optional
const { KeyClient, CryptographyClient } = await import('@azure/keyvault-keys');
const { DefaultAzureCredential } = await import('@azure/identity');
const credential = new DefaultAzureCredential();
const keyClient = new KeyClient(this.vaultUrl, credential);
const key = await keyClient.getKey(this.mekName);
if (!key.id) {
throw new Error(`AkvKeyProvider: MEK '${this.mekName}' not found in ${this.vaultUrl}`);
}
this.cryptoClient = new CryptographyClient(key.id, credential);
return this.cryptoClient as never;
}
async wrapKey(dek: Buffer): Promise<{ wrappedKey: string; mekVersion: string }> {
const client = await this.getClient();
const result = await client.wrapKey('RSA-OAEP-256', new Uint8Array(dek));
return {
wrappedKey: Buffer.from(result.result).toString('hex'),
mekVersion: this.mekName,
};
}
async unwrapKey(wrappedKeyHex: string, _mekVersion: string): Promise<Buffer> {
const client = await this.getClient();
const wrappedBytes = new Uint8Array(Buffer.from(wrappedKeyHex, 'hex'));
const result = await client.unwrapKey('RSA-OAEP-256', wrappedBytes);
return Buffer.from(result.result);
}
}