node.jsamazon-web-servicesencryptionamazon-auroraamazon-kms

Decrypt AWS RDS Aurora Activty Stream in Javascript (nodejs)


The AWS RDS Aurora mysql cluster activity streams are enabled and publishes activities through kinesis, encrypted with a customer managed KMS key. I'm receiving the records in a lambda function with Nodejs runtime. For each record, I have encrypted and base64 encoded fields databaseActivityEvents and dataKey.

To decrypt it, the documentation at AWS says:

Take the following steps to decrypt the contents of the databaseActivityEvents field:

  1. Decrypt the value in the key JSON field using the KMS key you provided when starting database activity stream. Doing so returns the data encryption key in clear text.

  2. Base64-decode the value in the databaseActivityEvents JSON field to obtain the ciphertext, in binary format, of the audit payload.

  3. Decrypt the binary ciphertext with the data encryption key that you decoded in the first step.

  4. Decompress the decrypted payload.

For step 1, I can decrypt the data-key with following code:

import { KMSClient, DecryptCommand } from "@aws-sdk/client-kms"

...

const encryptedData = data.databaseActivityEvents
const encryptedKey = data.key

const inputKms = { 
  CiphertextBlob: new Buffer.from(encryptedKey, 'base64'),
  EncryptionContext: {'aws:rds:dbc-id': "cluster-[id]"}, 
}

const client = new KMSClient({ region: "us-east-1" })
const kmsCommand = new DecryptCommand(inputKms)
const kmsResponse = await client.send(kmsCommand)

const decryptedKey = kmsResponse.Plaintext

Step 2 is a basic conversion, too.

But I cannot accomplish step 3 whatever I do. How can I decrypt the data in nodejs/javascript?

Please note that, I cannot directly use native node crypto package because the data is in a format determined by the aws-encrption-sdk described here. It requires specific and complex handling, parsing which would be much harder. The solution should include the official aws-sdk which is aware of the format.

Here are some findings by me during the seek of the solution. I found a working java code in aws documentations that accomplishes the step 3:

private static byte[] decrypt(final byte[] decoded, final byte[] decodedDataKey) throws IOException {
    // Create a JCE master key provider using the random key and an AES-GCM encryption algorithm
    final JceMasterKey masterKey = JceMasterKey.getInstance(new SecretKeySpec(decodedDataKey, "AES"),
            "BC", "DataKey", "AES/GCM/NoPadding");
    try (final CryptoInputStream<JceMasterKey> decryptingStream = CRYPTO.createDecryptingStream(masterKey, new ByteArrayInputStream(decoded));
         final ByteArrayOutputStream out = new ByteArrayOutputStream()) {
        IOUtils.copy(decryptingStream, out);
        return out.toByteArray();
    }
}

But I couldn't manage to find a similar straight forward usage in javascript version of aws-crypto package. Instead, decrypt methods require "keyring" objects. But there is a critical gap in the documentations, which is clearly insufficient, that how the content of activity streams plays with keyrings in this specific context.

I tried to use KmsKeyringNode as follows: (not sure the correct usage due to insufficient documentation)

export async function decryptByKmsKeyring(encDataBase64: string,  encDataKeyBase64: string) {
  const { decrypt } = buildClient(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
  const keyring = new KmsKeyringNode({
    generatorKeyId: 'arn:aws:kms:us-east-1:[account_id]:key/[key_id]',
    keyIds: [encDataKeyBase64]
  })
  const result = await decrypt(keyring, Buffer.from(encDataBase64, 'base64'))
  console.log("plaintext:", result.plaintext)
  console.log("messageHeader:", result.messageHeader)
  return result
}

But this got error "Malformed resource." Tried to troubleshoot it but couldn't find anything.

I also tried RawAesKeyringNode: (again, not sure the correct usage due to insufficient documentation)

export async function decryptRawAesKeyring(encDataBase64: string, key: Uint8Array) {
  const { decrypt } = buildClient(CommitmentPolicy.FORBID_ENCRYPT_ALLOW_DECRYPT)
  const wrappingSuite = RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING
  const keyring = new RawAesKeyringNode({
    keyName: "BC",
    keyNamespace: "aws-kms",
    wrappingSuite,
    unencryptedMasterKey: key
  })
  const result = await decrypt(keyring, Buffer.from(encDataBase64, 'base64'))
  console.log("plaintext:", result.plaintext)
  console.log("messageHeader:", result.messageHeader)
  return result
}

But this got error "unencryptedDataKey has not been set." Tried to troubleshoot it but couldn't find anything.

How can I decrypt the data coming from activity streams in nodejs/javascript?


Solution

  • Copying the answer from the related Github issue: https://github.com/aws/aws-encryption-sdk-javascript/issues/1194:


    First, it appears that the implementation of RDS Aurora Activity Streams is to use KMS to encrypt a raw AES key used as a JceMasterKey in the AWS Encryption SDK. Though AWS KMS is involved, this approach is not compatible with the KMSMasterKey or KMS keyrings included in the AWS ESDK (which automatically handles making requests to KMS and manages its own encryption context).

    Second, since you are using Javascript to decrypt, you have to "convert" to using keyrings instead of master keys/key providers. This means that you have to use the RawAesKeyring with the "manually" decrypted key material instead of the Java equivalent (JceMasterKey).

    Finally, it looks like the issue with the code snippet in the StackOverflow post is with the key name/namespace. Per the example, the provider ID and key ID are "BC" and "DataKey", and these terms translate to key namespace and key name when used with a keyring. If you use those values instead, the decryption should succeed.