S3 - node:cryptoを使用したAES復号化、S3ファイルストリーム操作

# 目的

S3上の暗号化されたファイルをStreamで復号化、アップロードする

# 暗号化の種類

- 暗号化方式: AES
- 鍵ビット長: 128bit
- ブロック暗号のモード: CBC
- 初期ベクトルIV: ファイル先頭16byte

暗号化の詳細は以下を参照
[暗号技術入門04 ブロック暗号のモード〜ブロック暗号をどのように繰り返すのか〜](http://www.spiritek.co.jp/spkblog/2016/12/01/%E6%9A%97%E5%8F%B7%E6%8A%80%E8%A1%93%E5%85%A5%E9%96%8004-%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF%E6%9A%97%E5%8F%B7%E3%81%AE%E3%83%A2%E3%83%BC%E3%83%89%E3%80%9C%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF/)

# `node:crypto`の復号化実装例

[Class: Decipher](https://nodejs.org/api/crypto.html#class-decipher)に以下3パターンの例がある

- Example: Using Decipher objects as streams:
- Example: Using Decipher and piped streams:
- Example: Using the decipher.update() and decipher.final() methods:

今回使用するのは2つ目の入力用/出力用のStreamをpipeで繋ぐ方法

# ローカルの暗号化されたファイルをStreamで復号化

```javascript
import fs from 'node:fs';
import crypto from 'node:crypto';

const password = 'T012345678901234';

// ファイルの先頭16byteをIVとして返却する
async function getIv(fileName) {
    const src = fs.createReadStream(fileName, { start: 0, end: 15 });
    let iv = undefined;

    return new Promise((resolve, reject) => {
      src.on('data', chunk => {
        if (iv === undefined) {
          iv = chunk;
        }
      });
      src.on('error', error => {
        reject(error);
      });
      src.on('close', () => resolve(iv));
    });
}

// ファイルの17byte以降を復号化、ファイル出力する
async function decodeFile(fileName, iv) {
  
  const src = fs.createReadStream(fileName, { start: 16 });
  const dest = fs.createWriteStream('dest.txt');
  
  return new Promise((resolve, reject) => {
    const decipher = crypto.createDecipheriv('aes-128-cbc', password, iv);

    src.pipe(decipher).pipe(dest);
    src.on('error', error => {
      dest.destroy(error);
      reject(error);
    });
    src.on('close', () => resolve());
  });
}

async function main() {
  try {
    const fileName = 'file.bytes';
    const iv = await getIv(fileName);
    await decodeFile(fileName, iv);
  } catch (err) {
    console.log(err);
  }
}

await main();
```

# S3上の暗号化されたファイルをStreamで復号化

- [Streamを使ってS3から別のS3へCSVをコピーする](https://dev.classmethod.jp/articles/nodejs-aws-sdk-v3-csv-streaming-in-s3/#toc-4)
- [Cannot pipe output to S3 with @aws-sdk/lib-storage.](https://github.com/lovell/sharp/issues/3313#issuecomment-1204340889)

S3用のSDK`@aws-sdk/client-s3`で、S3上のファイルをStreamとして取り出すことができる
StreamをS3上にアップロードするためには、`@aws-sdk/lib-storage`を使用する
ただし、pipeを使用には`node:stream`の`PassThrough`でアップロード用のStreamを作成する必要がある

```javascript
import crypto from 'node:crypto';
import path from 'node:path';
import { PassThrough } from 'node:stream';

import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';

async function decryptFile(bucket, key) {
  const region = 'ap-northeast-1';
  const s3Client = new S3Client({
    region,
  });

  const getParam = {
    Bucket: bucket,
    Key: key,
  };

  // ファイルの先頭16byteをIVとする
  const response = await s3Client.send(
    new GetObjectCommand({
      ...getParam,
      Range: 'bytes=0-15',
    }),
  );
  const iv = await response.Body.transformToByteArray();

  // ファイルの17byte以降を入力Streamとする
  const inStream = (
    await s3Client.send(
      new GetObjectCommand({
        ...getParam,
        Range: 'bytes=16-',
      }),
    )
  ).Body;

  // 出力Streamを用意する
  const outStream = new PassThrough();

  const password = 'T012345678901234';
  const decipher = crypto.createDecipheriv('aes-128-cbc', password, iv);
  inStream.pipe(decipher).pipe(outStream);

  const upload = new Upload({
    client: s3Client,
    params: {
      Bucket: bucket,
      Key: `${path.dirname(key)}/dest.txt`,
      Body: outStream,
    },
  });
  await upload.done();
}
```

# その他参考

- [Node.jsでStreamを読むとき、chunkを文字列として結合してはいけない](https://zenn.dev/kazuma1989/scraps/766839441c03f9)