Decrypt text with AWS KMS in NodeJs - node.js

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","...

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!

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"))
}

EncryptionContext is a must for this to work.
Let's say the name of EnvironmentVariable is Secret
The code below reads the EnvironmentVariable called Secret and returns decrypted secret as plain text in the body.
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',
},
});
});
};

Related

How to read JSON from S3 by AWS Lambda Node.js 18.x runtime?

#TBA gave the solution.
The root cause is not by runtime. It came from SDK v3.
Point: Do not update the code with mixed things (like both of runtime & SDK version together 🥲)
Thanks again, TBA.
I was using Node.js 14.x version runtime Lambda to read some json file from S3.
Brief code is below
const AWS = require("aws-sdk");
const s3 = new AWS.S3();
exports.handler = (event) => {
const { bucketName, objKey } = event
const params = {
Bucket: bucketName,
Key: objKey
};
return new Promise((resolve) => {
s3.getObject(params, async (err, data) =>{
if (err) console.log(err, err.stack);
else {
const contents = JSON.parse(data.Body)
resolve(contents);
}
});
})
};
and it returned the json data as I expected.
And today I tried to create a new lambda with runtime Node.js 18.x but it returned null or some errors...
Q) Could you give me some advice to solve this 🥲 ?
+) I used same json file for each lambda
+) Not sure why, but in my case, data.Body.toString() didn't work (I saw some answers in stackoverflow provide that and tried but no lucks)
Thanks in advance!
Case A (returns null)
import { S3Client, GetObjectCommand } from "#aws-sdk/client-s3";
const s3Client = new S3Client({ region: "ap-northeast-2" });
export const handler = (event) => {
const { objKey, bucketName } = event;
const params={
Bucket: bucketName,
Key: objKey
};
const getObjCommand = new GetObjectCommand(params);
return new Promise((resolve) => {
s3Client.send(getObjCommand, async (err, data) =>{
if (err) console.log(err, err.stack);
else {
const list = JSON.parse(data.Body)
resolve(list);
}
});
})
};
Case B (returns "Unexpected token o in JSON at position 1")
export const handler = async (event) => {
const { objKey, bucketName } = event;
const params={
Bucket: bucketName,
Key: objKey
};
const getObjCommand = new GetObjectCommand(params);
const response = await s3Client.send(getObjCommand)
console.log("JSON.parse(response.Body)", JSON.parse(response.Body))
};
Case C (returns "TypeError: Converting circular structure to JSON")
export const handler = async (event) => {
const { objKey, bucketName } = event;
const params={
Bucket: bucketName,
Key: objKey
};
const getObjCommand = new GetObjectCommand(params);
try {
const response = await s3Client.send(getObjCommand)
return JSON.stringify(response.Body)
} catch(err) {
console.log("error", err)
return err
}
};

NodeJS API to upload image to S3 not returning response

I am trying to upload image to AWS S3 bucket using NodeJS. The issue I am facing it is while the image is getting saved but the API is returning 404(Not Found). Here is my controller code:
async UploadProfileImage(ctx) {
try {
var file = ctx.request.files.profileImage;
if (file) {
fs.readFile(file.path, (err, fileData) => {
var resp = s3Utility.UploadProfileImageToS3(file.name, fileData);
//Not reaching here. Although E3 tag printing in console.
console.log(resp);
ctx.status = 200;
ctx.body = { response: 'file Uploaded!' };
});
}
else {
ctx.status = 400;
ctx.body = { response: 'File not found!' };
}
} catch (error) {
ctx.status = 500;
ctx.body = { response: 'There was an error. Please try again later!' };
}
}
Utility Class I am using is:
const AWS = require('aws-sdk');
const crypto = require("crypto");
var fs = require('fs');
const mime = require('mime-types');
export class S3Utility {
constructor() { }
async UploadProfileImageToS3(fileName, data) {
let randomId = crypto.randomBytes(16).toString("hex");
AWS.config.update({ region: "Region", accessKeyId: "KeyID", secretAccessKey: "SecretAccessKey" });
var s3 = new AWS.S3();
var imageName = randomId + fileName;
var params = {
Bucket: "BucketName"
Key: imageName,
Body: data,
ContentType: mime.lookup(fileName)
};
return new Promise((resolve, reject) => {
s3.putObject(params, function (err, data) {
if (err) {
console.log('Error: ', err);
reject(new Error(err.message));
} else {
console.log(data);
resolve({
response: data,
uploadedFileName: imageName
});
}
});
});
}
}
const s3Utility: S3Utility = new S3Utility();
export default s3Utility;
The code is uploading file on S3 but it is not returning proper response. Upon testing this endpoint on postman, I get "Not Found" message. However, I can see E Tag getting logged in console. I don't know what is going wrong here. I am sure it has something to do with promise. Can someone please point out or fix the mistake?
Edit:
Using async fs.readFile does the trick.
const fs = require('fs').promises;
const fileData = await fs.readFile(file.path, "binary");
var resp = await s3Utility.UploadProfileImageToS3(file.name, fileData);

