/** * @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 { 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); } }