I am developing a code to validate openssl keys using NodeJs in AWS Lambda. I have a requirement to fetch the public key from the DynamoDB and to validate with the user given private key. As of now, I have two classes, one is to fetch the data from the dynamodb and second is to validate public key and private. But because of asynchronous NodeJS executing my Second class first and is not waiting until my first class gets the data from dynamoDB.
Please help me to execute the code in sequence.
I have tried Promise, bluebird and callback But I'm not able to understand them
const AWS = require('aws-sdk')
const dynamodb = new AWS.DynamoDB();
var promise = require('promise');
exports.handler = async (event) => {
var privatek = '-----BEGIN RSA PRIVATE KEY-----\n'+
'MIICWwIBAAKBgQC2rWURD7fK/3B0W7d36BJnv4ITzSd+K6o+itgHkqe+0EOvoOn2\n'+
'yHK3J11j2c+BkgAcdfwYaBFhH7Gubvyt0TLAKJvxi6cIbD4DVJqoTwJPzgdCczKZ\n'+
'AdhevoYam3t/Q454pW5N7IoF5IzMgPypRbPhi7JnkqcE1/CIXC3hrysMeQIDAQAB\n'+
'AoGANFPlEIcVGdQkDWC8ZF+Y7hkglLV+q5iscq/pA/pRjMoxqVyJyIRQwABJszGQ\n'+
'TEhbOcveQ8uDtvOSPSpTvSKgy4fxmH0/RuypTYcAD/BN76T1DDODSsyn+KuNOdko\n'+
'x6bo30wexmBL/itya9VJMBM49iMMYtYBtOuoJGamMc+vUQECQQDxaWk9alNa37Yb\n'+
'SAfQRGoU7xJvuVQ8qHBY0EgCzYwaMkWuWKkk8GA058PezUxEjwZN8ZRVsYO2YHG1\n'+
'3w3vcF+ZAkEAwbdf1ZVpPEsVyXeftnnu5uPxjN6SGqojV1M1/QXQJaFVd0SFAWMY\n'+
'LE1tqFI6KEfQ1huehvwXhja6HU5z4p+f4QJAce/xRpYvHx2koj2dynLvqk+nYOmU\n'+
'U0igNZqf0grXC+ocLwwTUKbOkUmtjTNRwq3KKPFStBsi8emU4WST/CUKSQJAE2Af\n'+
'AsLl+rTb4gHIBL1fatKjx14/qNEZpdNZ1AvvzMO9Q6ej0gayVUQNUseer4a3WaL7\n'+
'kS7Hv5HbvbCqIKGsoQJAS3LAPW5Wpc2rHFJQDryIGdVnZLW6YdFXGudzMRjHsIB0\n'+
'UMKUywmjRf45ugQMBKJ+iesDwNLmXtOjcB9AdGRz5w==\n'+
'-----END RSA PRIVATE KEY-----';
var publick = '-----BEGIN PUBLIC KEY-----\n'+
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC2rWURD7fK/3B0W7d36BJnv4IT\n'+
'zSd+K6o+itgHkqe+0EOvoOn2yHK3J11j2c+BkgAcdfwYaBFhH7Gubvyt0TLAKJvx\n'+
'i6cIbD4DVJqoTwJPzgdCczKZAdhevoYam3t/Q454pW5N7IoF5IzMgPypRbPhi7Jn\n'+
'kqcE1/CIXC3hrysMeQIDAQAB\n'+
'-----END PUBLIC KEY-----';
console.log(event["pk"]);
const table = "MARKETPLACE_USER_RELATED_APPS_TABLE";
var dynamo = new Dynamo(table);
var prik = dynamo.read(dynamo.client());
console.log(prik);
var clientid = '12345'; //event['client_id']
var unique_id = parseInt(Math.random()*10000000000000000).toString();
var time_stamp = new Date();
var Authkey = clientid + ':' + unique_id + ':' + time_stamp;
console.log(Authkey+"Authkey");
console.log(time_stamp + 'time_stamp');
var cryp = new CryptoVerify(publick,privatek, Authkey);
console.log(cryp.hash+'hash key');
console.log(event['pk'])
const response = {
statusCode: 200,
body: cryp.verify(cryp.signature())
};
return response
};
function CryptoVerify(publickey, privatekey, authkey)
{
this.crypto = require ('crypto')
this.hash = this.crypto.createHash('sha256')
.update(authkey)
.digest('base64');
console.log(this.hash+'haskkk');
this.signature = function(){
try{
var signer = this.crypto.createSign('sha1');
signer.update(this.hash);
var sign = signer.sign(privatekey,'base64');
console.log(sign + "Sign")
}
catch(e)
{
var sign = "Private key Errpr: Not matching with the Public
key\nExact " +e;
}
return sign;
}
this.verify = function(sign){
var verifier = this.crypto.createVerify('sha1');
verifier.update(this.hash);
try {
this.ver = verifier.verify(publickey, sign,'base64');
console.log(this.ver);
}
catch (e){
console.log("Private key Error: Not matching with the Public
key/n Exact Error:"+e);
this.ver = e
}
return this.ver;
}
}
function Dynamo(table)
{
this.params = {Key :{"email_id":{S:"testuser#tml.com"},
"app_name":{S:"EDH"}},
TableName: table};
this.op = {};
this.client = function(){
try{
var dynamodb = new AWS.DynamoDB();
}
catch(e){
console.log("Error: Class DynamoDB is not defined");
}
return dynamodb;};
this.read = function(dy){
try{
dy.getItem(this.params, function(err, data){
if(err) console.log(err, err.stack);
else {
this.op = data;
console.log(this.op);
}
});
}
catch(e){
console.log('Please check getIten function to clear the error');
}
return this.op;
};
}
I expect result with the dynamoDB value to be fetched first then Validate class.
Use callback version of lambda handler or when using asyc await wrap all the callback into a promise or promisify the methods using Bluebird's promisify all method.
const AWS = require('aws-sdk')
const dynamodb = new AWS.DynamoDB();
var promise = require('promise');
exports.handler = async (event) => {
var privatek = '-----BEGIN RSA PRIVATE KEY-----\n'+
'MIICWwIBAAKBgQC2rWURD7fK/3B0W7d36BJnv4ITzSd+K6o+itgHkqe+0EOvoOn2\n'+
'yHK3J11j2c+BkgAcdfwYaBFhH7Gubvyt0TLAKJvxi6cIbD4DVJqoTwJPzgdCczKZ\n'+
'AdhevoYam3t/Q454pW5N7IoF5IzMgPypRbPhi7JnkqcE1/CIXC3hrysMeQIDAQAB\n'+
'AoGANFPlEIcVGdQkDWC8ZF+Y7hkglLV+q5iscq/pA/pRjMoxqVyJyIRQwABJszGQ\n'+
'TEhbOcveQ8uDtvOSPSpTvSKgy4fxmH0/RuypTYcAD/BN76T1DDODSsyn+KuNOdko\n'+
'x6bo30wexmBL/itya9VJMBM49iMMYtYBtOuoJGamMc+vUQECQQDxaWk9alNa37Yb\n'+
'SAfQRGoU7xJvuVQ8qHBY0EgCzYwaMkWuWKkk8GA058PezUxEjwZN8ZRVsYO2YHG1\n'+
'3w3vcF+ZAkEAwbdf1ZVpPEsVyXeftnnu5uPxjN6SGqojV1M1/QXQJaFVd0SFAWMY\n'+
'LE1tqFI6KEfQ1huehvwXhja6HU5z4p+f4QJAce/xRpYvHx2koj2dynLvqk+nYOmU\n'+
'U0igNZqf0grXC+ocLwwTUKbOkUmtjTNRwq3KKPFStBsi8emU4WST/CUKSQJAE2Af\n'+
'AsLl+rTb4gHIBL1fatKjx14/qNEZpdNZ1AvvzMO9Q6ej0gayVUQNUseer4a3WaL7\n'+
'kS7Hv5HbvbCqIKGsoQJAS3LAPW5Wpc2rHFJQDryIGdVnZLW6YdFXGudzMRjHsIB0\n'+
'UMKUywmjRf45ugQMBKJ+iesDwNLmXtOjcB9AdGRz5w==\n'+
'-----END RSA PRIVATE KEY-----';
var publick = '-----BEGIN PUBLIC KEY-----\n'+
'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC2rWURD7fK/3B0W7d36BJnv4IT\n'+
'zSd+K6o+itgHkqe+0EOvoOn2yHK3J11j2c+BkgAcdfwYaBFhH7Gubvyt0TLAKJvx\n'+
'i6cIbD4DVJqoTwJPzgdCczKZAdhevoYam3t/Q454pW5N7IoF5IzMgPypRbPhi7Jn\n'+
'kqcE1/CIXC3hrysMeQIDAQAB\n'+
'-----END PUBLIC KEY-----';
console.log(event["pk"]);
const table = "MARKETPLACE_USER_RELATED_APPS_TABLE";
var dynamo = new Dynamo(table);
var prik = await dynamo.read(dynamo.client());
console.log(prik);
var clientid = '12345'; //event['client_id']
var unique_id = parseInt(Math.random()*10000000000000000).toString();
var time_stamp = new Date();
var Authkey = clientid + ':' + unique_id + ':' + time_stamp;
console.log(Authkey+"Authkey");
console.log(time_stamp + 'time_stamp');
var cryp = new CryptoVerify(publick,privatek, Authkey);
console.log(cryp.hash+'hash key');
console.log(event['pk'])
const response = {
statusCode: 200,
body: cryp.verify(cryp.signature())
};
return response
};
function CryptoVerify(publickey, privatekey, authkey)
{
this.crypto = require ('crypto')
this.hash = this.crypto.createHash('sha256')
.update(authkey)
.digest('base64');
console.log(this.hash+'haskkk');
this.signature = function(){
try{
var signer = this.crypto.createSign('sha1');
signer.update(this.hash);
var sign = signer.sign(privatekey,'base64');
console.log(sign + "Sign")
}
catch(e)
{
var sign = "Private key Errpr: Not matching with the Public
key\nExact " +e;
}
return sign;
}
this.verify = function(sign){
var verifier = this.crypto.createVerify('sha1');
verifier.update(this.hash);
try {
this.ver = verifier.verify(publickey, sign,'base64');
console.log(this.ver);
}
catch (e){
console.log("Private key Error: Not matching with the Public
key/n Exact Error:"+e);
this.ver = e
}
return this.ver;
}
}
function Dynamo(table)
{
this.params = {Key :{"email_id":{S:"testuser#tml.com"},
"app_name":{S:"EDH"}},
TableName: table};
this.op = {};
this.client = function(){
try{
var dynamodb = new AWS.DynamoDB();
}
catch(e){
console.log("Error: Class DynamoDB is not defined");
}
return dynamodb;};
this.read = function(dy){
return new Promise((resolve, reject) => {
try{
dy.getItem(this.params, function(err, data){
if(err) console.log(err, err.stack);
else {
this.op = data;
console.log(this.op);
return resolve(data);
}
});
}
catch(e){
console.log('Please check getIten function to clear the error');
reject('Error')
}
});
//return this.op;
};
}
Thanks all. I have got the solution to the question by searching more on the internet. AWS itself is providing "Promise" method for every service.
I just included promise function as follows:
dynamodb.getItem(params).promise().then(function(data, err){
if(err)
//do something
else
//do something
});
Related
I have an api post end point which would update a customer's information in dynamodb. It is set to authenticate using AWS_IAM. I am getting 403 from my lambda when calling this api. I have allowed execute-api:Invoke permission to the api for the role lambda uses. I see in this post that I need to create a canonical request. I was able to come up with the below code and I still get a 403. I can't figure out what is missing and wish if a different eye can spot the problem. Please help!
"use strict";
const https = require("https");
const crypto = require("crypto");
exports.handler = async (event, context, callback) => {
try {
var attributes = {
customerId: 1,
body: { firstName: "abc", lastName: "xyz" }
};
await updateUsingApi(attributes.customerId, attributes.body)
.then((result) => {
var jsonResult = JSON.parse(result);
if (jsonResult.statusCode === 200) {
callback(null, {
statusCode: jsonResult.statusCode,
statusMessage: "Attributes saved successfully!"
});
} else {
callback(null, jsonResult);
}
})
.catch((err) => {
console.log("error: ", err);
callback(null, err);
});
} catch (error) {
console.error("error: ", error);
callback(null, error);
}
};
function sign(key, message) {
return crypto.createHmac("sha256", key).update(message).digest();
}
function getSignatureKey(key, dateStamp, regionName, serviceName) {
var kDate = sign("AWS4" + key, dateStamp);
var kRegion = sign(kDate, regionName);
var kService = sign(kRegion, serviceName);
var kSigning = sign(kService, "aws4_request");
return kSigning;
}
function updateUsingApi(customerId, newAttributes) {
var request = {
partitionKey: `MY_CUSTOM_PREFIX_${customerId}`,
sortKey: customerId,
payLoad: newAttributes
};
var data = JSON.stringify(request);
var apiHost = new URL(process.env.REST_API_INVOKE_URL).hostname;
var apiMethod = "POST";
var path = `/stage/postEndPoint`;
var { amzdate, authorization, contentType } = getHeaders(host, method, path);
const options = {
host: host,
path: path,
method: method,
headers: {
"X-Amz-Date": amzdate,
Authorization: authorization,
"Content-Type": contentType,
"Content-Length": data.length
}
};
return new Promise((resolve, reject) => {
const req = https.request(options, (res) => {
if (res && res.statusCode !== 200) {
console.log("response from api", res);
}
var response = {
statusCode: res.statusCode,
statusMessage: res.statusMessage
};
resolve(JSON.stringify(response));
});
req.on("error", (e) => {
console.log("error", e);
reject(e.message);
});
req.write(data);
req.end();
});
}
function getHeaders(host, method, path) {
var algorithm = "AWS4-HMAC-SHA256";
var region = "us-east-1";
var serviceName = "execute-api";
var secretKey = process.env.AWS_SECRET_ACCESS_KEY;
var accessKey = process.env.AWS_ACCESS_KEY_ID;
var contentType = "application/x-amz-json-1.0";
var now = new Date();
var amzdate = now
.toJSON()
.replace(/[-:]/g, "")
.replace(/\.[0-9]*/, "");
var datestamp = now.toJSON().replace(/-/g, "").replace(/T.*/, "");
var canonicalHeaders = `content-type:${contentType}\nhost:${host}\nx-amz-date:${amzdate}\n`;
var signedHeaders = "content-type;host;x-amz-date";
var payloadHash = crypto.createHash("sha256").update("").digest("hex");
var canonicalRequest = [
method,
path,
canonicalHeaders,
signedHeaders,
payloadHash
].join("/n");
var credentialScope = [datestamp, region, serviceName, "aws4_request"].join(
"/"
);
const sha56 = crypto
.createHash("sha256")
.update(canonicalRequest)
.digest("hex");
var stringToSign = [algorithm, amzdate, credentialScope, sha56].join("\n");
var signingKey = getSignatureKey(secretKey, datestamp, region, serviceName);
var signature = crypto
.createHmac("sha256", signingKey)
.update(stringToSign)
.digest("hex");
var authorization = `${algorithm} Credential=${accessKey}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
return { amzdate, authorization, contentType };
}
I'm setting up an attendance check on a streaming video with aws-rekognition, when a person is identified then a lambda should write it to a DynamoDB. the insertDynamo()works fine alone(when I'm not calling it as function),but when I put it inside a function, it doesn't write to the DynamoDB table. Any idea on what am doing wrong?
var AWS = require('aws-sdk');
var today = new Date();
var date = today.getFullYear()+'-'+(today.getMonth()+1)+'-'+today.getDate();
var hour = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
exports.handler = async (event, context) => {
//console.log('Received event:', JSON.stringify(event, null, 2));
event.Records.forEach((record) => {
// Kinesis data is base64 encoded so decode here
const load = new Buffer(record.kinesis.data, 'base64').toString('ascii');
const payload = JSON.parse(load);
if(payload.FaceSearchResponse != null)
{
payload.FaceSearchResponse.forEach((face) => {
if(face.MatchedFaces != null &&
Object.keys(face.MatchedFaces).length > 0)
{
var id = JSON.stringify(face.MatchedFaces[0].Face.ExternalImageId, null, 4);
//this is hard code it ---needs to split string from kinesis(id)
insertDynamo(date,hour,'0001');
}
else
{
//do nothing
}
});
}
});
return `Successfully processed ${event.Records.length} records.`;
};
var insertDynamo = function(date,hour,id){
exports.handler = async (event,context) => {
const documentClient = new AWS.DynamoDB.DocumentClient();
let responseBody = "";
let statusCode = 0;
const params = {
TableName: "users",
Item:{
badgeNumber: id,
assistance:{
date:date,
hour:hour
}
},
ConditionExpression: 'attribute_not_exists(badgenumber)'
};
try {
const data = await documentClient.put(params).promise();
responseBody = JSON.stringify(data);
statusCode = 201;
} catch (err) {
responseBody = `Unable to put product: ${err}`;
statusCode = 403;
}
const response = {
statusCode: statusCode,
headers: {
"Content-Type": "application/json"
},
body:responseBody
}
return response
}
};
Your lambda function will finish immediate when it has been called, because you use .forEach to loop through all the records. This mean all your requests to insert to DynamoDB will be cancel before they finish their yobs.
I have 2 solutions for your case:
Wait until lambda callback stack is clear
Just prepend a "config" line to your lambda function
context.callbackWaitsForEmptyEventLoop = true;
Use old school for loop instead of forEach (recommended). forEach use callback style to solve each item, then it will not work as our expectly with async/await keyword.
var AWS = require('aws-sdk');
var today = new Date();
var date = today.getFullYear() + '-' + (today.getMonth() + 1) + '-' + today.getDate();
var hour = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds();
exports.handler = async (event, context) => {
//console.log('Received event:', JSON.stringify(event, null, 2));
for (const record of event.Records) { // for of instead of forEach
const load = new Buffer(record.kinesis.data, 'base64').toString('ascii');
const payload = JSON.parse(load);
if (payload.FaceSearchResponse != null) {
for (const face of payload.FaceSearchResponse) { // for of instead of forEach
if (face.MatchedFaces != null &&
Object.keys(face.MatchedFaces).length > 0) {
var id = JSON.stringify(face.MatchedFaces[0].Face.ExternalImageId, null, 4);
//this is hard code it ---needs to split string from kinesis(id)
await insertDynamo(date, hour, '0001'); // wait until task finish then solve next item
}
else {
//do nothing
}
}
}
}
return `Successfully processed ${event.Records.length} records.`;
};
var insertDynamo = function (date, hour, id) {
// What is this?????????
// exports.handler = async (event, context) => {
// }
const documentClient = new AWS.DynamoDB.DocumentClient();
const params = {
TableName: "users",
Item: {
badgeNumber: id,
assistance: {
date: date,
hour: hour
}
},
ConditionExpression: 'attribute_not_exists(badgenumber)'
};
return documentClient.put(params).promise(); // enough for us
};
I am declaring a function using Node.js, but it is being non callable.
I do not understand what is wrong since I declared it as the same way as the other functions and this is the only one that is being non callable.
My code:
const mysql = require('mysql');
const mainKey = '';
const con = mysql.createConnection({
host: "localhost",
user: "root",
database: "hotels"
});
function getUsers() {
return new Promise(function(resolve, reject) {
var users = new Array();
const sql = "SELECT * FROM users";
con.query(sql, function(err, result, fields) {
if(err) throw err;
users = [];
for(var i = 0; i<result.length; i++) {
users.push([result[i].id, result[i].user, result[i].password]);
}
resolve(users);
});
});
}
function regUser(user, password, key) { //this function is non callable
return new Promise(function(resolve, reject){
console.log(test)
});
}
function getHotelNames(idUser) {
return new Promise(function(resolve, reject){
var hotelNames = new Array();
const sql = "SELECT * FROM hotels WHERE user=" + idUser;
con.query(sql, function (err, result, fields) {
if (err) throw err;
hotelNames = [];
for(var i=0; i<result.length; i++) {
hotelNames.push(escape(result[i].name));
}
resolve(hotelNames);
});
})
}
function getURLs() {
return new Promise(function(resolve, reject){
var urlsHotels = new Array();
const sql = "SELECT * FROM hotels";
con.query(sql, function (err, result) {
if (err) throw err;
urlsHotels = [];
for(var i=0; i<result.length; i++) {
urlsHotels.push(result[i].url);
}
resolve(urlsHotels);
});
})
}
function insertValues(hotelNames, url, name, idUser) {
return new Promise(function(resolve, reject){
const id = hotelNames.length+1
const sql = "INSERT INTO hotels (id, name, url, user) VALUES (" + id + ", '" + name + "', '" + url + "', " + idUser +")";
con.query(sql, function (err, result) {
if (err) resolve(['errorNewHotel']); //throw err;
resolve(['NewHotel'])
});
})
}
function deleteValues(name) {
return new Promise(function(resolve, reject){
const sql = "DELETE FROM hotels WHERE name = '" + name + "'";
console.log(sql)
con.query(sql, function (err, result) {
if (err) resolve(['errorDeletingHotel']); //throw err;
resolve(['deletedHotel'])
});
})
}
const funcGetUsers = async ()=> {
const users = await getUsers();
return users;
}
const funcRegisterUser = async (user, password, key)=> {
const regUser = await regUser(user, password, key); //I am calling the function here
return regUser;
}
const funcGetHotelNames = async (idUser)=> {
const hotelNames = await getHotelNames(idUser);
return hotelNames;
}
const funcGetURLs = async ()=> {
const urls = await getURLs();
return urls;
}
const funcInsertValues = async(hotelNames, url, name, idUser)=> {
const message = await insertValues(hotelNames, url, name, idUser);
return message;
}
const funcDeleteValues = async(name)=> {
const message = await deleteValues(name);
return message;
}
module.exports.funcGetUsers = funcGetUsers;
module.exports.funcRegisterUser = funcRegisterUser;
module.exports.funcGetHotelNames = funcGetHotelNames;
module.exports.funcGetURLs = funcGetURLs;
module.exports.funcInsertValues = funcInsertValues;
module.exports.funcDeleteValues = funcDeleteValues;
Even my code editor, Visual Studio Code says it is never read
That is what I get when I try to run function:
I do not think the issue comes from the main file (server.js), so I have tried to copy all the code in a new file and Visual Studio was still saying that it is never read while the other functions were fine.
What am I doing wrong? I do not get it.
Thank you in advance.
The real Javascript error is:
ReferenceError: regUser is not defined
In
const funcRegisterUser = async (user, password, key)=> {
const regUser = await regUser(user, password, key); //I am calling the function here
return regUser;
}
You're trying to assign to a variable named regUser while also calling a function named regUser. But, because you declare a variable named regUser inside that function, any references to a variable named regUser inside that function will refer to that regUser variable. So your await regUser(...) is trying to await the variable which has not been assigned to yet - it's in the temporal dead zone.
Just use a different variable name, and you'll avoid the name collision:
const funcRegisterUser = async (user, password, key)=> {
const result = await regUser(user, password, key);
return result;
}
Or, just return the Promise itself, no need to await something you immediately return:
const funcRegisterUser = (user, password, key) => (
regUser(user, password, key)
);
Or, even better, since funcRegisterUser is just calling regUser, maybe leave out funcRegisterUser entirely, and just export regUser:
module.exports.funcRegisterUser = regUser;
The problem is not in the function, but how you call it.
const regUser = await regUser(user, password, key);
This line will create a new constant regUser as undefined, then try to invoke it as a function. Your function is shadowed in outer scope, and thus inaccessible. The solution is simple: change the name of your constant.
I tried to get a signed URL in a node.js app, my code is shown below.
var AWS = require('aws-sdk');
const fs = require("fs");
var options = { keypairId: 'keypairId', privateKeyPath: 'privateKeyPath', expireTime: (new Date().getTime() + 3000) };
var url = 'cloudfrontURL' + objectpath;
const key = fs.readFileSync('privateKeyPath').toString("ascii");
const id = 'keypairId';
const signer = new AWS.CloudFront.Signer(id, key);
const params = {
url: url,
expires: 1538999532,
};
signer.getSignedUrl(params, function (err, data) {
if (err) { console.log(err) }
console.log(data);
});
but I am getting an error, which is shown below
AWS.CloudFront.Signer is not a constructor
What is the reason for this?
I cannot figure out how to upload files into AWS S3 using KMS encryption from the Node JS SDK. I keep getting a 403: Access Denied error. I am able to get files from AWS S3 using KMS.
I am reusing most of the code from https://github.com/gilt/node-s3-encryption-client
Main Class
var fs = require('fs'),
AWS = require('aws-sdk'),
crypt = require("./crypt"),
kms,
s3;
const metadataCipherAlgorithm = 'cipher-algorithm',
metadataDecryptedEncoding = 'decrypted-encoding'
metadataKmsKeyName = 'x-amz-key';
/**
* Constructor - Initializes S3 sdk connection
*/
function S3FileStreamer(key, secret, region) {
if (region) {
AWS.config.region = region;
}
//set credentials if passed in
if (key && secret) {
AWS.config.update({accessKeyId: key, secretAccessKey: secret})
}
s3 = new AWS.S3({signatureVersion: "v4"});
kms = new AWS.KMS({apiVersion: '2014-11-01'});
}
S3FileStreamer.prototype.uploadFile = function(bucket, key, kmsKey, filename, onComplete) {
var params = {
Bucket: bucket,
Key: key,
Body: fs.readFileSync(filename),
ContentType: getMimeType(filename)
};
params.KmsParams = {
KeyId: kmsKey,
KeySpec: 'AES_256'
}
kmsUpload(params, function(err, data) {
if (err) onComplete(err, null);
else {
onComplete(err, data);
}
});
};
function kmsUpload(params, callback) {
var kmsParams = params.KmsParams
if (kmsParams && kmsParams.KeyId) {
kms.generateDataKey(kmsParams, function(err, kmsData) {
if (err) {
callback(err, null);
} else {
var helper = new crypt.Helper(kmsData.Plaintext.toString('base64'), {algorithm: params.CipherAlgorithm, decryptedEncoding: params.DecryptedEncoding});
params.Body = helper.encrypt(params.Body);
params.Metadata = params.Metadata || {};
params.Metadata[metadataKmsKeyName] = kmsData.CiphertextBlob.toString('base64');
if (params.CipherAlgorithm) params.Metadata[metadataCipherAlgorithm] = params.CipherAlgorithm;
if (params.DecryptedEncoding) params.Metadata[metadataDecryptedEncoding] = params.DecryptedEncoding;
putObject(params, callback);
}
})
} else {
putObject(params, callback);
}
}
function putObject(params, callback) {
delete params.KmsParams;
delete params.CipherAlgorithm;
delete params.DecryptedEncoding;
s3.putObject(params, callback);
}
Crypt class
var crypto = require('crypto');
/*
options:
algorithm: Anything from crypto.getCiphers()
decryptedEncoding: 'utf8', 'ascii', or 'binary'
outputEncoding: 'binary', 'base64', or 'hex'
*/
exports.Helper = function(password, options) {
this.password = password;
options = options || {};
this.algorithm = options.algorithm || 'aes-256-cbc';
this.decryptedEncoding = options.decryptedEncoding || 'utf8';
this.encryptedEncoding = options.encryptedEncoding || 'base64';
}
exports.Helper.prototype.encrypt = function(unencrypted) {
var cipher = crypto.createCipher(this.algorithm, this.password);
return cipher.update(unencrypted, this.decryptedEncoding, this.encryptedEncoding) + cipher.final(this.encryptedEncoding);
}
exports.Helper.prototype.decrypt = function(encrypted) {
var decipher = crypto.createDecipher(this.algorithm, this.password);
return decipher.update(encrypted, this.encryptedEncoding, this.decryptedEncoding) + decipher.final(this.decryptedEncoding);
}
Is there something I am missing here, an extra metadata tag that needs to be set?
Is the keyId parameter that is passed to the kms generateDataKey method supposed to be in some sort of unique format? I am just simply passing in my key.
While the other answer about checking permissions is undoubtedly correct. I had a hard time finding a good example of how to decode S3 objects using the AES GCM encryption algorithm. I managed to get this code to work based on the aws ruby sdk (as I found the node-s3-encryption-client a little old).
/**
* Decrypt s3 file data
* #param {object} objectData result of s3 get call
* #param {Function} callback function(err, data) returns error or decrypted data
*/
function decrypt(objectData, callback) {
var metadata = objectData.Metadata || {};
var kmsKeyBase64 = metadata['x-amz-key-v2'];
var iv = metadata['x-amz-iv'];
var tagLen = (metadata['x-amz-tag-len'] || 0)/8;
var algo = metadata['x-amz-cek-alg'];
var encryptionContext = JSON.parse(metadata['x-amz-matdesc']);
switch (algo) {
case 'AES/GCM/NoPadding':
algo = 'aes-256-gcm';
break;
case 'AES/CBC/PKCS5Padding':
algo = 'aes-256-cbc';
break;
default:
callback(new Error('Unsupported algorithm: ' + algo), null);
return;
}
if (typeof (kmsKeyBase64) === 'undefined') {
callback(new Error('Missing key in metadata'), null);
return;
}
var kmsKeyBuffer = new Buffer(kmsKeyBase64, 'base64');
kms.decrypt({
CiphertextBlob: kmsKeyBuffer,
EncryptionContext: encryptionContext
}, function(err, kmsData) {
if (err) {
callback(err, null);
} else {
var decipher = crypto.createDecipheriv(algo,
kmsData.Plaintext,
new Buffer(iv, 'base64'));
if (tagLen !== 0) {
// the tag is appended to the data buffer
var tag = objectData.Body.slice(-tagLen);
decipher.setAuthTag(tag);
}
var data = objectData.Body.slice(0,-tagLen);
var dec = decipher.update(data, 'binary', 'utf8');
dec += decipher.final('utf8');
console.log("Decoded:", dec);
callback(null, dec);
}
});
}
Thanks for all the help. I figured out the solution to my question.
I went back to using just using the aws-sdk node module and took out all the code I got from the node-s3-encryption-client module.
All I needed to do in order to successfully upload a file into Amazon S3 using KMS encryption was to add two parameters before passing my params object to the putObject method. These parameters were ServerSideEncryption and SSEKMSKeyId as shown below. It now works!
var params = {
Bucket: bucket,
Key: key,
Body: fs.readFileSync(filename),
ContentType: getMimeType(filename),
ServerSideEncryption: 'aws:kms',
SSEKMSKeyId: kmsKey
};
s3.putObject(params, function(err, data) {
if (err) {
console.log(err);
} else {
console.log(data);
});