Storing username with post confirmation trigger (AWS, Lambda, DynamoDB)

I try to store some parameters in a AWS DynamoDB with Cognito post confirmation trigger.
The lambda is written in node.js but I am not able to store the username of the signed up cognito user.
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient({region: 'eu-central-1'});
exports.handler = async (event, context, callback) => {
console.log(event);
const username = event.userName;
await createMessage(username).then(() => {
callback(null, {
statusCode: 201,
body: '',
headers: {
'Access Control Allow Origin' : '*'
}
});
}).catch((err) => {
console.error(err);
});
};
function createMessage(username){
const familyid = (new Date()).getTime().toString(36) + Math.random().toString(36).slice(2);
const params = {
TableName: 'eldertech',
Item: {
'UserId' : username,
'message' : familyid
}
};
return dynamodb.put(params).promise();
}
My test:
{
"username":"admin",
"email":"admin#admin.com",
"userId": "AD87S"
}
The error:
ERROR ValidationException: One or more parameter values were invalid: Missing the key UserId in the item
Can somebody please help a newbie?
Okay, I just had a typo....
const username = event.username;
No capital n in username...

AWS Lambda using s3 getObject function and putItem function to insert it into DynamoDB but nothing happens

this is the node.js code:
'use strict';
const AWS = require("aws-sdk");
AWS.config.update({
region: 'eu-west-1'});
const docClient = new AWS.DynamoDB.DocumentClient();
const tableName = 'Fair';
const s3 = new AWS.S3();
exports.handler = async (event) => {
var getParams = {
Bucket: 'dataforfair', //s3 bucket name
Key: 'fairData.json' //s3 file location
}
const data = await s3.getObject(getParams).promise()
.then( (data) => {
//parse JSON
let fairInformations = JSON.parse(data.Body.toString());
fairInformations.forEach(function(fairInformationEntry) {
console.log(fairInformationEntry);
var params = {
TableName: tableName,
Item: {
"year": fairInformationEntry.year,
"fairName": fairInformationEntry.fairName,
"info": fairInformationEntry.info
}
};
docClient.put(params, function(err, data) {
console.log('*****test');
if (err) {
console.error("Unable to add fairInformation", fairInformationEntry.fairName, ". Error JSON:", JSON.stringify(err, null, 2));
} else {
console.log("PutItem succeeded:", fairInformationEntry.fairName);
}
});
});
})
.catch((err) => {
console.log(err);
});
const response = {
statusCode: 200,
body: JSON.stringify(data),
};
return response;
};
Hello everyone,
I want to put the data into the Dynamo DB after getting the JSON file from the s3 Bucket. Getting the JSON works and the console.log(fairInformationEntry); is also still triggered, but the docClient.put() never gets called. I am getting no error, nothing. I do not know what is wrong and why it is not working. I have the right IAM role and access to everything I need.
I hope you can help me!
The problem is mixup of promise, callback and async/await. You are also trying to do asynchronous operation inside foreach. The code should look something like this
"use strict";
const AWS = require("aws-sdk");
AWS.config.update({
region: "eu-west-1"
});
const docClient = new AWS.DynamoDB.DocumentClient();
const tableName = "Fair";
const s3 = new AWS.S3();
exports.handler = async event => {
var getParams = {
Bucket: "dataforfair", //s3 bucket name
Key: "fairData.json" //s3 file location
};
const data = await s3.getObject(getParams).promise();
//parse JSON
let fairInformations = JSON.parse(data.Body.toString());
await Promise.all(
fairInformations.map(fairInformationEntry => {
console.log(fairInformationEntry);
var params = {
TableName: tableName,
Item: {
year: fairInformationEntry.year,
fairName: fairInformationEntry.fairName,
info: fairInformationEntry.info
}
};
return docClient.put(params).promise();
})
);
const response = {
statusCode: 200,
body: JSON.stringify(data)
};
return response;
};
Hope this helps

