JS/TSは、バイナリデータをBASE64エンコードするビルトイン機能が無いため、大人しくライブラリを使うか自前でロジックを用意する必要がありました。
結局はライブラリを使用する場合が多いのですが、機密性の高いデータを扱う場面もあるため、ブラックボックスになるのも精神衛生的に良くないなぁと、ずっと思っていました。
とはいえ、アルファベットテーブルを拵えて6ビット区切になるようビットシフトして文字列加算して...を毎回用意するのも気が引けていました。
しかしある時「実はビルトイン機能を組み合わせるだけで簡単に出来るのでは?」と単純な思い付きで書いたコードがすんなり動いてしまったので、本当に大丈夫なのか有識者の意見も欲しく、こうして記事にしてみました。
本体コード
function base64encode(data:Uint8Array){
return btoa([...data].map(n => String.fromCharCode(n)).join(""));
}
function base64decode(data:string){
return new Uint8Array([...atob(data)].map(s => s.charCodeAt(0)));
}
エンコードは以下のような手順です。
- バイト配列を配列リテラルへ展開
- 1バイト(1要素)ずつASCII文字列(
\0
~ÿ
)へ変換 - 全要素を結合しBASE64エンコード
デコードは以下の手順です。
- BASE64デコードし配列リテラルへ展開
- 1文字(1要素)ずつASCII値(
0x00
~0xFF
)へ変換 - 配列リテラルをバイト配列へ変換
検証コード
const sample = new Uint8Array(new Float32Array(0x00400000).map(() => Math.random()).buffer);
async function hash(data:Uint8Array){
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", data))].map(n => n.toString(16).toUpperCase().padStart(2, "0")).join("");
}
const result = base64decode(base64encode(sample));
console.log(hash(sample));
console.log(hash(result));
上記のように、何回かランダムデータを流し込んでハッシュを取りましたが、全て一致していたので、制御文字を含む文字列へ変換することによるデータ欠損...なんてことは無さそうでした。
また、個人的に気になった「atob()
/ btoa()
がそんな大量の入出力に耐えれるのかよ」問題も、とりあえず100MBくらいなら動作したので、実用には耐えれそうな感じでした。
ただ、これらは同期関数なので、大容量データを変換したいなら、本体関数はWebWorkerなどの別プロセスへ置いて、データはTransferableオブジェクトとして転送する方法が賢いかも知れません。
これは使えるのでは...!?