CryptoJSで暗号化したファイルをどうしてもシェル上で復号したかった


やりたかったこと

  1. Node.jsのCryptoJSで暗号化したファイルを保存する
  2. そのファイルをシェル上で読み込み、復号する
暗号化処理(Node.js)
const crypto = require('crypto');
const fs = require('fs');

/*
 * passphrase <String>
 * salt <String>
 * keylen <Number>
 * iv <String>
 * pass <String> ファイルパス
 * inputPath <String> ファイルパス
 * outputPath <String> ファイルパス
 */

// 暗号
const key = crypto.scryptSync(passphrase, salt, keylen);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
fs.writeFileSync(pass, passphrase);

// 読み込み => 暗号化 => 書き出し
const input = fs.createReadStream(inputPath);
const output = fs.createWriteStream(outputPath);
input.pipe(cipher).pipe(appendiv).pipe(output);
復号処理(Shell)
openssl -aes-256-cbc -d \
     -pass pass:<pass> \
     -salt <salt> \
     -in <inputPath> \
     -out <outputPath>

結果: bad decrypt

できなかったこと(めんどくさくなったこと)

opensslコマンドで復号

opensslが取り扱うsaltあり暗号文は、ヘッダとして Salted__ という8byteを持つ前提になっている。

暗号文とpassのみを渡して復号

IVを使って暗号化するとき、理論上passと暗号文からkeyとIVを復元できるが、手軽にこれを実現するライブラリを見つけられなかった。

やったこと

  1. 暗号化処理でIVを暗号文に連結して渡すように変更
  2. シェルスクリプト側でもNode.jsのCryptoJSを利用して復号するように変更

※IVは平文のまま安全でない経路で受け渡しできるとされている

暗号化処理(Node.js)
const crypto = require('crypto');
const fs = require('fs');


// 暗号
const key = crypto.scryptSync(passphrase, salt, keylen);
const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
fs.writeFileSync(pass, passphrase);

const input = fs.createReadStream(inputPath);
/* ↓追加処理↓ */
// https://stackoverflow.com/questions/41594042/node-transform-stream-append-string-to-end
const appendiv = new stream.Transform({
    transform(chunk, encoding, callback) {
        callback(null, chunk);
    },
    flush(callback) {
        this.push(iv);
        callback();
    }
});
/* ↑追加処理↑ */
const output = fs.createWriteStream(outputPath);
// 読み込み => 暗号化 => IVを末尾に付加 => 書き出し
input.pipe(cipher).pipe(appendiv).pipe(output);
node-script.js
/* 復号処理(Node.jsスクリプト) */

/*
 * IV_LEN <Number> IVの長さ
 */

exports.decipher = async(options) => {
    const pass = fs.readFileSync(options.pass).toString(/* passphraseの文字コードに注意 */); 
    const key = crypto.scryptSync(pass, options.salt, options.keylen); 
    const input = fs.readFileSync(options.inputPath);
    const iv = input.slice(-1 * IV_LEN); // 後ろからIV_LEN文字がIV

    const cipher = crypto.createDecipheriv(options.cipher, key, iv);
    let decrypted = cipher.update(input.slice(0, -1 * IV_LEN)); // 後ろからIV_LEN文字目より前が暗号文
    decrypted = Buffer.concat([decrypted, cipher.final()]);

    const output = fs.writeFileSync(options.outputPath, decrypted);
}

decipher.js
/* cacjsによりcliコマンド化 */

const cac = require('cac');
const cli = cac();
const nodeScript = require('./node-script');

cli
    .command('decipher', '復号する')
    .option('--pass <pass>', 'passが記載されたファイル')
    .option('--salt <salt>', 'ソルト文字列')
    .option('--keylen <keylen>', 'keyの長さ')
    .option('--inputPath <inputPath>', '暗号化されたファイル')
    .option('--outputPath <outputPath>', '復号されたファイル')
    .action(async (options) => {
        await nodeUtils.decipher(options);
    });

cli.help();

cli.parse();
シェルスクリプト
node decipher.js decipher \
  --pass <pass> \
  --salt <salt> \
  --keylen <keylen> \
  --in <inputPath> \
  --out <outputPath>

わかったこと

暗号化と復号は同じツールを使ってやるのが楽。