JLumos
JLumos

Reputation: 127

Decrypt text with AWS KMS in NodeJs

I am trying to decrypt some text encrypted with AWS KMS using aws-sdk and NodeJs. I started to play today with NodeJs so I am a newbie with it. I have this problem resolved with Java but I am trying to migrate an existing Alexa skill from Java to NodeJs.

The code to decrypt is:

function decrypt(buffer) {
    const kms = new aws.KMS({
        accessKeyId: 'accessKeyId',
        secretAccessKey: 'secretAccessKey',
        region: 'eu-west-1'
    });
    return new Promise((resolve, reject) => {
        let params = {
            "CiphertextBlob" : buffer,
        };
        kms.decrypt(params, (err, data) => {
            if (err) {
                reject(err);
            } else {
                resolve(data.Plaintext);
            }
        });
    });
};

When I run this code with a correct CiphertextBlob, I get this error:

Promise {
  <rejected> { MissingRequiredParameter: Missing required key 'CiphertextBlob' in params
    at ParamValidator.fail (D:\Developing\abono-transportes-js\node_modules\aws-sdk\lib\param_validator.js:50:37)
    at ParamValidator.validateStructure (D:\Developing\abono-transportes-js\node_modules\aws-sdk\lib\param_validator.js:61:14)
    at ParamValidator.validateMember (D:\Developing\abono-transportes-js\node_modules\aws-sdk\lib\param_validator.js:88:21)
    at ParamValidator.validate (D:\Developing\abono-transportes-js\node_modules\aws-sdk\lib\param_validator.js:34:10)
    at Request.VALIDATE_PARAMETERS (D:\Developing\abono-transportes-js\node_modules\aws-sdk\lib\event_listeners.js:126:42)
    at Request.callListeners (D:\Developing\abono-transportes-js\node_modules\aws-sdk\lib\sequential_executor.js:106:20)
    at callNextListener (D:\Developing\abono-transportes-js\node_modules\aws-sdk\lib\sequential_executor.js:96:12)
    at D:\Developing\abono-transportes-js\node_modules\aws-sdk\lib\event_listeners.js:86:9
    at finish (D:\Developing\abono-transportes-js\node_modules\aws-sdk\lib\config.js:349:7)
    at D:\Developing\abono-transportes-js\node_modules\aws-sdk\lib\config.js:367:9
  message: 'Missing required key \'CiphertextBlob\' in params',
  code: 'MissingRequiredParameter',
  time: 2019-06-30T20:29:18.890Z } }

I don't understand why I am receiving that if CiphertextBlob is in the params variable.

Anyone knows? Thanks in advance!

EDIT 01/07

Test to code the feature: First function:

const CheckExpirationDateHandler = {
    canHandle(handlerInput) {
        return handlerInput.requestEnvelope.request.type === 'IntentRequest'
            && handlerInput.requestEnvelope.request.intent.name === 'TtpConsultaIntent';
    },
    handle(handlerInput) {

        var fecha = "";
        var speech = "";

        userData = handlerInput.attributesManager.getSessionAttributes();

        if (Object.keys(userData).length === 0) {
            speech = consts.No_Card_Registered;
        } else {
            console.log("Retrieving expiration date from 3rd API");
            fecha = crtm.expirationDate(cipher.decrypt(userData.code.toString()));
            speech = "Tu abono caducará el " + fecha;
        }

        return handlerInput.responseBuilder
            .speak(speech)
            .shouldEndSession(true)
            .getResponse();

    }
}

Decrypt function provided with a log:

// source is plaintext
async function decrypt(source) {

    console.log("Decrypt func INPUT: " + source)
    const params = {
        CiphertextBlob: Buffer.from(source, 'base64'),
    };
    const { Plaintext } = await kms.decrypt(params).promise();
    return Plaintext.toString();
};

Output:

