Decrypting cognito codes with KMS client from aws-sdk-v3

1k Views Asked by At

I am following this instruction to implement custom message sender in Cognito https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-custom-sms-sender.html

All works well with similar code (I use Typescript on AWS Lambda):

import {buildClient, CommitmentPolicy, KmsKeyringNode} from '@aws-crypto/client-node';
import b64 from 'base64-js';

const {decrypt} = buildClient(CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT);
const keyring = new KmsKeyringNode({keyIds: ["my-key-arn"]});

...
const {plaintext} = await decrypt(keyring, b64.toByteArray(event.request.code));
console.log(plainttext.toString()) // prints plain text exactly as I need

However, this library @aws-crypto/client-node makes my bundle really huge, almost 20MB! Probably because it depends on some of older AWS libs...

I used to use modular libraries like @aws-sdk/xxx which indeed give much smaller bundles.

I have found that for encrypt/decrypt I can use @aws-sdk/client-kms. But it doesn't work!

I am trying the following code:

import {KMSClient, DecryptCommand} from "@aws-sdk/client-kms";
import b64 from 'base64-js';

const client = new KMSClient;
await client.send(new DecryptCommand({CiphertextBlob: b64.toByteArray(event.request.code), KeyId: 'my-key-arn'}))

Which gives me an error:

InvalidCiphertextException: UnknownError
    at deserializeAws_json1_1InvalidCiphertextExceptionResponse (/projectdir/node_modules/@aws-sdk/client-kms/dist-cjs/protocols/Aws_json1_1.js:3157:23)
    at deserializeAws_json1_1DecryptCommandError (/projectdir/node_modules/@aws-sdk/client-kms/dist-cjs/protocols/Aws_json1_1.js:850:25)
    at process.processTicksAndRejections (node:internal/process/task_queues:95:5)
    at async /projectdir/node_modules/@aws-sdk/middleware-serde/dist-cjs/deserializerMiddleware.js:7:24
    at async /projectdir/node_modules/@aws-sdk/middleware-signing/dist-cjs/middleware.js:14:20
    at async StandardRetryStrategy.retry (/projectdir/node_modules/@aws-sdk/middleware-retry/dist-cjs/StandardRetryStrategy.js:51:46)
    at async /projectdir/node_modules/@aws-sdk/middleware-logger/dist-cjs/loggerMiddleware.js:6:22
    at async REPL7:1:33 {
  '$fault': 'client',
  '$metadata': {
    httpStatusCode: 400,
    requestId: '<uuid>',
    extendedRequestId: undefined,
    cfId: undefined,
    attempts: 1,
    totalRetryDelay: 0
  },
  __type: 'InvalidCiphertextException'
}

What am I doing wrong? Does this KMSClient support what I need?

I have also tried AWS CLI aws kms decrypt --ciphertext-blob ... command, gives me exactly same response. Though if I encrypt and decrypt any random message like "hello world", it works like a charm.

What am I doing wrong and what is so special about Cognito code ciphertext so I have to decrypt it somehow another way?

3

There are 3 best solutions below

1
On

I have managed to solve my task. I have realized that indeed it does not simply uses KMS to encrypt the text, the encryption/decryption process is much more complicated.

There is reference page https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/message-format.html

It describes how the message looks like, with all the headers and body, with IV, AAD, keys, etc... I have written my own script to parse it all and properly decrypt, it worked! Probably it's too long to share... I suggest to use the reference instead. Hopefully in future they will publish proper modular version of SDK.

The one from '@aws-crypto' didn't work for me, probably doesn't support all the protocols properly. This might be not the truth at the moment you are reading it.

0
On

Short answer: Cognito does not use KMS to encrypt the text, it uses the Encryption SDK. So you cannot use KMS to decrypt Cognito ciphertext.

Longer answer: I spent the past day trying to get a Python email-sender-trigger function working against Cognito using boto3 and the KMS client until I found another post (somewhere?) explaining that Cognito does not encrypt data using KMS, rather the Encryption SDK. Of course these two encryption mechanisms are not compatible.

For JavaScript and Node.js applications, it looks like you have an alternative to including the entire crypto-client: https://www.npmjs.com/package/@aws-crypto/decrypt-node

If all you are doing is decrypting, the above package will let you decrypt using the Encryption SDK and it's only 159KB.

0
On

Because apparently this doesn't exist anywhere else on the internet and it's not explicitly documented by AWS for python, here's the code to make this work:

# ...previous code...

import base64

import aws_encryption_sdk
from aws_encryption_sdk import CommitmentPolicy

# Get the code from the event
code: str = event["request"]["code"]

# Set up an encryption client with an explicit commitment policy. If you do not explicitly choose a
# commitment policy, REQUIRE_ENCRYPT_REQUIRE_DECRYPT is used by default.
client = aws_encryption_sdk.EncryptionSDKClient(commitment_policy=CommitmentPolicy.REQUIRE_ENCRYPT_ALLOW_DECRYPT)

# Create an AWS KMS master key provider
kms_kwargs = dict(key_ids=[KMS_KEY_ARN])
kms_key_provider = aws_encryption_sdk.StrictAwsKmsMasterKeyProvider(**kms_kwargs)

# Decrypt the encrypted message using the AWS Encryption SDK. It returns the decrypted message and the header.
plaintext_bytes, decrypted_message_header = client.decrypt(source=base64.b64decode(code), key_provider=kms_key_provider)

template_kwargs = {"verificationCode": plaintext_bytes.decode("utf-8")}

The docs clearly state "Amazon Cognito uses the AWS encryption SDK to encrypt the secrets, temporary passwords and codes that authorize your users' API requests."

They also do provide an example in JS (and the aws-encryption-sdk python library has some decent examples), but what no one mentions is that you need to use base64.b64decode on the Cognito code in order for this to work.

Otherwise, you'll get an aws_encryption_sdk.exceptions.NotSupportedError: Unsupported version error.