やりたかったこと
- Node.jsのCryptoJSで暗号化したファイルを保存する
- そのファイルをシェル上で読み込み、復号する
暗号化処理(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を復元できるが、手軽にこれを実現するライブラリを見つけられなかった。
やったこと
- 暗号化処理でIVを暗号文に連結して渡すように変更
- シェルスクリプト側でも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>
わかったこと
暗号化と復号は同じツールを使ってやるのが楽。