Decrypting multiple env. variables in AWS Lambda

I've got a number of encrypted environmental variables I need to decrypt in an AWS Lambda function. They give an example bit of code, but I'd rather not run a huge chunk for each value I need to decrypt:
const AWS = require('aws-sdk');
const encrypted = process.env['my_password'];
let decrypted;
function processEvent(event, context, callback) {
// TODO handle the event here
}
exports.handler = (event, context, callback) => {
if (decrypted) {
processEvent(event, context, callback);
} else {
// Decrypt code should run once and variables stored outside of the function
// handler so that these are decrypted once per container
const kms = new AWS.KMS();
kms.decrypt({ CiphertextBlob: new Buffer(encrypted, 'base64') }, (err, data) => {
if (err) {
console.log('Decrypt error:', err);
return callback(err);
}
decrypted = data.Plaintext.toString('ascii');
processEvent(event, context, callback);
});
}
};
I'm wondering if the AWS SDK includes a function that lets me decrypt multiple values at once. Failing that, is there a way to elegantly chain these calls together so they don't take up ~75 lines of my otherwise simple function?
You can use promises to achieve this. See the example below for decrypting both a username and password via KMS. You can add as many additional decryption promises to the decryptPromises array as you'd like:
const AWS = require('aws-sdk');
const encrypted = {
username: process.env.username,
password: process.env.password
};
let decrypted = {};
function processEvent(event, context, callback) {
//do work
}
exports.handler = (event, context, callback) => {
if ( decrypted.username && decrypted.password ) {
processEvent(event, context, callback);
} else {
const kms = new AWS.KMS();
const decryptPromises = [
kms.decrypt( { CiphertextBlob: new Buffer(encrypted.username, 'base64') } ).promise(),
kms.decrypt( { CiphertextBlob: new Buffer(encrypted.password, 'base64') } ).promise()
];
Promise.all( decryptPromises ).then( data => {
decrypted.username = data[0].Plaintext.toString('ascii');
decrypted.password = data[1].Plaintext.toString('ascii');
processEvent(event, context, callback);
}).catch( err => {
console.log('Decrypt error:', err);
return callback(err);
});
}
};
You can find more information on how promises have been implimented for the AWS SDK in the Support for Promises in the SDK documentation.
I created a class to decrypt variables in amazon lambda. It uses async await instead of Promises.all. You do not need to import lodash library. You can modifiy the bellow class to not use it (use forEach instead).
var _ = require('lodash/core');
const AWS = require('aws-sdk');
class EnvVarsDecryptor {
constructor(encryptedVariables) {
this.encryptedVariables = encryptedVariables;
this.decrypted = {};
}
isDecrypted() {
return _.every(this.encryptedVariables, (e) => this.decrypted[e] != undefined && this.decrypted[e] != null);
}
async decryptVars() {
const kms = new AWS.KMS();
try {
for ( let index = 0; index < this.encryptedVariables.length; index++) {
const encrypted = this.encryptedVariables[index];
const data = await kms.decrypt({CiphertextBlob: new Buffer(process.env[encrypted], 'base64') }).promise();
this.decrypted[encrypted] = data.Plaintext.toString('ascii');
}
} catch( e) {
console.error(e);
}
return this.decrypted;
}
}
module.exports = EnvVarsDecryptor;
This is a sample illustrating how using the function:
exports.handler = async (event) => {
if (!decryptor.isDecrypted()) {
await decryptor.decryptVars();
}
console.log(decryptor.decrypted);
return `Successfully processed ${event.Records.length} messages.`;
};

Resources