▼ おーぷん ▼
ライブラリなしでデータの暗号化と復号をしたくて調べたら思ったより複雑だったのでメモです
まずこれが暗号化と復号をするサンプルです
main 関数がサンプルの実行なのでここを変更して色々試せます
ここからは詳しい解説です
この関数にアルゴリズムとキーと暗号化したいデータを入れます
(注意)crypto.subtle は https のページか拡張機能じゃないと使えなくなってます
◯ RSA-OAEP
◯ AES-CTR
◯ AES-CBC
◯ AES-GCM
どれがいいのかよくわからないので、ブロックチェーンて聞いたことあるし
くらいな感じで AES-CBC にしました
アルゴリズムに応じた追加のデータが必要で、 AES-CBC の場合は iv という 16 バイトの初期ベクトルが必要みたいです
これはランダムなものでよくて、毎回同じじゃないほうが良いらしいのでランダムなデータを作ります
ランダムなデータの作成には crypto.getRandomValues を使います
欲しい長さ分の Uint8Array を引数に渡すとランダムな値を入れてくれます
iv は暗号データを復号するときにも使うようで同じものじゃないといけません
これ自体はパスワードみたいに隠すものでもないみたいなので結果の一部として return に含めています
iv は 16 バイト固定なので、復号のときは最初の 16 バイトを iv にして残りを暗号データとして復号します
普通に作る場合は crypto.subtle.generateKey を使います
しかし、これだとユーザのパスワードと関連付けられません
パスワードからキーを作るには代わりに crypto.subtle.importKey を使えば良いようです
importKey を使うときのパスワードは、ユーザが入力するような自由な文字数にはできず、決められた長さのバイト列にしないといけません
ここでは 256bit にするので、 SHA-256 のハッシュ値を使います
SHA-256 を計算するには crypto.subtle.digest を使います
そのときにパスワードの文字列はバイト列に変換する必要があります
これは TextEncoder を使うと簡単に変換できます
encodeText 関数がこの変換をするための関数です
これでキーを作るのに必要データは準備できたのでキーを作れます
importKey はいくつかのフォーマットに対応していて、今回みたいなパスワードからキーを作りたいときは "raw" を選択します
次の引数には SHA-256 で作った 256bit のバイト列を入れます
その次には、このキーを使うアルゴリズムを指定します
その次は crypto.subtle.exportKey や crypto.subtle.wrapKey を使えるかどうかを指定します
特に使う予定はないので false です
最後にこのキーをどういう用途で使うかを設定します
暗号化と復号なので encrypt と decrypt を指定しました
この処理をまとめて、パスワードを入れるとキーを取得できるようにしたのが passphraseToKey 関数です
これもパスワードと同じでバイト列にしないといけないので encodeText 関数で変換します
encrypt 関数はキーとデータを入れると AES-CBC アルゴリズムで暗号化するようにしてるので、この関数に入れれば暗号化された結果を取得できます
結果は Uint8Array のバイト列です
ファイルに保存したりするならちょっとした変換が必要です
この関数にアルゴリズムとキーと暗号化されたデータを入れます
アルゴリズムは暗号化のときに使ったものと一緒です
iv も必要なので encrypt 関数の結果の最初の 16 バイトを取り出して iv にします
キーも暗号化のときに使ったものと一緒です
passphraseToKey に同じパスワードを入れると同じキーになります
ユーザが入力したパスワードが間違ってると間違ったキーになって復号に失敗します
データは encrypt の結果から iv の 16 バイトを取り除いた残りの部分です
decrypt 関数に渡したものが正しければ暗号化する前のデータが取得できます
ただしこれもバイト列になっていて、データを encodeText に通したあとのものです
文字列に戻すには TextDecoder を使います
main 関数ではバイト列が一致してることの確認だけなのでデコードは省略してます
まずこれが暗号化と復号をするサンプルです
const encrypt = async (key, data) => {
const iv = crypto.getRandomValues(new Uint8Array(16))
const result = await crypto.subtle.encrypt(
{
name: "AES-CBC",
iv,
},
key,
data
)
return new Uint8Array([...iv, ...new Uint8Array(result)])
}
const decrypt = async (key, cipher) => {
const iv = cipher.slice(0, 16)
const data = cipher.slice(16)
const result = await crypto.subtle.decrypt(
{
name: "AES-CBC",
iv,
},
key,
data
)
return new Uint8Array(result)
}
const encodeText = text => new TextEncoder().encode(text)
const passphraseToKey = async passphrase => {
const pass256 = await crypto.subtle.digest(
{ name: "SHA-256" },
encodeText(passphrase)
)
const key = await crypto.subtle.importKey(
"raw",
pass256,
{
name: "AES-CBC",
length: 256,
},
false,
["encrypt", "decrypt"]
)
return key
}
const main = async () => {
const key = await passphraseToKey("password")
const text = JSON.stringify({ a: 1, b: "ああ" })
const bin = encodeText(text)
console.log({ key, bin })
const encrypted = await encrypt(key, bin)
console.log({ encrypted })
const decrypted = await decrypt(key, encrypted)
console.log({ decrypted })
console.log(bin.join() === decrypted.join())
}
main()
main 関数がサンプルの実行なのでここを変更して色々試せます
ここからは詳しい解説です
暗号化
暗号化をするために使う関数は crypto.subtle.encrypt ですこの関数にアルゴリズムとキーと暗号化したいデータを入れます
(注意)crypto.subtle は https のページか拡張機能じゃないと使えなくなってます
アルゴリズム
アルゴリズムはここを見ると 4 種類あるようです◯ RSA-OAEP
◯ AES-CTR
◯ AES-CBC
◯ AES-GCM
どれがいいのかよくわからないので、ブロックチェーンて聞いたことあるし
くらいな感じで AES-CBC にしました
アルゴリズムに応じた追加のデータが必要で、 AES-CBC の場合は iv という 16 バイトの初期ベクトルが必要みたいです
これはランダムなものでよくて、毎回同じじゃないほうが良いらしいのでランダムなデータを作ります
ランダムなデータの作成には crypto.getRandomValues を使います
欲しい長さ分の Uint8Array を引数に渡すとランダムな値を入れてくれます
const iv = crypto.getRandomValues(new Uint8Array(16))
iv は暗号データを復号するときにも使うようで同じものじゃないといけません
これ自体はパスワードみたいに隠すものでもないみたいなので結果の一部として return に含めています
return new Uint8Array([...iv, ...new Uint8Array(result)])
iv は 16 バイト固定なので、復号のときは最初の 16 バイトを iv にして残りを暗号データとして復号します
キー
次にキーですが、パスワード文字列ではなくて特別なフォーマットのキーデータが必要です普通に作る場合は crypto.subtle.generateKey を使います
しかし、これだとユーザのパスワードと関連付けられません
パスワードからキーを作るには代わりに crypto.subtle.importKey を使えば良いようです
importKey を使うときのパスワードは、ユーザが入力するような自由な文字数にはできず、決められた長さのバイト列にしないといけません
ここでは 256bit にするので、 SHA-256 のハッシュ値を使います
SHA-256 を計算するには crypto.subtle.digest を使います
そのときにパスワードの文字列はバイト列に変換する必要があります
これは TextEncoder を使うと簡単に変換できます
encodeText 関数がこの変換をするための関数です
const encodeText = text => new TextEncoder().encode(text)
これでキーを作るのに必要データは準備できたのでキーを作れます
const key = await crypto.subtle.importKey(
"raw",
pass256,
{
name: "AES-CBC",
length: 256,
},
false,
["encrypt", "decrypt"]
)
importKey はいくつかのフォーマットに対応していて、今回みたいなパスワードからキーを作りたいときは "raw" を選択します
次の引数には SHA-256 で作った 256bit のバイト列を入れます
その次には、このキーを使うアルゴリズムを指定します
その次は crypto.subtle.exportKey や crypto.subtle.wrapKey を使えるかどうかを指定します
特に使う予定はないので false です
最後にこのキーをどういう用途で使うかを設定します
暗号化と復号なので encrypt と decrypt を指定しました
この処理をまとめて、パスワードを入れるとキーを取得できるようにしたのが passphraseToKey 関数です
データ
最後の必要なものは暗号化するデータですこれもパスワードと同じでバイト列にしないといけないので encodeText 関数で変換します
encrypt 関数はキーとデータを入れると AES-CBC アルゴリズムで暗号化するようにしてるので、この関数に入れれば暗号化された結果を取得できます
結果は Uint8Array のバイト列です
ファイルに保存したりするならちょっとした変換が必要です
復号
復号に使う関数は crypto.subtle.decrypt ですこの関数にアルゴリズムとキーと暗号化されたデータを入れます
アルゴリズムは暗号化のときに使ったものと一緒です
iv も必要なので encrypt 関数の結果の最初の 16 バイトを取り出して iv にします
キーも暗号化のときに使ったものと一緒です
passphraseToKey に同じパスワードを入れると同じキーになります
ユーザが入力したパスワードが間違ってると間違ったキーになって復号に失敗します
データは encrypt の結果から iv の 16 バイトを取り除いた残りの部分です
decrypt 関数に渡したものが正しければ暗号化する前のデータが取得できます
ただしこれもバイト列になっていて、データを encodeText に通したあとのものです
文字列に戻すには TextDecoder を使います
main 関数ではバイト列が一致してることの確認だけなのでデコードは省略してます
▲ くろーず ▲