0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

[JavaScript] Web Crypto API を使用して RSAES-OAEP データ暗号化および復号化を行う

Last updated at Posted at 2023-03-10

JavaScript で RSAES-OAEP データ暗号化および復号化を行う方法について説明します。

RSAES-OAEP と RSA-OAEP は同じ意味です。

1. 暗号化および復号化のための準備

1.1. 文字列と Uint8ArrayArrayBuffer の変換

暗号化および復号化で文字列を Uint8Array 型や ArrayBuffer 型で扱うため、TextEncoder および TextDecoder を用いて変換します。

const textEncoder = new TextEncoder();
const encodeString = string => textEncoder.encode(string);

const textDecoder = new TextDecoder();
const decodeString = buffer => textDecoder.decode(buffer);

参考「TextEncode.encode() - Web API | MDN
参考「TextDecoder.decode() - Web APIs | MDN

1.2. Base64 エンコードおよびデコード

バイナリデータをテキストとして扱うため Base64 を利用することにします。

JavaScript での Base64 エンコードおよびデコードに関する説明は別記事にしました。

参考「[JavaScript] Unicode 文字列やバイナリデータを Base64 エンコードおよびデコードする - Qiita

本記事では以下のコードを利用することにします。

const encodeBinaryString = binaryString => Uint8Array.from(
	binaryString,
	binaryChar => binaryChar.charCodeAt(0),
);

const decodeBinaryString = uint8Array => uint8Array.reduce(
	(binaryString, uint8) => binaryString + String.fromCharCode(uint8),
	'',
);

const encodeBase64 = uint8Array => {
	const binaryString = decodeBinaryString(uint8Array);
	const base64 = btoa(binaryString);
	return base64;
};

const decodeBase64 = base64 => {
	const binaryString = atob(base64);
	const uint8Array = encodeBinaryString(binaryString);
	return uint8Array;
};

2. 暗号化および復号化

2.1. 暗号鍵の生成

crypto.subtle.generateKey 関数で暗号鍵を生成できます。

本記事では、RSA 鍵の長さは 3072 ビット (384 バイト) とし、メッセージ・ダイジェスト・アルゴリズムに SHA-256 を使用することにします。

RSA 鍵を生成する際の公開指数は 65537 ($= 2^{16} + 1$) が使われることが多く、本記事でもそれを使用することにします (バイト配列で表現すると [0x01, 0x00, 0x01] になります) 。

参考「SubtleCrypto.generateKey() - Web API | MDN
参考「RsaHashedKeyGenParams - Web API | MDN

参考「公開指数(public exponent)について | GMOグローバルサイン サポート

crypto.subtle.generateKey 関数で生成される暗号鍵は CryptoKey 型になっています。

crypto.subtle.exportKey 関数で CryptoKey 型の鍵を外部出力可能な形式に変換した後、バイナリデータを Base64 エンコードして文字列化します。

参考「SubtleCrypto.exportKey() - Web APIs | MDN

RSA 公開鍵は SubjectPublicKeyInfo 形式 (spki)、RSA 秘密鍵は PKCS #8 (Private-Key Information) 形式 (pkcs8) で出力します。

参考「PKCS #8 - Supported formats - SubtleCrypto.importKey() - Web APIs | MDN
参考「RFC 5208 - Public-Key Cryptography Standards (PKCS) #8: Private-Key Information Syntax Specification Version 1.2
参考「SubjectPublicKeyInfo - Supported formats - SubtleCrypto.importKey() - Web APIs | MDN

const exportPublicKey = async key => {
	const keyArrayBuffer = await crypto.subtle.exportKey('spki', key);
	const keyBase64 = encodeBase64(new Uint8Array(keyArrayBuffer));
	return keyBase64;
};

const exportPrivateKey = async key => {
	const keyArrayBuffer = await crypto.subtle.exportKey('pkcs8', key);
	const keyBase64 = encodeBase64(new Uint8Array(keyArrayBuffer));
	return keyBase64;
};

const generateKeyPair = async () => {

	const keyPair = await crypto.subtle.generateKey(
		{
			name: 'RSA-OAEP',
			modulusLength: 3072,
			publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
			hash: 'SHA-256',
		},
		true,
		['encrypt', 'decrypt'],
	);

	const publicKey = await exportPublicKey(keyPair.publicKey);
	const privateKey = await exportPrivateKey(keyPair.privateKey);

	return {
		publicKey,
		privateKey,
	};

};

