日本国内のシステムでは、いまだに文字コードとしてSJIS(シフトJIS, Shift JIS)が使われているものがある。電文の送受信にあたり、暗号化が必要となる場合は暗号化ライブラリで文字コードを指定して暗号化・復号する必要がある。
Node.jsでSJIS文字列を3DES暗号化したのでメモする。
※セキュリティに関する内容を含むため、十分に注意して実装ください。本記事ではライブラリの組み合わせ方をメイン観点としています。
- 参考
- 環境情報
- TypeScript
- SJISバイト列のbase64をcrypto-jsに渡すのがポイント
- パディング
- JavaScript
- エラーメモ
- まとめ - Node.jsでSJIS文字列を3DES暗号化する
- 関連情報
参考
こちらの記事を参考にさせていただいたのですが、手元でうまく動かず、抜粋して動作を確認しました。
こちらも実装したいことは近い。
環境情報
Node.jsバージョン
- Node.js:
v16.17.0
- npm:
8.19.2
- TypeScript:
4.8.3
使用ライブラリ
- crypto-js:
4.1.1
- iconv-lite:
0.6.3
TypeScript
npm install
npm install crypto-js npm install --save-dev @types/crypto-js npm install iconv-lite
encrypt.ts
暗号化方式は説明割愛。2バイト3文字なので、パディングとして半角スペース2文字を追加する。パディング動的生成は後述。パディング誤りは後段のエラー参照。
import { TripleDES, mode, pad, enc } from "crypto-js"; import * as iconv from "iconv-lite"; const tripleDesEncryptKey = enc.Utf8.parse("123456789012345678901234"); const tripleDesIv = enc.Utf8.parse("12345678"); // SJIS console.log(iconv.encode("テスト ", "Shift_JIS")); console.log(iconv.encode("テスト ", "Shift_JIS").toString()); console.log(iconv.encode("テスト ", "Shift_JIS").toString("base64")); // UTF-8 // console.log(iconv.encode("テスト ", "UTF-8")); // console.log(iconv.encode("テスト ", "UTF-8").toString("base64")); // encrypt const encrypt: string = TripleDES.encrypt( enc.Base64.parse(iconv.encode("テスト ", "Shift_JIS").toString("base64")), tripleDesEncryptKey, { iv: tripleDesIv, mode: mode.CBC, padding: pad.NoPadding } ).toString(); console.log("encrypt:" +encrypt); // decrypt const decrypt: string = TripleDES.decrypt((encrypt), tripleDesEncryptKey, { iv: tripleDesIv, mode: mode.CBC, padding: pad.NoPadding } ).toString(enc.Base64); console.log("decrypt(base64): "+decrypt); console.log("decrypt: "+iconv.decode(Buffer.from(decrypt,"base64"),"Shift_JIS"));
実行結果
$ ts-node ./encrypt.ts <Buffer 83 65 83 58 83 67 20 20> �e�X�g g2WDWINnICA= encrypt:/q0hZkvJsZo= decrypt(base64): g2WDWINnICA= decrypt: テスト
暗号化対象文字列はターミナル上では文字化けする。(期待通り)
※decrypt文字列の末尾には半角空白2文字が残っている
SJISバイト列のbase64をcrypto-jsに渡すのがポイント
const encrypt: string = TripleDES.encrypt( enc.Base64.parse(iconv.encode("テスト ", "Shift_JIS").toString("base64")),
部分について、
(method) CipherHelper.encrypt(message: string | CryptoJS.lib.WordArray, key: string | CryptoJS.lib.WordArray, cfg?: CipherOption | undefined): CryptoJS.lib.CipherParams
対象文字列はstring, CryptoJS.lib.WordArrayのいずれかとなるが、stringを渡すとUTF-8で扱われてしまうため、CryptoJS.lib.WordArrayを使用する。CryptoJS.lib.WordArrayを生成するとき、enc.xxx.parseでSJISは直接扱えないようなので、iconvでSJISバイト列をbase64にしたものを渡す。
復号するときも同様に、base64からバイト列にして、SJISとしてデコードする。
パディング
3DES暗号化にあたり、対象文字バイト列は8の倍数とする必要がある。このときも文字列をバイト列で扱うことにより、SJISを想定通り扱えるようにする。
挙動確認のためconsole.logメイン。8の倍数のときはパディング追加しないよう二重で剰余計算しているが、ifとの効率比較は未検証。
import * as iconv from "iconv-lite"; const str = "テスト"; const str_sjis = iconv.encode(str, "Shift_JIS"); const length = Buffer.byteLength(iconv.encode(str, "Shift_JIS")); const padding = Buffer.alloc((8 - (length % 8)) % 8, " "); console.log(iconv.encode(str, "Shift_JIS").toString()); console.log(iconv.encode(str, "Shift_JIS").toString("base64")); console.log(iconv.encode(str, "Shift_JIS")); console.log(Buffer.byteLength(iconv.encode(str, "Shift_JIS"))); console.log(padding); console.log(Buffer.concat([str_sjis,padding])); console.log(Buffer.concat([str_sjis,padding]).toString()); console.log(iconv.decode(Buffer.concat([str_sjis,padding]),"Shift_JIS").toString());
"テスト": 半角スペース2つ追加される。
$ ts-node ./padding.ts �e�X�g g2WDWINn <Buffer 83 65 83 58 83 67> 6 <Buffer 20 20> <Buffer 83 65 83 58 83 67 20 20> g2WDWINnICA= テスト
"テストテ": パディング不要
�e�X�g�e g2WDWINng2U= <Buffer 83 65 83 58 83 67 83 65> 8 <Buffer > <Buffer 83 65 83 58 83 67 83 65> g2WDWINng2U= テストテ
"テストテスト": 半角スペース4つ追加される。
�e�X�g�e�X�g g2WDWINng2WDWINn <Buffer 83 65 83 58 83 67 83 65 83 58 83 67> 12 <Buffer 20 20 20 20> <Buffer 83 65 83 58 83 67 83 65 83 58 83 67 20 20 20 20> g2WDWINng2WDWINnICAgIA== テストテスト
"test": 半角文字列でも期待通り。
test dGVzdA== <Buffer 74 65 73 74> 4 <Buffer 20 20 20 20> <Buffer 74 65 73 74 20 20 20 20> dGVzdCAgICA= test
パディング生成版 encrypt.ts
import { TripleDES, mode, pad, enc } from "crypto-js"; import * as iconv from "iconv-lite"; const tripleDesEncryptKey = enc.Utf8.parse("123456789012345678901234"); const tripleDesIv = enc.Utf8.parse("12345678"); const str = "テスト"; const str_sjis = iconv.encode(str, "Shift_JIS"); const length = Buffer.byteLength(iconv.encode(str, "Shift_JIS")); const padding = Buffer.alloc((8 - (length % 8)) % 8, " "); // encrypt const encrypt: string = TripleDES.encrypt( enc.Base64.parse(Buffer.concat([str_sjis,padding]).toString("base64")), tripleDesEncryptKey, { iv: tripleDesIv, mode: mode.CBC, padding: pad.NoPadding } ).toString(); console.log("encrypt:" +encrypt); // decrypt const decrypt: string = TripleDES.decrypt((encrypt), tripleDesEncryptKey, { iv: tripleDesIv, mode: mode.CBC, padding: pad.NoPadding } ).toString(enc.Base64); console.log("decrypt(base64): "+decrypt); console.log("decrypt: "+iconv.decode(Buffer.from(decrypt,"base64"),"Shift_JIS"));
JavaScript
npm install
npm install crypto-js npm install iconv-lite
encrypt.js
基本は同上。解説はTypeScript段落参照。import
をrequire
に置き換え、変数の型宣言を削除する。
var CryptoJS = require("crypto-js"); var TripleDES = CryptoJS.TripleDES; var mode = CryptoJS.mode; var pad = CryptoJS.pad; var enc = CryptoJS.enc; var iconv = require("iconv-lite"); const tripleDesEncryptKey = enc.Utf8.parse("123456789012345678901234"); const tripleDesIv = enc.Utf8.parse("12345678"); const str = "テスト"; const str_sjis = iconv.encode(str, "Shift_JIS"); const length = Buffer.byteLength(iconv.encode(str, "Shift_JIS")); const padding = Buffer.alloc((8 - (length % 8)) % 8, " "); // encrypt const encrypt = TripleDES.encrypt( enc.Base64.parse(Buffer.concat([str_sjis,padding]).toString("base64")), tripleDesEncryptKey, { iv: tripleDesIv, mode: mode.CBC, padding: pad.NoPadding } ).toString(); console.log("encrypt:" +encrypt); // decrypt const decrypt = TripleDES.decrypt((encrypt), tripleDesEncryptKey, { iv: tripleDesIv, mode: mode.CBC, padding: pad.NoPadding } ).toString(enc.Base64); console.log("decrypt(base64): "+decrypt); console.log("decrypt: "+iconv.decode(Buffer.from(decrypt,"base64"),"Shift_JIS"));
実行結果
$ node ./encrypt.js encrypt:/q0hZkvJsZo= decrypt(base64): g2WDWINnICA= decrypt: テスト
エラーメモ
パディング誤り
$ ts-node ./encrypt.ts <Buffer 83 65 83 58 83 67 20 20> �e�X�g g2WDWINnICA= encrypt:1jo7Sks1 decrypt(base64): 3wnYCS71 decrypt: ゚ リ .�
暗号化・復号自体はされるが、内容が解読できない。
他の言語だとこのようなエラーが出力される。
Input length not multiple of 8 bytes
inputがUTF-8
enc.Base64.parse(iconv.encode("テスト ", "UTF-8").toString("base64")),
としinputがUTF-8の場合
(enc.Utf8.parse("テスト ")
相当)
(UTF-8 3バイトに合わせてパディング調整)
$ node ./encrypt.js <Buffer 83 65 83 58 83 67 20 20> �e�X�g g2WDWINnICA= encrypt:aGUiZwVSUk3FOASm0ZxQcg== decrypt(base64): 44OG44K544OIICAgICAgIA== decrypt: 繝�繧ケ繝�
見慣れた文字化けで復元すると??ス??
となっている。
単純なstr.padEndは日本語処理不可
console.log(str.padEnd(length + 8 - (length % 8), "x"));
とすると、マルチバイト文字を1文字カウントするため期待動作とならない。(見やすくするためxでパディング)
testxxxx テストxxxxx
まとめ - Node.jsでSJIS文字列を3DES暗号化する
crypto-jsとiconv-liteを組み合わせ、SJISバイト列のbase64を渡すことで、SJIS文字列を3DES暗号化することができた。