AWS SESの受信メールを暗号化してs3に保存しLambdaで読み込む

公開日時
ses

SESでメール受信を行う際にS3にデータの保存ができる。

この時「Encrypt Message」にチェックを入れると、KMSを使ってS3に保存するデータの暗号化を行うことができる。

この暗号化されたデータをLambdaで複合しようとした際にハマったので対応方法を載せておく。

ドキュメントを読んでもイマイチよく分からず、↓を参考にして複合処理を実装した。

'use strict';

const crypto = require("crypto");
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
const kms = new AWS.KMS();

const s3Bucket = process.env.S3_BUCKET;
const s3PathPrefix = process.env.S3_PATH_PREFIX;

// https://stackoverflow.com/questions/54543410/decrypt-ses-message-from-s3-with-kms-node
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 Error('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 body = objectData.Body
      const data = body.slice(0, -tagLen);
      const decipher = crypto.createDecipheriv( algo, res.Plaintext, Buffer.from(iv, 'base64'));
      if (tagLen !== 0) {
        const tag = body.slice(-tagLen);
        decipher.setAuthTag(tag);
      }
      let dec = decipher.update(data, 'binary', 'utf8');
      dec += decipher.final('utf8');
      return dec;
    }).catch((err) => {
      console.error(err.message);
      throw new Error('Not able to decrypt message: ', err);
    });

  return returnValue;
};

exports.handler =  async function(event, context) {
  const messageId = event.Records[0]['ses']['mail']['messageId'];
  const request = {
    Bucket: s3Bucket,
    Key: `${s3PathPrefix}${messageId}`,
  };

  try {
    const objectData = await s3.getObject(request).promise();
    const decryptedMessage = await decryptS3Message(objectData);

    console.log(decryptedMessage);

    return { status: 'success' };
  } catch (error) {
    console.error(error, error.stack);
    return Error;
  }
};

これで複合したメールデータをLambdaで扱えるようになった。

RubyやGoのSDKではS3::Encryption::Clientが使えるようだが、jsやpythonにはないので今回のように独自実装する必要があり大変。

js SDKでもS3::Encryption::Clientサポート来ないかなぁ。

参考


Related #lambda

マネージメントコンソール上のエディタでLambdaのコードを書く際にnpmライブラリを追加したい

ローカルでライブラリをインストールしてからインポートする必要があった

Amazon API Gatewayのタイムアウト設定は最大29秒まで

上限緩和もできないので注意

AWS SESでメールを受信してGmailに転送する

独自ドメインメールの送受信ができるようになった

Lambdaでaws cli configureを設定できるようにする

AWS_CONFIG_FILE=/tmp/.aws/configを設定した

CDK aws-lambda-nodejsのビルド時間を短縮する

Parcel v2.0.0-beta.1を使ってローカルでバンドルする

s3の署名付きURLが有効期限より前に見れなくなってしまう

IAMロールではなくIAMユーザの権限でURLを生成する必要があった