// 
const keyPair = await generateKeyPair();

const publicKeyBase64 = keyPair.publicKey;
console.log(publicKeyBase64);

const privateKeyBase64 = keyPair.privateKey;
console.log(privateKeyBase64);
実行結果の例
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

(※ここでは例として秘密鍵も記載していますが、実際には秘密鍵を第三者に知られないようにする必要があります。)

2.2. 暗号化

データ暗号化は crypto.subtle.encrypt 関数を用います。

参考「SubtleCrypto.encrypt() - Web APIs | MDN

crypto.subtle.encrypt 関数で使用する暗号鍵 (ここでは RSA 公開鍵) は CryptoKey 型で指定するため、暗号鍵の文字列を Base64 デコードして crypto.subtle.importKey 関数で CryptoKey 型に変換します。

参考「SubtleCrypto.importKey() - Web APIs | MDN

const importPublicKey = async keyBase64 => {
	const keyUint8Array = decodeBase64(keyBase64);
	const key = await crypto.subtle.importKey(
		'spki',
		keyUint8Array,
		{
			name: 'RSA-OAEP',
			hash: 'SHA-256',
		},
		false,
		['encrypt'],
	);
	return key;
};

const encryptString = async (keyBase64, plaintext) => {
	const key = await importPublicKey(keyBase64);
	const plaintextUint8Array = encodeString(plaintext);
	const ciphertextArrayBuffer = await crypto.subtle.encrypt(
		{
			name: 'RSA-OAEP',
		},
		key,
		plaintextUint8Array,
	);
	const ciphertextBase64 = encodeBase64(new Uint8Array(ciphertextArrayBuffer));
	return ciphertextBase64;
};

// 
const publicKeyBase64 = 'MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAuXRtjroFtFYc3CakJALtvka9Rrq5aqN8BV0aQtQtc9OYYDG8+A1WukGxl+Q0jr0GF45BW/d2fvWFE/D4wygTzyQf4kjoIuwto001ENq3wXPWij/TfDx0YSp1zm3CL4hIFBnzyZbHL7mBabHCsjXtakApo19sXViQqdiRyXRj4CeYhoi3jJY4fWut+eqxlF+2J6L0NrpDjuAvdtgThg+HTyTI0mgBJihDM2Lr4Ca1zMbEL+6mF9oZlVS9Nqzw+YBu5aaAjTGIly1qGcdBTM2Bc3u4tx9OopYOFoXbdmjwEsk0XpDMa2BVWEfrQm5VE5oq2PuR/4U7ynuBJcyOQPBSdNK1hQYPH+Y5OJlksZRlipLk7p809afcH/iGNTShpm/TYkWT0xzS6aupZB0t8ldfI5OVFnUY+N6Yh7OeaxZ8dDjJ97VYaSq/d3N7O/IJzp/ZKRp6r0p51pR/4Y9iU182M0kEUdDQMuO7B5u+awof9eSiovbHzd1DBHIrsoc7z+97AgMBAAE=';

const plaintextA = '犬と猫、そして兎';
console.log(plaintextA);

const ciphertextBase64 = await encryptString(publicKeyBase64, plaintextA);
console.log(ciphertextBase64);
実行結果
犬と猫、そして兎
BHSdH2hu6GXOkBSbEMlU6wLUCiXNqnNRQ0s3gvHx12TBlFR8BT5vcnz6eymbCCp+e2xPPOXUXvqdl9/x6ihO90p9pXWQsf5YyvvSkWZwwd5+GWfSEFtu5/OiQyzvCNxYVP+m3rCUamvGIBN/8lLdO4Yd+jSKqRomzS4jQq/HxoTeUDSBle5O5DHhR+8AZaH1rR5vgXzmRm1Herh92HjKK9dtSXRoF7TW2D80H6QlMP3UMeKQ/1vGFkl7aaVVhRFaE+hF0sv2/B+cfSkB8DG5SDbCt7iQyWDIYOPGSzGfo058Ukqr+FnqhuXqzUsORZVeL/ui9Of4hSPsfRfeOQmFih8OJ3T0/vxW6/oNiEghyOIZT1sapWZPPWWrLgBKT1nMxmYuaUZbMW4SnIjVgeqE31IbklS/b6L3i93U08dS6lh5gBevy5rBEy15B1HNTzOjohP+bofpZdh+lEEnfur8CQ5r6nPTFrk0S4bpxPOY/H2XqRcj9r23PqnWzQV2Zf/b

