modules/kms-keyring-node/src/kms_hkeyring_node_helpers.ts (310 lines of code) (raw):

// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { EncryptedDataKey, NodeEncryptionMaterial, unwrapDataKey, NodeAlgorithmSuite, NodeDecryptionMaterial, NodeBranchKeyMaterial, KeyringTraceFlag, needs, EncryptionContext, } from '@aws-crypto/material-management' import { IKmsHierarchicalKeyRingNode } from './kms_hkeyring_node' import { createCipheriv, createDecipheriv, createHash, randomBytes, } from 'crypto' import { CryptographicMaterialsCache } from '@aws-crypto/cache-material' import { kdfCounterMode } from '@aws-crypto/kdf-ctr-mode-node' import { CACHE_ENTRY_ID_DIGEST_ALGORITHM, CIPHERTEXT_STRUCTURE, DECRYPT_FLAGS, DERIVED_BRANCH_KEY_LENGTH, ENCRYPT_FLAGS, KDF_DIGEST_ALGORITHM_SHA_256, KEY_DERIVATION_LABEL, PROVIDER_ID_HIERARCHY, PROVIDER_ID_HIERARCHY_AS_BYTES, } from './constants' import { BranchKeyIdSupplier } from '@aws-crypto/kms-keyring' import { serializeFactory, uuidv4Factory } from '@aws-crypto/serialize' export const stringToUtf8Bytes = (input: string): Buffer => Buffer.from(input, 'utf-8') export const utf8BytesToString = (input: Buffer): string => input.toString('utf-8') const stringToHexBytes = (input: string): Uint8Array => new Uint8Array(Buffer.from(input, 'hex')) const hexBytesToString = (input: Uint8Array): string => Buffer.from(input).toString('hex') export const { uuidv4ToCompressedBytes, decompressBytesToUuidv4 } = uuidv4Factory(stringToHexBytes, hexBytesToString) export function getBranchKeyId( { branchKeyId, branchKeyIdSupplier }: IKmsHierarchicalKeyRingNode, { encryptionContext }: NodeEncryptionMaterial | NodeDecryptionMaterial ): string { // use the branch key id attribute if it was set, otherwise use the branch key // id supplier. The constructor ensures that either the branch key id or // supplier is supplied to the keyring return ( branchKeyId || (branchKeyIdSupplier as BranchKeyIdSupplier).getBranchKeyId( encryptionContext ) ) } const RESOURCE_ID = new Uint8Array([0x02]) const NULL_BYTE = new Uint8Array([0x00]) const DECRYPTION_SCOPE = new Uint8Array([0x02]) const ENCRYPTION_SCOPE = new Uint8Array([0x01]) //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#appendix-a-cache-entry-identifier-formulas //# When accessing the underlying cryptographic materials cache, //# the hierarchical keyring MUST use the formulas specified in this appendix //# in order to compute the [cache entry identifier](../cryptographic-materials-cache.md#cache-identifier). export function getCacheEntryId( logicalKeyStoreName: Buffer, partitionId: Buffer, branchKeyId: string, versionAsBytes?: Buffer ): string { // get branch key id as a byte array const branchKeyIdAsBytes = stringToUtf8Bytes(branchKeyId) let entryInfo //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#resource-suffix //# The aforementioned 4 definitions ([Resource Identifier](#resource-identifier), //# [Scope Identifier](#scope-identifier), [Partition ID](#partition-id-1), and //# [Resource Suffix](#resource-suffix)) MUST be appended together with the null byte, 0x00, //# and the SHA384 of the result should be taken as the final cache identifier. if (versionAsBytes) { //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials //# When the hierarchical keyring receives an OnDecrypt request, //# it MUST calculate the cache entry identifier as the //# SHA-384 hash of the following byte strings, in the order listed: //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials //# All the above fields must be separated by a single NULL_BYTE `0x00`. //# //# | Field | Length (bytes) | Interpreted as | //# | ---------------------- | -------------- | ------------------- | //# | Resource ID | 1 | bytes | //# | Null Byte | 1 | `0x00` | //# | Scope ID | 1 | bytes | //# | Null Byte | 1 | `0x00` | //# | Partition ID | Variable | bytes | //# | Null Byte | 1 | `0x00` | //# | Logical Key Store Name | Variable | UTF-8 Encoded Bytes | //# | Null Byte | 1 | `0x00` | //# | Branch Key ID | Variable | UTF-8 Encoded Bytes | //# | Null Byte | 1 | `0x00` | //# | branch-key-version | 36 | UTF-8 Encoded Bytes | entryInfo = Buffer.concat([ //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials //# - MUST be the Resource ID for the Hierarchical Keyring (0x02) RESOURCE_ID, NULL_BYTE, //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials //# - MUST be the Scope ID for Decrypt (0x02) DECRYPTION_SCOPE, NULL_BYTE, //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials //# - MUST be the Partition ID for the Hierarchical Keyring partitionId, NULL_BYTE, //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials //# - MUST be the UTF8 encoded Logical Key Store Name of the keystore for the Hierarchical Keyring logicalKeyStoreName, NULL_BYTE, //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials //# - MUST be the UTF8 encoded branch-key-id branchKeyIdAsBytes, NULL_BYTE, //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#decryption-materials //# - MUST be the UTF8 encoded branch-key-version versionAsBytes, ]) } else { //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials //# When the hierarchical keyring receives an OnEncrypt request, //# the cache entry identifier MUST be calculated as the //# SHA-384 hash of the following byte strings, in the order listed: //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials //# All the above fields must be separated by a single NULL_BYTE `0x00`. //# //# | Field | Length (bytes) | Interpreted as | //# | ---------------------- | -------------- | ------------------- | //# | Resource ID | 1 | bytes | //# | Null Byte | 1 | `0x00` | //# | Scope ID | 1 | bytes | //# | Null Byte | 1 | `0x00` | //# | Partition ID | Variable | bytes | //# | Null Byte | 1 | `0x00` | //# | Logical Key Store Name | Variable | UTF-8 Encoded Bytes | //# | Null Byte | 1 | `0x00` | //# | Branch Key ID | Variable | UTF-8 Encoded Bytes | entryInfo = Buffer.concat([ //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials //# - MUST be the Resource ID for the Hierarchical Keyring (0x02) RESOURCE_ID, NULL_BYTE, //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials //# - MUST be the Scope ID for Encrypt (0x01) ENCRYPTION_SCOPE, NULL_BYTE, //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials //# - MUST be the Partition ID for the Hierarchical Keyring partitionId, NULL_BYTE, //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials //# - MUST be the UTF8 encoded Logical Key Store Name of the keystore for the Hierarchical Keyring logicalKeyStoreName, NULL_BYTE, //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#encryption-materials //# - MUST be the UTF8 encoded branch-key-id branchKeyIdAsBytes, ]) } // hash the branch key id buffer with sha512 return createHash(CACHE_ENTRY_ID_DIGEST_ALGORITHM) .update(entryInfo) .digest() .toString() } export async function getBranchKeyMaterials( hKeyring: IKmsHierarchicalKeyRingNode, cmc: CryptographicMaterialsCache<NodeAlgorithmSuite>, branchKeyId: string, cacheEntryId: string, branchKeyVersion?: string ): Promise<NodeBranchKeyMaterial> { const { keyStore, cacheLimitTtl } = hKeyring //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt //# The hierarchical keyring MUST attempt to find [branch key materials](../structures.md#branch-key-materials) //# from the underlying [cryptographic materials cache](../local-cryptographic-materials-cache.md). const cacheEntry = cmc.getBranchKeyMaterial(cacheEntryId) let branchKeyMaterials: NodeBranchKeyMaterial // if the cache entry is false, branch key materials were not found if (!cacheEntry || hKeyring.cacheEntryHasExceededLimits(cacheEntry)) { //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt //# If this is NOT true, then we MUST treat the cache entry as expired. //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt //# If this is NOT true, then we MUST treat the cache entry as expired. //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt //# If a cache entry is not found or the cache entry is expired, the hierarchical keyring MUST attempt to obtain the branch key materials //# by querying the backing branch keystore specified in the [retrieve OnEncrypt branch key materials](#query-branch-keystore-onencrypt) section. //# If the keyring is not able to retrieve [branch key materials](../structures.md#branch-key-materials) //# through the underlying cryptographic materials cache or //# it no longer has access to them through the backing keystore, OnEncrypt MUST fail. //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt //# Otherwise, OnEncrypt MUST fail. //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt //# Otherwise, OnDecrypt MUST fail. //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt //# OnEncrypt MUST call the Keystore's [GetActiveBranchKey](../branch-key-store.md#getactivebranchkey) operation with the following inputs: //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt //# OnDecrypt MUST call the Keystore's [GetBranchKeyVersion](../branch-key-store.md#getbranchkeyversion) operation with the following inputs: branchKeyMaterials = branchKeyVersion ? await keyStore.getBranchKeyVersion(branchKeyId, branchKeyVersion) : // The complice needs a line //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt //# OnEncrypt MUST call the Keystore's [GetActiveBranchKey](../branch-key-store.md#getactivebranchkey) operation with the following inputs: //# - the `branchKeyId` used in this operation await keyStore.getActiveBranchKey(branchKeyId) //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#query-branch-keystore-onencrypt //# If the Keystore's GetActiveBranchKey operation succeeds //# the keyring MUST put the returned branch key materials in the cache using the //# formula defined in [Appendix A](#appendix-a-cache-entry-identifier-formulas). //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt //# If the Keystore's GetBranchKeyVersion operation succeeds //# the keyring MUST put the returned branch key materials in the cache using the //# formula defined in [Appendix A](#appendix-a-cache-entry-identifier-formulas). cmc.putBranchKeyMaterial(cacheEntryId, branchKeyMaterials, cacheLimitTtl) } else { //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt //# If a cache entry is found and the entry's TTL has not expired, the hierarchical keyring MUST use those branch key materials for key unwrapping. //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt //# If a cache entry is found and the entry's TTL has not expired, the hierarchical keyring MUST use those branch key materials for key wrapping. branchKeyMaterials = cacheEntry.response } return branchKeyMaterials } //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt //# If the input [encryption materials](../structures.md#encryption-materials) do not contain a plaintext data key, //# OnEncrypt MUST generate a random plaintext data key, according to the key length defined in the [algorithm suite](../algorithm-suites.md#encryption-key-length). //# The process used to generate this random plaintext data key MUST use a secure source of randomness. export function getPlaintextDataKey(material: NodeEncryptionMaterial) { // get the pdk from the encryption material whether it is already set or we // must randomly generate it return new Uint8Array( material.hasUnencryptedDataKey ? unwrapDataKey(material.getUnencryptedDataKey()) : randomBytes(material.suite.keyLengthBytes) ) } //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#branch-key-wrapping //# To derive and encrypt a data key the keyring will follow the same key derivation and encryption as [AWS KMS](https://rwc.iacr.org/2018/Slides/Gueron.pdf). //# The hierarchical keyring MUST: //# 1. Generate a 16 byte random `salt` using a secure source of randomness //# 1. Generate a 12 byte random `IV` using a secure source of randomness //# 1. Use a [KDF in Counter Mode with a Pseudo Random Function with HMAC SHA 256](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-108r1.pdf) to derive a 32 byte `derivedBranchKey` data key with the following inputs: //# - Use the `salt` as the salt. //# - Use the branch key as the `key`. //# - Use the UTF8 Encoded value "aws-kms-hierarchy" as the label. //# 1. Encrypt a plaintext data key with the `derivedBranchKey` using `AES-GCM-256` with the following inputs: //# - MUST use the `derivedBranchKey` as the AES-GCM cipher key. //# - MUST use the plain text data key that will be wrapped by the `derivedBranchKey` as the AES-GCM message. //# - MUST use the derived `IV` as the AES-GCM IV. //# - MUST use an authentication tag byte of length 16. //# - MUST use the serialized [AAD](#branch-key-wrapping-and-unwrapping-aad) as the AES-GCM AAD. //# If OnEncrypt fails to do any of the above, OnEncrypt MUST fail. export function wrapPlaintextDataKey( pdk: Uint8Array, branchKeyMaterials: NodeBranchKeyMaterial, { encryptionContext }: NodeEncryptionMaterial, utf8Sorting: boolean ): Uint8Array { // get what we need from branch key material to wrap the pdk const branchKey = branchKeyMaterials.branchKey() const { branchKeyIdentifier, branchKeyVersion: branchKeyVersionAsBytes } = branchKeyMaterials // compress the branch key version utf8 bytes const branchKeyVersionAsBytesCompressed = Buffer.from( uuidv4ToCompressedBytes(utf8BytesToString(branchKeyVersionAsBytes)) ) const branchKeyIdAsBytes = stringToUtf8Bytes(branchKeyIdentifier) // generate salt and IV const salt = randomBytes(CIPHERTEXT_STRUCTURE.saltLength) const iv = randomBytes(CIPHERTEXT_STRUCTURE.ivLength) // derive a key from the branch key const derivedBranchKey = kdfCounterMode({ digestAlgorithm: KDF_DIGEST_ALGORITHM_SHA_256, ikm: branchKey, nonce: salt, purpose: KEY_DERIVATION_LABEL, expectedLength: DERIVED_BRANCH_KEY_LENGTH, }) // set up additional auth data const wrappedAad = wrapAad( branchKeyIdAsBytes, branchKeyVersionAsBytesCompressed, encryptionContext, utf8Sorting ) // encrypt the pdk into an edk const cipher = createCipheriv('aes-256-gcm', derivedBranchKey, iv).setAAD( wrappedAad ) const edkCiphertext = Buffer.concat([cipher.update(pdk), cipher.final()]) const authTag = cipher.getAuthTag() // wrap the edk into a ciphertext const ciphertext = new Uint8Array( Buffer.concat([ salt, iv, branchKeyVersionAsBytesCompressed, edkCiphertext, authTag, ]) ) return ciphertext } //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#branch-key-wrapping-and-unwrapping-aad //# To Encrypt and Decrypt the `wrappedDerivedBranchKey` the keyring MUST include the following values as part of the AAD for //# the AES Encrypt/Decrypt calls. //# To construct the AAD, the keyring MUST concatenate the following values //# 1. "aws-kms-hierarchy" as UTF8 Bytes //# 1. Value of `branch-key-id` as UTF8 Bytes //# 1. [version](../structures.md#branch-key-version) as Bytes //# 1. [encryption context](structures.md#encryption-context-1) from the input //# [encryption materials](../structures.md#encryption-materials) according to the [encryption context serialization specification](../structures.md#serialization). //# | Field | Length (bytes) | Interpreted as | //# | ------------------- | -------------- | ---------------------------------------------------- | //# | "aws-kms-hierarchy" | 17 | UTF-8 Encoded | //# | branch-key-id | Variable | UTF-8 Encoded | //# | version | 16 | Bytes | //# | encryption context | Variable | [Encryption Context](../structures.md#serialization) | //# If the keyring cannot serialize the encryption context, the operation MUST fail. export function wrapAad( branchKeyIdAsBytes: Buffer, version: Buffer, 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 * However, we _only_ need the ADD. * So, I just slice off the length. */ const aad = Buffer.from( serializeEncryptionContext(encryptionContext).slice(2) ) return Buffer.concat([ PROVIDER_ID_HIERARCHY_AS_BYTES, branchKeyIdAsBytes, version, aad, ]) } //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#onencrypt //# Otherwise, OnEncrypt MUST append a new [encrypted data key](../structures.md#encrypted-data-key) //# to the encrypted data key list in the [encryption materials](../structures.md#encryption-materials), constructed as follows: //# - [ciphertext](../structures.md#ciphertext): MUST be serialized as the [hierarchical keyring ciphertext](#ciphertext) //# - [key provider id](../structures.md#key-provider-id): MUST be UTF8 Encoded "aws-kms-hierarchy" //# - [key provider info](../structures.md#key-provider-information): MUST be the UTF8 Encoded AWS DDB response `branch-key-id` export function modifyEncryptionMaterial( encryptionMaterial: NodeEncryptionMaterial, pdk: Uint8Array, edk: Uint8Array, wrappingKeyName: string ): NodeEncryptionMaterial { // if the pdk was already set in the encryption material, we should not reset if (!encryptionMaterial.hasUnencryptedDataKey) { encryptionMaterial.setUnencryptedDataKey(pdk, { keyNamespace: PROVIDER_ID_HIERARCHY, keyName: wrappingKeyName, flags: KeyringTraceFlag.WRAPPING_KEY_GENERATED_DATA_KEY, }) } // add the edk (that we created during onEncrypt) to the encryption material return encryptionMaterial.addEncryptedDataKey( new EncryptedDataKey({ providerId: PROVIDER_ID_HIERARCHY, providerInfo: wrappingKeyName, encryptedDataKey: edk, }), ENCRYPT_FLAGS ) } //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ondecrypt //# The set of encrypted data keys MUST first be filtered to match this keyring’s configuration. For the encrypted data key to match: //# - Its provider ID MUST match the UTF8 Encoded value of “aws-kms-hierarchy”. //# - Deserialize the key provider info, if deserialization fails the next EDK in the set MUST be attempted. //# - The deserialized key provider info MUST be UTF8 Decoded and MUST match this keyring's configured `Branch Key Identifier`. export function filterEdk( branchKeyId: string, { providerId, providerInfo }: EncryptedDataKey ): boolean { // check if the edk matches the keyring's configuration according to provider // id and info (the edk object should have been wrapped by the branch key // configured in this keyring or decryption material's encryption context) //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#getitem-branch-keystore-ondecrypt //# - Deserialize the UTF8-Decoded `branch-key-id` from the [key provider info](../structures.md#key-provider-information) of the [encrypted data key](../structures.md#encrypted-data-key) //# and verify this is equal to the configured or supplied `branch-key-id`. return providerId === PROVIDER_ID_HIERARCHY ? branchKeyId === providerInfo : false } //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#ciphertext //# The following table describes the fields that form the ciphertext for this keyring. //# The bytes are appended in the order shown. //# The Encryption Key is variable. //# It will be whatever length is represented by the algorithm suite. //# Because all the other values are constant, //# this variability in the encryption key does not impact the format. //# | Field | Length (bytes) | Interpreted as | //# | ------------------ | -------------- | -------------- | //# | Salt | 16 | bytes | //# | IV | 12 | bytes | //# | Version | 16 | bytes | //# | Encrypted Key | Variable | bytes | //# | Authentication Tag | 16 | bytes | export function destructureCiphertext( ciphertext: Uint8Array, { keyLengthBytes }: NodeAlgorithmSuite ) { // what we expect the length of the edk object's ciphertext to be. This // depends on the byte key length specified by the algorithm suite const expectedCiphertextLength = CIPHERTEXT_STRUCTURE.saltLength + CIPHERTEXT_STRUCTURE.ivLength + CIPHERTEXT_STRUCTURE.branchKeyVersionCompressedLength + keyLengthBytes + CIPHERTEXT_STRUCTURE.authTagLength /* Precondition: The edk ciphertext must have the correct length */ needs( ciphertext.length === expectedCiphertextLength, `The encrypted data key ciphertext must be ${expectedCiphertextLength} bytes long` ) let start = 0 let end = 0 // extract the salt from the edk ciphertext start = end end += CIPHERTEXT_STRUCTURE.saltLength const salt = Buffer.from(ciphertext.subarray(start, end)) // extract the IV from the edk ciphertext start = end end += CIPHERTEXT_STRUCTURE.ivLength const iv = Buffer.from(ciphertext.subarray(start, end)) // extract the compressed branch key version from the edk ciphertext start = end end += CIPHERTEXT_STRUCTURE.branchKeyVersionCompressedLength const branchKeyVersionAsBytesCompressed = Buffer.from( ciphertext.subarray(start, end) ) // extract the encrypted data key from the edk ciphertext start = end end += keyLengthBytes const encryptedDataKey = Buffer.from(ciphertext.subarray(start, end)) // extract the auth tag from the edk ciphertext start = end end += CIPHERTEXT_STRUCTURE.authTagLength const authTag = Buffer.from(ciphertext.subarray(start, end)) return { salt, iv, branchKeyVersionAsBytesCompressed, encryptedDataKey, authTag, } } //= aws-encryption-sdk-specification/framework/aws-kms/aws-kms-hierarchical-keyring.md#branch-key-unwrapping //# To decrypt an encrypted data key with a branch key, the hierarchical keyring MUST: //# 1. Deserialize the 16 byte random `salt` from the [edk ciphertext](../structures.md#ciphertext). //# 1. Deserialize the 12 byte random `IV` from the [edk ciphertext](../structures.md#ciphertext). //# 1. Deserialize the 16 byte `version` from the [edk ciphertext](../structures.md#ciphertext). //# 1. Deserialize the `encrypted key` from the [edk ciphertext](../structures.md#ciphertext). //# 1. Deserialize the `authentication tag` from the [edk ciphertext](../structures.md#ciphertext). //# 1. Use a [KDF in Counter Mode with a Pseudo Random Function with HMAC SHA 256](https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-108r1.pdf) to derive //# the 32 byte `derivedBranchKey` data key with the following inputs: //# - Use the `salt` as the salt. //# - Use the branch key as the `key`. //# 1. Decrypt the encrypted data key with the `derivedBranchKey` using `AES-GCM-256` with the following inputs: //# - It MUST use the `encrypted key` obtained from deserialization as the AES-GCM input ciphertext. //# - It MUST use the `authentication tag` obtained from deserialization as the AES-GCM input authentication tag. //# - It MUST use the `derivedBranchKey` as the AES-GCM cipher key. //# - It MUST use the `IV` obtained from deserialization as the AES-GCM input IV. //# - It MUST use the serialized [encryption context](#branch-key-wrapping-and-unwrapping-aad) as the AES-GCM AAD. //# If OnDecrypt fails to do any of the above, OnDecrypt MUST fail. export function unwrapEncryptedDataKey( ciphertext: Uint8Array, branchKeyMaterials: NodeBranchKeyMaterial, { encryptionContext, suite }: NodeDecryptionMaterial, utf8Sorting: boolean ) { // get what we need from the branch key materials to unwrap the edk const branchKey = branchKeyMaterials.branchKey() const { branchKeyIdentifier } = branchKeyMaterials const branchKeyIdAsBytes = stringToUtf8Bytes(branchKeyIdentifier) // get the salt, iv, edk, and auth tag from the edk ciphertext const { salt, iv, encryptedDataKey, authTag, branchKeyVersionAsBytesCompressed, } = destructureCiphertext(ciphertext, suite) // derive a key from the branch key const derivedBranchKey = kdfCounterMode({ digestAlgorithm: KDF_DIGEST_ALGORITHM_SHA_256, ikm: branchKey, nonce: salt, purpose: KEY_DERIVATION_LABEL, expectedLength: DERIVED_BRANCH_KEY_LENGTH, }) // set up additional auth data const wrappedAad = wrapAad( branchKeyIdAsBytes, branchKeyVersionAsBytesCompressed, encryptionContext, utf8Sorting ) // decipher the edk to get the udk/pdk const decipher = createDecipheriv('aes-256-gcm', derivedBranchKey, iv) .setAAD(wrappedAad) .setAuthTag(authTag) const udk = Buffer.concat([ decipher.update(encryptedDataKey), decipher.final(), ]) return new Uint8Array(udk) } export function modifyDencryptionMaterial( decryptionMaterial: NodeDecryptionMaterial, udk: Uint8Array, wrappingKeyName: string ): NodeDecryptionMaterial { // modify the decryption material by setting the plaintext data key return decryptionMaterial.setUnencryptedDataKey(udk, { keyNamespace: PROVIDER_ID_HIERARCHY, keyName: wrappingKeyName, flags: DECRYPT_FLAGS, }) }