この記事は暗号化に全く詳しくない人が書いています。
遊びで使うなら問題ないと思いますが、実用する際は決して内容を鵜呑みにせず、専門家を頼ってください。
Web Crypto APIとは
Web Crypto APIは、ざっくり言うとWeb上で簡単な暗号化ができるAPIです。
これにはwindow.crypto
からアクセスできます。
window.crypto
はCrypto
インターフェースのオブジェクトになっていて、randomUUID
メソッドやsubtle
プロパティを持っています。
この記事では、subtle
プロパティに入っているSubtleCrypto
に焦点を当てて解説します。
SubtleCrypto
SubtleCrypto
は、実際に暗号化などの処理が定義されているインターフェースです。
window.crypto.subtle
から利用できます。
ここでは以下の処理を紹介します。
- 鍵の生成
- 文字列の暗号化 / 復号
鍵の生成
文字列を暗号化したり復号するには鍵が必要です。
鍵の生成にはgenerateKey
を使用します。
generateKey
関数はCryptoKeyPair
オブジェクトで履行されるプロミスを返します。
CryptoKeyPair
は公開鍵と秘密鍵のペアで、以下のプロパティを持っています。
-
publickey
: 公開鍵を示すCryptoKey
オブジェクト -
privateKey
: 秘密鍵を示すCryptoKey
オブジェクト
使ってみる
generateKey
は以下の引数を取ります。
-
algorithm
: 鍵のアルゴリズム -
extractable
: 鍵をエクスポートできるかどうか、今回はtrue
-
keyUsages
: 生成した鍵で何ができるかを入れる配列-
algorithm
に対応していない文字列を入れるとエラーになる -
RSA-OAEP
はencrypt
(暗号化)とdecrypt
(復号)ができる
-
RSA-OAEP
の場合、1のalgorithm
にはRsaHashedKeyGenParams
オブジェクトを入れます。
例えば以下のように呼び出せます。
// RsaHashedKeyGenParamsオブジェクト
const algorithm = {
name: 'RSA-OAEP', // アルゴリズムの名前
modulusLength: 4096, // 法(?)の長さ
publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 公開指数(?)
hash: 'SHA-256' // ダイジェスト関数に使うアルゴリズム名
}
// generateKeyで鍵を生成する
const keyPair = await window.crypto.subtle.generateKey(
algorithm,
true, // エクスポートできるようにする
['encrypt', 'decrypt'] // 暗号化と復号に使う
)
keyPair.publicKey // CryptoKey
keyPair.privateKey // CryptoKey
暗号化 / 復号
ここからはencrypt
やdecrypt
関数を使って、実際に文字列を暗号化したり復号する処理を書いていきます。
暗号化
暗号化にはencrypt
関数を使います。
encrypt
を使うと、暗号化する前の文章(平文)であるArrayBuffer
から、暗号化したArrayBuffer
を取得できます。
この関数は以下の引数を取ります。
-
algorithm
: ここではRsaOaepParams
オブジェクト -
key
: 暗号化に使う公開鍵のCryptoKey
オブジェクト -
data
: 暗号化したいArrayBuffer
暗号化したい文字列をArrayBuffer
に変換するにはTextEncoder
が使えます。
// RSA-OAEPの鍵を生成する
const keyPair = await window.crypto.subtle.generateKey(/* ... */)
// 暗号化したいデータ
const text = 'Hello World!'
const textBuffer = new TextEncoder().encode(text)
// encryptで暗号化する
const cipherBuf = await window.crypto.subtle.encrypt(
{ name: 'RSA-OAEP' }, // algorithm
keyPair.publicKey,
textBuffer
)
cipherBuf // 暗号化されたArrayBuffer
base64
とarrayBuffer
を変換する
暗号化したArrayBuffer
が生成できたところで、次はこれをBase64文字列に変換します。
これはバイナリのデータをASCII文字列にする感じです。
といっても現在のWeb APIには直接変換する方法がないので、変換する関数を自分で定義します。
ここでは、こちらの記事のコードから型定義を取って使うことにします。
arrayBufferToBase64
: ArrayBuffer
をBase64文字列に変換する関数
function arrayBufferToBase64(buffer) {
const str = String.fromCharCode.apply(
null,
new Uint8Array(buffer),
)
return window.btoa(str)
}
// 呼び出し例
const cipherBuf = await window.crypto.subtle.encrypt(/* ... */)
arrayBufferToBase64(cipherBuf) // 例: WPlIHJOKjTmzxq...
base64ToArrayBuffer
: Base64文字列をArrayBuffer
に変換する関数
function base64ToArrayBuffer(base64) {
const str = window.atob(base64)
const buf = new ArrayBuffer(str.length)
const bufView = new Uint8Array(buf)
for (let i = 0, strLen = str.length; i < strLen; i++) {
bufView[i] = str.charCodeAt(i)
}
return buf
}
// 呼び出し例
const cipherText = // 暗号文のBase64文字列
base64ToArrayBuffer(cipherText) // ArrayBuffer
復号
暗号化されたArrayBuffer
を復号するにはdecrypt
関数を使います。
decrypt
は以下の引数を取ります。
-
algorithm
:RsaOaepParams
オブジェクト -
key
: 復号に使用する秘密鍵を示すCryptoKey
-
data
: 暗号化されているArrayBuffer
例えば、以下のように呼び出せます。
// RSA-OAEPの鍵
const keyPair = // ...
// keyPairの公開鍵で暗号化されたArrayBuffer
const cipherBuf = base64ToArrayBuffer(/* ... */)
// 復号
const textBuf = await window.crypto.subtle.decrypt(
{ name: 'RSA-OAEP' },
keyPair.privateKey,
cipherBuf
) // ArrayBuffer
通しで呼び出す
ということで、ここまでの関数を通しで呼び出してみようと思います。
とその前に、ここで一度、平文の文字列から暗号文(Base64)ができるまで、そして暗号文が平文に復号されるまでの過程を整理します。
これは大まかに以下のシーケンス図のようになります。
ということで、次はこれをコードにしてみます。
コードでは以下のことをやってみます。
- 鍵を生成する
- 文字列を一度暗号化する
- 2の文字列を復号して出力する
実際のコードはこちらです。
// 鍵の生成に使うアルゴリズム
const keyGenAlgorithm = {
name: 'RSA-OAEP',
modulusLength: 4096,
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: 'SHA-256',
}
// 暗号化や復号に使うアルゴリズム
const encryptAlgorithm = { name: 'RSA-OAEP' }
// 鍵を生成する
const keyPair = await window.crypto.subtle.generateKey(
keyGenAlgorithm,
true, // エクスポートできるようにする
['encrypt', 'decrypt'] // 暗号化と復号ができるようにする
)
// 0. 暗号化したい文章(平文)
const text = 'Hello Crypto!'
async function encrypt(text) {
// 1. 平文をArrayBufferに変換
const textBuf = new TextEncoder().encode(text)
// 2. 暗号化
const cipherBuf = await window.crypto.subtle.encrypt(
encryptAlgorithm,
keyPair.publicKey,
textBuf
)
// 3. cipherBufをBase64文字列に変換
const cipherBase64 = arrayBufferToBase64(cipherBuf)
return cipherBase64
}
const cipher = await encrypt(text) // XwZXGbI7Inopw...
// 0. 復号したいBase64文字列
const cipher = await encrypt(text)
async function decrypt(cipherBase64) {
// 1. 暗号文をArrayBufferに変換
const cipherBuf = base64ToArrayBuffer(cipherBase64)
// 2. 復号
const textBuf = await window.crypto.subtle.decrypt(
encryptAlgorithm,
keyPair.privateKey,
cipherBuf
)
// 3. textBufを文字列に変換
const text = new TextDecoder().decode(textBuf)
return text
}
const text2 = await decrypt(cipher)
console.log(text2) // 'Hello Crypto!'