10

Let me start out by saying that it feels like this question is asked a lot, but none of the answers in the questions seem to resolve the issue I'm experiencing.

I am writing a lambda function in NodeJS. Everything about it works great except for trying to decrypt an encrypted environment variable.

In trying to get this to work, I've commented everything else about my Lambda out and I still get no results. Here's the code I'm working with right now:

const aws = require('aws-sdk')
exports.handler = async (event, context, callback) => {
    const kms = new aws.KMS()

    let params = {
      //CiphertextBlob: Buffer.from(process.env.SECRET_KEY, 'base64')
      CiphertextBlob: process.env.SECRET_KEY
    }

    console.log('before decrypt')
    console.log('SECRET_KEY', process.env.SECRET_KEY)

    kms.decrypt(params, function (err, data) {
      console.log('decrypt')
      if (err) console.log(err, err.stack) // an error occurred
      else     console.log(data)           // successful response
    })

    console.log('after decrypt')
}

The Lambda runs successfully, there is no error experienced. Here is the output from this code:

START RequestId: c3a83ca7-0f7a-11e9-84f1-a5f7503df368 Version: $LATEST
2019-01-03T17:12:36.726Z    c3a83ca7-0f7a-11e9-84f1-a5f7503df368    before decrypt
2019-01-03T17:12:36.763Z    c3a83ca7-0f7a-11e9-84f1-a5f7503df368    SECRET_KEY Encoded key string that I'm not putting in here
2019-01-03T17:12:36.765Z    c3a83ca7-0f7a-11e9-84f1-a5f7503df368    after decrypt
END RequestId: c3a83ca7-0f7a-11e9-84f1-a5f7503df368
REPORT RequestId: c3a83ca7-0f7a-11e9-84f1-a5f7503df368  Duration: 699.51 ms Billed Duration: 700 ms     Memory Size: 128 MB Max Memory Used: 40 MB  

As you can see, none of the console logs inside the decrypt callback actually show up, and I don't know why.

Using the buffer version of the secret key (line 6) instead of the plaintext version of the key (line 7) doesn't have any effect on the output either.

Can someone please help me figure out what I'm missing?

Lisa
  • 2,102
  • 7
  • 26
  • 44
  • I guess you could try by executing callback() inside your kms.decrypt call, right now it seems that your code is called synchronously – Kieper Jan 03 '19 at 18:49
  • @Kieper Node isn't the primary language I code in. Do you mean like this? kms.decrypt(params, function (err, data) { console.log('decrypt') if (err) { console.log(err, err.stack) callback.done('error', error) } else { console.log(data) callback.success() } }) – Lisa Jan 03 '19 at 18:54
  • That didn't seem to have any impact – Lisa Jan 03 '19 at 18:54
  • I thought more like in here https://docs.aws.amazon.com/lambda/latest/dg/nodejs-prog-model-handler.html#nodejs-prog-model-handler-callback – Kieper Jan 03 '19 at 19:22
  • I'm not entirely sure I did this right, but it doesn't seem to have had any effect. There is no change to the output logs when I do this: kms.decrypt(params, function (err, data) { console.log('decrypt') if (err) { console.log(err, err.stack) callback(null, 'error') } else { console.log(data) callback(null, 'success') } }) – Lisa Jan 03 '19 at 19:50

2 Answers2

17

This is the solution my coworker helped me with.

const aws = require('aws-sdk')
const kms = new aws.KMS()
exports.handler = async (event, context, callback) => {
  let params = {
    CiphertextBlob: Buffer.from(process.env.SECRET_KEY, 'base64')
  }

  let secret = null
  try {
    const decrypted = await kms.decrypt(params).promise()
    secret = decrypted.Plaintext.toString('utf-8')
  }
  catch (exception) {
    console.error(exception)
  }
}
Lisa
  • 2,102
  • 7
  • 26
  • 44
0

Lisa, It is important to understand where your original code came from and why it failed. So the code originated from here - https://docs.aws.amazon.com/kms/latest/developerguide/programming-encryption.html - which is the official AWS documentation. In the Decrypting a data key section, Node.js tab you can find an example very similar to yours - which unfortunately as of today still contains the problem you are complaining about. Now the problem is that the example uses the callback style (now I am quoting from AWS documentation):

kmsClient.decrypt({ CiphertextBlob, KeyId }, (err, data) => {
  if (err) console.log(err, err.stack); // an error occurred
  else {
    const { Plaintext } = data;
    ...
  }
});

BTW the parameter "{ CiphertextBlob, KeyId }" is also wrong in the documentation. It should be { CiphertextBlob: theBlob, KeyId: theKey }. Also the KeyId parameter is not really necessary, and indeed you did not include it in the approved answer - see https://docs.aws.amazon.com/kms/latest/developerguide/programming-encryption.html - "The KeyId parameter is not required when decrypting with symmetric encryption KMS keys. AWS KMS can get the KMS key that was used to encrypt the data from the metadata in the ciphertext blob."

Now if you include your original code as-is in a handler, the decrypt command will run asynchronously and your callback will be called BUT the handler method will not wait for spawned threads! Instead the handler will finish immediately and your Lambda function will terminate before you have a chance to see the output from your callback.

The solution in the approved answer -

const decrypted = await kms.decrypt(params).promise()

works because the handler waits for the decrypt command to finish before continuing with the execution.

There IS a way however to use the callback style and still make the handler wait for the completion of the decrypt call. This is a much more complicated solution than the approved answer, but I will include it here for the sake of understanding, and maybe someone will want to use this style:

const AWS = require('aws-sdk');
const kms = new AWS.KMS();

module.exports.handler = async (event, context, callback) => {
    const queryStringParameters = event.queryStringParameters;
    let cipherText= queryStringParameters.mySecretParameter;
    let ciphertextBlob = Buffer.from(cipherText, 'base64');
    const params = {
        CiphertextBlob: ciphertextBlob
    };
    const decryptionPromise = new Promise((resolve, reject) => {
        kms.decrypt(params, (err, data) => {
            if (err) {
                reject(err);
            } else {
                const { Plaintext } = data;
                resolve(Plaintext.toString('utf-8'));
            }
        });
    });
    try {
        const decryptedPlaintext = await decryptionPromise;

        // Do something with the decrypted plaintext
        console.log("Decrypted plaintext:", decryptedPlaintext);

        return {
            statusCode: 200,
            body: "Decryption successful"
        };
    } catch (error) {
        console.error("Error decrypting:", error);
        return {
            statusCode: 500,
            body: "Decryption failed"
        };
    }
};

The line "const decryptedPlaintext = await decryptionPromise;" ensures that the handler waits for completion of the decrypt call, and therefore your Lambda function will not terminate prematurely.

Jacobs2000
  • 856
  • 2
  • 15
  • 25