2.3. 復号化

データ復号化は crypto.subtle.decrypt 関数を用います。

参考「SubtleCrypto.decrypt() - Web APIs | MDN

crypto.subtle.decrypt 関数で使用する復号鍵 (ここでは RSA 秘密鍵) は CryptoKey 型で指定するため、復号鍵の文字列を Base64 デコードして crypto.subtle.importKey 関数で CryptoKey 型に変換します。

参考「SubtleCrypto.importKey() - Web APIs | MDN

const importPrivateKey = async keyBase64 => {
	const keyUint8Array = decodeBase64(keyBase64);
	const key = await crypto.subtle.importKey(
		'pkcs8',
		keyUint8Array,
		{
			name: 'RSA-OAEP',
			hash: 'SHA-256',
		},
		false,
		['decrypt'],
	);
	return key;
};

const decryptString = async (keyBase64, ciphertextBase64) => {
	const key = await importPrivateKey(keyBase64);
	const ciphertextUint8Array = decodeBase64(ciphertextBase64);
	const plaintextArrayBuffer = await crypto.subtle.decrypt(
		{
			name: 'RSA-OAEP',
		},
		key,
		ciphertextUint8Array,
	);
	const plaintext = decodeString(plaintextArrayBuffer);
	return plaintext;
};

// 
const privateKeyBase64 = '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';

const ciphertextBase64 = 'BHSdH2hu6GXOkBSbEMlU6wLUCiXNqnNRQ0s3gvHx12TBlFR8BT5vcnz6eymbCCp+e2xPPOXUXvqdl9/x6ihO90p9pXWQsf5YyvvSkWZwwd5+GWfSEFtu5/OiQyzvCNxYVP+m3rCUamvGIBN/8lLdO4Yd+jSKqRomzS4jQq/HxoTeUDSBle5O5DHhR+8AZaH1rR5vgXzmRm1Herh92HjKK9dtSXRoF7TW2D80H6QlMP3UMeKQ/1vGFkl7aaVVhRFaE+hF0sv2/B+cfSkB8DG5SDbCt7iQyWDIYOPGSzGfo058Ukqr+FnqhuXqzUsORZVeL/ui9Of4hSPsfRfeOQmFih8OJ3T0/vxW6/oNiEghyOIZT1sapWZPPWWrLgBKT1nMxmYuaUZbMW4SnIjVgeqE31IbklS/b6L3i93U08dS6lh5gBevy5rBEy15B1HNTzOjohP+bofpZdh+lEEnfur8CQ5r6nPTFrk0S4bpxPOY/H2XqRcj9r23PqnWzQV2Zf/b';
console.log(ciphertextBase64);

const plaintextB = await decryptString(privateKeyBase64, ciphertextBase64);
console.log(plaintextB);

(※ここでは例として秘密鍵も記載していますが、実際には秘密鍵を第三者に知られないようにする必要があります。)

実行結果
BHSdH2hu6GXOkBSbEMlU6wLUCiXNqnNRQ0s3gvHx12TBlFR8BT5vcnz6eymbCCp+e2xPPOXUXvqdl9/x6ihO90p9pXWQsf5YyvvSkWZwwd5+GWfSEFtu5/OiQyzvCNxYVP+m3rCUamvGIBN/8lLdO4Yd+jSKqRomzS4jQq/HxoTeUDSBle5O5DHhR+8AZaH1rR5vgXzmRm1Herh92HjKK9dtSXRoF7TW2D80H6QlMP3UMeKQ/1vGFkl7aaVVhRFaE+hF0sv2/B+cfSkB8DG5SDbCt7iQyWDIYOPGSzGfo058Ukqr+FnqhuXqzUsORZVeL/ui9Of4hSPsfRfeOQmFih8OJ3T0/vxW6/oNiEghyOIZT1sapWZPPWWrLgBKT1nMxmYuaUZbMW4SnIjVgeqE31IbklS/b6L3i93U08dS6lh5gBevy5rBEy15B1HNTzOjohP+bofpZdh+lEEnfur8CQ5r6nPTFrk0S4bpxPOY/H2XqRcj9r23PqnWzQV2Zf/b
犬と猫、そして兎
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?