Reputation: 918
I am not able to decrypt my messages I receive from my S3 bucket. They are encrypted with a KMS key. I use Node and Typescript.
I have tried some stuff but arrent able to make it work. Looking in to this links: https://github.com/gilt/node-s3-encryption-client/issues/3 and https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/SES.html
My code look like this now:
import * as AWS from 'aws-sdk';
import * as crypto from 'crypto';
const s3 = new AWS.S3({ apiVersion: '2006-03-01', region: 'eu-west-1' });
const kms = new AWS.KMS({ apiVersion: '2014-11-01', region: 'eu-west-1' });
export const handler = LambdaUtils.lambdaHandler( 'onebox-email-service-SendMailToL4PFunction', async (event) => {
const record = event.Records[0];
const request = {
Bucket: record.s3.bucket.name,
Key: record.s3.object.key
};
const data = await s3.getObject(request).promise();
const decryptData = await decryptSES(data);
return decryptData;
}
);
export const decryptSES = async (objectData) => {
const metadata = objectData.Metadata || {};
const kmsKeyBase64 = metadata['x-amz-key-v2'];
const iv = metadata['x-amz-iv'];
const tagLen = (metadata['x-amz-tag-len'] || 0) / 8;
let algo = metadata['x-amz-cek-alg'];
const 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:
log.error({Message: 'Unsupported algorithm: ' + algo});
return;
}
if (typeof (kmsKeyBase64) === 'undefined') {
log.error('Error');
}
const kmsKeyBuffer = new Buffer(kmsKeyBase64, 'base64');
const returnValue = await kms.decrypt({ CiphertextBlob: kmsKeyBuffer, EncryptionContext: encryptionContext }, (err, kmsData) => {
if (err) {
log.error({err});
return null;
} else {
const data = objectData.Body.slice(0, -tagLen);
const decipher = crypto.createDecipheriv( algo, kmsKeys.Plaintext[0], new Buffer(iv, 'base64'));
if (tagLen !== 0) {
const tag = objectData.Body.slice(-tagLen);
decipher.setAuthTag(tag);
}
let dec = decipher.update(data, 'binary', 'utf8');
dec += decipher.final('utf8');
return dec;
}
}).promise();
return returnValue;
};
I get error in my lambda that look like this:
2019-02-05T17:06:19.015Z d9593ef7-635b-47b2-b881-ede2a396f88e Error: Invalid key length at new Decipheriv (crypto.js:267:16) at Object.createDecipheriv (crypto.js:627:10) at Response.l.decrypt (/var/task/email-from-s3.js:592:232696) at Request. (/var/runtime/node_modules/aws-sdk/lib/request.js:364:18) at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:105:20) at Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:77:10) at Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:683:14) at Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10) at AcceptorStateMachine.runTo (/var/runtime/node_modules/aws-sdk/lib/state_machine.js:14:12) at /var/runtime/node_modules/aws-sdk/lib/state_machine.js:26:10 at Request. (/var/runtime/node_modules/aws-sdk/lib/request.js:38:9) at Request. (/var/runtime/node_modules/aws-sdk/lib/request.js:685:12) at Request.callListeners (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:115:18) at Request.emit (/var/runtime/node_modules/aws-sdk/lib/sequential_executor.js:77:10) at Request.emit (/var/runtime/node_modules/aws-sdk/lib/request.js:683:14) at Request.transition (/var/runtime/node_modules/aws-sdk/lib/request.js:22:10)
What I can see in my logs I get the encrypted message from my s3 bucket, but then it is not possible to decrypt it.
Can someone please help me with this? I use Node and Typescript.
Upvotes: 1
Views: 1753
Reputation: 58
After trying hard to get a grip on decrypting SES encrypted emails using the AWS SDK v3 for JavaScript, I hit a wall because there wasn't much clear info out there.
But thankfully, this thread helped a ton! Here's the code I ended up with after picking up tips from everyone here. Hopefully, it'll save others some headaches too:
**This code is based on the AWS SDK for JavaScript v3. **
Init S3Client
and execute GetObjectCommand
const client = new S3Client({
region: process.env.AWS_REGION as string,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,
},
});
const command = new GetObjectCommand({
Bucket: bucket,
Key: key,
});
Get Metadata
and Body
using the S3Client
const { Metadata, Body } = await client.send(command);
Run decrypt
functionand store the string result in decrypted
variable. Now you can do whatever you need with the content
const decrypted = await decrypt(Metadata, Body as Readable);
decrypt.ts
import { KMSClient, DecryptCommand } from '@aws-sdk/client-kms';
import { createDecipheriv } from 'crypto';
import { Readable } from 'stream';
export const decrypt = async (Metadata: Record<string, string>, Body: Readable): Promise<string> => {
const body = await streamToString(Body);
const Plaintext = await getKMS(Metadata);
const { key, iv, tag, data } = getDecryptionParams(Metadata, body, Plaintext);
//'aes-256-gcm' is required as algorithm in order to use setAuthTag method
const decipher = createDecipheriv('aes-256-gcm', key, iv).setAuthTag(tag);
const decryptedBuffer = decipher.update(data);
const decrypted = decryptedBuffer.toString('utf8') + decipher.final('utf8');
return decrypted;
};
const getDecryptionParams = (Metadata: Record<string, string>, body: Buffer, Plaintext: Uint8Array) => {
const key = Buffer.from(Plaintext);
const iv = Buffer.from(Metadata['x-amz-iv'], 'base64');
const taglen = parseInt(Metadata['x-amz-tag-len'], 10) / 8;
const tag = body.subarray(body.length - taglen);
const data = body.subarray(0, body.length - taglen);
return { key, iv, tag, data };
};
const getKMS = async (Metadata: Record<string, string>): Promise<Uint8Array> => {
const client = new KMSClient({
region: process.env.AWS_REGION as string,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,
},
});
const { Plaintext } = await client.send(
new DecryptCommand({
CiphertextBlob: Buffer.from(Metadata['x-amz-key-v2'], 'base64'),
EncryptionContext: JSON.parse(Metadata['x-amz-matdesc']),
})
);
if (!Plaintext) {
throw new Error('No Plaintext found');
}
return Plaintext;
};
const streamToString = (stream: Readable): Promise<Buffer> =>
new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
stream.on('data', (chunk) => chunks.push(chunk));
stream.on('error', reject);
stream.on('end', () => resolve(Buffer.concat(chunks)));
});
Upvotes: 0
Reputation: 918
I was got some help from coworker and we could figuring it out. The problem was with the
const decipher = crypto.createDecipheriv( algo, kmsKeys.Plaintext[0], new Buffer(iv, 'base64'));
We needed to change the kms.Plaintext
to kms.Plaintext as Buffer
and it start working. I post my hole funktion here if someone needs it for later.
import * as AWS from 'aws-sdk';
import * as crypto from 'crypto';
const kms = new AWS.KMS({ apiVersion: '2014-11-01', region: 'eu-west-1' });
export const decryptS3Message = async (objectData) => {
const metadata = objectData.Metadata || {};
const kmsKeyBase64 = metadata['x-amz-key-v2'];
const iv = metadata['x-amz-iv'];
const tagLen = (metadata['x-amz-tag-len'] || 0) / 8;
let algo = metadata['x-amz-cek-alg'];
const 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:
throw new ErrorUtils.NotFoundError('Unsupported algorithm: ' + algo);
}
if (typeof (kmsKeyBase64) === 'undefined') {
return null;
}
const kmsKeyBuffer = Buffer.from(kmsKeyBase64, 'base64');
const returnValue = await kms.decrypt({ CiphertextBlob: kmsKeyBuffer, EncryptionContext: encryptionContext }).promise()
.then((res) => {
const data = objectData.Body.slice(0, -tagLen);
const decipher = crypto.createDecipheriv( algo, res.Plaintext as Buffer, Buffer.from(iv, 'base64'));
if (tagLen !== 0) {
const tag = objectData.Body.slice(-tagLen);
decipher.setAuthTag(tag);
}
let dec = decipher.update(data, 'binary', 'utf8');
dec += decipher.final('utf8');
return dec;
}).catch((err) => {
throw new ErrorUtils.InternalServerError('Not able to decrypt message: ', err);
});
return returnValue;
};
Upvotes: 0