diff --git a/modules/branch-keystore-node/test/fixtures.ts b/modules/branch-keystore-node/test/fixtures.ts index 6dcaa164c..79c7b374b 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, @@ -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, 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 0e449e11b..fc411f640 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) + serializeFactory(fromUtf8, sorting) return { buildEncryptionMaterialCacheKey, diff --git a/modules/decrypt-node/test/decrypt.test.ts b/modules/decrypt-node/test/decrypt.test.ts index 1aa07f171..040f3c72a 100644 --- a/modules/decrypt-node/test/decrypt.test.ts +++ b/modules/decrypt-node/test/decrypt.test.ts @@ -214,6 +214,50 @@ 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, + }) + 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, + }) + 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, + }) + 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 9b5712834..aa20d4242 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 ccaeae1f3..b8770a0ee 100644 --- a/modules/encrypt-browser/src/encrypt.ts +++ b/modules/encrypt-browser/src/encrypt.ts @@ -31,7 +31,7 @@ import { import { fromUtf8 } from '@aws-sdk/util-utf8-browser' import { getWebCryptoBackend } from '@aws-crypto/web-crypto-backend' -const serialize = serializeFactory(fromUtf8) +const serialize = serializeFactory(fromUtf8, { utf8Sorting: true }) const { messageAADContentString, messageAAD } = aadFactory(fromUtf8) export interface EncryptInput { diff --git a/modules/encrypt-node/src/encrypt_stream.ts b/modules/encrypt-node/src/encrypt_stream.ts index 3205c2c2a..62d74f2c2 100644 --- a/modules/encrypt-node/src/encrypt_stream.ts +++ b/modules/encrypt-node/src/encrypt_stream.ts @@ -34,7 +34,7 @@ import { Duplex } from 'stream' const fromUtf8 = (input: string) => Buffer.from(input, 'utf8') const { serializeMessageHeader, headerAuthIv, buildMessageHeader } = - serializeFactory(fromUtf8) + serializeFactory(fromUtf8, { utf8Sorting: true }) export interface EncryptStreamInput { suiteId?: AlgorithmSuiteIdentifier diff --git a/modules/encrypt-node/src/framed_encrypt_stream.ts b/modules/encrypt-node/src/framed_encrypt_stream.ts index b33724717..f5988446e 100644 --- a/modules/encrypt-node/src/framed_encrypt_stream.ts +++ b/modules/encrypt-node/src/framed_encrypt_stream.ts @@ -18,7 +18,7 @@ import { } from '@aws-crypto/material-management-node' const fromUtf8 = (input: string) => Buffer.from(input, 'utf8') -const serialize = serializeFactory(fromUtf8) +const serialize = serializeFactory(fromUtf8, { utf8Sorting: true }) const { finalFrameHeader, frameHeader } = serialize const aadUtility = aadFactory(fromUtf8) diff --git a/modules/integration-node/src/decrypt_materials_manager_node.ts b/modules/integration-node/src/decrypt_materials_manager_node.ts index 7aefbd1b3..24fe669b7 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/integration-node/src/integration_tests.ts b/modules/integration-node/src/integration_tests.ts index 8793bccf6..93f6d3f4f 100644 --- a/modules/integration-node/src/integration_tests.ts +++ b/modules/integration-node/src/integration_tests.ts @@ -22,14 +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.FORBID_ENCRYPT_ALLOW_DECRYPT -) import { ZipFile } from 'yazl' import { createWriteStream } from 'fs' import { v4 } from 'uuid' @@ -58,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 @@ -147,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/kms-keyring-node/src/kms_hkeyring_node.ts b/modules/kms-keyring-node/src/kms_hkeyring_node.ts index 601a8e1bc..f0a5ad8c2 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 } 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 constructor({ branchKeyId, @@ -113,6 +115,7 @@ export class KmsHierarchicalKeyRingNode cache, maxCacheSize, partitionId, + utf8Sorting, }: KmsHierarchicalKeyRingNodeInput) { super() @@ -256,6 +259,12 @@ export class KmsHierarchicalKeyRingNode readOnlyProperty(this, 'maxCacheSize', maxCacheSize) readOnlyProperty(this, '_cmc', cache) + if (utf8Sorting === undefined) { + readOnlyProperty(this, '_utf8Sorting', false) + } else { + readOnlyProperty(this, '_utf8Sorting', utf8Sorting) + } + Object.freeze(this) /* Postcondition: The HKR object must be frozen */ } @@ -299,7 +308,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 +438,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 d9b842d60..d142c9341 100644 --- a/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts +++ b/modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts @@ -45,8 +45,6 @@ const hexBytesToString = (input: Uint8Array): string => Buffer.from(input).toString('hex') export const { uuidv4ToCompressedBytes, decompressBytesToUuidv4 } = uuidv4Factory(stringToHexBytes, hexBytesToString) -export const { serializeEncryptionContext } = - serializeFactory(stringToUtf8Bytes) export function getBranchKeyId( { branchKeyId, branchKeyIdSupplier }: IKmsHierarchicalKeyRingNode, @@ -290,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() @@ -319,7 +318,8 @@ export function wrapPlaintextDataKey( const wrappedAad = wrapAad( branchKeyIdAsBytes, branchKeyVersionAsBytesCompressed, - encryptionContext + encryptionContext, + utf8Sorting ) // encrypt the pdk into an edk @@ -361,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 @@ -528,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() @@ -557,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/fixtures.ts b/modules/kms-keyring-node/test/fixtures.ts index 16cd9935f..264498ff2 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 = 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 bc4a0a22d..cf95070b1 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(false) + }) + + 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/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts b/modules/kms-keyring-node/test/kms_hkeyring_node.helpers.test.ts index fc954d036..243182d18 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,7 +5,6 @@ import { randomBytes } from 'crypto' import { wrapAad, destructureCiphertext, - serializeEncryptionContext, unwrapEncryptedDataKey, wrapPlaintextDataKey, } from '../src/kms_hkeyring_node_helpers' @@ -17,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', () => { @@ -109,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') }) @@ -128,7 +140,8 @@ describe('KmsHierarchicalKeyRingNode: helpers', () => { wrapAad( branchKeyIdAsBytes, branchKeyVersionAsBytes, - unserializeableEc as any + unserializeableEc as any, + utf8Sorting.utf8Sorting ) ).to.throw() }) @@ -137,7 +150,8 @@ describe('KmsHierarchicalKeyRingNode: helpers', () => { const wrappedAad = wrapAad( branchKeyIdAsBytes, branchKeyVersionAsBytes, - encryptionContext + encryptionContext, + utf8Sorting.utf8Sorting ) let startIdx = 0 @@ -200,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) } @@ -255,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/material-management-node/src/index.ts b/modules/material-management-node/src/index.ts index 4f95723e6..7560fbdba 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 4f3dc7f51..52ff81c9d 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 667a30997..11fe828fc 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 f68cb699b..8ac3e5f1f 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', () => { 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 d152e0419..129969434 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 @@ -35,7 +35,6 @@ import { getWebCryptoBackend, getNonZeroByteBackend, } from '@aws-crypto/web-crypto-backend' -const { serializeEncryptionContext } = serializeFactory(fromUtf8) const { rawAesEncryptedDataKey } = rawAesEncryptedDataKeyFactory( toUtf8, fromUtf8 @@ -53,6 +52,7 @@ export type RawAesKeyringWebCryptoInput = { keyName: string masterKey: AwsEsdkJsCryptoKey wrappingSuite: WrappingSuiteIdentifier + utf8Sorting?: boolean } export class RawAesKeyringWebCrypto extends KeyringWebCrypto { @@ -60,10 +60,12 @@ export class RawAesKeyringWebCrypto extends KeyringWebCrypto { public declare keyName: string declare _wrapKey: WrapKey declare _unwrapKey: UnwrapKey + public declare _utf8Sorting: boolean 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. */ @@ -78,6 +80,15 @@ export class RawAesKeyringWebCrypto extends KeyringWebCrypto { flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, }) + 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 7d8b51336..698d94f16 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 @@ -28,7 +28,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) const { rawAesEncryptedDataKey } = rawAesEncryptedDataKeyFactory( toUtf8, fromUtf8 @@ -40,6 +39,7 @@ export type RawAesKeyringNodeInput = { keyName: string unencryptedMasterKey: Uint8Array wrappingSuite: WrappingSuiteIdentifier + utf8Sorting?: boolean } export class RawAesKeyringNode extends KeyringNode { @@ -47,11 +47,18 @@ export class RawAesKeyringNode extends KeyringNode { public declare keyName: string declare _wrapKey: WrapKey declare _unwrapKey: UnwrapKey + public declare _utf8Sorting: boolean 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. */ @@ -66,6 +73,15 @@ export class RawAesKeyringNode extends KeyringNode { flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, }) + 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: 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/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 a76de9a59..ce513a331 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(false) + }) + + 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', () => { @@ -167,3 +202,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 utf8SortingKeyring = new RawAesKeyringNode({ + keyName, + keyNamespace, + unencryptedMasterKey, + wrappingSuite, + utf8Sorting: true, + }) + // the default is not to utf8 sort + const noUtf8SortingKeyring = 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 b470f9620..b40c1bb22 100644 --- a/modules/serialize/src/serialize_factory.ts +++ b/modules/serialize/src/serialize_factory.ts @@ -26,9 +26,17 @@ 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, + sorting: SerializeOptions +) { return { frameIv, nonFramedBodyIv, @@ -88,11 +96,21 @@ export function serializeFactory(fromUtf8: (input: any) => Uint8Array) { function encodeEncryptionContext( encryptionContext: EncryptionContext ): 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)) + // 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) .map((entries) => entries.map(fromUtf8)) + .sort(([aKey], [bKey]) => compare(aKey, bKey)) .map(([key, value]) => concatBuffers( uInt16BE(key.byteLength), @@ -101,7 +119,41 @@ export function serializeFactory(fromUtf8: (input: any) => Uint8Array) { 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) { diff --git a/modules/serialize/src/types.ts b/modules/serialize/src/types.ts index 06f8afe02..a7dff4249 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 +} diff --git a/modules/serialize/test/deserialize_header_v2.test.ts b/modules/serialize/test/deserialize_header_v2.test.ts index eba227a41..4a64c654b 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) + 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/fixtures.ts b/modules/serialize/test/fixtures.ts index 38280deff..5e7962ea1 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 55ebe601c..ab6228e8f 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,9 @@ 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 +57,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 +70,9 @@ 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 +87,9 @@ 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,7 +102,9 @@ 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', @@ -120,7 +128,9 @@ 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', @@ -142,7 +152,9 @@ 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', @@ -153,9 +165,108 @@ describe('serializeFactory:serializeEncryptionContext', () => { 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) + const { serializeEncryptionContext } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) const test = serializeEncryptionContext({}) expect(test).to.be.instanceof(Uint8Array) @@ -166,7 +277,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) + const { serializeEncryptedDataKeys } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) const test = serializeEncryptedDataKeys([ { providerInfo: 'firstKey', @@ -189,7 +302,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) + const { serializeMessageHeader } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) const test = serializeMessageHeader({ version: SerializationVersion.V1, type: ObjectType.CUSTOMER_AE_DATA, @@ -225,7 +340,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) + const { serializeMessageHeader } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) const test = serializeMessageHeader({ version: SerializationVersion.V1, type: ObjectType.CUSTOMER_AE_DATA, @@ -260,7 +377,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) + const { serializeMessageHeader } = serializeFactory(fromUtf8, { + utf8Sorting: false, + }) expect(() => serializeMessageHeader({ version: -1 } as any)).to.throw( 'Unsupported version.' ) diff --git a/package.json b/package.json index 4840bc9e6..7ead06ffb 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 diff --git a/wallaby.conf.js b/wallaby.conf.js index ce8621cc3..ad4f0307f 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' },