designetwork

ネットワークを軸としたIT技術メモ

Node.jsでSJIS文字列を3DES暗号化する

日本国内のシステムでは、いまだに文字コードとしてSJIS(シフトJIS, Shift JIS)が使われているものがある。電文の送受信にあたり、暗号化が必要となる場合は暗号化ライブラリで文字コードを指定して暗号化・復号する必要がある。

Node.jsでSJIS文字列を3DES暗号化したのでメモする。
※セキュリティに関する内容を含むため、十分に注意して実装ください。本記事ではライブラリの組み合わせ方をメイン観点としています。

参考

qiita.com

こちらの記事を参考にさせていただいたのですが、手元でうまく動かず、抜粋して動作を確認しました。

ja.stackoverflow.com

こちらも実装したいことは近い。

環境情報

Node.jsバージョン

  • Node.js: v16.17.0
  • npm: 8.19.2
  • TypeScript: 4.8.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段落参照。importrequireに置き換え、変数の型宣言を削除する。

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暗号化することができた。

関連情報

designetwork.daichi703n.com