From 9a775558b76b862a525d7224a150783d2a91d05f Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Tue, 5 Aug 2025 15:54:45 -0700 Subject: [PATCH 1/3] First cut --- .../rules/encryption/encrypt-executor.ts | 67 +++++++++++++++++-- 1 file changed, 63 insertions(+), 4 deletions(-) diff --git a/schemaregistry/rules/encryption/encrypt-executor.ts b/schemaregistry/rules/encryption/encrypt-executor.ts index c0973229..84101bc4 100644 --- a/schemaregistry/rules/encryption/encrypt-executor.ts +++ b/schemaregistry/rules/encryption/encrypt-executor.ts @@ -33,6 +33,8 @@ const ENCRYPT_KMS_TYPE = 'encrypt.kms.type' const ENCRYPT_DEK_ALGORITHM = 'encrypt.dek.algorithm' // EncryptDekExpiryDays represents dek expiry days const ENCRYPT_DEK_EXPIRY_DAYS = 'encrypt.dek.expiry.days' +// EncryptAlternateKmsKeyIds represents alternate kms key IDs +const ENCRYPT_ALTERNATE_KMS_KEY_IDS = 'encrypt.alternate.kms.key.ids' // MillisInDay represents number of milliseconds in a day const MILLIS_IN_DAY = 24 * 60 * 60 * 1000 @@ -387,7 +389,7 @@ export class EncryptionExecutorTransform { } let encryptedDek: Buffer | null = null if (!kek.shared) { - kmsClient = getKmsClient(this.executor.config!, kek) + kmsClient = new KmsClientWrapper(this.executor.config!, kek) // Generate new dek const rawDek = this.cryptor.generateKey() encryptedDek = await kmsClient.encrypt(rawDek) @@ -407,7 +409,7 @@ export class EncryptionExecutorTransform { const keyMaterialBytes = await this.executor.client!.getDekKeyMaterialBytes(dek) if (keyMaterialBytes == null) { if (kmsClient == null) { - kmsClient = getKmsClient(this.executor.config!, kek) + kmsClient = new KmsClientWrapper(this.executor.config!, kek) } const encryptedKeyMaterialBytes = await this.executor.client!.getDekEncryptedKeyMaterialBytes(dek) const rawDek = await kmsClient.decrypt(encryptedKeyMaterialBytes!) @@ -579,8 +581,8 @@ export class EncryptionExecutorTransform { } } -function getKmsClient(config: Map, kek: Kek): KmsClient { - let keyUrl = kek.kmsType + '://' + kek.kmsKeyId +function getKmsClient(config: Map, kmsType: string, kmsKeyId: string): KmsClient { + let keyUrl = kmsType + '://' + kmsKeyId let kmsClient = Registry.getKmsClient(keyUrl) if (kmsClient == null) { let kmsDriver = Registry.getKmsDriver(keyUrl) @@ -641,3 +643,60 @@ export class FieldEncryptionExecutorTransform implements FieldTransform { } } +export class KmsClientWrapper implements KmsClient { + private config: Map + private kek: Kek + private kekId: string + private kmsKeyIds: string[] + + constructor(config: Map, kek: Kek) { + this.config = config + this.kek = kek + this.kekId = kek.kmsType + '://' + kek.kmsKeyId + this.kmsKeyIds = this.getKmsKeyIds() + } + + getKmsKeyIds(): string[] { + let kmsKeyIds = [this.kek.kmsKeyId!] + if (this.kek.kmsProps != null) { + let alternateKmsKeyIds = this.kek.kmsProps[ENCRYPT_ALTERNATE_KMS_KEY_IDS] + if (alternateKmsKeyIds != null) { + kmsKeyIds = kmsKeyIds.concat(alternateKmsKeyIds.split(',').map(id => id.trim())) + } + } + return kmsKeyIds + } + + supported(keyUri: string): boolean { + return this.kekId === keyUri + } + + async encrypt(rawKey: Buffer): Promise { + for (let i = 0; i < this.kmsKeyIds.length; i++) { + try { + let kmsClient = getKmsClient(this.config, this.kek.kmsType!, this.kmsKeyIds[i]) + return await kmsClient.encrypt(rawKey) + } catch (e) { + if (i === this.kmsKeyIds.length - 1) { + throw new RuleError(`failed to encrypt key with all KMS keys: ${e}`) + } + } + } + throw new RuleError('failed to encrypt key with all KMS keys') + } + + async decrypt(encryptedKey: Buffer): Promise { + for (let i = 0; i < this.kmsKeyIds.length; i++) { + try { + let kmsClient = getKmsClient(this.config, this.kek.kmsType!, this.kmsKeyIds[i]) + return await kmsClient.decrypt(encryptedKey) + } catch (e) { + if (i === this.kmsKeyIds.length - 1) { + throw new RuleError(`failed to decrypt key with all KMS keys: ${e}`) + } + } + } + throw new RuleError('failed to decrypt key with all KMS keys') + } +} + From 7e1d0a669bae09ce5a03745724fcaabaf1d12cb5 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Thu, 7 Aug 2025 14:51:43 -0700 Subject: [PATCH 2/3] Minor cleanup --- schemaregistry/rules/encryption/encrypt-executor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/schemaregistry/rules/encryption/encrypt-executor.ts b/schemaregistry/rules/encryption/encrypt-executor.ts index 84101bc4..ab5a2181 100644 --- a/schemaregistry/rules/encryption/encrypt-executor.ts +++ b/schemaregistry/rules/encryption/encrypt-executor.ts @@ -682,7 +682,7 @@ export class KmsClientWrapper implements KmsClient { } } } - throw new RuleError('failed to encrypt key with all KMS keys') + throw new RuleError('no KEK found for encryption') } async decrypt(encryptedKey: Buffer): Promise { @@ -696,7 +696,7 @@ export class KmsClientWrapper implements KmsClient { } } } - throw new RuleError('failed to decrypt key with all KMS keys') + throw new RuleError('no KEK found for decryption') } } From 1efe92c81efd96fdfbe3910d9ae65c7f6fa38ef4 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Fri, 8 Aug 2025 16:16:58 -0700 Subject: [PATCH 3/3] Add test --- .../rules/encryption/encrypt-executor.ts | 12 ++-- schemaregistry/test/serde/avro.spec.ts | 63 +++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/schemaregistry/rules/encryption/encrypt-executor.ts b/schemaregistry/rules/encryption/encrypt-executor.ts index ab5a2181..b6349460 100644 --- a/schemaregistry/rules/encryption/encrypt-executor.ts +++ b/schemaregistry/rules/encryption/encrypt-executor.ts @@ -658,11 +658,15 @@ export class KmsClientWrapper implements KmsClient { getKmsKeyIds(): string[] { let kmsKeyIds = [this.kek.kmsKeyId!] + let alternateKmsKeyIds: string | undefined if (this.kek.kmsProps != null) { - let alternateKmsKeyIds = this.kek.kmsProps[ENCRYPT_ALTERNATE_KMS_KEY_IDS] - if (alternateKmsKeyIds != null) { - kmsKeyIds = kmsKeyIds.concat(alternateKmsKeyIds.split(',').map(id => id.trim())) - } + alternateKmsKeyIds = this.kek.kmsProps[ENCRYPT_ALTERNATE_KMS_KEY_IDS] + } + if (alternateKmsKeyIds == null) { + alternateKmsKeyIds = this.config.get(ENCRYPT_ALTERNATE_KMS_KEY_IDS) + } + if (alternateKmsKeyIds != null) { + kmsKeyIds = kmsKeyIds.concat(alternateKmsKeyIds.split(',').map(id => id.trim())) } return kmsKeyIds } diff --git a/schemaregistry/test/serde/avro.spec.ts b/schemaregistry/test/serde/avro.spec.ts index a07d8e99..7580939e 100644 --- a/schemaregistry/test/serde/avro.spec.ts +++ b/schemaregistry/test/serde/avro.spec.ts @@ -1193,6 +1193,69 @@ describe('AvroSerializer', () => { expect(obj2.boolField).toEqual(obj.boolField); expect(obj2.bytesField).toEqual(obj.bytesField); }) + it('encryption with alternate keks', async () => { + let conf: ClientConfig = { + baseURLs: [baseURL], + cacheCapacity: 1000 + } + let client = SchemaRegistryClient.newClient(conf) + let serConfig: AvroSerializerConfig = { + useLatestVersion: true, + ruleConfig: { + secret: 'mysecret', + 'encrypt.alternate.kms.key.ids': 'mykey2,mykey3' + } + } + let ser = new AvroSerializer(client, SerdeType.VALUE, serConfig) + let dekClient = encryptionExecutor.client! + + let encRule: Rule = { + name: 'test-encrypt', + kind: 'TRANSFORM', + mode: RuleMode.WRITEREAD, + type: 'ENCRYPT_PAYLOAD', + params: { + 'encrypt.kek.name': 'kek1', + 'encrypt.kms.type': 'local-kms', + 'encrypt.kms.key.id': 'mykey', + }, + onFailure: 'ERROR,NONE' + } + let ruleSet: RuleSet = { + encodingRules: [encRule] + } + + let info: SchemaInfo = { + schemaType: 'AVRO', + schema: demoSchema, + ruleSet + } + + await client.register(subject, info, false) + + let obj = { + intField: 123, + doubleField: 45.67, + stringField: 'hi', + boolField: true, + bytesField: Buffer.from([1, 2]), + } + let bytes = await ser.serialize(topic, obj) + + let deserConfig: AvroDeserializerConfig = { + ruleConfig: { + secret: 'mysecret' + } + } + let deser = new AvroDeserializer(client, SerdeType.VALUE, deserConfig) + encryptionExecutor.client = dekClient + let obj2 = await deser.deserialize(topic, bytes) + expect(obj2.intField).toEqual(obj.intField); + expect(obj2.doubleField).toBeCloseTo(obj.doubleField, 0.001); + expect(obj2.stringField).toEqual(obj.stringField); + expect(obj2.boolField).toEqual(obj.boolField); + expect(obj2.bytesField).toEqual(obj.bytesField); + }) it('deterministic encryption', async () => { let conf: ClientConfig = { baseURLs: [baseURL],