- 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
69 lines
2.4 KiB
TypeScript
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);
|
|
}
|
|
}
|