From c5ce506ace3552b65a433e46a23286f279da49bb Mon Sep 17 00:00:00 2001 From: rishav-karanjit Date: Fri, 30 Aug 2024 16:26:51 -0700 Subject: [PATCH 01/13] chore: upgrade packages to fix dependency vulnerabilities --- aws-encryption-sdk-specification | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/aws-encryption-sdk-specification b/aws-encryption-sdk-specification index 6fd8f886..c35fbd91 160000 --- a/aws-encryption-sdk-specification +++ b/aws-encryption-sdk-specification @@ -1 +1 @@ -Subproject commit 6fd8f886f708afeb89bcfb2a618ca57bb2bd48cd +Subproject commit c35fbd91b28303d69813119088c44b5006395eb4 diff --git a/package.json b/package.json index 4840bc9e..7ead06ff 100644 --- a/package.json +++ b/package.json @@ -144,4 +144,4 @@ "webpack": "^5.94.0", "webpack-cli": "^4.7.2" } -} +} \ No newline at end of file From 656c25763cdddf2ac2b7c9b1309e2aead9346ed5 Mon Sep 17 00:00:00 2001 From: Jose Corella Date: Wed, 12 Mar 2025 15:44:33 -0700 Subject: [PATCH 02/13] fix: add serializationOptions flag for AAD UTF8 sorting --- ...yptographic_materials_cache_key_helpers.ts | 4 +- modules/decrypt-browser/src/decrypt_client.ts | 4 +- modules/decrypt-browser/test/decrypt.test.ts | 2 +- modules/decrypt-node/src/decrypt_client.ts | 2 + modules/encrypt-browser/src/encrypt.ts | 7 +- modules/encrypt-browser/src/encrypt_client.ts | 2 + modules/encrypt-node/src/encrypt_client.ts | 2 + modules/encrypt-node/src/encrypt_stream.ts | 13 +-- .../encrypt-node/src/framed_encrypt_stream.ts | 11 ++- .../test/framed_encrypt_stream.test.ts | 13 ++- .../src/kms_hkeyring_node_helpers.ts | 7 +- .../test/kms_hkeyring_node.helpers.test.ts | 3 +- modules/material-management/src/types.ts | 1 + .../src/raw_aes_keyring_browser.ts | 11 +-- .../src/raw_aes_keyring_node.ts | 11 ++- modules/serialize/src/serialize_factory.ts | 83 ++++++++++++++----- modules/serialize/src/types.ts | 4 + .../test/deserialize_header_v2.test.ts | 2 +- .../serialize/test/serialize_factory.test.ts | 36 ++++---- 19 files changed, 147 insertions(+), 71 deletions(-) diff --git a/modules/cache-material/src/build_cryptographic_materials_cache_key_helpers.ts b/modules/cache-material/src/build_cryptographic_materials_cache_key_helpers.ts index 0e449e11..68f6f804 100644 --- a/modules/cache-material/src/build_cryptographic_materials_cache_key_helpers.ts +++ b/modules/cache-material/src/build_cryptographic_materials_cache_key_helpers.ts @@ -22,7 +22,7 @@ export function buildCryptographicMaterialsCacheKeyHelpers< sha512: (...data: (Uint8Array | string)[]) => Promise ): CryptographicMaterialsCacheKeyHelpersInterface { const { serializeEncryptionContext, serializeEncryptedDataKey } = - serializeFactory(fromUtf8) + serializeFactory(fromUtf8, {utf8Sorting: false}) return { buildEncryptionMaterialCacheKey, @@ -80,7 +80,7 @@ export function buildCryptographicMaterialsCacheKeyHelpers< * However, the RAW Keyring wants _only_ the ADD. * So, I just slice off the length. */ - const serializedContext = serializeEncryptionContext(context).slice(2) + const serializedContext = serializeEncryptionContext(context, {utf8Sorting: false}).slice(2) return sha512(serializedContext) } } diff --git a/modules/decrypt-browser/src/decrypt_client.ts b/modules/decrypt-browser/src/decrypt_client.ts index cf160057..e2577db0 100644 --- a/modules/decrypt-browser/src/decrypt_client.ts +++ b/modules/decrypt-browser/src/decrypt_client.ts @@ -22,7 +22,7 @@ export function buildDecrypt( } { const { commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, - maxEncryptedDataKeys = false, + maxEncryptedDataKeys = false } = typeof options === 'string' ? { commitmentPolicy: options } : options /* Precondition: browser buildDecrypt needs a valid commitmentPolicy. */ @@ -35,7 +35,7 @@ export function buildDecrypt( const clientOptions: ClientOptions = { commitmentPolicy, - maxEncryptedDataKeys, + maxEncryptedDataKeys } return { decrypt: _decrypt.bind({}, clientOptions), diff --git a/modules/decrypt-browser/test/decrypt.test.ts b/modules/decrypt-browser/test/decrypt.test.ts index cc6a951e..a3de669c 100644 --- a/modules/decrypt-browser/test/decrypt.test.ts +++ b/modules/decrypt-browser/test/decrypt.test.ts @@ -46,7 +46,7 @@ describe('decrypt', () => { it('Precondition: _decrypt needs a valid commitmentPolicy.', async () => { await expect( _decrypt( - { commitmentPolicy: 'fake_policy' as any, maxEncryptedDataKeys: false }, + { commitmentPolicy: 'fake_policy' as any, maxEncryptedDataKeys: false}, {} as any, {} as any ) diff --git a/modules/decrypt-node/src/decrypt_client.ts b/modules/decrypt-node/src/decrypt_client.ts index 14889990..ac223479 100644 --- a/modules/decrypt-node/src/decrypt_client.ts +++ b/modules/decrypt-node/src/decrypt_client.ts @@ -31,6 +31,7 @@ export function buildDecrypt( const { commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, maxEncryptedDataKeys = false, + utf8Sorting = false } = typeof options === 'string' ? { commitmentPolicy: options } : options /* Precondition: node buildDecrypt needs a valid commitmentPolicy. */ @@ -44,6 +45,7 @@ export function buildDecrypt( const clientOptions: ClientOptions = { commitmentPolicy, maxEncryptedDataKeys, + utf8Sorting } return { decryptUnsignedMessageStream: _decryptStream.bind( diff --git a/modules/encrypt-browser/src/encrypt.ts b/modules/encrypt-browser/src/encrypt.ts index ccaeae1f..0d2e9bcb 100644 --- a/modules/encrypt-browser/src/encrypt.ts +++ b/modules/encrypt-browser/src/encrypt.ts @@ -31,7 +31,6 @@ import { import { fromUtf8 } from '@aws-sdk/util-utf8-browser' import { getWebCryptoBackend } from '@aws-crypto/web-crypto-backend' -const serialize = serializeFactory(fromUtf8) const { messageAADContentString, messageAAD } = aadFactory(fromUtf8) export interface EncryptInput { @@ -47,7 +46,7 @@ export interface EncryptResult { } export async function _encrypt( - { commitmentPolicy, maxEncryptedDataKeys }: ClientOptions, + { commitmentPolicy, maxEncryptedDataKeys, utf8Sorting }: ClientOptions, cmm: KeyringWebCrypto | WebCryptoMaterialsManager, plaintext: Uint8Array, { @@ -122,6 +121,10 @@ export async function _encrypt( const { getSubtleEncrypt, keyCommitment } = await getEncryptInfo(messageId) + const maybeUtf8Sorting = utf8Sorting ?? false; + + const serialize = serializeFactory(fromUtf8, {utf8Sorting: maybeUtf8Sorting}) + const messageHeader = serialize.buildMessageHeader({ suite: material.suite, encryptedDataKeys: material.encryptedDataKeys, diff --git a/modules/encrypt-browser/src/encrypt_client.ts b/modules/encrypt-browser/src/encrypt_client.ts index 27ad4943..fca2f2ad 100644 --- a/modules/encrypt-browser/src/encrypt_client.ts +++ b/modules/encrypt-browser/src/encrypt_client.ts @@ -23,6 +23,7 @@ export function buildEncrypt( const { commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, maxEncryptedDataKeys = false, + utf8Sorting = false } = typeof options === 'string' ? { commitmentPolicy: options } : options /* Precondition: browser buildEncrypt needs a valid commitmentPolicy. */ @@ -36,6 +37,7 @@ export function buildEncrypt( const clientOptions: ClientOptions = { commitmentPolicy, maxEncryptedDataKeys, + utf8Sorting } return { encrypt: _encrypt.bind({}, clientOptions), diff --git a/modules/encrypt-node/src/encrypt_client.ts b/modules/encrypt-node/src/encrypt_client.ts index 482f397a..97779e33 100644 --- a/modules/encrypt-node/src/encrypt_client.ts +++ b/modules/encrypt-node/src/encrypt_client.ts @@ -27,6 +27,7 @@ export function buildEncrypt( const { commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, maxEncryptedDataKeys = false, + utf8Sorting = false } = typeof options === 'string' ? { commitmentPolicy: options } : options /* Precondition: node buildEncrypt needs a valid commitmentPolicy. */ @@ -40,6 +41,7 @@ export function buildEncrypt( const clientOptions: ClientOptions = { commitmentPolicy, maxEncryptedDataKeys, + utf8Sorting } return { encryptStream: _encryptStream.bind({}, clientOptions), diff --git a/modules/encrypt-node/src/encrypt_stream.ts b/modules/encrypt-node/src/encrypt_stream.ts index 3205c2c2..7d5a2725 100644 --- a/modules/encrypt-node/src/encrypt_stream.ts +++ b/modules/encrypt-node/src/encrypt_stream.ts @@ -33,8 +33,6 @@ import { pipeline } from 'readable-stream' import { Duplex } from 'stream' const fromUtf8 = (input: string) => Buffer.from(input, 'utf8') -const { serializeMessageHeader, headerAuthIv, buildMessageHeader } = - serializeFactory(fromUtf8) export interface EncryptStreamInput { suiteId?: AlgorithmSuiteIdentifier @@ -53,7 +51,7 @@ export interface EncryptStreamInput { * @param op EncryptStreamInput */ export function _encryptStream( - { commitmentPolicy, maxEncryptedDataKeys }: ClientOptions, + { commitmentPolicy, maxEncryptedDataKeys, utf8Sorting }: ClientOptions, cmm: KeyringNode | NodeMaterialsManager, op: EncryptStreamInput = {} ): Duplex { @@ -111,8 +109,9 @@ export function _encryptStream( 'maxEncryptedDataKeys exceeded.' ) + const maybeUtf8Sorting = utf8Sorting ?? false; const { getCipher, messageHeader, rawHeader, dispose, getSigner } = - getEncryptionInfo(material, frameLength) + getEncryptionInfo(material, frameLength, maybeUtf8Sorting) wrappingStream.emit('MessageHeader', messageHeader) @@ -120,6 +119,7 @@ export function _encryptStream( getCipher, messageHeader, dispose, + maybeUtf8Sorting, { plaintextLength, suite: material.suite } ) const signatureStream = new SignatureStream(getSigner) @@ -140,9 +140,12 @@ export function _encryptStream( export function getEncryptionInfo( material: NodeEncryptionMaterial, - frameLength: number + frameLength: number, + utf8Sorting: boolean ) { const { getCipherInfo, dispose, getSigner } = getEncryptHelper(material) + const { serializeMessageHeader, headerAuthIv, buildMessageHeader } = + serializeFactory(fromUtf8, {utf8Sorting}) const { suite, encryptionContext, encryptedDataKeys } = material const { ivLength, messageFormat } = material.suite diff --git a/modules/encrypt-node/src/framed_encrypt_stream.ts b/modules/encrypt-node/src/framed_encrypt_stream.ts index b3372471..d73b37a2 100644 --- a/modules/encrypt-node/src/framed_encrypt_stream.ts +++ b/modules/encrypt-node/src/framed_encrypt_stream.ts @@ -18,8 +18,6 @@ import { } from '@aws-crypto/material-management-node' const fromUtf8 = (input: string) => Buffer.from(input, 'utf8') -const serialize = serializeFactory(fromUtf8) -const { finalFrameHeader, frameHeader } = serialize const aadUtility = aadFactory(fromUtf8) interface AccumulatingFrame { @@ -47,6 +45,7 @@ export function getFramedEncryptStream( getCipher: GetCipher, messageHeader: MessageHeader, dispose: () => void, + utf8Sorting: boolean, { plaintextLength, suite, @@ -107,6 +106,7 @@ export function getFramedEncryptStream( getCipher, isFinalFrame: false, suite, + utf8Sorting }) // Reset frame state for next frame @@ -129,6 +129,7 @@ export function getFramedEncryptStream( getCipher, isFinalFrame: true, suite, + utf8Sorting }) this._flushEncryptFrame(encryptFrame) @@ -205,10 +206,11 @@ type EncryptFrameInput = { getCipher: GetCipher isFinalFrame: boolean suite: NodeAlgorithmSuite + utf8Sorting: boolean } export function getEncryptFrame(input: EncryptFrameInput): EncryptFrame { - const { pendingFrame, messageHeader, getCipher, isFinalFrame, suite } = input + const { pendingFrame, messageHeader, getCipher, isFinalFrame, suite, utf8Sorting } = input const { sequenceNumber, contentLength, content } = pendingFrame const { frameLength, contentType, messageId } = messageHeader /* Precondition: The content length MUST correlate with the frameLength. @@ -226,6 +228,9 @@ export function getEncryptFrame(input: EncryptFrameInput): EncryptFrame { isFinalFrame, })}` ) + const serialize = serializeFactory(fromUtf8, {utf8Sorting}) + const { finalFrameHeader, frameHeader } = serialize + const frameIv = serialize.frameIv(suite.ivLength, sequenceNumber) const bodyHeader = Buffer.from( isFinalFrame diff --git a/modules/encrypt-node/test/framed_encrypt_stream.test.ts b/modules/encrypt-node/test/framed_encrypt_stream.test.ts index 37e9a2e3..4c6b2b4d 100644 --- a/modules/encrypt-node/test/framed_encrypt_stream.test.ts +++ b/modules/encrypt-node/test/framed_encrypt_stream.test.ts @@ -28,6 +28,7 @@ describe('getFramedEncryptStream', () => { getCipher, {} as any, () => {}, + false, {} as any ) expect(test._transform).is.a('function') @@ -36,13 +37,13 @@ describe('getFramedEncryptStream', () => { it('Precondition: plaintextLength must be within bounds.', () => { const getCipher: any = () => {} expect(() => - getFramedEncryptStream(getCipher, {} as any, () => {}, { + getFramedEncryptStream(getCipher, {} as any, () => {}, false, { plaintextLength: -1, suite, }) ).to.throw(Error, 'plaintextLength out of bounds.') expect(() => - getFramedEncryptStream(getCipher, {} as any, () => {}, { + getFramedEncryptStream(getCipher, {} as any, () => {}, false, { plaintextLength: Number.MAX_SAFE_INTEGER + 1, suite, }) @@ -52,7 +53,7 @@ describe('getFramedEncryptStream', () => { * I want to make sure that I don't have an errant off by 1 error. */ expect(() => - getFramedEncryptStream(getCipher, {} as any, () => {}, { + getFramedEncryptStream(getCipher, {} as any, () => {}, false, { plaintextLength: Number.MAX_SAFE_INTEGER, suite, }) @@ -61,7 +62,7 @@ describe('getFramedEncryptStream', () => { it('Precondition: Must not process more than plaintextLength.', () => { const getCipher: any = () => {} - const test = getFramedEncryptStream(getCipher, {} as any, () => {}, { + const test = getFramedEncryptStream(getCipher, {} as any, () => {}, false, { plaintextLength: 8, suite, }) @@ -78,6 +79,7 @@ describe('getFramedEncryptStream', () => { getCipher, { frameLength } as any, () => {}, + false, {} as any ) @@ -112,6 +114,7 @@ describe('getEncryptFrame', () => { encryptedDataKeys: [], }, suite, + utf8Sorting: false, } const test1 = getEncryptFrame(input) expect(test1.content).to.equal(input.pendingFrame.content) @@ -146,6 +149,7 @@ describe('getEncryptFrame', () => { encryptedDataKeys: [], }, suite, + utf8Sorting: false } expect(() => getEncryptFrame(inputFinalFrameToLarge)).to.throw( @@ -172,6 +176,7 @@ describe('getEncryptFrame', () => { encryptedDataKeys: [], }, suite, + utf8Sorting: false } // Make sure that it must be equal as long as we are here... diff --git a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts index d9b842d6..e3dbb52a 100644 --- a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts +++ b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts @@ -33,7 +33,7 @@ import { PROVIDER_ID_HIERARCHY_AS_BYTES, } from './constants' import { BranchKeyIdSupplier } from '@aws-crypto/kms-keyring' -import { serializeFactory, uuidv4Factory } from '@aws-crypto/serialize' +import { serializeFactory, SerializeOptions, uuidv4Factory } from '@aws-crypto/serialize' export const stringToUtf8Bytes = (input: string): Buffer => Buffer.from(input, 'utf-8') @@ -45,8 +45,9 @@ const hexBytesToString = (input: Uint8Array): string => Buffer.from(input).toString('hex') export const { uuidv4ToCompressedBytes, decompressBytesToUuidv4 } = uuidv4Factory(stringToHexBytes, hexBytesToString) +export const utf8Sorting: SerializeOptions = {utf8Sorting: false} export const { serializeEncryptionContext } = - serializeFactory(stringToUtf8Bytes) + serializeFactory(stringToUtf8Bytes, utf8Sorting) export function getBranchKeyId( { branchKeyId, branchKeyIdSupplier }: IKmsHierarchicalKeyRingNode, @@ -372,7 +373,7 @@ export function wrapAad( * So, I just slice off the length. */ const aad = Buffer.from( - serializeEncryptionContext(encryptionContext).slice(2) + serializeEncryptionContext(encryptionContext, utf8Sorting).slice(2) ) return Buffer.concat([ diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts index fc954d03..90ca3110 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts @@ -6,6 +6,7 @@ import { wrapAad, destructureCiphertext, serializeEncryptionContext, + utf8Sorting, unwrapEncryptedDataKey, wrapPlaintextDataKey, } from '../src/kms_hkeyring_node_helpers' @@ -156,7 +157,7 @@ describe('KmsHierarchicalKeyRingNode: helpers', () => { ).to.deep.equal(branchKeyVersionAsBytes) startIdx += branchKeyVersionAsBytes.length - const expectedAad = serializeEncryptionContext(encryptionContext).slice(2) + const expectedAad = serializeEncryptionContext(encryptionContext, utf8Sorting).slice(2) expect(wrappedAad.subarray(startIdx)).to.deep.equal(expectedAad) }) }) diff --git a/modules/material-management/src/types.ts b/modules/material-management/src/types.ts index 7548ae53..9d4d895f 100644 --- a/modules/material-management/src/types.ts +++ b/modules/material-management/src/types.ts @@ -122,6 +122,7 @@ export type AwsEsdkCreateSecretKey = (key: Uint8Array) => AwsEsdkKeyObject export interface ClientOptions { commitmentPolicy: CommitmentPolicy maxEncryptedDataKeys: number | false + utf8Sorting?: boolean | false } export type Newable = { new (...args: any[]): T } diff --git a/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts b/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts index d152e041..b7cc481b 100644 --- a/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts +++ b/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts @@ -18,7 +18,7 @@ import { importForWebCryptoDecryptionMaterial, AwsEsdkJsCryptoKey, } from '@aws-crypto/material-management-browser' -import { serializeFactory, concatBuffers } from '@aws-crypto/serialize' +import { serializeFactory, concatBuffers, SerializeOptions } from '@aws-crypto/serialize' import { _onEncrypt, _onDecrypt, @@ -35,7 +35,6 @@ import { getWebCryptoBackend, getNonZeroByteBackend, } from '@aws-crypto/web-crypto-backend' -const { serializeEncryptionContext } = serializeFactory(fromUtf8) const { rawAesEncryptedDataKey } = rawAesEncryptedDataKeyFactory( toUtf8, fromUtf8 @@ -77,14 +76,16 @@ export class RawAesKeyringWebCrypto extends KeyringWebCrypto { keyName, flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, }) - + + const serializeOptions: SerializeOptions= {utf8Sorting: false} + const { serializeEncryptionContext } = serializeFactory(fromUtf8, serializeOptions) const _wrapKey = async (material: WebCryptoEncryptionMaterial) => { /* The AAD section is uInt16BE(length) + AAD * see: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html#header-aad * However, the RAW Keyring wants _only_ the ADD. * So, I just slice off the length. */ - const aad = serializeEncryptionContext(material.encryptionContext).slice( + const aad = serializeEncryptionContext(material.encryptionContext, serializeOptions).slice( 2 ) const { keyNamespace, keyName } = this @@ -107,7 +108,7 @@ export class RawAesKeyringWebCrypto extends KeyringWebCrypto { * However, the RAW Keyring wants _only_ the ADD. * So, I just slice off the length. */ - const aad = serializeEncryptionContext(material.encryptionContext).slice( + const aad = serializeEncryptionContext(material.encryptionContext, serializeOptions).slice( 2 ) const { keyNamespace, keyName } = this diff --git a/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts b/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts index 7d8b5133..5a2f81e7 100644 --- a/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts +++ b/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts @@ -14,7 +14,7 @@ import { NodeAlgorithmSuite, } from '@aws-crypto/material-management-node' import { randomBytes, createCipheriv, createDecipheriv } from 'crypto' -import { serializeFactory, concatBuffers } from '@aws-crypto/serialize' +import { serializeFactory, concatBuffers, SerializeOptions} from '@aws-crypto/serialize' import { _onEncrypt, _onDecrypt, @@ -28,7 +28,7 @@ import { const fromUtf8 = (input: string) => Buffer.from(input, 'utf8') const toUtf8 = (input: Uint8Array) => Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString('utf8') -const { serializeEncryptionContext } = serializeFactory(fromUtf8) +const { serializeEncryptionContext } = serializeFactory(fromUtf8, {utf8Sorting: false}) const { rawAesEncryptedDataKey } = rawAesEncryptedDataKeyFactory( toUtf8, fromUtf8 @@ -66,6 +66,8 @@ export class RawAesKeyringNode extends KeyringNode { flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, }) + // default will be false for the first release and then flipped to true + const serializeOptions: SerializeOptions= {utf8Sorting: false} const _wrapKey = async (material: NodeEncryptionMaterial) => { /* The AAD section is uInt16BE(length) + AAD * see: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html#header-aad @@ -73,7 +75,7 @@ export class RawAesKeyringNode extends KeyringNode { * So, I just slice off the length. */ const { buffer, byteOffset, byteLength } = serializeEncryptionContext( - material.encryptionContext + material.encryptionContext, serializeOptions ).slice(2) const aad = Buffer.from(buffer, byteOffset, byteLength) const { keyNamespace, keyName } = this @@ -98,7 +100,8 @@ export class RawAesKeyringNode extends KeyringNode { * So, I just slice off the length. */ const { buffer, byteOffset, byteLength } = serializeEncryptionContext( - material.encryptionContext + material.encryptionContext, + serializeOptions ).slice(2) const aad = Buffer.from(buffer, byteOffset, byteLength) // const aad = Buffer.concat(encodeEncryptionContext(context || {})) diff --git a/modules/serialize/src/serialize_factory.ts b/modules/serialize/src/serialize_factory.ts index b470f962..69898c8f 100644 --- a/modules/serialize/src/serialize_factory.ts +++ b/modules/serialize/src/serialize_factory.ts @@ -26,9 +26,12 @@ import { SerializationVersion, } from './identifiers' import { uInt16BE, uInt8, uInt32BE } from './uint_util' -import { MessageHeader, MessageHeaderV1, MessageHeaderV2 } from './types' +import { MessageHeader, MessageHeaderV1, MessageHeaderV2, SerializeOptions } from './types' -export function serializeFactory(fromUtf8: (input: any) => Uint8Array) { +export function serializeFactory( + fromUtf8: (input: any) => Uint8Array, + utf8Sorting: SerializeOptions +) { return { frameIv, nonFramedBodyIv, @@ -86,26 +89,66 @@ export function serializeFactory(fromUtf8: (input: any) => Uint8Array) { } function encodeEncryptionContext( - encryptionContext: EncryptionContext + encryptionContext: EncryptionContext, + serializeOptions: SerializeOptions ): Uint8Array[] { - return ( - Object.entries(encryptionContext) - /* Precondition: The serialized encryption context entries must be sorted by UTF-8 key value. */ - .sort(([aKey], [bKey]) => aKey.localeCompare(bKey)) - .map((entries) => entries.map(fromUtf8)) - .map(([key, value]) => - concatBuffers( - uInt16BE(key.byteLength), - key, - uInt16BE(value.byteLength), - value + const {utf8Sorting} = serializeOptions + if (utf8Sorting) { + return ( + Object.entries(encryptionContext) + .map((entries) => entries.map(fromUtf8)) + .sort(([aKey], [bKey]) => compare(aKey,bKey)) + .map(([key, value]) => + concatBuffers( + uInt16BE(key.byteLength), + key, + uInt16BE(value.byteLength), + value + ) ) - ) - ) + ) + } else { + return ( + Object.entries(encryptionContext) + /* Precondition: The serialized encryption context entries must be sorted by UTF-8 key value. */ + .sort(([aKey], [bKey]) => aKey.localeCompare(bKey)) + .map((entries) => entries.map(fromUtf8)) + .map(([key, value]) => + concatBuffers( + uInt16BE(key.byteLength), + key, + uInt16BE(value.byteLength), + value + ) + ) + ) + } + } + + function compare(a: Uint8Array, b: Uint8Array): number { + for (let i = 0; i < a.byteLength; i++) { + if (a[i] < b[i]) { + return -1 + } + + if (a[i] > b[i]) { + return 1 + } + } + if (a.byteLength > b.byteLength) { + return 1 + } + if (a.byteLength < b.byteLength) { + return -1 + } + return 0 } - function serializeEncryptionContext(encryptionContext: EncryptionContext) { - const encryptionContextElements = encodeEncryptionContext(encryptionContext) + function serializeEncryptionContext( + encryptionContext: EncryptionContext, + serializeOptions: SerializeOptions + ) { + const encryptionContextElements = encodeEncryptionContext(encryptionContext, serializeOptions) /* Check for early return (Postcondition): If there is no context then the length of the _whole_ serialized portion is 0. * This is part of the specification of the AWS Encryption SDK Message Format. @@ -163,7 +206,7 @@ export function serializeFactory(fromUtf8: (input: any) => Uint8Array) { uInt8(messageHeader.type), uInt16BE(messageHeader.suiteId), messageHeader.messageId, - serializeEncryptionContext(messageHeader.encryptionContext), + serializeEncryptionContext(messageHeader.encryptionContext, utf8Sorting), serializeEncryptedDataKeys(messageHeader.encryptedDataKeys), new Uint8Array([messageHeader.contentType]), new Uint8Array([0, 0, 0, 0]), @@ -177,7 +220,7 @@ export function serializeFactory(fromUtf8: (input: any) => Uint8Array) { uInt8(messageHeader.version), uInt16BE(messageHeader.suiteId), messageHeader.messageId, - serializeEncryptionContext(messageHeader.encryptionContext), + serializeEncryptionContext(messageHeader.encryptionContext, utf8Sorting), serializeEncryptedDataKeys(messageHeader.encryptedDataKeys), new Uint8Array([messageHeader.contentType]), uInt32BE(messageHeader.frameLength), diff --git a/modules/serialize/src/types.ts b/modules/serialize/src/types.ts index 06f8afe0..79d7aa68 100644 --- a/modules/serialize/src/types.ts +++ b/modules/serialize/src/types.ts @@ -105,3 +105,7 @@ export interface AlgorithmSuiteConstructor { export interface DeserializeOptions { maxEncryptedDataKeys: number | false } + +export interface SerializeOptions { + utf8Sorting: boolean | false +} \ No newline at end of file diff --git a/modules/serialize/test/deserialize_header_v2.test.ts b/modules/serialize/test/deserialize_header_v2.test.ts index eba227a4..9983828c 100644 --- a/modules/serialize/test/deserialize_header_v2.test.ts +++ b/modules/serialize/test/deserialize_header_v2.test.ts @@ -141,7 +141,7 @@ describe('serializeMessageHeaderV2', () => { }) const { buildMessageHeader, serializeMessageHeader } = - serializeFactory(fromUtf8) + serializeFactory(fromUtf8, {utf8Sorting: false}) /* There is a compatibility bug in JS for encodeEncryptionContext. * The encryption context is sorted lexically. diff --git a/modules/serialize/test/serialize_factory.test.ts b/modules/serialize/test/serialize_factory.test.ts index 55ebe601..360eee3b 100644 --- a/modules/serialize/test/serialize_factory.test.ts +++ b/modules/serialize/test/serialize_factory.test.ts @@ -21,7 +21,7 @@ describe('serializeFactory:frameIv', () => { const fromUtf8 = () => { throw new Error('not used') } - const { frameIv } = serializeFactory(fromUtf8) + const { frameIv } = serializeFactory(fromUtf8, {utf8Sorting: false}) const test = frameIv(12, 1) expect(test).to.be.instanceof(Uint8Array) expect(test.byteLength).to.eql(12) @@ -32,7 +32,7 @@ describe('serializeFactory:frameIv', () => { const fromUtf8 = () => { throw new Error('not used') } - const { frameIv } = serializeFactory(fromUtf8) + const { frameIv } = serializeFactory(fromUtf8, {utf8Sorting: false}) expect(() => frameIv(12, 0)).to.throw() }) }) @@ -42,7 +42,7 @@ describe('serializeFactory:nonFramedBodyIv', () => { const fromUtf8 = () => { throw new Error('not used') } - const { nonFramedBodyIv } = serializeFactory(fromUtf8) + const { nonFramedBodyIv } = serializeFactory(fromUtf8, {utf8Sorting: false}) const test = nonFramedBodyIv(12) expect(test).to.be.instanceof(Uint8Array) expect(test.byteLength).to.eql(12) @@ -55,7 +55,7 @@ describe('serializeFactory:headerAuthIv', () => { const fromUtf8 = () => { throw new Error('not used') } - const { headerAuthIv } = serializeFactory(fromUtf8) + const { headerAuthIv } = serializeFactory(fromUtf8, {utf8Sorting: false}) const test = headerAuthIv(12) expect(test).to.be.instanceof(Uint8Array) expect(test.byteLength).to.eql(12) @@ -68,7 +68,7 @@ describe('serializeFactory:frameHeader', () => { const fromUtf8 = () => { throw new Error('not used') } - const { frameHeader, frameIv } = serializeFactory(fromUtf8) + const { frameHeader, frameIv } = serializeFactory(fromUtf8, {utf8Sorting: false}) const sequenceNumber = 1 const iv = frameIv(12, sequenceNumber) const test = frameHeader(sequenceNumber, iv) @@ -83,7 +83,7 @@ describe('serializeFactory:finalFrameHeader', () => { const fromUtf8 = () => { throw new Error('not used') } - const { finalFrameHeader, frameIv } = serializeFactory(fromUtf8) + const { finalFrameHeader, frameIv } = serializeFactory(fromUtf8, {utf8Sorting: false}) const sequenceNumber = 1 const iv = frameIv(12, sequenceNumber) const test = finalFrameHeader(sequenceNumber, iv, 999) @@ -96,11 +96,11 @@ describe('serializeFactory:finalFrameHeader', () => { describe('serializeFactory:encodeEncryptionContext', () => { it('should return rational byte array', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { encodeEncryptionContext } = serializeFactory(fromUtf8) + const { encodeEncryptionContext } = serializeFactory(fromUtf8, {utf8Sorting: false}) const test = encodeEncryptionContext({ information: '\u00bd + \u00bc = \u00be', some: 'public', - }) + }, {utf8Sorting: false}) expect(test).to.be.instanceof(Array) expect(test.length).to.eql(2) expect(test[0]).to.be.instanceof(Uint8Array) @@ -120,11 +120,11 @@ describe('serializeFactory:encodeEncryptionContext', () => { it('Precondition: The serialized encryption context entries must be sorted by UTF-8 key value.', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { encodeEncryptionContext } = serializeFactory(fromUtf8) + const { encodeEncryptionContext } = serializeFactory(fromUtf8, {utf8Sorting: false}) const test = encodeEncryptionContext({ some: 'public', information: '\u00bd + \u00bc = \u00be', - }) + }, {utf8Sorting: false}) expect(test[0]).to.deep.equal( new Uint8Array([ 0, 11, 105, 110, 102, 111, 114, 109, 97, 116, 105, 111, 110, 0, 12, 194, @@ -142,11 +142,11 @@ describe('serializeFactory:encodeEncryptionContext', () => { describe('serializeFactory:serializeEncryptionContext', () => { it('should return rational context bytes', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { serializeEncryptionContext } = serializeFactory(fromUtf8) + const { serializeEncryptionContext } = serializeFactory(fromUtf8, {utf8Sorting: false}) const test = serializeEncryptionContext({ some: 'public', information: '\u00bd + \u00bc = \u00be', - }) + }, {utf8Sorting: false}) expect(test).to.be.instanceof(Uint8Array) expect(test.byteLength).to.eql(45) @@ -155,8 +155,8 @@ describe('serializeFactory:serializeEncryptionContext', () => { it('Check for early return (Postcondition): If there is no context then the length of the _whole_ serialized portion is 0.', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { serializeEncryptionContext } = serializeFactory(fromUtf8) - const test = serializeEncryptionContext({}) + const { serializeEncryptionContext } = serializeFactory(fromUtf8, {utf8Sorting: false}) + const test = serializeEncryptionContext({}, {utf8Sorting: false}) expect(test).to.be.instanceof(Uint8Array) expect(test.byteLength).to.eql(2) @@ -166,7 +166,7 @@ describe('serializeFactory:serializeEncryptionContext', () => { describe('serializeFactory:serializeEncryptedDataKeys', () => { it('should return a rational data key section', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { serializeEncryptedDataKeys } = serializeFactory(fromUtf8) + const { serializeEncryptedDataKeys } = serializeFactory(fromUtf8, {utf8Sorting: false}) const test = serializeEncryptedDataKeys([ { providerInfo: 'firstKey', @@ -189,7 +189,7 @@ describe('serializeFactory:serializeEncryptedDataKeys', () => { describe('serializeFactory:serializeMessageHeader', () => { it('should return a rational raw header', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { serializeMessageHeader } = serializeFactory(fromUtf8) + const { serializeMessageHeader } = serializeFactory(fromUtf8, {utf8Sorting: false}) const test = serializeMessageHeader({ version: SerializationVersion.V1, type: ObjectType.CUSTOMER_AE_DATA, @@ -225,7 +225,7 @@ describe('serializeFactory:serializeMessageHeader', () => { it('should return a header with 0,0 for context length and _not_ 0,0 for element count', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { serializeMessageHeader } = serializeFactory(fromUtf8) + const { serializeMessageHeader } = serializeFactory(fromUtf8, {utf8Sorting: false}) const test = serializeMessageHeader({ version: SerializationVersion.V1, type: ObjectType.CUSTOMER_AE_DATA, @@ -260,7 +260,7 @@ describe('serializeFactory:serializeMessageHeader', () => { it('Precondition: Must be a version that can be serialized.', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { serializeMessageHeader } = serializeFactory(fromUtf8) + const { serializeMessageHeader } = serializeFactory(fromUtf8, {utf8Sorting: false}) expect(() => serializeMessageHeader({ version: -1 } as any)).to.throw( 'Unsupported version.' ) From 3bf4e00e554d387c5b4447529dd3dad6ccb2539c Mon Sep 17 00:00:00 2001 From: Jose Corella Date: Thu, 13 Mar 2025 11:25:23 -0700 Subject: [PATCH 03/13] format --- ...yptographic_materials_cache_key_helpers.ts | 6 +- modules/decrypt-browser/src/decrypt_client.ts | 4 +- modules/decrypt-browser/test/decrypt.test.ts | 2 +- modules/decrypt-node/src/decrypt_client.ts | 4 +- modules/encrypt-browser/src/encrypt.ts | 10 ++- modules/encrypt-browser/src/encrypt_client.ts | 4 +- modules/encrypt-node/src/encrypt_client.ts | 4 +- modules/encrypt-node/src/encrypt_stream.ts | 4 +- .../encrypt-node/src/framed_encrypt_stream.ts | 17 ++-- .../test/framed_encrypt_stream.test.ts | 4 +- .../src/kms_hkeyring_node_helpers.ts | 14 ++- .../test/kms_hkeyring_node.helpers.test.ts | 5 +- .../src/raw_aes_keyring_browser.ts | 29 ++++--- .../src/raw_aes_keyring_node.ts | 15 +++- modules/serialize/src/serialize_factory.ts | 38 +++++---- modules/serialize/src/types.ts | 2 +- .../test/deserialize_header_v2.test.ts | 6 +- .../serialize/test/serialize_factory.test.ts | 85 +++++++++++++------ 18 files changed, 164 insertions(+), 89 deletions(-) diff --git a/modules/cache-material/src/build_cryptographic_materials_cache_key_helpers.ts b/modules/cache-material/src/build_cryptographic_materials_cache_key_helpers.ts index 68f6f804..580025a2 100644 --- a/modules/cache-material/src/build_cryptographic_materials_cache_key_helpers.ts +++ b/modules/cache-material/src/build_cryptographic_materials_cache_key_helpers.ts @@ -22,7 +22,7 @@ export function buildCryptographicMaterialsCacheKeyHelpers< sha512: (...data: (Uint8Array | string)[]) => Promise ): CryptographicMaterialsCacheKeyHelpersInterface { const { serializeEncryptionContext, serializeEncryptedDataKey } = - serializeFactory(fromUtf8, {utf8Sorting: false}) + serializeFactory(fromUtf8, { utf8Sorting: false }) return { buildEncryptionMaterialCacheKey, @@ -80,7 +80,9 @@ export function buildCryptographicMaterialsCacheKeyHelpers< * However, the RAW Keyring wants _only_ the ADD. * So, I just slice off the length. */ - const serializedContext = serializeEncryptionContext(context, {utf8Sorting: false}).slice(2) + const serializedContext = serializeEncryptionContext(context, { + utf8Sorting: false, + }).slice(2) return sha512(serializedContext) } } diff --git a/modules/decrypt-browser/src/decrypt_client.ts b/modules/decrypt-browser/src/decrypt_client.ts index e2577db0..cf160057 100644 --- a/modules/decrypt-browser/src/decrypt_client.ts +++ b/modules/decrypt-browser/src/decrypt_client.ts @@ -22,7 +22,7 @@ export function buildDecrypt( } { const { commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, - maxEncryptedDataKeys = false + maxEncryptedDataKeys = false, } = typeof options === 'string' ? { commitmentPolicy: options } : options /* Precondition: browser buildDecrypt needs a valid commitmentPolicy. */ @@ -35,7 +35,7 @@ export function buildDecrypt( const clientOptions: ClientOptions = { commitmentPolicy, - maxEncryptedDataKeys + maxEncryptedDataKeys, } return { decrypt: _decrypt.bind({}, clientOptions), diff --git a/modules/decrypt-browser/test/decrypt.test.ts b/modules/decrypt-browser/test/decrypt.test.ts index a3de669c..cc6a951e 100644 --- a/modules/decrypt-browser/test/decrypt.test.ts +++ b/modules/decrypt-browser/test/decrypt.test.ts @@ -46,7 +46,7 @@ describe('decrypt', () => { it('Precondition: _decrypt needs a valid commitmentPolicy.', async () => { await expect( _decrypt( - { commitmentPolicy: 'fake_policy' as any, maxEncryptedDataKeys: false}, + { commitmentPolicy: 'fake_policy' as any, maxEncryptedDataKeys: false }, {} as any, {} as any ) diff --git a/modules/decrypt-node/src/decrypt_client.ts b/modules/decrypt-node/src/decrypt_client.ts index ac223479..571e477f 100644 --- a/modules/decrypt-node/src/decrypt_client.ts +++ b/modules/decrypt-node/src/decrypt_client.ts @@ -31,7 +31,7 @@ export function buildDecrypt( const { commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, maxEncryptedDataKeys = false, - utf8Sorting = false + utf8Sorting = false, } = typeof options === 'string' ? { commitmentPolicy: options } : options /* Precondition: node buildDecrypt needs a valid commitmentPolicy. */ @@ -45,7 +45,7 @@ export function buildDecrypt( const clientOptions: ClientOptions = { commitmentPolicy, maxEncryptedDataKeys, - utf8Sorting + utf8Sorting, } return { decryptUnsignedMessageStream: _decryptStream.bind( diff --git a/modules/encrypt-browser/src/encrypt.ts b/modules/encrypt-browser/src/encrypt.ts index 0d2e9bcb..2d2835f7 100644 --- a/modules/encrypt-browser/src/encrypt.ts +++ b/modules/encrypt-browser/src/encrypt.ts @@ -121,10 +121,12 @@ export async function _encrypt( const { getSubtleEncrypt, keyCommitment } = await getEncryptInfo(messageId) - const maybeUtf8Sorting = utf8Sorting ?? false; - - const serialize = serializeFactory(fromUtf8, {utf8Sorting: maybeUtf8Sorting}) - + const maybeUtf8Sorting = utf8Sorting ?? false + + const serialize = serializeFactory(fromUtf8, { + utf8Sorting: maybeUtf8Sorting, + }) + const messageHeader = serialize.buildMessageHeader({ suite: material.suite, encryptedDataKeys: material.encryptedDataKeys, diff --git a/modules/encrypt-browser/src/encrypt_client.ts b/modules/encrypt-browser/src/encrypt_client.ts index fca2f2ad..0b4afc1e 100644 --- a/modules/encrypt-browser/src/encrypt_client.ts +++ b/modules/encrypt-browser/src/encrypt_client.ts @@ -23,7 +23,7 @@ export function buildEncrypt( const { commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, maxEncryptedDataKeys = false, - utf8Sorting = false + utf8Sorting = false, } = typeof options === 'string' ? { commitmentPolicy: options } : options /* Precondition: browser buildEncrypt needs a valid commitmentPolicy. */ @@ -37,7 +37,7 @@ export function buildEncrypt( const clientOptions: ClientOptions = { commitmentPolicy, maxEncryptedDataKeys, - utf8Sorting + utf8Sorting, } return { encrypt: _encrypt.bind({}, clientOptions), diff --git a/modules/encrypt-node/src/encrypt_client.ts b/modules/encrypt-node/src/encrypt_client.ts index 97779e33..82f80549 100644 --- a/modules/encrypt-node/src/encrypt_client.ts +++ b/modules/encrypt-node/src/encrypt_client.ts @@ -27,7 +27,7 @@ export function buildEncrypt( const { commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, maxEncryptedDataKeys = false, - utf8Sorting = false + utf8Sorting = false, } = typeof options === 'string' ? { commitmentPolicy: options } : options /* Precondition: node buildEncrypt needs a valid commitmentPolicy. */ @@ -41,7 +41,7 @@ export function buildEncrypt( const clientOptions: ClientOptions = { commitmentPolicy, maxEncryptedDataKeys, - utf8Sorting + utf8Sorting, } return { encryptStream: _encryptStream.bind({}, clientOptions), diff --git a/modules/encrypt-node/src/encrypt_stream.ts b/modules/encrypt-node/src/encrypt_stream.ts index 7d5a2725..56d94b95 100644 --- a/modules/encrypt-node/src/encrypt_stream.ts +++ b/modules/encrypt-node/src/encrypt_stream.ts @@ -109,7 +109,7 @@ export function _encryptStream( 'maxEncryptedDataKeys exceeded.' ) - const maybeUtf8Sorting = utf8Sorting ?? false; + const maybeUtf8Sorting = utf8Sorting ?? false const { getCipher, messageHeader, rawHeader, dispose, getSigner } = getEncryptionInfo(material, frameLength, maybeUtf8Sorting) @@ -145,7 +145,7 @@ export function getEncryptionInfo( ) { const { getCipherInfo, dispose, getSigner } = getEncryptHelper(material) const { serializeMessageHeader, headerAuthIv, buildMessageHeader } = - serializeFactory(fromUtf8, {utf8Sorting}) + serializeFactory(fromUtf8, { utf8Sorting }) const { suite, encryptionContext, encryptedDataKeys } = material const { ivLength, messageFormat } = material.suite diff --git a/modules/encrypt-node/src/framed_encrypt_stream.ts b/modules/encrypt-node/src/framed_encrypt_stream.ts index d73b37a2..f245b9af 100644 --- a/modules/encrypt-node/src/framed_encrypt_stream.ts +++ b/modules/encrypt-node/src/framed_encrypt_stream.ts @@ -106,7 +106,7 @@ export function getFramedEncryptStream( getCipher, isFinalFrame: false, suite, - utf8Sorting + utf8Sorting, }) // Reset frame state for next frame @@ -129,7 +129,7 @@ export function getFramedEncryptStream( getCipher, isFinalFrame: true, suite, - utf8Sorting + utf8Sorting, }) this._flushEncryptFrame(encryptFrame) @@ -210,7 +210,14 @@ type EncryptFrameInput = { } export function getEncryptFrame(input: EncryptFrameInput): EncryptFrame { - const { pendingFrame, messageHeader, getCipher, isFinalFrame, suite, utf8Sorting } = input + const { + pendingFrame, + messageHeader, + getCipher, + isFinalFrame, + suite, + utf8Sorting, + } = input const { sequenceNumber, contentLength, content } = pendingFrame const { frameLength, contentType, messageId } = messageHeader /* Precondition: The content length MUST correlate with the frameLength. @@ -228,9 +235,9 @@ export function getEncryptFrame(input: EncryptFrameInput): EncryptFrame { isFinalFrame, })}` ) - const serialize = serializeFactory(fromUtf8, {utf8Sorting}) + const serialize = serializeFactory(fromUtf8, { utf8Sorting }) const { finalFrameHeader, frameHeader } = serialize - + const frameIv = serialize.frameIv(suite.ivLength, sequenceNumber) const bodyHeader = Buffer.from( isFinalFrame diff --git a/modules/encrypt-node/test/framed_encrypt_stream.test.ts b/modules/encrypt-node/test/framed_encrypt_stream.test.ts index 4c6b2b4d..3b192df8 100644 --- a/modules/encrypt-node/test/framed_encrypt_stream.test.ts +++ b/modules/encrypt-node/test/framed_encrypt_stream.test.ts @@ -149,7 +149,7 @@ describe('getEncryptFrame', () => { encryptedDataKeys: [], }, suite, - utf8Sorting: false + utf8Sorting: false, } expect(() => getEncryptFrame(inputFinalFrameToLarge)).to.throw( @@ -176,7 +176,7 @@ describe('getEncryptFrame', () => { encryptedDataKeys: [], }, suite, - utf8Sorting: false + utf8Sorting: false, } // Make sure that it must be equal as long as we are here... diff --git a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts index e3dbb52a..bb7fc48c 100644 --- a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts +++ b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts @@ -33,7 +33,11 @@ import { PROVIDER_ID_HIERARCHY_AS_BYTES, } from './constants' import { BranchKeyIdSupplier } from '@aws-crypto/kms-keyring' -import { serializeFactory, SerializeOptions, uuidv4Factory } from '@aws-crypto/serialize' +import { + serializeFactory, + SerializeOptions, + uuidv4Factory, +} from '@aws-crypto/serialize' export const stringToUtf8Bytes = (input: string): Buffer => Buffer.from(input, 'utf-8') @@ -45,9 +49,11 @@ const hexBytesToString = (input: Uint8Array): string => Buffer.from(input).toString('hex') export const { uuidv4ToCompressedBytes, decompressBytesToUuidv4 } = uuidv4Factory(stringToHexBytes, hexBytesToString) -export const utf8Sorting: SerializeOptions = {utf8Sorting: false} -export const { serializeEncryptionContext } = - serializeFactory(stringToUtf8Bytes, utf8Sorting) +export const utf8Sorting: SerializeOptions = { utf8Sorting: false } +export const { serializeEncryptionContext } = serializeFactory( + stringToUtf8Bytes, + utf8Sorting +) export function getBranchKeyId( { branchKeyId, branchKeyIdSupplier }: IKmsHierarchicalKeyRingNode, diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts index 90ca3110..1e59502b 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts @@ -157,7 +157,10 @@ describe('KmsHierarchicalKeyRingNode: helpers', () => { ).to.deep.equal(branchKeyVersionAsBytes) startIdx += branchKeyVersionAsBytes.length - const expectedAad = serializeEncryptionContext(encryptionContext, utf8Sorting).slice(2) + const expectedAad = serializeEncryptionContext( + encryptionContext, + utf8Sorting + ).slice(2) expect(wrappedAad.subarray(startIdx)).to.deep.equal(expectedAad) }) }) diff --git a/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts b/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts index b7cc481b..28398d78 100644 --- a/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts +++ b/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts @@ -18,7 +18,11 @@ import { importForWebCryptoDecryptionMaterial, AwsEsdkJsCryptoKey, } from '@aws-crypto/material-management-browser' -import { serializeFactory, concatBuffers, SerializeOptions } from '@aws-crypto/serialize' +import { + serializeFactory, + concatBuffers, + SerializeOptions, +} from '@aws-crypto/serialize' import { _onEncrypt, _onDecrypt, @@ -76,18 +80,22 @@ export class RawAesKeyringWebCrypto extends KeyringWebCrypto { keyName, flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, }) - - const serializeOptions: SerializeOptions= {utf8Sorting: false} - const { serializeEncryptionContext } = serializeFactory(fromUtf8, serializeOptions) + + const serializeOptions: SerializeOptions = { utf8Sorting: false } + const { serializeEncryptionContext } = serializeFactory( + fromUtf8, + serializeOptions + ) const _wrapKey = async (material: WebCryptoEncryptionMaterial) => { /* The AAD section is uInt16BE(length) + AAD * see: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html#header-aad * However, the RAW Keyring wants _only_ the ADD. * So, I just slice off the length. */ - const aad = serializeEncryptionContext(material.encryptionContext, serializeOptions).slice( - 2 - ) + const aad = serializeEncryptionContext( + material.encryptionContext, + serializeOptions + ).slice(2) const { keyNamespace, keyName } = this return aesGcmWrapKey( @@ -108,9 +116,10 @@ export class RawAesKeyringWebCrypto extends KeyringWebCrypto { * However, the RAW Keyring wants _only_ the ADD. * So, I just slice off the length. */ - const aad = serializeEncryptionContext(material.encryptionContext, serializeOptions).slice( - 2 - ) + const aad = serializeEncryptionContext( + material.encryptionContext, + serializeOptions + ).slice(2) const { keyNamespace, keyName } = this return aesGcmUnwrapKey( diff --git a/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts b/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts index 5a2f81e7..c1385859 100644 --- a/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts +++ b/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts @@ -14,7 +14,11 @@ import { NodeAlgorithmSuite, } from '@aws-crypto/material-management-node' import { randomBytes, createCipheriv, createDecipheriv } from 'crypto' -import { serializeFactory, concatBuffers, SerializeOptions} from '@aws-crypto/serialize' +import { + serializeFactory, + concatBuffers, + SerializeOptions, +} from '@aws-crypto/serialize' import { _onEncrypt, _onDecrypt, @@ -28,7 +32,9 @@ import { const fromUtf8 = (input: string) => Buffer.from(input, 'utf8') const toUtf8 = (input: Uint8Array) => Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString('utf8') -const { serializeEncryptionContext } = serializeFactory(fromUtf8, {utf8Sorting: false}) +const { serializeEncryptionContext } = serializeFactory(fromUtf8, { + utf8Sorting: false, +}) const { rawAesEncryptedDataKey } = rawAesEncryptedDataKeyFactory( toUtf8, fromUtf8 @@ -67,7 +73,7 @@ export class RawAesKeyringNode extends KeyringNode { }) // default will be false for the first release and then flipped to true - const serializeOptions: SerializeOptions= {utf8Sorting: false} + const serializeOptions: SerializeOptions = { utf8Sorting: false } const _wrapKey = async (material: NodeEncryptionMaterial) => { /* The AAD section is uInt16BE(length) + AAD * see: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html#header-aad @@ -75,7 +81,8 @@ export class RawAesKeyringNode extends KeyringNode { * So, I just slice off the length. */ const { buffer, byteOffset, byteLength } = serializeEncryptionContext( - material.encryptionContext, serializeOptions + material.encryptionContext, + serializeOptions ).slice(2) const aad = Buffer.from(buffer, byteOffset, byteLength) const { keyNamespace, keyName } = this diff --git a/modules/serialize/src/serialize_factory.ts b/modules/serialize/src/serialize_factory.ts index 69898c8f..67fd2dfd 100644 --- a/modules/serialize/src/serialize_factory.ts +++ b/modules/serialize/src/serialize_factory.ts @@ -26,7 +26,12 @@ import { SerializationVersion, } from './identifiers' import { uInt16BE, uInt8, uInt32BE } from './uint_util' -import { MessageHeader, MessageHeaderV1, MessageHeaderV2, SerializeOptions } from './types' +import { + MessageHeader, + MessageHeaderV1, + MessageHeaderV2, + SerializeOptions, +} from './types' export function serializeFactory( fromUtf8: (input: any) => Uint8Array, @@ -92,21 +97,19 @@ export function serializeFactory( encryptionContext: EncryptionContext, serializeOptions: SerializeOptions ): Uint8Array[] { - const {utf8Sorting} = serializeOptions + const { utf8Sorting } = serializeOptions if (utf8Sorting) { - return ( - Object.entries(encryptionContext) - .map((entries) => entries.map(fromUtf8)) - .sort(([aKey], [bKey]) => compare(aKey,bKey)) - .map(([key, value]) => - concatBuffers( - uInt16BE(key.byteLength), - key, - uInt16BE(value.byteLength), - value - ) + return Object.entries(encryptionContext) + .map((entries) => entries.map(fromUtf8)) + .sort(([aKey], [bKey]) => compare(aKey, bKey)) + .map(([key, value]) => + concatBuffers( + uInt16BE(key.byteLength), + key, + uInt16BE(value.byteLength), + value ) - ) + ) } else { return ( Object.entries(encryptionContext) @@ -130,7 +133,7 @@ export function serializeFactory( if (a[i] < b[i]) { return -1 } - + if (a[i] > b[i]) { return 1 } @@ -148,7 +151,10 @@ export function serializeFactory( encryptionContext: EncryptionContext, serializeOptions: SerializeOptions ) { - const encryptionContextElements = encodeEncryptionContext(encryptionContext, serializeOptions) + const encryptionContextElements = encodeEncryptionContext( + encryptionContext, + serializeOptions + ) /* Check for early return (Postcondition): If there is no context then the length of the _whole_ serialized portion is 0. * This is part of the specification of the AWS Encryption SDK Message Format. diff --git a/modules/serialize/src/types.ts b/modules/serialize/src/types.ts index 79d7aa68..a7dff424 100644 --- a/modules/serialize/src/types.ts +++ b/modules/serialize/src/types.ts @@ -108,4 +108,4 @@ export interface DeserializeOptions { export interface SerializeOptions { utf8Sorting: boolean | false -} \ No newline at end of file +} diff --git a/modules/serialize/test/deserialize_header_v2.test.ts b/modules/serialize/test/deserialize_header_v2.test.ts index 9983828c..4a64c654 100644 --- a/modules/serialize/test/deserialize_header_v2.test.ts +++ b/modules/serialize/test/deserialize_header_v2.test.ts @@ -140,8 +140,10 @@ describe('serializeMessageHeaderV2', () => { SdkSuite: WebCryptoAlgorithmSuite, }) - const { buildMessageHeader, serializeMessageHeader } = - serializeFactory(fromUtf8, {utf8Sorting: false}) + const { buildMessageHeader, serializeMessageHeader } = serializeFactory( + fromUtf8, + { utf8Sorting: false } + ) /* There is a compatibility bug in JS for encodeEncryptionContext. * The encryption context is sorted lexically. diff --git a/modules/serialize/test/serialize_factory.test.ts b/modules/serialize/test/serialize_factory.test.ts index 360eee3b..78e6f664 100644 --- a/modules/serialize/test/serialize_factory.test.ts +++ b/modules/serialize/test/serialize_factory.test.ts @@ -21,7 +21,7 @@ describe('serializeFactory:frameIv', () => { const fromUtf8 = () => { throw new Error('not used') } - const { frameIv } = serializeFactory(fromUtf8, {utf8Sorting: false}) + const { frameIv } = serializeFactory(fromUtf8, { utf8Sorting: false }) const test = frameIv(12, 1) expect(test).to.be.instanceof(Uint8Array) expect(test.byteLength).to.eql(12) @@ -32,7 +32,7 @@ describe('serializeFactory:frameIv', () => { const fromUtf8 = () => { throw new Error('not used') } - const { frameIv } = serializeFactory(fromUtf8, {utf8Sorting: false}) + const { frameIv } = serializeFactory(fromUtf8, { utf8Sorting: false }) expect(() => frameIv(12, 0)).to.throw() }) }) @@ -42,7 +42,9 @@ describe('serializeFactory:nonFramedBodyIv', () => { const fromUtf8 = () => { throw new Error('not used') } - const { nonFramedBodyIv } = serializeFactory(fromUtf8, {utf8Sorting: false}) + const { nonFramedBodyIv } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) const test = nonFramedBodyIv(12) expect(test).to.be.instanceof(Uint8Array) expect(test.byteLength).to.eql(12) @@ -55,7 +57,7 @@ describe('serializeFactory:headerAuthIv', () => { const fromUtf8 = () => { throw new Error('not used') } - const { headerAuthIv } = serializeFactory(fromUtf8, {utf8Sorting: false}) + const { headerAuthIv } = serializeFactory(fromUtf8, { utf8Sorting: false }) const test = headerAuthIv(12) expect(test).to.be.instanceof(Uint8Array) expect(test.byteLength).to.eql(12) @@ -68,7 +70,9 @@ describe('serializeFactory:frameHeader', () => { const fromUtf8 = () => { throw new Error('not used') } - const { frameHeader, frameIv } = serializeFactory(fromUtf8, {utf8Sorting: false}) + const { frameHeader, frameIv } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) const sequenceNumber = 1 const iv = frameIv(12, sequenceNumber) const test = frameHeader(sequenceNumber, iv) @@ -83,7 +87,9 @@ describe('serializeFactory:finalFrameHeader', () => { const fromUtf8 = () => { throw new Error('not used') } - const { finalFrameHeader, frameIv } = serializeFactory(fromUtf8, {utf8Sorting: false}) + const { finalFrameHeader, frameIv } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) const sequenceNumber = 1 const iv = frameIv(12, sequenceNumber) const test = finalFrameHeader(sequenceNumber, iv, 999) @@ -96,11 +102,16 @@ describe('serializeFactory:finalFrameHeader', () => { describe('serializeFactory:encodeEncryptionContext', () => { it('should return rational byte array', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { encodeEncryptionContext } = serializeFactory(fromUtf8, {utf8Sorting: false}) - const test = encodeEncryptionContext({ - information: '\u00bd + \u00bc = \u00be', - some: 'public', - }, {utf8Sorting: false}) + const { encodeEncryptionContext } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) + const test = encodeEncryptionContext( + { + information: '\u00bd + \u00bc = \u00be', + some: 'public', + }, + { utf8Sorting: false } + ) expect(test).to.be.instanceof(Array) expect(test.length).to.eql(2) expect(test[0]).to.be.instanceof(Uint8Array) @@ -120,11 +131,16 @@ describe('serializeFactory:encodeEncryptionContext', () => { it('Precondition: The serialized encryption context entries must be sorted by UTF-8 key value.', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { encodeEncryptionContext } = serializeFactory(fromUtf8, {utf8Sorting: false}) - const test = encodeEncryptionContext({ - some: 'public', - information: '\u00bd + \u00bc = \u00be', - }, {utf8Sorting: false}) + const { encodeEncryptionContext } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) + const test = encodeEncryptionContext( + { + some: 'public', + information: '\u00bd + \u00bc = \u00be', + }, + { utf8Sorting: false } + ) expect(test[0]).to.deep.equal( new Uint8Array([ 0, 11, 105, 110, 102, 111, 114, 109, 97, 116, 105, 111, 110, 0, 12, 194, @@ -142,11 +158,16 @@ describe('serializeFactory:encodeEncryptionContext', () => { describe('serializeFactory:serializeEncryptionContext', () => { it('should return rational context bytes', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { serializeEncryptionContext } = serializeFactory(fromUtf8, {utf8Sorting: false}) - const test = serializeEncryptionContext({ - some: 'public', - information: '\u00bd + \u00bc = \u00be', - }, {utf8Sorting: false}) + const { serializeEncryptionContext } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) + const test = serializeEncryptionContext( + { + some: 'public', + information: '\u00bd + \u00bc = \u00be', + }, + { utf8Sorting: false } + ) expect(test).to.be.instanceof(Uint8Array) expect(test.byteLength).to.eql(45) @@ -155,8 +176,10 @@ describe('serializeFactory:serializeEncryptionContext', () => { it('Check for early return (Postcondition): If there is no context then the length of the _whole_ serialized portion is 0.', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { serializeEncryptionContext } = serializeFactory(fromUtf8, {utf8Sorting: false}) - const test = serializeEncryptionContext({}, {utf8Sorting: false}) + const { serializeEncryptionContext } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) + const test = serializeEncryptionContext({}, { utf8Sorting: false }) expect(test).to.be.instanceof(Uint8Array) expect(test.byteLength).to.eql(2) @@ -166,7 +189,9 @@ describe('serializeFactory:serializeEncryptionContext', () => { describe('serializeFactory:serializeEncryptedDataKeys', () => { it('should return a rational data key section', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { serializeEncryptedDataKeys } = serializeFactory(fromUtf8, {utf8Sorting: false}) + const { serializeEncryptedDataKeys } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) const test = serializeEncryptedDataKeys([ { providerInfo: 'firstKey', @@ -189,7 +214,9 @@ describe('serializeFactory:serializeEncryptedDataKeys', () => { describe('serializeFactory:serializeMessageHeader', () => { it('should return a rational raw header', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { serializeMessageHeader } = serializeFactory(fromUtf8, {utf8Sorting: false}) + const { serializeMessageHeader } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) const test = serializeMessageHeader({ version: SerializationVersion.V1, type: ObjectType.CUSTOMER_AE_DATA, @@ -225,7 +252,9 @@ describe('serializeFactory:serializeMessageHeader', () => { it('should return a header with 0,0 for context length and _not_ 0,0 for element count', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { serializeMessageHeader } = serializeFactory(fromUtf8, {utf8Sorting: false}) + const { serializeMessageHeader } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) const test = serializeMessageHeader({ version: SerializationVersion.V1, type: ObjectType.CUSTOMER_AE_DATA, @@ -260,7 +289,9 @@ describe('serializeFactory:serializeMessageHeader', () => { it('Precondition: Must be a version that can be serialized.', () => { const fromUtf8 = (input: string) => Buffer.from(input) - const { serializeMessageHeader } = serializeFactory(fromUtf8, {utf8Sorting: false}) + const { serializeMessageHeader } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) expect(() => serializeMessageHeader({ version: -1 } as any)).to.throw( 'Unsupported version.' ) From 5cb2f8c8277577be47c3620b40aec6c37a9f0d88 Mon Sep 17 00:00:00 2001 From: Jose Corella Date: Thu, 13 Mar 2025 11:40:59 -0700 Subject: [PATCH 04/13] point spec to same place as public remote --- aws-encryption-sdk-specification | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws-encryption-sdk-specification b/aws-encryption-sdk-specification index c35fbd91..6fd8f886 160000 --- a/aws-encryption-sdk-specification +++ b/aws-encryption-sdk-specification @@ -1 +1 @@ -Subproject commit c35fbd91b28303d69813119088c44b5006395eb4 +Subproject commit 6fd8f886f708afeb89bcfb2a618ca57bb2bd48cd From 9b4d98855f1d095c7113365985591c16cf8b4cc3 Mon Sep 17 00:00:00 2001 From: Jose Corella Date: Mon, 17 Mar 2025 16:26:18 -0700 Subject: [PATCH 05/13] refactor and some tests --- ...yptographic_materials_cache_key_helpers.ts | 13 +- .../kms-keyring-node/src/kms_hkeyring_node.ts | 20 ++- .../src/kms_hkeyring_node_helpers.ts | 31 ++-- .../test/kms_hkeyring_node.helpers.test.ts | 39 ++++-- .../src/raw_aes_keyring_browser.ts | 21 +-- .../src/raw_aes_keyring_node.ts | 27 ++-- .../test/raw_aes_keyring_node.test.ts | 64 +++++++++ modules/serialize/src/serialize_factory.ts | 22 ++- modules/serialize/test/fixtures.ts | 10 ++ .../serialize/test/serialize_factory.test.ts | 132 +++++++++++++++--- 10 files changed, 288 insertions(+), 91 deletions(-) diff --git a/modules/cache-material/src/build_cryptographic_materials_cache_key_helpers.ts b/modules/cache-material/src/build_cryptographic_materials_cache_key_helpers.ts index 580025a2..fc411f64 100644 --- a/modules/cache-material/src/build_cryptographic_materials_cache_key_helpers.ts +++ b/modules/cache-material/src/build_cryptographic_materials_cache_key_helpers.ts @@ -8,7 +8,11 @@ import { EncryptedDataKey, EncryptionContext, } from '@aws-crypto/material-management' -import { serializeFactory, uInt16BE } from '@aws-crypto/serialize' +import { + serializeFactory, + uInt16BE, + SerializeOptions, +} from '@aws-crypto/serialize' import { compare } from './portable_compare' // 512 bits of 0 for padding between hashes in decryption materials cache ID generation. @@ -21,8 +25,9 @@ export function buildCryptographicMaterialsCacheKeyHelpers< toUtf8: (input: Uint8Array) => string, sha512: (...data: (Uint8Array | string)[]) => Promise ): CryptographicMaterialsCacheKeyHelpersInterface { + const sorting: SerializeOptions = { utf8Sorting: true } const { serializeEncryptionContext, serializeEncryptedDataKey } = - serializeFactory(fromUtf8, { utf8Sorting: false }) + serializeFactory(fromUtf8, sorting) return { buildEncryptionMaterialCacheKey, @@ -80,9 +85,7 @@ export function buildCryptographicMaterialsCacheKeyHelpers< * However, the RAW Keyring wants _only_ the ADD. * So, I just slice off the length. */ - const serializedContext = serializeEncryptionContext(context, { - utf8Sorting: false, - }).slice(2) + const serializedContext = serializeEncryptionContext(context).slice(2) return sha512(serializedContext) } } diff --git a/modules/kms-keyring-node/src/kms_hkeyring_node.ts b/modules/kms-keyring-node/src/kms_hkeyring_node.ts index 601a8e1b..942b77bc 100644 --- a/modules/kms-keyring-node/src/kms_hkeyring_node.ts +++ b/modules/kms-keyring-node/src/kms_hkeyring_node.ts @@ -74,6 +74,7 @@ export interface KmsHierarchicalKeyRingNodeInput { //= type=implication //# - MAY provide a [Partition ID](#partition-id) partitionId?: string + utf8Sorting?: boolean | true } export interface IKmsHierarchicalKeyRingNode extends KeyringNode { @@ -104,6 +105,7 @@ export class KmsHierarchicalKeyRingNode public declare maxCacheSize?: number public declare _cmc: CryptographicMaterialsCache declare readonly _partition: Buffer + public declare utf8Sorting: boolean | true constructor({ branchKeyId, @@ -113,6 +115,7 @@ export class KmsHierarchicalKeyRingNode cache, maxCacheSize, partitionId, + utf8Sorting, }: KmsHierarchicalKeyRingNodeInput) { super() @@ -256,6 +259,17 @@ export class KmsHierarchicalKeyRingNode readOnlyProperty(this, 'maxCacheSize', maxCacheSize) readOnlyProperty(this, '_cmc', cache) + if (utf8Sorting) { + needs( + typeof utf8Sorting === 'boolean', + 'The branch key id must be a string' + ) + } else { + utf8Sorting = true + } + + readOnlyProperty(this, 'utf8Sorting', utf8Sorting) + Object.freeze(this) /* Postcondition: The HKR object must be frozen */ } @@ -299,7 +313,8 @@ export class KmsHierarchicalKeyRingNode const edk = wrapPlaintextDataKey( pdk, branchKeyMaterials, - encryptionMaterial + encryptionMaterial, + this.utf8Sorting ) // return the modified encryption material with the new edk and newly @@ -428,7 +443,8 @@ export class KmsHierarchicalKeyRingNode udk = unwrapEncryptedDataKey( ciphertext, branchKeyMaterials, - decryptionMaterial + decryptionMaterial, + this.utf8Sorting ) } catch (e) { //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt diff --git a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts index bb7fc48c..d142c934 100644 --- a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts +++ b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts @@ -33,11 +33,7 @@ import { PROVIDER_ID_HIERARCHY_AS_BYTES, } from './constants' import { BranchKeyIdSupplier } from '@aws-crypto/kms-keyring' -import { - serializeFactory, - SerializeOptions, - uuidv4Factory, -} from '@aws-crypto/serialize' +import { serializeFactory, uuidv4Factory } from '@aws-crypto/serialize' export const stringToUtf8Bytes = (input: string): Buffer => Buffer.from(input, 'utf-8') @@ -49,11 +45,6 @@ const hexBytesToString = (input: Uint8Array): string => Buffer.from(input).toString('hex') export const { uuidv4ToCompressedBytes, decompressBytesToUuidv4 } = uuidv4Factory(stringToHexBytes, hexBytesToString) -export const utf8Sorting: SerializeOptions = { utf8Sorting: false } -export const { serializeEncryptionContext } = serializeFactory( - stringToUtf8Bytes, - utf8Sorting -) export function getBranchKeyId( { branchKeyId, branchKeyIdSupplier }: IKmsHierarchicalKeyRingNode, @@ -297,7 +288,8 @@ export function getPlaintextDataKey(material: NodeEncryptionMaterial) { export function wrapPlaintextDataKey( pdk: Uint8Array, branchKeyMaterials: NodeBranchKeyMaterial, - { encryptionContext }: NodeEncryptionMaterial + { encryptionContext }: NodeEncryptionMaterial, + utf8Sorting: boolean ): Uint8Array { // get what we need from branch key material to wrap the pdk const branchKey = branchKeyMaterials.branchKey() @@ -326,7 +318,8 @@ export function wrapPlaintextDataKey( const wrappedAad = wrapAad( branchKeyIdAsBytes, branchKeyVersionAsBytesCompressed, - encryptionContext + encryptionContext, + utf8Sorting ) // encrypt the pdk into an edk @@ -368,10 +361,14 @@ export function wrapPlaintextDataKey( export function wrapAad( branchKeyIdAsBytes: Buffer, version: Buffer, - encryptionContext: EncryptionContext + encryptionContext: EncryptionContext, + utf8Sorting: boolean ) { /* Precondition: Branch key version must be 16 bytes */ needs(version.length === 16, 'Branch key version must be 16 bytes') + const { serializeEncryptionContext } = serializeFactory(stringToUtf8Bytes, { + utf8Sorting: utf8Sorting, + }) /* The AAD section is uInt16BE(length) + AAD * see: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html#header-aad @@ -379,7 +376,7 @@ export function wrapAad( * So, I just slice off the length. */ const aad = Buffer.from( - serializeEncryptionContext(encryptionContext, utf8Sorting).slice(2) + serializeEncryptionContext(encryptionContext).slice(2) ) return Buffer.concat([ @@ -535,7 +532,8 @@ export function destructureCiphertext( export function unwrapEncryptedDataKey( ciphertext: Uint8Array, branchKeyMaterials: NodeBranchKeyMaterial, - { encryptionContext, suite }: NodeDecryptionMaterial + { encryptionContext, suite }: NodeDecryptionMaterial, + utf8Sorting: boolean ) { // get what we need from the branch key materials to unwrap the edk const branchKey = branchKeyMaterials.branchKey() @@ -564,7 +562,8 @@ export function unwrapEncryptedDataKey( const wrappedAad = wrapAad( branchKeyIdAsBytes, branchKeyVersionAsBytesCompressed, - encryptionContext + encryptionContext, + utf8Sorting ) // decipher the edk to get the udk/pdk diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts index 1e59502b..243182d1 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts @@ -5,8 +5,6 @@ import { randomBytes } from 'crypto' import { wrapAad, destructureCiphertext, - serializeEncryptionContext, - utf8Sorting, unwrapEncryptedDataKey, wrapPlaintextDataKey, } from '../src/kms_hkeyring_node_helpers' @@ -18,6 +16,7 @@ import { NodeDecryptionMaterial, NodeEncryptionMaterial, } from '@aws-crypto/material-management' +import { serializeFactory, SerializeOptions } from '@aws-crypto/serialize' import { v4 } from 'uuid' describe('KmsHierarchicalKeyRingNode: helpers', () => { @@ -110,11 +109,23 @@ describe('KmsHierarchicalKeyRingNode: helpers', () => { const encryptionContext = { key: 'value', } + const utf8Sorting: SerializeOptions = { utf8Sorting: false } + const stringToUtf8Bytes = (input: string): Buffer => + Buffer.from(input, 'utf-8') + const { serializeEncryptionContext } = serializeFactory( + stringToUtf8Bytes, + utf8Sorting + ) it('Precondition: Branch key version must be 16 bytes ', () => { const badVersion = randomBytes(15) expect(() => - wrapAad(branchKeyIdAsBytes, badVersion, encryptionContext) + wrapAad( + branchKeyIdAsBytes, + badVersion, + encryptionContext, + utf8Sorting.utf8Sorting + ) ).to.throw('Branch key version must be 16 bytes') }) @@ -129,7 +140,8 @@ describe('KmsHierarchicalKeyRingNode: helpers', () => { wrapAad( branchKeyIdAsBytes, branchKeyVersionAsBytes, - unserializeableEc as any + unserializeableEc as any, + utf8Sorting.utf8Sorting ) ).to.throw() }) @@ -138,7 +150,8 @@ describe('KmsHierarchicalKeyRingNode: helpers', () => { const wrappedAad = wrapAad( branchKeyIdAsBytes, branchKeyVersionAsBytes, - encryptionContext + encryptionContext, + utf8Sorting.utf8Sorting ) let startIdx = 0 @@ -157,10 +170,7 @@ describe('KmsHierarchicalKeyRingNode: helpers', () => { ).to.deep.equal(branchKeyVersionAsBytes) startIdx += branchKeyVersionAsBytes.length - const expectedAad = serializeEncryptionContext( - encryptionContext, - utf8Sorting - ).slice(2) + const expectedAad = serializeEncryptionContext(encryptionContext).slice(2) expect(wrappedAad.subarray(startIdx)).to.deep.equal(expectedAad) }) }) @@ -204,15 +214,18 @@ describe('KmsHierarchicalKeyRingNode: helpers', () => { algSuite, encryptionContext ) + const utf8Sorting: SerializeOptions = { utf8Sorting: false } const actualPdk = unwrapEncryptedDataKey( wrapPlaintextDataKey( expectedPdk, branchKeyMaterial, - encryptionMaterial + encryptionMaterial, + utf8Sorting.utf8Sorting ), branchKeyMaterial, - decryptionMaterial + decryptionMaterial, + utf8Sorting.utf8Sorting ) expect(actualPdk).to.deep.equal(expectedPdk) } @@ -259,12 +272,14 @@ describe('KmsHierarchicalKeyRingNode: helpers', () => { algSuite, encryptionContext ) + const utf8Sorting: SerializeOptions = { utf8Sorting: false } expect(() => unwrapEncryptedDataKey( ciphertext, branchKeyMaterial, - decryptionMaterial + decryptionMaterial, + utf8Sorting.utf8Sorting ) ).to.throw('Unsupported state or unable to authenticate data') } diff --git a/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts b/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts index 28398d78..e4c093ee 100644 --- a/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts +++ b/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts @@ -56,6 +56,7 @@ export type RawAesKeyringWebCryptoInput = { keyName: string masterKey: AwsEsdkJsCryptoKey wrappingSuite: WrappingSuiteIdentifier + utf8Sorting?: boolean | true } export class RawAesKeyringWebCrypto extends KeyringWebCrypto { @@ -66,7 +67,8 @@ export class RawAesKeyringWebCrypto extends KeyringWebCrypto { constructor(input: RawAesKeyringWebCryptoInput) { super() - const { keyName, keyNamespace, masterKey, wrappingSuite } = input + const { keyName, keyNamespace, masterKey, wrappingSuite, utf8Sorting } = + input /* Precondition: AesKeyringWebCrypto needs identifying information for encrypt and decrypt. */ needs(keyName && keyNamespace, 'Identifying information must be defined.') /* Precondition: RawAesKeyringWebCrypto requires a wrappingSuite to be a valid RawAesWrappingSuite. */ @@ -81,7 +83,8 @@ export class RawAesKeyringWebCrypto extends KeyringWebCrypto { flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, }) - const serializeOptions: SerializeOptions = { utf8Sorting: false } + const maybeUtf8Sorting = utf8Sorting ?? true + const serializeOptions: SerializeOptions = { utf8Sorting: maybeUtf8Sorting } const { serializeEncryptionContext } = serializeFactory( fromUtf8, serializeOptions @@ -92,10 +95,9 @@ export class RawAesKeyringWebCrypto extends KeyringWebCrypto { * However, the RAW Keyring wants _only_ the ADD. * So, I just slice off the length. */ - const aad = serializeEncryptionContext( - material.encryptionContext, - serializeOptions - ).slice(2) + const aad = serializeEncryptionContext(material.encryptionContext).slice( + 2 + ) const { keyNamespace, keyName } = this return aesGcmWrapKey( @@ -116,10 +118,9 @@ export class RawAesKeyringWebCrypto extends KeyringWebCrypto { * However, the RAW Keyring wants _only_ the ADD. * So, I just slice off the length. */ - const aad = serializeEncryptionContext( - material.encryptionContext, - serializeOptions - ).slice(2) + const aad = serializeEncryptionContext(material.encryptionContext).slice( + 2 + ) const { keyNamespace, keyName } = this return aesGcmUnwrapKey( diff --git a/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts b/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts index c1385859..9f17bc10 100644 --- a/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts +++ b/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts @@ -32,9 +32,6 @@ import { const fromUtf8 = (input: string) => Buffer.from(input, 'utf8') const toUtf8 = (input: Uint8Array) => Buffer.from(input.buffer, input.byteOffset, input.byteLength).toString('utf8') -const { serializeEncryptionContext } = serializeFactory(fromUtf8, { - utf8Sorting: false, -}) const { rawAesEncryptedDataKey } = rawAesEncryptedDataKeyFactory( toUtf8, fromUtf8 @@ -46,6 +43,7 @@ export type RawAesKeyringNodeInput = { keyName: string unencryptedMasterKey: Uint8Array wrappingSuite: WrappingSuiteIdentifier + utf8Sorting?: boolean | true } export class RawAesKeyringNode extends KeyringNode { @@ -57,7 +55,13 @@ export class RawAesKeyringNode extends KeyringNode { constructor(input: RawAesKeyringNodeInput) { super() - const { keyName, keyNamespace, unencryptedMasterKey, wrappingSuite } = input + const { + keyName, + keyNamespace, + unencryptedMasterKey, + wrappingSuite, + utf8Sorting, + } = input /* Precondition: AesKeyringNode needs identifying information for encrypt and decrypt. */ needs(keyName && keyNamespace, 'Identifying information must be defined.') /* Precondition: RawAesKeyringNode requires wrappingSuite to be a valid RawAesWrappingSuite. */ @@ -72,8 +76,13 @@ export class RawAesKeyringNode extends KeyringNode { flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, }) - // default will be false for the first release and then flipped to true - const serializeOptions: SerializeOptions = { utf8Sorting: false } + const maybeUtf8Sorting = utf8Sorting ?? true + // default will be true + const serializeOptions: SerializeOptions = { utf8Sorting: maybeUtf8Sorting } + const { serializeEncryptionContext } = serializeFactory( + fromUtf8, + serializeOptions + ) const _wrapKey = async (material: NodeEncryptionMaterial) => { /* The AAD section is uInt16BE(length) + AAD * see: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html#header-aad @@ -81,8 +90,7 @@ export class RawAesKeyringNode extends KeyringNode { * So, I just slice off the length. */ const { buffer, byteOffset, byteLength } = serializeEncryptionContext( - material.encryptionContext, - serializeOptions + material.encryptionContext ).slice(2) const aad = Buffer.from(buffer, byteOffset, byteLength) const { keyNamespace, keyName } = this @@ -107,8 +115,7 @@ export class RawAesKeyringNode extends KeyringNode { * So, I just slice off the length. */ const { buffer, byteOffset, byteLength } = serializeEncryptionContext( - material.encryptionContext, - serializeOptions + material.encryptionContext ).slice(2) const aad = Buffer.from(buffer, byteOffset, byteLength) // const aad = Buffer.concat(encodeEncryptionContext(context || {})) diff --git a/modules/raw-aes-keyring-node/test/raw_aes_keyring_node.test.ts b/modules/raw-aes-keyring-node/test/raw_aes_keyring_node.test.ts index a76de9a5..9f6dcf50 100644 --- a/modules/raw-aes-keyring-node/test/raw_aes_keyring_node.test.ts +++ b/modules/raw-aes-keyring-node/test/raw_aes_keyring_node.test.ts @@ -167,3 +167,67 @@ describe('RawAesKeyringNode encrypt/decrypt', () => { expect(test.hasValidKey()).to.equal(true) }) }) + +describe('RawAesKeyringNode High utf8 code points inn encryption context', () => { + const wrappingSuite = + RawAesWrappingSuiteIdentifier.AES128_GCM_IV12_TAG16_NO_PADDING + const unencryptedMasterKey = new Uint8Array(128 / 8) + const keyNamespace = 'keyNamespace' + const keyName = 'keyName' + const noUtf8SortingKeyring = new RawAesKeyringNode({ + keyName, + keyNamespace, + unencryptedMasterKey, + wrappingSuite, + utf8Sorting: false, + }) + // the default is to utf8 sort + const utf8SortingKeyring = new RawAesKeyringNode({ + keyName, + keyNamespace, + unencryptedMasterKey, + wrappingSuite, + }) + + const encryptionContext = { + '𝄢': 'a', + a: 'a', + } + let encryptedDataKey: EncryptedDataKey + + it('when set to true we can decrypt, false otherwise', async () => { + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256 + ) + const material = new NodeEncryptionMaterial(suite, encryptionContext) + const test = await utf8SortingKeyring.onEncrypt(material) + expect(test.hasValidKey()).to.equal(true) + const udk = unwrapDataKey(test.getUnencryptedDataKey()) + expect(udk).to.have.lengthOf(suite.keyLengthBytes) + expect(test.encryptedDataKeys).to.have.lengthOf(1) + const [edk] = test.encryptedDataKeys + expect(edk.providerId).to.equal(keyNamespace) + encryptedDataKey = edk + }) + it('can decrypt an EncryptedDataKey', async () => { + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256 + ) + const material = new NodeDecryptionMaterial(suite, encryptionContext) + const test = await utf8SortingKeyring.onDecrypt(material, [ + encryptedDataKey, + ]) + expect(test.hasValidKey()).to.equal(true) + }) + + it('cannot decrypt an EncryptedDataKey with a non utf8 sorting Keyring', async () => { + const suite = new NodeAlgorithmSuite( + AlgorithmSuiteIdentifier.ALG_AES128_GCM_IV12_TAG16_HKDF_SHA256_ECDSA_P256 + ) + const material = new NodeDecryptionMaterial(suite, encryptionContext) + const test = await noUtf8SortingKeyring.onDecrypt(material, [ + encryptedDataKey, + ]) + expect(test.hasValidKey()).to.equal(false) + }) +}) diff --git a/modules/serialize/src/serialize_factory.ts b/modules/serialize/src/serialize_factory.ts index 67fd2dfd..ff4907a1 100644 --- a/modules/serialize/src/serialize_factory.ts +++ b/modules/serialize/src/serialize_factory.ts @@ -35,7 +35,7 @@ import { export function serializeFactory( fromUtf8: (input: any) => Uint8Array, - utf8Sorting: SerializeOptions + sorting: SerializeOptions ) { return { frameIv, @@ -94,10 +94,10 @@ export function serializeFactory( } function encodeEncryptionContext( - encryptionContext: EncryptionContext, - serializeOptions: SerializeOptions + encryptionContext: EncryptionContext ): Uint8Array[] { - const { utf8Sorting } = serializeOptions + // use closure value from the serializeFactory + const { utf8Sorting } = sorting if (utf8Sorting) { return Object.entries(encryptionContext) .map((entries) => entries.map(fromUtf8)) @@ -147,14 +147,8 @@ export function serializeFactory( return 0 } - function serializeEncryptionContext( - encryptionContext: EncryptionContext, - serializeOptions: SerializeOptions - ) { - const encryptionContextElements = encodeEncryptionContext( - encryptionContext, - serializeOptions - ) + function serializeEncryptionContext(encryptionContext: EncryptionContext) { + const encryptionContextElements = encodeEncryptionContext(encryptionContext) /* Check for early return (Postcondition): If there is no context then the length of the _whole_ serialized portion is 0. * This is part of the specification of the AWS Encryption SDK Message Format. @@ -212,7 +206,7 @@ export function serializeFactory( uInt8(messageHeader.type), uInt16BE(messageHeader.suiteId), messageHeader.messageId, - serializeEncryptionContext(messageHeader.encryptionContext, utf8Sorting), + serializeEncryptionContext(messageHeader.encryptionContext), serializeEncryptedDataKeys(messageHeader.encryptedDataKeys), new Uint8Array([messageHeader.contentType]), new Uint8Array([0, 0, 0, 0]), @@ -226,7 +220,7 @@ export function serializeFactory( uInt8(messageHeader.version), uInt16BE(messageHeader.suiteId), messageHeader.messageId, - serializeEncryptionContext(messageHeader.encryptionContext, utf8Sorting), + serializeEncryptionContext(messageHeader.encryptionContext), serializeEncryptedDataKeys(messageHeader.encryptedDataKeys), new Uint8Array([messageHeader.contentType]), uInt32BE(messageHeader.frameLength), diff --git a/modules/serialize/test/fixtures.ts b/modules/serialize/test/fixtures.ts index 38280def..5e7962ea 100644 --- a/modules/serialize/test/fixtures.ts +++ b/modules/serialize/test/fixtures.ts @@ -98,6 +98,16 @@ export function basicEncryptionContext() { return new Uint8Array([0,43,0,2,0,11,105,110,102,111,114,109,97,116,105,111,110,0,12,194,189,32,43,32,194,188,32,61,32,194,190,0,4,115,111,109,101,0,6,112,117,98,108,105,99]); } +export function encryptionContextWithHighUtf8CodePoint() { + // prettier-ignore + return new Uint8Array([0,25,0,2,0,4,115,111,109,101,0,6,112,117,98,108,105,99,0,4,240,157,132,162,0,1,97]); +} + +export function encryptionContextWithHighUtf8CodePointWithReservedKeyword() { + // prettier-ignore + return new Uint8Array([0,42,0,2,0,21,97,119,115,45,99,114,121,112,116,111,45,112,117,98,108,105,99,45,107,101,121,0,6,112,117,98,108,105,99,0,4,240,157,132,162,0,1,97]); +} + export function missingDataEncryptionContext() { // prettier-ignore return new Uint8Array([0,43,0,2,0,11,105,110,102,111,114,109,97,116,105,111,110,0,12,194,189,32,43,32,194,188,32,61,32,194,190,0,4,115,111,109,101,0,6,112,117,98,108]); diff --git a/modules/serialize/test/serialize_factory.test.ts b/modules/serialize/test/serialize_factory.test.ts index 78e6f664..ab6228e8 100644 --- a/modules/serialize/test/serialize_factory.test.ts +++ b/modules/serialize/test/serialize_factory.test.ts @@ -105,13 +105,10 @@ describe('serializeFactory:encodeEncryptionContext', () => { const { encodeEncryptionContext } = serializeFactory(fromUtf8, { utf8Sorting: false, }) - const test = encodeEncryptionContext( - { - information: '\u00bd + \u00bc = \u00be', - some: 'public', - }, - { utf8Sorting: false } - ) + const test = encodeEncryptionContext({ + information: '\u00bd + \u00bc = \u00be', + some: 'public', + }) expect(test).to.be.instanceof(Array) expect(test.length).to.eql(2) expect(test[0]).to.be.instanceof(Uint8Array) @@ -134,13 +131,10 @@ describe('serializeFactory:encodeEncryptionContext', () => { const { encodeEncryptionContext } = serializeFactory(fromUtf8, { utf8Sorting: false, }) - const test = encodeEncryptionContext( - { - some: 'public', - information: '\u00bd + \u00bc = \u00be', - }, - { utf8Sorting: false } - ) + const test = encodeEncryptionContext({ + some: 'public', + information: '\u00bd + \u00bc = \u00be', + }) expect(test[0]).to.deep.equal( new Uint8Array([ 0, 11, 105, 110, 102, 111, 114, 109, 97, 116, 105, 111, 110, 0, 12, 194, @@ -161,25 +155,119 @@ describe('serializeFactory:serializeEncryptionContext', () => { const { serializeEncryptionContext } = serializeFactory(fromUtf8, { utf8Sorting: false, }) - const test = serializeEncryptionContext( - { - some: 'public', - information: '\u00bd + \u00bc = \u00be', - }, - { utf8Sorting: false } - ) + const test = serializeEncryptionContext({ + some: 'public', + information: '\u00bd + \u00bc = \u00be', + }) expect(test).to.be.instanceof(Uint8Array) expect(test.byteLength).to.eql(45) expect(test).to.deep.equal(fixtures.basicEncryptionContext()) }) + describe('serializeFactory:serializeEncryptionContext utf8Sorting', () => { + it('should return rational context bytes', () => { + const fromUtf8 = (input: string) => Buffer.from(input) + const { serializeEncryptionContext: serializeWithUtf8Sorting } = + serializeFactory(fromUtf8, { + utf8Sorting: true, + }) + const { serializeEncryptionContext: serializeWithoutUtf8Sorting } = + serializeFactory(fromUtf8, { + utf8Sorting: false, + }) + // same test as serializeFactory:serializeEncryptionContext to show that with simple ec + // we still have the same serialized encryption context. + const test = serializeWithUtf8Sorting({ + some: 'public', + information: '\u00bd + \u00bc = \u00be', + }) + const test2 = serializeWithoutUtf8Sorting({ + some: 'public', + information: '\u00bd + \u00bc = \u00be', + }) + + expect(test).to.be.instanceof(Uint8Array) + expect(test.byteLength).to.eql(45) + expect(test).to.deep.equal(fixtures.basicEncryptionContext()) + expect(test).to.deep.equal(test2) + }) + }) + + it('test utf8 serialization with high utf8 codepoints', () => { + const fromUtf8 = (input: string) => Buffer.from(input) + const { serializeEncryptionContext: serializeWithUtf8Sorting } = + serializeFactory(fromUtf8, { + utf8Sorting: true, + }) + const { serializeEncryptionContext: serializeWithoutUtf8Sorting } = + serializeFactory(fromUtf8, { + utf8Sorting: false, + }) + // same test as serializeFactory:serializeEncryptionContext to show that with simple ec + // we still have the same serialized encryption context. + const test = serializeWithUtf8Sorting({ + some: 'public', + '𝄢': 'a', + }) + const test2 = serializeWithoutUtf8Sorting({ + some: 'public', + '𝄢': 'a', + }) + + expect(test).to.be.instanceof(Uint8Array) + expect(test.byteLength).to.eql(27) + expect(test).to.deep.equal( + fixtures.encryptionContextWithHighUtf8CodePoint() + ) + + expect(test2).to.be.instanceof(Uint8Array) + expect(test2.byteLength).to.eql(27) + + expect(test2).to.not.deep.equal( + fixtures.encryptionContextWithHighUtf8CodePoint() + ) + expect(test).to.not.deep.equal(test2) + }) + + it('test utf8 serialization with high utf8 codepoints and reserved ec keyword', () => { + const fromUtf8 = (input: string) => Buffer.from(input) + const { serializeEncryptionContext: serializeWithUtf8Sorting } = + serializeFactory(fromUtf8, { + utf8Sorting: true, + }) + const { serializeEncryptionContext: serializeWithoutUtf8Sorting } = + serializeFactory(fromUtf8, { + utf8Sorting: false, + }) + // same test as serializeFactory:serializeEncryptionContext to show that with simple ec + // we still have the same serialized encryption context. + const utf8SortedTest = serializeWithUtf8Sorting({ + 'aws-crypto-public-key': 'public', + '𝄢': 'a', + }) + const stringSortedTest = serializeWithoutUtf8Sorting({ + 'aws-crypto-public-key': 'public', + '𝄢': 'a', + }) + + expect(utf8SortedTest).to.be.instanceof(Uint8Array) + expect(utf8SortedTest.byteLength).to.eql(44) + expect(utf8SortedTest).to.deep.equal( + fixtures.encryptionContextWithHighUtf8CodePointWithReservedKeyword() + ) + + expect(stringSortedTest).to.be.instanceof(Uint8Array) + expect(stringSortedTest.byteLength).to.eql(44) + expect(utf8SortedTest).to.not.deep.equal(stringSortedTest) + }) + it('Check for early return (Postcondition): If there is no context then the length of the _whole_ serialized portion is 0.', () => { const fromUtf8 = (input: string) => Buffer.from(input) const { serializeEncryptionContext } = serializeFactory(fromUtf8, { utf8Sorting: false, }) - const test = serializeEncryptionContext({}, { utf8Sorting: false }) + const test = serializeEncryptionContext({}) expect(test).to.be.instanceof(Uint8Array) expect(test.byteLength).to.eql(2) From 9c8c8d9774d058996b5d52ef3233f21e9fb104c4 Mon Sep 17 00:00:00 2001 From: Jose Corella Date: Tue, 18 Mar 2025 15:33:16 -0700 Subject: [PATCH 06/13] remove option from client and more tests --- modules/decrypt-node/src/decrypt_client.ts | 2 - modules/decrypt-node/test/decrypt.test.ts | 47 +++++++++++++++++++ modules/decrypt-node/test/fixtures.ts | 37 ++++++++++++++- modules/encrypt-browser/src/encrypt.ts | 9 +--- modules/encrypt-browser/src/encrypt_client.ts | 2 - modules/encrypt-node/src/encrypt_client.ts | 2 - modules/encrypt-node/src/encrypt_stream.ts | 13 ++--- .../encrypt-node/src/framed_encrypt_stream.ts | 18 ++----- .../test/framed_encrypt_stream.test.ts | 13 ++--- .../integration-node/src/integration_tests.ts | 7 +-- .../kms-keyring-node/src/kms_hkeyring_node.ts | 18 +++---- .../kms_hkeyring_node.constructor.test.ts | 32 +++++++++++++ modules/material-management/src/types.ts | 1 - .../src/raw_aes_keyring_node.ts | 13 +++-- .../test/raw_aes_keyring_node.test.ts | 35 ++++++++++++++ wallaby.conf.js | 3 ++ 16 files changed, 187 insertions(+), 65 deletions(-) diff --git a/modules/decrypt-node/src/decrypt_client.ts b/modules/decrypt-node/src/decrypt_client.ts index 571e477f..14889990 100644 --- a/modules/decrypt-node/src/decrypt_client.ts +++ b/modules/decrypt-node/src/decrypt_client.ts @@ -31,7 +31,6 @@ export function buildDecrypt( const { commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, maxEncryptedDataKeys = false, - utf8Sorting = false, } = typeof options === 'string' ? { commitmentPolicy: options } : options /* Precondition: node buildDecrypt needs a valid commitmentPolicy. */ @@ -45,7 +44,6 @@ export function buildDecrypt( const clientOptions: ClientOptions = { commitmentPolicy, maxEncryptedDataKeys, - utf8Sorting, } return { decryptUnsignedMessageStream: _decryptStream.bind( diff --git a/modules/decrypt-node/test/decrypt.test.ts b/modules/decrypt-node/test/decrypt.test.ts index 1aa07f17..d24f1cee 100644 --- a/modules/decrypt-node/test/decrypt.test.ts +++ b/modules/decrypt-node/test/decrypt.test.ts @@ -214,6 +214,53 @@ describe('decrypt', () => { }) ).to.rejectedWith(Error, 'maxEncryptedDataKeys exceeded.') }) + + it('will fail to decrypt ciphertext with high utf8 codepoints and no utf8 sorting on keyring', async () => { + const { decrypt } = buildDecrypt({ + commitmentPolicy: CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT, + maxEncryptedDataKeys: 3, + utf8Sorting: false, + }) + const hkeyring = fixtures.hKeyring(false) + const ciphertext = fixtures.hierarchyMessageWithHighUtf8CodePoints() + + await expect( + decrypt(hkeyring, ciphertext, { + encoding: 'base64', + }) + ).to.rejectedWith(Error, 'Unsupported state or unable to authenticate data') + }) + + it('will decrypt ciphertext with high utf8 codepoints with a keyring configured to do utf8 sorting', async () => { + const { decrypt } = buildDecrypt({ + commitmentPolicy: CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT, + maxEncryptedDataKeys: 3, + utf8Sorting: false, + }) + const hkeyring = fixtures.hKeyring(true) + const ciphertext = fixtures.hierarchyMessageWithHighUtf8CodePoints() + const { plaintext } = await decrypt(hkeyring, ciphertext, { + encoding: 'base64', + }) + expect(plaintext).to.deep.equal(Buffer.from('Hello World')) + }) + + it('will decrypt ciphertext with high utf8 codepoints with a multikeyring with both utf8 and no utf8 sorting', async () => { + const { decrypt } = buildDecrypt({ + commitmentPolicy: CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT, + maxEncryptedDataKeys: 3, + utf8Sorting: false, + }) + const hkeyringUtf8 = fixtures.hKeyring(true) + const hkeyringNoUtf8 = fixtures.hKeyring(false) + const multikeyring = fixtures.multiKeyring(hkeyringUtf8, hkeyringNoUtf8) + + const ciphertext = fixtures.hierarchyMessageWithHighUtf8CodePoints() + const { plaintext } = await decrypt(multikeyring, ciphertext, { + encoding: 'base64', + }) + expect(plaintext).to.deep.equal(Buffer.from('Hello World')) + }) }) function chunkCipherTextStream(ciphertext: Buffer, { size }: { size: number }) { diff --git a/modules/decrypt-node/test/fixtures.ts b/modules/decrypt-node/test/fixtures.ts index 9b571283..aa20d424 100644 --- a/modules/decrypt-node/test/fixtures.ts +++ b/modules/decrypt-node/test/fixtures.ts @@ -8,8 +8,10 @@ import { NodeEncryptionMaterial, KeyringNode, KeyringTraceFlag, + MultiKeyringNode, } from '@aws-crypto/material-management-node' - +import { KmsHierarchicalKeyRingNode } from '@aws-crypto/kms-keyring-node' +import { BranchKeyStoreNode } from '@aws-crypto/branch-keystore-node' export function base64CiphertextAlgAes256GcmIv12Tag16HkdfSha384EcdsaP384() { return 'AYADeJgnuW8vpQmi5QoqHIZWhjkAcAACABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREFuWXRGRWV3Wm0rMjhLaElHcHg4UmhrYVVhTGNjSnB5ZjFud0lWUUZHbXlwZ3poSDJYZFNJQko0c0tpU0gzY2t6dz09AAZzaW1wbGUAB2NvbnRleHQAAQABawABawADAAAAAgAAAAAMAAAABQAAAAAAAAAAAAAAABqRZqpijpYGNM6P1L/78AUAAAABAAAAAAAAAAAAAAABIg1k1IeKV+CPUVBnpUkgyVUUZl7wAAAAAgAAAAAAAAAAAAAAAjl6P288VtjjKYeZA7mSeeJgjIUHbAAAAAMAAAAAAAAAAAAAAAO7OY+25yJkVcFvMMXn7VztyOhuIQoAAAAEAAAAAAAAAAAAAAAEG6jOHAz3NwyxgUjm5XFNMBx+2CCvAAAABQAAAAAAAAAAAAAABYRtGxVPUKbha73ay/kYrpl8Drik2gAAAAYAAAAAAAAAAAAAAAbosyHzP31p9EdOf3+dSa5gGfRW9e0AAAAHAAAAAAAAAAAAAAAHsulmBR4FQMbTk+00j5Fa/jD73/UJAAAACAAAAAAAAAAAAAAACMKgPZWTdDKzdPhXQDenInSRW/eOLgAAAAkAAAAAAAAAAAAAAAkdfSyNpBYk9XbFhf6DUnr2acw5lC4AAAAKAAAAAAAAAAAAAAAKnJpofr1UwwPy/+aqviMTrHXgOhM8AAAACwAAAAAAAAAAAAAAC9lvtW1lzA9RGUjnIGadlEhLxRC/FAAAAAwAAAAAAAAAAAAAAAyqJBaQEdmkOUX7uCki3Gh17YlQU3MAAAANAAAAAAAAAAAAAAANEK36ZE9VLiIj2X50N73UHEUtm0BbAAAADgAAAAAAAAAAAAAADkkr1fxL3qLbbC7OSDHqDnrBonOwxQAAAA8AAAAAAAAAAAAAAA8qcNFG+ofU3sOEZd8OXB/rkz0vDa8AAAAQAAAAAAAAAAAAAAAQ3KdsWJ/P8hF8aOhQdQP3v1KBDpB5AAAAEQAAAAAAAAAAAAAAEWyQGXefoGv9ZDfXUi93q+wUQGPzVwAAABIAAAAAAAAAAAAAABIDL/v5IY/z+s28FWzVo46vKNjOEeoAAAATAAAAAAAAAAAAAAATy1uc+McQfMJD8GrAJUaKlyTbXgFgAAAAFAAAAAAAAAAAAAAAFB6Sh2Po4oetBUwm1ABP9F9e1T70GAAAABUAAAAAAAAAAAAAABWm2oOg6agE6jzm3iDZ1brMSTHCOG8AAAAWAAAAAAAAAAAAAAAWsdIbfir5Dame3Uxkri54N2P7rqn6AAAAFwAAAAAAAAAAAAAAF6iPI1YW4fZzyL/355ZHBOLG3VPf1AAAABgAAAAAAAAAAAAAABj5Kjd5Twiu6bpb4o+jas0LRRJFH64AAAAZAAAAAAAAAAAAAAAZTf4xiUOtHeZmi+80M3Oay452R/rJAAAAGgAAAAAAAAAAAAAAGp+ET0LYxOX4JEL8gJudVVPW6qIv3AAAABsAAAAAAAAAAAAAABuTreBPGwJ2bftxQ6Kjwekfth4vWtsAAAAcAAAAAAAAAAAAAAAcdLoFVjR+yx4NVo1BSxv8Llya90EFAAAAHQAAAAAAAAAAAAAAHcFqEIL2wsYK36KQHyJqvJTiF/6nlQAAAB4AAAAAAAAAAAAAAB57QTT/UVRxBucxfhQRYeEU0mUeFxcAAAAfAAAAAAAAAAAAAAAfJyKwIcAURvMfN/Gd5MchygA20EYHAAAAIAAAAAAAAAAAAAAAILXRfQjIux8TeED/TdHHdLuaUEWWZgAAACEAAAAAAAAAAAAAACEi1SsfUozCXF0mCT/tHN8zVvSyWF4AAAAiAAAAAAAAAAAAAAAiFPt44yxRbwruA1F5YkYNokeDLmdiAAAAIwAAAAAAAAAAAAAAIwqdX86PI6IZgTs2SMHo4tLExClkIgAAACQAAAAAAAAAAAAAACQJGEuD6oBPBXU8iupaaNJFzEH/zKcAAAAlAAAAAAAAAAAAAAAlyQiA+1xRREA/qe5Djux6WaPEyUzhAAAAJgAAAAAAAAAAAAAAJqsZT21o1ikdiLkExG949WuTdw1mQQAAACcAAAAAAAAAAAAAACekCgcIX2x9/3zx982dDXfKUQSqARQAAAAoAAAAAAAAAAAAAAAocSNt9kEXLUF0Mydaj4MiBo1WrmGGAAAAKQAAAAAAAAAAAAAAKRHbcJJmpG367RxDInqlcBefk34RbgAAACoAAAAAAAAAAAAAACqmDdWYD/QVD9isxpCTm4KE+j6HKdMAAAArAAAAAAAAAAAAAAAreua98WTPIWH6dSAdzfYWPM9q9hoGAAAALAAAAAAAAAAAAAAALA+DQHkvoxKqVP3dmTQoM17QR4hz1gAAAC0AAAAAAAAAAAAAAC3TCjJBU0hDgBiC/bAHZe5T9CoMfTQAAAAuAAAAAAAAAAAAAAAujkLmjR2G1at5H5QHzKg/B2zNIH+mAAAALwAAAAAAAAAAAAAAL6+0F5aK0j3xqvgrsjmkzt7rZYUQQAAAADAAAAAAAAAAAAAAADDZMoeMElExOKgTTa0/gKqBPiRAqF4AAAAxAAAAAAAAAAAAAAAxbk1Qj+CqjC+gruT6bljBsQD5YTBVAAAAMgAAAAAAAAAAAAAAMhjQQjFR5A9Kn5ot/h4nqKrDTZJsNgAAADMAAAAAAAAAAAAAADO2SB3R/RrukhQx7/jxmjWiLknnnj0AAAA0AAAAAAAAAAAAAAA0wXykERn6CEIMhDCuLhUBmVn6fCu7AAAANQAAAAAAAAAAAAAANf7M3//4JJPLi+mmkKec2QrmuprdigAAADYAAAAAAAAAAAAAADadAVLY8PSrHytIi05tgse0HdyYVikAAAA3AAAAAAAAAAAAAAA3dj606o4y/YZw7gGHrD6JrGWQULV2AAAAOAAAAAAAAAAAAAAAOPgZF/TYVQogBfVMR6P4q5YWnSozUwAAADkAAAAAAAAAAAAAADl41/2WlW/Aq+EVJSHVH8eolMg7stIAAAA6AAAAAAAAAAAAAAA6IdfaZedkARnjm0CYxQhB28ljrigJAAAAOwAAAAAAAAAAAAAAO5PRn7sBV99dQJosnpj8Dy61bUW//QAAADwAAAAAAAAAAAAAADwkmUiXJJBJ4KvATXEeY1b2cOVPDOr/////AAAAPQAAAAAAAAAAAAAAPQAAAABAZDjPrFjtf/NJrKKMK2W9AGgwZgIxAN4h4KUn2VHZhxd/PQlZSmawzL1txgo79vsZjVhV15xqyMZLLcpNuNmK3hNHA83v+AIxAP0Sga/B1gZuyGmQK2cSnDdRIL6bmAzzeTiMcjRoJ6KrYRbLwg8mzmdQLgdvSoPtFg==' } @@ -74,6 +76,10 @@ export function fourEdksMessage() { return 'AYAAFO/VP38MeBJoKQshvIaF8TgAAAAEAAR0ZW1wABhrZXkzAAAAgAAAAAzIotUMc7SaFTbzk0EAIKgxmVzmYedtQj5od6KglliFAx7G3OBYHMgyvbGQVJSyAAR0ZW1wABhrZXkwAAAAgAAAAAwBe+86+eb8+uYOeoIAIFmT8yZvThnsJigzsRen9OJc0kuGE+rJyalk+yF5VdNBAAR0ZW1wABhrZXkyAAAAgAAAAAy939QOrzUF3XKc0m8AICSGMg1tdgULYD15Jr7RWkFgqCXjtwyUK86xqrU+OzV9AAR0ZW1wABhrZXkxAAAAgAAAAAxE6lJVWjxWLtvnkBYAIJUl4vhbLEjNS/3g3of4T/QvAR7TGPJZgv7cLqOP0T7uAgAAAAAMAAAQAAAAAAAAAAAAAAAAAOMcqPpQVjBzbYAHIPjMM1T/////AAAAAQAAAAAAAAAAAAAAAQAAAAQPcr1WkUGY1IDMmCgdibk0zwg4Yg==' } +export function hierarchyMessageWithHighUtf8CodePoints() { + return 'AgV4PI3AdgfggyWInxz6XkfmQMbOd7/RgCN9HTTg7KeiczgAaQACABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREE2MTQvUGlNNTkyNkZwN3NWeUltVjhNZVZ4eEJNeFRpajI5Y3ozeFVZaFIwT29wbHJjT0sxNWFrR25BcENiOGkxZz09AALCtgAE8JOJqQABABFhd3Mta21zLWhpZXJhcmNoeQAkNmUxZjVlZGMtOWE3MC00YThlLWFkZmItMzdhZWVmZGVhOWU2AFx9XILjG4l3+qJ1BzpOHfZ5mf0eHmX4r3+q16U1LRGGFoVxS2mPKAqwWMUSwe2tNCe3G6kANAmRi4pyD2FK0VpuHx5FbpNgP3BR+U3cHYXiSg1vMMYEMl3usnss1QIAABAA1/mKQ+4BMR0aajtum3RQQxKfvFpi5DeCTVt0V8x7ibGHP7FCZzQWujM7M/rcfkeo/////wAAAAEAAAAAAAAAAAAAAAEAAAALxddd43JEIrllMceZonlyQGtPTX1zTsf+vwChAGcwZQIxALKuLqVHZNuvXhyjjRzs8ysgtJqvcVgvIgX1ExBKHueZLP7XsZOSUG/4SqxMFXGzuQIwOf1zLim/I65beZF1p1az1gyD+UzpWIa/vg9y0wjsYPDUEb9sUyUVU3etQ+y0LwI2' +} + export function decryptKeyring(): KeyringNode { class TestKeyring extends KeyringNode { async _onEncrypt(): Promise { @@ -95,6 +101,35 @@ export function decryptKeyring(): KeyringNode { return new TestKeyring() } +export function hKeyring(utf8Sorting: boolean): KeyringNode { + const keyStoreTableName = 'KeyStoreDdbTable' + const logicalKeyStoreName = keyStoreTableName + const kmsKeyId = + 'arn:aws:kms:us-west-2:370957321024:key/9d989aa2-2f9c-438c-a745-cc57d3ad0126' + const keyStore = new BranchKeyStoreNode({ + storage: { ddbTableName: keyStoreTableName }, + logicalKeyStoreName: logicalKeyStoreName, + kmsConfiguration: { identifier: kmsKeyId }, + }) + const branchKeyId = '6e1f5edc-9a70-4a8e-adfb-37aeefdea9e6' + return new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl: 600, // 10 min + utf8Sorting, + }) +} + +export function multiKeyring( + keyring1: KeyringNode, + keyring2: KeyringNode +): KeyringNode { + return new MultiKeyringNode({ + generator: keyring1, + children: [keyring2], + }) +} + export interface VectorTest { ciphertext: string commitment: string diff --git a/modules/encrypt-browser/src/encrypt.ts b/modules/encrypt-browser/src/encrypt.ts index 2d2835f7..b8770a0e 100644 --- a/modules/encrypt-browser/src/encrypt.ts +++ b/modules/encrypt-browser/src/encrypt.ts @@ -31,6 +31,7 @@ import { import { fromUtf8 } from '@aws-sdk/util-utf8-browser' import { getWebCryptoBackend } from '@aws-crypto/web-crypto-backend' +const serialize = serializeFactory(fromUtf8, { utf8Sorting: true }) const { messageAADContentString, messageAAD } = aadFactory(fromUtf8) export interface EncryptInput { @@ -46,7 +47,7 @@ export interface EncryptResult { } export async function _encrypt( - { commitmentPolicy, maxEncryptedDataKeys, utf8Sorting }: ClientOptions, + { commitmentPolicy, maxEncryptedDataKeys }: ClientOptions, cmm: KeyringWebCrypto | WebCryptoMaterialsManager, plaintext: Uint8Array, { @@ -121,12 +122,6 @@ export async function _encrypt( const { getSubtleEncrypt, keyCommitment } = await getEncryptInfo(messageId) - const maybeUtf8Sorting = utf8Sorting ?? false - - const serialize = serializeFactory(fromUtf8, { - utf8Sorting: maybeUtf8Sorting, - }) - const messageHeader = serialize.buildMessageHeader({ suite: material.suite, encryptedDataKeys: material.encryptedDataKeys, diff --git a/modules/encrypt-browser/src/encrypt_client.ts b/modules/encrypt-browser/src/encrypt_client.ts index 0b4afc1e..27ad4943 100644 --- a/modules/encrypt-browser/src/encrypt_client.ts +++ b/modules/encrypt-browser/src/encrypt_client.ts @@ -23,7 +23,6 @@ export function buildEncrypt( const { commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, maxEncryptedDataKeys = false, - utf8Sorting = false, } = typeof options === 'string' ? { commitmentPolicy: options } : options /* Precondition: browser buildEncrypt needs a valid commitmentPolicy. */ @@ -37,7 +36,6 @@ export function buildEncrypt( const clientOptions: ClientOptions = { commitmentPolicy, maxEncryptedDataKeys, - utf8Sorting, } return { encrypt: _encrypt.bind({}, clientOptions), diff --git a/modules/encrypt-node/src/encrypt_client.ts b/modules/encrypt-node/src/encrypt_client.ts index 82f80549..482f397a 100644 --- a/modules/encrypt-node/src/encrypt_client.ts +++ b/modules/encrypt-node/src/encrypt_client.ts @@ -27,7 +27,6 @@ export function buildEncrypt( const { commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT, maxEncryptedDataKeys = false, - utf8Sorting = false, } = typeof options === 'string' ? { commitmentPolicy: options } : options /* Precondition: node buildEncrypt needs a valid commitmentPolicy. */ @@ -41,7 +40,6 @@ export function buildEncrypt( const clientOptions: ClientOptions = { commitmentPolicy, maxEncryptedDataKeys, - utf8Sorting, } return { encryptStream: _encryptStream.bind({}, clientOptions), diff --git a/modules/encrypt-node/src/encrypt_stream.ts b/modules/encrypt-node/src/encrypt_stream.ts index 56d94b95..62d74f2c 100644 --- a/modules/encrypt-node/src/encrypt_stream.ts +++ b/modules/encrypt-node/src/encrypt_stream.ts @@ -33,6 +33,8 @@ import { pipeline } from 'readable-stream' import { Duplex } from 'stream' const fromUtf8 = (input: string) => Buffer.from(input, 'utf8') +const { serializeMessageHeader, headerAuthIv, buildMessageHeader } = + serializeFactory(fromUtf8, { utf8Sorting: true }) export interface EncryptStreamInput { suiteId?: AlgorithmSuiteIdentifier @@ -51,7 +53,7 @@ export interface EncryptStreamInput { * @param op EncryptStreamInput */ export function _encryptStream( - { commitmentPolicy, maxEncryptedDataKeys, utf8Sorting }: ClientOptions, + { commitmentPolicy, maxEncryptedDataKeys }: ClientOptions, cmm: KeyringNode | NodeMaterialsManager, op: EncryptStreamInput = {} ): Duplex { @@ -109,9 +111,8 @@ export function _encryptStream( 'maxEncryptedDataKeys exceeded.' ) - const maybeUtf8Sorting = utf8Sorting ?? false const { getCipher, messageHeader, rawHeader, dispose, getSigner } = - getEncryptionInfo(material, frameLength, maybeUtf8Sorting) + getEncryptionInfo(material, frameLength) wrappingStream.emit('MessageHeader', messageHeader) @@ -119,7 +120,6 @@ export function _encryptStream( getCipher, messageHeader, dispose, - maybeUtf8Sorting, { plaintextLength, suite: material.suite } ) const signatureStream = new SignatureStream(getSigner) @@ -140,12 +140,9 @@ export function _encryptStream( export function getEncryptionInfo( material: NodeEncryptionMaterial, - frameLength: number, - utf8Sorting: boolean + frameLength: number ) { const { getCipherInfo, dispose, getSigner } = getEncryptHelper(material) - const { serializeMessageHeader, headerAuthIv, buildMessageHeader } = - serializeFactory(fromUtf8, { utf8Sorting }) const { suite, encryptionContext, encryptedDataKeys } = material const { ivLength, messageFormat } = material.suite diff --git a/modules/encrypt-node/src/framed_encrypt_stream.ts b/modules/encrypt-node/src/framed_encrypt_stream.ts index f245b9af..f5988446 100644 --- a/modules/encrypt-node/src/framed_encrypt_stream.ts +++ b/modules/encrypt-node/src/framed_encrypt_stream.ts @@ -18,6 +18,8 @@ import { } from '@aws-crypto/material-management-node' const fromUtf8 = (input: string) => Buffer.from(input, 'utf8') +const serialize = serializeFactory(fromUtf8, { utf8Sorting: true }) +const { finalFrameHeader, frameHeader } = serialize const aadUtility = aadFactory(fromUtf8) interface AccumulatingFrame { @@ -45,7 +47,6 @@ export function getFramedEncryptStream( getCipher: GetCipher, messageHeader: MessageHeader, dispose: () => void, - utf8Sorting: boolean, { plaintextLength, suite, @@ -106,7 +107,6 @@ export function getFramedEncryptStream( getCipher, isFinalFrame: false, suite, - utf8Sorting, }) // Reset frame state for next frame @@ -129,7 +129,6 @@ export function getFramedEncryptStream( getCipher, isFinalFrame: true, suite, - utf8Sorting, }) this._flushEncryptFrame(encryptFrame) @@ -206,18 +205,10 @@ type EncryptFrameInput = { getCipher: GetCipher isFinalFrame: boolean suite: NodeAlgorithmSuite - utf8Sorting: boolean } export function getEncryptFrame(input: EncryptFrameInput): EncryptFrame { - const { - pendingFrame, - messageHeader, - getCipher, - isFinalFrame, - suite, - utf8Sorting, - } = input + const { pendingFrame, messageHeader, getCipher, isFinalFrame, suite } = input const { sequenceNumber, contentLength, content } = pendingFrame const { frameLength, contentType, messageId } = messageHeader /* Precondition: The content length MUST correlate with the frameLength. @@ -235,9 +226,6 @@ export function getEncryptFrame(input: EncryptFrameInput): EncryptFrame { isFinalFrame, })}` ) - const serialize = serializeFactory(fromUtf8, { utf8Sorting }) - const { finalFrameHeader, frameHeader } = serialize - const frameIv = serialize.frameIv(suite.ivLength, sequenceNumber) const bodyHeader = Buffer.from( isFinalFrame diff --git a/modules/encrypt-node/test/framed_encrypt_stream.test.ts b/modules/encrypt-node/test/framed_encrypt_stream.test.ts index 3b192df8..37e9a2e3 100644 --- a/modules/encrypt-node/test/framed_encrypt_stream.test.ts +++ b/modules/encrypt-node/test/framed_encrypt_stream.test.ts @@ -28,7 +28,6 @@ describe('getFramedEncryptStream', () => { getCipher, {} as any, () => {}, - false, {} as any ) expect(test._transform).is.a('function') @@ -37,13 +36,13 @@ describe('getFramedEncryptStream', () => { it('Precondition: plaintextLength must be within bounds.', () => { const getCipher: any = () => {} expect(() => - getFramedEncryptStream(getCipher, {} as any, () => {}, false, { + getFramedEncryptStream(getCipher, {} as any, () => {}, { plaintextLength: -1, suite, }) ).to.throw(Error, 'plaintextLength out of bounds.') expect(() => - getFramedEncryptStream(getCipher, {} as any, () => {}, false, { + getFramedEncryptStream(getCipher, {} as any, () => {}, { plaintextLength: Number.MAX_SAFE_INTEGER + 1, suite, }) @@ -53,7 +52,7 @@ describe('getFramedEncryptStream', () => { * I want to make sure that I don't have an errant off by 1 error. */ expect(() => - getFramedEncryptStream(getCipher, {} as any, () => {}, false, { + getFramedEncryptStream(getCipher, {} as any, () => {}, { plaintextLength: Number.MAX_SAFE_INTEGER, suite, }) @@ -62,7 +61,7 @@ describe('getFramedEncryptStream', () => { it('Precondition: Must not process more than plaintextLength.', () => { const getCipher: any = () => {} - const test = getFramedEncryptStream(getCipher, {} as any, () => {}, false, { + const test = getFramedEncryptStream(getCipher, {} as any, () => {}, { plaintextLength: 8, suite, }) @@ -79,7 +78,6 @@ describe('getFramedEncryptStream', () => { getCipher, { frameLength } as any, () => {}, - false, {} as any ) @@ -114,7 +112,6 @@ describe('getEncryptFrame', () => { encryptedDataKeys: [], }, suite, - utf8Sorting: false, } const test1 = getEncryptFrame(input) expect(test1.content).to.equal(input.pendingFrame.content) @@ -149,7 +146,6 @@ describe('getEncryptFrame', () => { encryptedDataKeys: [], }, suite, - utf8Sorting: false, } expect(() => getEncryptFrame(inputFinalFrameToLarge)).to.throw( @@ -176,7 +172,6 @@ describe('getEncryptFrame', () => { encryptedDataKeys: [], }, suite, - utf8Sorting: false, } // Make sure that it must be equal as long as we are here... diff --git a/modules/integration-node/src/integration_tests.ts b/modules/integration-node/src/integration_tests.ts index 8793bccf..71372976 100644 --- a/modules/integration-node/src/integration_tests.ts +++ b/modules/integration-node/src/integration_tests.ts @@ -27,9 +27,10 @@ import { version } from './version' import { URL } from 'url' import got from 'got' import streamToPromise from 'stream-to-promise' -const { encrypt, decrypt, decryptUnsignedMessageStream } = buildClient( - CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT -) +const { encrypt, decrypt, decryptUnsignedMessageStream } = buildClient({ + commitmentPolicy: CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT, + maxEncryptedDataKeys: false, +}) import { ZipFile } from 'yazl' import { createWriteStream } from 'fs' import { v4 } from 'uuid' diff --git a/modules/kms-keyring-node/src/kms_hkeyring_node.ts b/modules/kms-keyring-node/src/kms_hkeyring_node.ts index 942b77bc..8a4b9654 100644 --- a/modules/kms-keyring-node/src/kms_hkeyring_node.ts +++ b/modules/kms-keyring-node/src/kms_hkeyring_node.ts @@ -74,7 +74,7 @@ export interface KmsHierarchicalKeyRingNodeInput { //= type=implication //# - MAY provide a [Partition ID](#partition-id) partitionId?: string - utf8Sorting?: boolean | true + utf8Sorting?: boolean } export interface IKmsHierarchicalKeyRingNode extends KeyringNode { @@ -105,7 +105,7 @@ export class KmsHierarchicalKeyRingNode public declare maxCacheSize?: number public declare _cmc: CryptographicMaterialsCache declare readonly _partition: Buffer - public declare utf8Sorting: boolean | true + public declare _utf8Sorting: boolean constructor({ branchKeyId, @@ -259,16 +259,10 @@ export class KmsHierarchicalKeyRingNode readOnlyProperty(this, 'maxCacheSize', maxCacheSize) readOnlyProperty(this, '_cmc', cache) - if (utf8Sorting) { - needs( - typeof utf8Sorting === 'boolean', - 'The branch key id must be a string' - ) - } else { + if (utf8Sorting === undefined) { utf8Sorting = true } - - readOnlyProperty(this, 'utf8Sorting', utf8Sorting) + readOnlyProperty(this, '_utf8Sorting', utf8Sorting) Object.freeze(this) /* Postcondition: The HKR object must be frozen */ @@ -314,7 +308,7 @@ export class KmsHierarchicalKeyRingNode pdk, branchKeyMaterials, encryptionMaterial, - this.utf8Sorting + this._utf8Sorting ) // return the modified encryption material with the new edk and newly @@ -444,7 +438,7 @@ export class KmsHierarchicalKeyRingNode ciphertext, branchKeyMaterials, decryptionMaterial, - this.utf8Sorting + this._utf8Sorting ) } catch (e) { //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts index bc4a0a22..a843dcab 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts @@ -280,6 +280,38 @@ describe('KmsHierarchicalKeyRingNode: constructor', () => { expect(Object.isFrozen(hkr)).equals(true) }) + it('utf8Sorting default value is set correctly', () => { + expect( + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + })._utf8Sorting + ).to.equal(true) + }) + + it('utf8Sorting value is set correctly', () => { + expect( + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + utf8Sorting: false, + })._utf8Sorting + ).to.equal(false) + }) + + it('utf8Sorting value is set correctly', () => { + expect( + new KmsHierarchicalKeyRingNode({ + branchKeyId, + keyStore, + cacheLimitTtl, + utf8Sorting: true, + })._utf8Sorting + ).to.equal(true) + }) + it('All attributes initialized correctly', () => { expect(hkr.branchKeyId).to.equal(branchKeyId) expect(hkr.branchKeyIdSupplier).to.equal(branchKeyIdSupplier) diff --git a/modules/material-management/src/types.ts b/modules/material-management/src/types.ts index 9d4d895f..7548ae53 100644 --- a/modules/material-management/src/types.ts +++ b/modules/material-management/src/types.ts @@ -122,7 +122,6 @@ export type AwsEsdkCreateSecretKey = (key: Uint8Array) => AwsEsdkKeyObject export interface ClientOptions { commitmentPolicy: CommitmentPolicy maxEncryptedDataKeys: number | false - utf8Sorting?: boolean | false } export type Newable = { new (...args: any[]): T } diff --git a/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts b/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts index 9f17bc10..960a6558 100644 --- a/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts +++ b/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts @@ -43,7 +43,7 @@ export type RawAesKeyringNodeInput = { keyName: string unencryptedMasterKey: Uint8Array wrappingSuite: WrappingSuiteIdentifier - utf8Sorting?: boolean | true + utf8Sorting?: boolean } export class RawAesKeyringNode extends KeyringNode { @@ -51,6 +51,7 @@ export class RawAesKeyringNode extends KeyringNode { public declare keyName: string declare _wrapKey: WrapKey declare _unwrapKey: UnwrapKey + public declare _utf8Sorting: boolean constructor(input: RawAesKeyringNodeInput) { super() @@ -76,9 +77,15 @@ export class RawAesKeyringNode extends KeyringNode { flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, }) - const maybeUtf8Sorting = utf8Sorting ?? true + if (utf8Sorting === undefined) { + readOnlyProperty(this, '_utf8Sorting', true) + } else { + readOnlyProperty(this, '_utf8Sorting', utf8Sorting) + } // default will be true - const serializeOptions: SerializeOptions = { utf8Sorting: maybeUtf8Sorting } + const serializeOptions: SerializeOptions = { + utf8Sorting: this._utf8Sorting, + } const { serializeEncryptionContext } = serializeFactory( fromUtf8, serializeOptions diff --git a/modules/raw-aes-keyring-node/test/raw_aes_keyring_node.test.ts b/modules/raw-aes-keyring-node/test/raw_aes_keyring_node.test.ts index 9f6dcf50..255dbd76 100644 --- a/modules/raw-aes-keyring-node/test/raw_aes_keyring_node.test.ts +++ b/modules/raw-aes-keyring-node/test/raw_aes_keyring_node.test.ts @@ -81,6 +81,41 @@ describe('RawAesKeyringNode::constructor', () => { }) ).to.throw() }) + + it('utf8Sorting default value is set properly', () => { + expect( + new RawAesKeyringNode({ + keyName, + keyNamespace, + unencryptedMasterKey, + wrappingSuite, + })._utf8Sorting + ).to.equal(true) + }) + + it('utf8Sorting value is set properly', () => { + expect( + new RawAesKeyringNode({ + keyName, + keyNamespace, + unencryptedMasterKey, + wrappingSuite, + utf8Sorting: false, + })._utf8Sorting + ).to.equal(false) + }) + + it('utf8Sorting value is set properly', () => { + expect( + new RawAesKeyringNode({ + keyName, + keyNamespace, + unencryptedMasterKey, + wrappingSuite, + utf8Sorting: true, + })._utf8Sorting + ).to.equal(true) + }) }) describe('RawAesKeyringNode::_filter', () => { diff --git a/wallaby.conf.js b/wallaby.conf.js index ce8621cc..ad4f0307 100644 --- a/wallaby.conf.js +++ b/wallaby.conf.js @@ -35,6 +35,9 @@ module.exports = function (wallaby) { }, env: { type: 'node', + AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID, + AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY, + AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN, params: { env: 'AWS_REGION=us-west-2;AWS_CONTAINER_CREDENTIALS_FULL_URI=http://127.0.0.1:9911' }, From 49c69c9637c1d3b75442947dabcc49e728fba578 Mon Sep 17 00:00:00 2001 From: Jose Corella Date: Tue, 18 Mar 2025 15:41:26 -0700 Subject: [PATCH 07/13] clean up --- modules/decrypt-node/test/decrypt.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/modules/decrypt-node/test/decrypt.test.ts b/modules/decrypt-node/test/decrypt.test.ts index d24f1cee..040f3c72 100644 --- a/modules/decrypt-node/test/decrypt.test.ts +++ b/modules/decrypt-node/test/decrypt.test.ts @@ -219,7 +219,6 @@ describe('decrypt', () => { const { decrypt } = buildDecrypt({ commitmentPolicy: CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT, maxEncryptedDataKeys: 3, - utf8Sorting: false, }) const hkeyring = fixtures.hKeyring(false) const ciphertext = fixtures.hierarchyMessageWithHighUtf8CodePoints() @@ -235,7 +234,6 @@ describe('decrypt', () => { const { decrypt } = buildDecrypt({ commitmentPolicy: CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT, maxEncryptedDataKeys: 3, - utf8Sorting: false, }) const hkeyring = fixtures.hKeyring(true) const ciphertext = fixtures.hierarchyMessageWithHighUtf8CodePoints() @@ -249,7 +247,6 @@ describe('decrypt', () => { const { decrypt } = buildDecrypt({ commitmentPolicy: CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT, maxEncryptedDataKeys: 3, - utf8Sorting: false, }) const hkeyringUtf8 = fixtures.hKeyring(true) const hkeyringNoUtf8 = fixtures.hKeyring(false) From 36671d6ce4b3dbcbf6a5c2e55c3d8f042cdd3ad8 Mon Sep 17 00:00:00 2001 From: Jose Corella Date: Wed, 19 Mar 2025 12:04:54 -0700 Subject: [PATCH 08/13] fix? integration node --- modules/integration-node/src/integration_tests.ts | 10 ++++++---- modules/material-management-node/src/index.ts | 1 + modules/material-management/src/algorithm_suites.ts | 13 +++++++++++++ modules/material-management/src/index.ts | 1 + .../test/algorithm_suites.test.ts | 11 +++++++++++ 5 files changed, 32 insertions(+), 4 deletions(-) diff --git a/modules/integration-node/src/integration_tests.ts b/modules/integration-node/src/integration_tests.ts index 71372976..93f6d3f4 100644 --- a/modules/integration-node/src/integration_tests.ts +++ b/modules/integration-node/src/integration_tests.ts @@ -22,15 +22,12 @@ import { MessageHeader, needs, DecryptOutput, + getCompatibleCommitmentPolicy, } from '@aws-crypto/client-node' import { version } from './version' import { URL } from 'url' import got from 'got' import streamToPromise from 'stream-to-promise' -const { encrypt, decrypt, decryptUnsignedMessageStream } = buildClient({ - commitmentPolicy: CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT, - maxEncryptedDataKeys: false, -}) import { ZipFile } from 'yazl' import { createWriteStream } from 'fs' import { v4 } from 'uuid' @@ -59,6 +56,9 @@ async function runDecryption( testVectorInfo: TestVectorInfo ): Promise { const cmm = decryptMaterialsManagerNode(testVectorInfo.keysInfo) + const { decrypt, decryptUnsignedMessageStream } = buildClient( + CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT + ) if (testVectorInfo.decryptionMethod == 'streaming-unsigned-only') { const plaintext: Buffer[] = [] let messageHeader: MessageHeader | false = false @@ -148,6 +148,8 @@ export async function testEncryptVector( handleEncryptResult: HandleEncryptResult ): Promise { const { name, keysInfo, encryptOp, plainTextData } = info + const commitmentPolicy = getCompatibleCommitmentPolicy(encryptOp.suiteId) + const { encrypt } = buildClient(commitmentPolicy) try { const cmm = encryptMaterialsManagerNode(keysInfo) const { result: encryptResult } = await encrypt( diff --git a/modules/material-management-node/src/index.ts b/modules/material-management-node/src/index.ts index 4f95723e..7560fbdb 100644 --- a/modules/material-management-node/src/index.ts +++ b/modules/material-management-node/src/index.ts @@ -39,4 +39,5 @@ export { MessageFormat, ClientOptions, Newable, + getCompatibleCommitmentPolicy, } from '@aws-crypto/material-management' diff --git a/modules/material-management/src/algorithm_suites.ts b/modules/material-management/src/algorithm_suites.ts index 4f3dc7f5..52ff81c9 100644 --- a/modules/material-management/src/algorithm_suites.ts +++ b/modules/material-management/src/algorithm_suites.ts @@ -250,6 +250,19 @@ export const CommitmentPolicySuites = Object.freeze({ }), }) +export function getCompatibleCommitmentPolicy( + suiteId: AlgorithmSuiteIdentifier +) { + // If it is a algorithm suite with no key commitment + // we use FORBID_ENCRYPT_ALLOW_DECRYPT + // otherwise we use REQUIRE_ENCRYPT_REQUIRE_DECRYPT + if (CommittingAlgorithmSuiteIdentifier[suiteId]) { + return CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT + } else { + return CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT + } +} + export type AlgorithmSuiteName = keyof typeof AlgorithmSuiteIdentifier export type AlgorithmSuiteTypeNode = 'node' export type AlgorithmSuiteTypeWebCrypto = 'webCrypto' diff --git a/modules/material-management/src/index.ts b/modules/material-management/src/index.ts index 667a3099..11fe828f 100644 --- a/modules/material-management/src/index.ts +++ b/modules/material-management/src/index.ts @@ -24,6 +24,7 @@ export { MessageFormat, NonCommittingAlgorithmSuiteIdentifier, CommittingAlgorithmSuiteIdentifier, + getCompatibleCommitmentPolicy, } from './algorithm_suites' export { WebCryptoAlgorithmSuite } from './web_crypto_algorithms' diff --git a/modules/material-management/test/algorithm_suites.test.ts b/modules/material-management/test/algorithm_suites.test.ts index f68cb699..8ac3e5f1 100644 --- a/modules/material-management/test/algorithm_suites.test.ts +++ b/modules/material-management/test/algorithm_suites.test.ts @@ -13,12 +13,23 @@ import { NonSigningAlgorithmSuiteIdentifier, SignaturePolicy, SignaturePolicySuites, + getCompatibleCommitmentPolicy, } from '../src/algorithm_suites' describe('AlgorithmSuiteIdentifier', () => { it('should be frozen', () => { expect(Object.isFrozen(AlgorithmSuiteIdentifier)).to.eql(true) }) + it('get compatible commitment policy', () => { + // 0x0014 is a non-commiting algorithm suite + expect(getCompatibleCommitmentPolicy(0x0014)).to.eql( + CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT + ) + // 0x0478 is a commiting algorithm suite + expect(getCompatibleCommitmentPolicy(0x0478)).to.eql( + CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT + ) + }) }) describe('AlgorithmSuite', () => { From 65552516bac4c689ea81d3fbd550ed5c611c0c4f Mon Sep 17 00:00:00 2001 From: Jose Corella Date: Wed, 19 Mar 2025 12:38:48 -0700 Subject: [PATCH 09/13] address feedback --- .../kms-keyring-node/src/kms_hkeyring_node.ts | 5 ++-- .../kms_hkeyring_node.constructor.test.ts | 2 +- .../src/raw_aes_keyring_browser.ts | 24 +++++++++---------- .../src/raw_aes_keyring_node.ts | 18 ++++---------- modules/serialize/src/serialize_factory.ts | 9 +++++++ 5 files changed, 30 insertions(+), 28 deletions(-) diff --git a/modules/kms-keyring-node/src/kms_hkeyring_node.ts b/modules/kms-keyring-node/src/kms_hkeyring_node.ts index 8a4b9654..f0a5ad8c 100644 --- a/modules/kms-keyring-node/src/kms_hkeyring_node.ts +++ b/modules/kms-keyring-node/src/kms_hkeyring_node.ts @@ -260,9 +260,10 @@ export class KmsHierarchicalKeyRingNode readOnlyProperty(this, '_cmc', cache) if (utf8Sorting === undefined) { - utf8Sorting = true + readOnlyProperty(this, '_utf8Sorting', false) + } else { + readOnlyProperty(this, '_utf8Sorting', utf8Sorting) } - readOnlyProperty(this, '_utf8Sorting', utf8Sorting) Object.freeze(this) /* Postcondition: The HKR object must be frozen */ diff --git a/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts index a843dcab..cf95070b 100644 --- a/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts +++ b/modules/kms-keyring-node/test/kms_hkeyring_node.constructor.test.ts @@ -287,7 +287,7 @@ describe('KmsHierarchicalKeyRingNode: constructor', () => { keyStore, cacheLimitTtl, })._utf8Sorting - ).to.equal(true) + ).to.equal(false) }) it('utf8Sorting value is set correctly', () => { diff --git a/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts b/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts index e4c093ee..12996943 100644 --- a/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts +++ b/modules/raw-aes-keyring-browser/src/raw_aes_keyring_browser.ts @@ -18,11 +18,7 @@ import { importForWebCryptoDecryptionMaterial, AwsEsdkJsCryptoKey, } from '@aws-crypto/material-management-browser' -import { - serializeFactory, - concatBuffers, - SerializeOptions, -} from '@aws-crypto/serialize' +import { serializeFactory, concatBuffers } from '@aws-crypto/serialize' import { _onEncrypt, _onDecrypt, @@ -56,7 +52,7 @@ export type RawAesKeyringWebCryptoInput = { keyName: string masterKey: AwsEsdkJsCryptoKey wrappingSuite: WrappingSuiteIdentifier - utf8Sorting?: boolean | true + utf8Sorting?: boolean } export class RawAesKeyringWebCrypto extends KeyringWebCrypto { @@ -64,6 +60,7 @@ export class RawAesKeyringWebCrypto extends KeyringWebCrypto { public declare keyName: string declare _wrapKey: WrapKey declare _unwrapKey: UnwrapKey + public declare _utf8Sorting: boolean constructor(input: RawAesKeyringWebCryptoInput) { super() @@ -83,12 +80,15 @@ export class RawAesKeyringWebCrypto extends KeyringWebCrypto { flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, }) - const maybeUtf8Sorting = utf8Sorting ?? true - const serializeOptions: SerializeOptions = { utf8Sorting: maybeUtf8Sorting } - const { serializeEncryptionContext } = serializeFactory( - fromUtf8, - serializeOptions - ) + if (utf8Sorting === undefined) { + readOnlyProperty(this, '_utf8Sorting', false) + } else { + readOnlyProperty(this, '_utf8Sorting', utf8Sorting) + } + // default will be false + const { serializeEncryptionContext } = serializeFactory(fromUtf8, { + utf8Sorting: this._utf8Sorting, + }) const _wrapKey = async (material: WebCryptoEncryptionMaterial) => { /* The AAD section is uInt16BE(length) + AAD * see: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html#header-aad diff --git a/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts b/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts index 960a6558..698d94f1 100644 --- a/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts +++ b/modules/raw-aes-keyring-node/src/raw_aes_keyring_node.ts @@ -14,11 +14,7 @@ import { NodeAlgorithmSuite, } from '@aws-crypto/material-management-node' import { randomBytes, createCipheriv, createDecipheriv } from 'crypto' -import { - serializeFactory, - concatBuffers, - SerializeOptions, -} from '@aws-crypto/serialize' +import { serializeFactory, concatBuffers } from '@aws-crypto/serialize' import { _onEncrypt, _onDecrypt, @@ -78,18 +74,14 @@ export class RawAesKeyringNode extends KeyringNode { }) if (utf8Sorting === undefined) { - readOnlyProperty(this, '_utf8Sorting', true) + readOnlyProperty(this, '_utf8Sorting', false) } else { readOnlyProperty(this, '_utf8Sorting', utf8Sorting) } - // default will be true - const serializeOptions: SerializeOptions = { + // default will be false + const { serializeEncryptionContext } = serializeFactory(fromUtf8, { utf8Sorting: this._utf8Sorting, - } - const { serializeEncryptionContext } = serializeFactory( - fromUtf8, - serializeOptions - ) + }) const _wrapKey = async (material: NodeEncryptionMaterial) => { /* The AAD section is uInt16BE(length) + AAD * see: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html#header-aad diff --git a/modules/serialize/src/serialize_factory.ts b/modules/serialize/src/serialize_factory.ts index ff4907a1..b40c1bb2 100644 --- a/modules/serialize/src/serialize_factory.ts +++ b/modules/serialize/src/serialize_factory.ts @@ -97,6 +97,15 @@ export function serializeFactory( encryptionContext: EncryptionContext ): Uint8Array[] { // use closure value from the serializeFactory + // If the encryption context contains high order + // utf8 code points the "old" implementation would sort these values + // based on their values, see the false branch of this function. + // This led to different sorting if using these high order utf8 code points, + // which led to decryption failures from other ESDK language implementations + // that correctly sorted the encryption context by sorting based on the utf8 + // values as opposed to the string value. + // See, https://github.com/aws/aws-encryption-sdk-javascript/issues/428 + // for mote details const { utf8Sorting } = sorting if (utf8Sorting) { return Object.entries(encryptionContext) From a7f2793f91e9055d79aa795e08fa067ed6ed3c3d Mon Sep 17 00:00:00 2001 From: Jose Corella Date: Wed, 19 Mar 2025 12:46:13 -0700 Subject: [PATCH 10/13] fix tests and use sorting in raw aes integration node --- .../src/decrypt_materials_manager_node.ts | 1 + .../test/raw_aes_keyring_node.test.ts | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/modules/integration-node/src/decrypt_materials_manager_node.ts b/modules/integration-node/src/decrypt_materials_manager_node.ts index 7aefbd1b..24fe669b 100644 --- a/modules/integration-node/src/decrypt_materials_manager_node.ts +++ b/modules/integration-node/src/decrypt_materials_manager_node.ts @@ -89,6 +89,7 @@ export function aesKeyring(keyInfo: AesKeyInfo, key: AESKey) { keyNamespace, unencryptedMasterKey, wrappingSuite, + utf8Sorting: true, }) } diff --git a/modules/raw-aes-keyring-node/test/raw_aes_keyring_node.test.ts b/modules/raw-aes-keyring-node/test/raw_aes_keyring_node.test.ts index 255dbd76..ce513a33 100644 --- a/modules/raw-aes-keyring-node/test/raw_aes_keyring_node.test.ts +++ b/modules/raw-aes-keyring-node/test/raw_aes_keyring_node.test.ts @@ -90,7 +90,7 @@ describe('RawAesKeyringNode::constructor', () => { unencryptedMasterKey, wrappingSuite, })._utf8Sorting - ).to.equal(true) + ).to.equal(false) }) it('utf8Sorting value is set properly', () => { @@ -209,15 +209,15 @@ describe('RawAesKeyringNode High utf8 code points inn encryption context', () => const unencryptedMasterKey = new Uint8Array(128 / 8) const keyNamespace = 'keyNamespace' const keyName = 'keyName' - const noUtf8SortingKeyring = new RawAesKeyringNode({ + const utf8SortingKeyring = new RawAesKeyringNode({ keyName, keyNamespace, unencryptedMasterKey, wrappingSuite, - utf8Sorting: false, + utf8Sorting: true, }) - // the default is to utf8 sort - const utf8SortingKeyring = new RawAesKeyringNode({ + // the default is not to utf8 sort + const noUtf8SortingKeyring = new RawAesKeyringNode({ keyName, keyNamespace, unencryptedMasterKey, From f780f82604e68b0a8f7dcd0775493a91e562841c Mon Sep 17 00:00:00 2001 From: Jose Corella Date: Tue, 8 Apr 2025 11:59:13 -0700 Subject: [PATCH 11/13] fix branch key id used in ci --- modules/kms-keyring-node/test/fixtures.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/kms-keyring-node/test/fixtures.ts b/modules/kms-keyring-node/test/fixtures.ts index 16cd9935..264498ff 100644 --- a/modules/kms-keyring-node/test/fixtures.ts +++ b/modules/kms-keyring-node/test/fixtures.ts @@ -10,8 +10,8 @@ import { export const DDB_TABLE_NAME = 'KeyStoreDdbTable' export const LOGICAL_KEYSTORE_NAME = DDB_TABLE_NAME -export const BRANCH_KEY_ID = '75789115-1deb-4fe3-a2ec-be9e885d1945' -export const BRANCH_KEY_ACTIVE_VERSION = 'fed7ad33-0774-4f97-aa5e-6c766fc8af9f' +export const BRANCH_KEY_ID = '3f43a9af-08c5-4317-b694-3d3e883dcaef' +export const BRANCH_KEY_ACTIVE_VERSION = 'a4905627-4b7f-4272-a847-f50dae245737' export const BRANCH_KEY_ID_WITH_EC = '4bb57643-07c1-419e-92ad-0df0df149d7c' export const KEY_ARN = From 34f6baf82c143182caab4e189c510dd7c9762046 Mon Sep 17 00:00:00 2001 From: Jose Corella Date: Tue, 8 Apr 2025 12:17:09 -0700 Subject: [PATCH 12/13] fix branch key id used key store tests --- modules/branch-keystore-node/test/fixtures.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/branch-keystore-node/test/fixtures.ts b/modules/branch-keystore-node/test/fixtures.ts index 6dcaa164..94889662 100644 --- a/modules/branch-keystore-node/test/fixtures.ts +++ b/modules/branch-keystore-node/test/fixtures.ts @@ -17,8 +17,8 @@ import { export const DDB_TABLE_NAME = 'KeyStoreDdbTable' export const LOGICAL_KEYSTORE_NAME = DDB_TABLE_NAME -export const BRANCH_KEY_ID = '75789115-1deb-4fe3-a2ec-be9e885d1945' -export const BRANCH_KEY_ACTIVE_VERSION = 'fed7ad33-0774-4f97-aa5e-6c766fc8af9f' +export const BRANCH_KEY_ID = '3f43a9af-08c5-4317-b694-3d3e883dcaef' +export const BRANCH_KEY_ACTIVE_VERSION = 'a4905627-4b7f-4272-a847-f50dae245737' export const BRANCH_KEY_ID_WITH_EC = '4bb57643-07c1-419e-92ad-0df0df149d7c' export const BRANCH_KEY_ACTIVE_VERSION_UTF8_BYTES = Buffer.from( BRANCH_KEY_ACTIVE_VERSION, From 78430745a1e87c9af84842099667c9f87cf8c032 Mon Sep 17 00:00:00 2001 From: Jose Corella Date: Tue, 8 Apr 2025 12:49:12 -0700 Subject: [PATCH 13/13] fix key store tests --- modules/branch-keystore-node/test/fixtures.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/branch-keystore-node/test/fixtures.ts b/modules/branch-keystore-node/test/fixtures.ts index 94889662..79c7b374 100644 --- a/modules/branch-keystore-node/test/fixtures.ts +++ b/modules/branch-keystore-node/test/fixtures.ts @@ -51,7 +51,7 @@ export const ENCRYPTED_ACTIVE_BRANCH_KEY = new EncryptedHierarchicalKey( [TYPE_FIELD]: BRANCH_KEY_ACTIVE_TYPE, [BRANCH_KEY_ACTIVE_VERSION_FIELD]: `branch:version:${BRANCH_KEY_ACTIVE_VERSION}` as BranchKeyVersionType, - [KEY_CREATE_TIME_FIELD]: '2023-07-12T17:34:06:000290Z', + [KEY_CREATE_TIME_FIELD]: '2025-04-04T22:29:59.000549Z', [HIERARCHY_VERSION_FIELD]: '1', [KMS_FIELD]: KEY_ARN, [TABLE_FIELD]: LOGICAL_KEYSTORE_NAME, @@ -60,7 +60,7 @@ export const ENCRYPTED_ACTIVE_BRANCH_KEY = new EncryptedHierarchicalKey( ) const ENCRYPTED_VERSION_BRANCH_KEY_CIPHERTEXT_BASE64 = - 'AQIBAHhTIzkciiF5TDB8qaCjctFmv6Dx+4yjarauOA4MtH0jwgFcb8VH4blkX0w7e59l8tl4AAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQM2tJUaqT5i07TTV9FAgEQgDsWBTM/N+rN+N7A1Js6TXVxbb64vt8eQ+G2LUs5yy98l11pXe78HZKnD+/YoUevUY1YDskV3ATRE+x2+g==' + 'AQIBAHhTIzkciiF5TDB8qaCjctFmv6Dx+4yjarauOA4MtH0jwgHZhG1KfZ/k1VQMBZzo0X+GAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMG5wDTuB2qzfR/mOKAgEQgDtbcAO39/bHj6BGaqgZTd3DSKHmpORsoaHLilWhAHryOlSjAiXK1NZxil7hOLcxjBzKE0QsMAaWJVtwag==' const ENCRYPTED_VERSION_BRANCH_KEY_CIPHERTEXT = new Uint8Array( // @ts-ignore Buffer.from(ENCRYPTED_VERSION_BRANCH_KEY_CIPHERTEXT_BASE64, 'base64') @@ -71,7 +71,7 @@ export const ENCRYPTED_VERSION_BRANCH_KEY = new EncryptedHierarchicalKey( [BRANCH_KEY_IDENTIFIER_FIELD]: BRANCH_KEY_ID, [TYPE_FIELD]: `branch:version:${BRANCH_KEY_ACTIVE_VERSION}` as BranchKeyVersionType, - [KEY_CREATE_TIME_FIELD]: '2023-07-12T17:34:06:000290Z', + [KEY_CREATE_TIME_FIELD]: '2025-04-04T22:29:59.000549Z', [HIERARCHY_VERSION_FIELD]: '1', [KMS_FIELD]: KEY_ARN, [TABLE_FIELD]: LOGICAL_KEYSTORE_NAME, @@ -84,7 +84,7 @@ export const ACTIVE_BRANCH_KEY: BranchKeyRecord = { [TYPE_FIELD]: BRANCH_KEY_ACTIVE_TYPE, [BRANCH_KEY_ACTIVE_VERSION_FIELD]: `branch:version:${BRANCH_KEY_ACTIVE_VERSION}` as BranchKeyVersionType, - [KEY_CREATE_TIME_FIELD]: '2023-07-12T17:34:06:000290Z', + [KEY_CREATE_TIME_FIELD]: '2025-04-04T22:29:59.000549Z', [HIERARCHY_VERSION_FIELD]: 1, [KMS_FIELD]: KEY_ARN, [BRANCH_KEY_FIELD]: ENCRYPTED_ACTIVE_BRANCH_KEY_CIPHERTEXT, @@ -94,7 +94,7 @@ export const VERSION_BRANCH_KEY: BranchKeyRecord = { [BRANCH_KEY_IDENTIFIER_FIELD]: BRANCH_KEY_ID, [TYPE_FIELD]: `branch:version:${BRANCH_KEY_ACTIVE_VERSION}` as BranchKeyVersionType, - [KEY_CREATE_TIME_FIELD]: '2023-07-12T17:34:06:000290Z', + [KEY_CREATE_TIME_FIELD]: '2025-04-04T22:29:59.000549Z', [HIERARCHY_VERSION_FIELD]: 1, [KMS_FIELD]: KEY_ARN, [BRANCH_KEY_FIELD]: ENCRYPTED_VERSION_BRANCH_KEY_CIPHERTEXT,