2019-07-01T19:01:12.814Z 38b45272-809d-4c84-b155-928bee61a4f8 INFO Retrieving expiration date from 3rd API 2019-07-01T19:01:12.814Z 38b45272-809d-4c84-b155-928bee61a4f8 INFO Decrypt func INPUT: AYADeHK9xoVE19u/3vBTiug3LuYAewACABVhd3MtY3J5cHRvLXB1YmxpYy1rZXkAREF4UW0rcW5PSElnY1ZnZ2l1bHQ2bzc3ZnFLZWZMM2J6YWJEdnFCNVNGNzEyZGVQZ1dXTDB3RkxsdDJ2dFlRaEY4UT09AA10dHBDYXJkTnVtYmVyAAt0aXRsZU51bWJlcgABAAdhd3Mta21zAEthcm46YXdzOmttczpldS13ZXN0LTE6MjQwMTE3MzU1MTg4OmtleS81YTRkNmFmZS03MzkxLTRkMDQtYmUwYi0zZDJlMWRhZTRkMmIAuAECAQB4sE8Iv75TZ0A9b/ila9Yi/3vTSja3wM7mN/B0ThqiHZEBxYsoWpX7jCqHMoeoYOkVtAAAAH4wfAYJKoZIhvcNAQcGoG8wbQIBADBoBgkqhkiG9w0BBwEwHgYJYIZIAWUDBAEuMBEEDNnGIwghz+b42E07KAIBEIA76sV3Gmp5ib99S9H4MnY0d1l............ 2019-07-01T19:01:12.925Z 38b45272-809d-4c84-b155-928bee61a4f8 INFO Error handled: handlerInput.responseBuilder.speak(...).shouldEndSession is not a function 2019-07-01T19:01:13.018Z 38b45272-809d-4c84-b155-928bee61a4f8 ERROR Unhandled Promise Rejection {"errorType":"Runtime.UnhandledPromiseRejection","errorMessage":"InvalidCiphertextException: null","stack":["Runtime.UnhandledPromiseRejection: InvalidCiphertextException: null","...

Upvotes: 3

Views: 15944

Answers (4)

Zia Ul Rehman
Zia Ul Rehman

Reputation: 2264

After a lot of hair pulling, this is what worked for me today(in context of JS lambda):

const { KMSClient, EncryptCommand, DecryptCommand } = require('@aws-sdk/client-kms');
const kmsClient = new KMSClient({ region: REGION });

const encrypt = async (text) => {
  const params = {
    KeyId: KMS_KEY_ARN,
    Plaintext: new TextEncoder().encode(text),
    EncryptionContext: { LambdaFunctionName: 'something_consistent' }
  };

  try {
    const command = new EncryptCommand(params);
    const data = await kmsClient.send(command);
    const encryptedBase64 = Buffer.from(data.CiphertextBlob).toString('base64');
    return encryptedBase64;
  } catch (error) {
    console.error('Error encrypting data:', error);
    throw error;
  }
}

const decrypt = async (encryptedData) => {
  let ciphertextBlob;

  if (typeof encryptedData === 'string') {
    // Convert base64 string to Uint8Array
    ciphertextBlob = Uint8Array.from(Buffer.from(encryptedData, 'base64'));
  } else if (encryptedData instanceof Uint8Array) {
    ciphertextBlob = encryptedData;
  } else if (Buffer.isBuffer(encryptedData)) {
    // Convert Buffer to Uint8Array
    ciphertextBlob = new Uint8Array(encryptedData);
  } else {
    throw new Error('Invalid encrypted data type');
  }

  const params = {
    CiphertextBlob: ciphertextBlob,
    EncryptionContext: { LambdaFunctionName: 'something_consistent' }
  };

  try {
    const command = new DecryptCommand(params);
    const data = await kmsClient.send(command);
    return new TextDecoder('utf-8').decode(data.Plaintext);
  } catch (error) {
    console.error('Error decrypting data:', error);
    throw error;
  }
}

Basically it was encoring issue. Might help you guys save some time.

Upvotes: 0

arvymetal
arvymetal

Reputation: 3263

I kept on getting this error in my AWS lambda when trying the accepted solution, using AWS KMS over an environment variable I had encrypted by using AWS user interface.

It worked for me with this code adapted from the AWS official solution:

decrypt.js

const AWS = require('aws-sdk');
AWS.config.update({ region: 'us-east-1' });

module.exports = async (env) => {
    const functionName = process.env.AWS_LAMBDA_FUNCTION_NAME;
    const encrypted = process.env[env];
    
    if (!process.env[env]) {
        throw Error(`Environment variable ${env} not found`) 
    }
    
    const kms = new AWS.KMS();
    try {
        const data = await kms.decrypt({
            CiphertextBlob: Buffer.from(process.env[env], 'base64'),
            EncryptionContext: { LambdaFunctionName: functionName },
        }).promise();
        console.info(`Environment variable ${env} decrypted`)
        return data.Plaintext.toString('ascii');
    } catch (err) {
        console.log('Decryption error:', err);
        throw err;
    }
}

To be used like this:

index.js

const decrypt = require("./decrypt.js")

exports.handler = async (event, context, callback) => {
    console.log(await decrypt("MY_CRYPTED_ENVIRONMENT_VARIABLE"))
}

Upvotes: 5

ajaysinghdav10d
ajaysinghdav10d

Reputation: 1957

  1. EncryptionContext is a must for this to work.
  2. Let's say the name of EnvironmentVariable is Secret
  3. The code below reads the EnvironmentVariable called Secret and returns decrypted secret as plain text in the body.
  4. Please see the function code posted below

'use strict';

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

exports.handler = (event, context, callback) => {
    const functionName = process.env.AWS_LAMBDA_FUNCTION_NAME;
    const encryptedSecret = process.env.Secret;

    kms.decrypt({
            CiphertextBlob: new Buffer(encryptedSecret, 'base64'),
            EncryptionContext: {
                LambdaFunctionName: functionName /*Providing the name of the function as the Encryption Context is a must*/
            },
        },
        (err, data) => {

            if (err) {
                /*Handle the error please*/
            }

            var decryptedSecret = data.Plaintext.toString('ascii');

            callback(null, {
                statusCode: 200,
                body: decryptedSecret,
                headers: {
                    'Content-Type': 'application/json',
                },
            });
        });
};

Upvotes: 1

Gompro
Gompro

Reputation: 2315

That either means you're missing key 'CiphertextBlob' or its value is undefined.

Please checkout the value you're passing in as buffer.

For reference, I also added my working code example that I used.

import { KMS } from 'aws-sdk';

import config from '../config';

const kms = new KMS({
  accessKeyId: config.aws.accessKeyId,
  secretAccessKey: config.aws.secretAccessKey,
  region: config.aws.region,
});

// source is plaintext
async function encrypt(source) {
  const params = {
    KeyId: config.aws.kmsKeyId,
    Plaintext: source,
  };
  const { CiphertextBlob } = await kms.encrypt(params).promise();

  // store encrypted data as base64 encoded string
  return CiphertextBlob.toString('base64');
}

// source is plaintext
async function decrypt(source) {
  const params = {
    CiphertextBlob: Buffer.from(source, 'base64'),
  };
  const { Plaintext } = await kms.decrypt(params).promise();
  return Plaintext.toString();
}

export default {
  encrypt,
  decrypt,
};

----- ADDED -----

I was able to reproduce your issue.

decrypt("this text has never been encrypted before!");

This code throws same error.

So if you pass plain text that has never been encrypted before or has been encrypted with different key, it throws InvalidCiphertextException: null.

Now I'll give you one usage example.

encrypt("hello world!") // this will return base64 encoded string
  .then(decrypt) // this one accepts encrypted string
  .then(decoded => console.log(decoded)); // hello world!

Upvotes: 14

Related Questions