node.jsでRSA公開鍵を使った鍵ペア生成や暗号化/復号をする例はたくさんあったのですが、javascriptでの例はあまりなかったのでやってみました。
途中、はまってしまったので、備忘録として残しておきます。
とは言いつつも、なんてことはない、browserifyを使っただけです。。。
使わせてもらったnpmモジュールは「node-rsa」です。Pure Javascriptという特徴に惹かれました。
rzcoder/node-rsa
https://github.com/rzcoder/node-rsa
(2019/3/2 追記)
せっかくなので、署名付与・署名検証も追加しておきました。
作成するモジュールの仕様
ブラウザ上のJavascriptから以下関数を呼び出せるモジュールとします。
- RSA公開鍵ペアの生成
- 公開鍵による暗号化
- 秘密鍵による復号
- 秘密鍵による署名付与
- 公開鍵による署名検証
- PEM形式をDER形式に変換
- DER形式をPEM形式に変換
ちなみに、暗号化時のパディング方式は「RSA_PKCS1_OAEP_PADDING」とします。PKCS#1 v2.0 with SHA-1のことです。
node.jsでモジュールを作成する
まずは、node.jsで期待する動作となることを確認しましょう。
npm init -y
npm install --save node-rsa
vi main.js
const NodeRSA = require('node-rsa');
function generateKeyPair(bits){
const key = new NodeRSA({b: bits});
var priv = key.exportKey('pkcs1-private-der');
var pub = key.exportKey('pkcs1-public-der');
return { public: pub, private: priv };
}
function pem2der(scheme, pem){
const rsa = new NodeRSA(pem, scheme + '-pem');
return rsa.exportKey(scheme + '-der');
}
function der2pem(scheme, key){
var der = Buffer.from(key);
const rsa = new NodeRSA(der, scheme + '-der');
return rsa.exportKey(scheme + '-pem');
}
function der2der(key, src_scheme, dest_scheme){
var der = Buffer.from(key);
const rsa = new NodeRSA(der, src_scheme + '-der');
return rsa.exportKey(dest_scheme+ '-der');
}
function publicEncrypt(key, buffer){
var input = Buffer.from(buffer);
var der = Buffer.from(key);
const rsa = new NodeRSA(der, 'pkcs1-public-der', { encryptionScheme : 'pkcs1_oaep' });
var enc = rsa.encrypt(input);
return enc;
}
function privateDecrypt(key, buffer){
var input = Buffer.from(buffer);
var der = Buffer.from(key);
const rsa = new NodeRSA(der, 'pkcs1-private-der', { encryptionScheme : 'pkcs1_oaep' });
var dec = rsa.decrypt(input);
return dec;
}
function sign(key, buffer){
var input = Buffer.from(buffer);
var der = Buffer.from(key);
const rsa = new NodeRSA(der, 'pkcs1-private-der');
var sig = rsa.sign(input);
return sig;
}
function verify(key, buffer, signature){
var input = Buffer.from(buffer);
var der = Buffer.from(key);
var sig = Buffer.from(signature)
const rsa = new NodeRSA(der, 'pkcs1-public-der');
var result = rsa.verify(input, sig);
return result;
}
module.exports = {
generateKeyPair: generateKeyPair,
publicEncrypt: publicEncrypt,
privateDecrypt: privateDecrypt,
sign: sign,
verify: verify,
der2pem: der2pem,
pem2der: pem2der,
der2der: der2der
};
ちなみに、はまった点を示しておきます。
publicEncryptやprivateDecryptでは、以下のような一見不要な処理をしています。ですが、なぜかこの処理を入れないと、ブラウザのJavascriptから呼び出したときにうまく復号してくれなかったんです。原因不明です。とりあえず動いているのでいいですが。。
var input = Buffer.from(buffer);
次は、それを呼び出すサンプルコードです。
var rsab = require('./main');
var key = rsab.generateKeyPair(1024);
console.log('private=', key.private);
console.log('public=', key.public);
var buffer = new Uint8Array(10);
var enc2 = rsab.publicEncrypt(key.public, buffer);
console.log('enc2=', enc2);
var dec2 = rsab.privateDecrypt(key.private, enc2);
console.log('OK? dec2=', dec2);
var pem = rsab.der2pem('pkcs1-private', key.private);
console.log(pem);
var der = rsab.pem2der('pkcs1-private', pem);
console.log(der);
var pem = rsab.der2pem('pkcs1-public', key.public);
console.log(pem);
var der = rsab.pem2der('pkcs1-public', pem);
console.log(der);
var sig = rsab.sign(key.private, 'Hello World');
console.log('sig=', sig);
var result = rsab.verify(key.public, 'Hello World', sig);
console.log('result=', result);
以下のような感じで出力されましたでしょうか?
private= <Buffer 30 82 02 5c 02 01 00 02 81 81 00 86 d6 34 4f d8 d8 2d 33 75 1e 04 95 bc a8 a0 24 30 c8 a0 d3 c6 b7 73 fe c1 81 8c 05 25 75 f1 1e a9 c5 3e 75 c5 04 2a ... >
public= <Buffer 30 81 89 02 81 81 00 86 d6 34 4f d8 d8 2d 33 75 1e 04 95 bc a8 a0 24 30 c8 a0 d3 c6 b7 73 fe c1 81 8c 05 25 75 f1 1e a9 c5 3e 75 c5 04 2a d5 65 a3 b4 ... >
enc2= <Buffer 22 b0 af e7 8c 2e 02 eb 73 f1 f1 d0 28 c0 a3 4b c5 cf 46 d1 82 f2 3d 12 32 d0 bf 95 e4 13 61 46 f1 79 32 ce 10 b9 ce 5d 8d 4e e6 0c d8 5c 3c 67 e0 96 ... >
OK? dec2= <Buffer 00 00 00 00 00 00 00 00 00 00>
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCG1jRP2NgtM3UeBJW8qKAkMMig08a3c/7BgYwFJXXxHqnFPnXF
BCrVZaO0LtPLmeV3HeqtK9W67TyBe/FMgXLdZ6cpxOco4NLBWp/7QP2MwVesf9g/
fm4LZIxeN9GXM/HIQOvbObyKRmd30CKHxjWkbnMTi72SG+Rc2RqhPJJPvwIDAQAB
AoGAV9VDQFwt/cvOV95+t+VUZB7PIkyx3qEV63F7B4MugAIMbytPxiX/zQCnkeEL
IE7AtkZrr6ClWl3dky9ssPyGGJJIVr7byMU1VdJlse2DIfIXW4kl+jo/MipXzb4I
dr1D/AT043WS7hpyDKNDyZzmf4dwJsp9cpOm/AqUz+LfdIECQQDlh0+EuihKpaYs
y4vtlHHWjZJPoLQQAtk57NnMJaqmEYuQYCnYs7RAuGoCRSc6DdgukZp+wSZ0WBn3
u0sSHTb/AkEAlmMtDN8y6nzLZvGzxQDgNxyzkNrSwmFb7fAa0+o2Pea0ps1ByrpX
vOuebwze69kk5bldyVM+npwQZ1xiKrmnQQJBAKEXMmwI6zZYxCQ0R2TbBnp6qfFQ
7I9AMI1C+ikZVodvUPBnTXdVyHCT/XLSbhGEnfExJ6lGjmKhYrhHrwxrjKkCQF+6
T7Hy3eE/gOZNksYjUZYjUfYyJJiRCsiB30Hnw5FRqsrGu0uFpFXgkeBUjA4LEh6d
CSMfNywVYae5uc9CkEECQH1xO+qrkWeGrrX3JaKjRmo13mYMfO9wwNOtINrlb9so
1om826G3vYPX4qrcWYC4pr0t8/7hN+4P80UNEnHCiac=
-----END RSA PRIVATE KEY-----
<Buffer 30 82 02 5c 02 01 00 02 81 81 00 86 d6 34 4f d8 d8 2d 33 75 1e 04 95 bc a8 a0 24 30 c8 a0 d3 c6 b7 73 fe c1 81 8c 05 25 75 f1 1e a9 c5 3e 75 c5 04 2a ... >
-----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAIbWNE/Y2C0zdR4ElbyooCQwyKDTxrdz/sGBjAUldfEeqcU+dcUEKtVl
o7Qu08uZ5Xcd6q0r1brtPIF78UyBct1npynE5yjg0sFan/tA/YzBV6x/2D9+bgtk
jF430Zcz8chA69s5vIpGZ3fQIofGNaRucxOLvZIb5FzZGqE8kk+/AgMBAAE=
-----END RSA PUBLIC KEY-----
<Buffer 30 81 89 02 81 81 00 86 d6 34 4f d8 d8 2d 33 75 1e 04 95 bc a8 a0 24 30 c8 a0 d3 c6 b7 73 fe c1 81 8c 05 25 75 f1 1e a9 c5 3e 75 c5 04 2a d5 65 a3 b4 ... >
sig= <Buffer 08 9f 2c 45 f2 e9 b4 8a f6 1e 88 e4 16 71 1a 07 18 fc 36 a5 8b d0 66 7d c6 b6 bc ff cf 51 b2 ca 5f d7 bb 7c 86 2e a0 be 97 15 4a 85 f6 f8 46 83 f2 db ... >
result= true
当然ながら、実行ごとに鍵ペアの鍵値や暗号文の値は異なります。
browserify化する
まだbrowserifyをインストールしていない場合は、インストールしておきます。
npm install browserify -g
それではJavascriptのモジュールを作成します。
browserify -r ./main.js:node-rsa-js -o node-rsa-js.js
これで、node-rsa-js.jsというJavascriptファイルが出来上がりました。-rで指定した「node-rsa-js」がJavascriptからrequireする名前になります。
ブラウザから使ってみる
以下のようなサンプルHTMLファイルを作成します。
<html>
<head>
</head>
<body>
<script src="node-rsa-js.js"></script>
<script>
var rsab = require('node-rsa-js');
var key = rsab.generateKeyPair(1024);
console.log('private=', key.private);
console.log('public=', key.public);
var buffer = new Uint8Array(10);
var enc2 = rsab.publicEncrypt(key.public, buffer);
console.log('enc2=', enc2);
var dec2 = rsab.privateDecrypt(key.private, enc2);
console.log('OK? dec2=', dec2);
var pem = rsab.der2pem('pkcs1-private', key.private);
console.log(pem);
var der = rsab.pem2der('pkcs1-private', pem);
console.log(der);
var pem = rsab.der2pem('pkcs1-public', key.public);
console.log(pem);
var der = rsab.pem2der('pkcs1-public', pem);
console.log(der);
var sig = rsab.sign(key.private, 'Hello World');
console.log('sig=', sig);
var result = rsab.verify(key.public, 'Hello World', sig);
console.log('result=', result);
</script>
</body>
</html>
このindex.html
と同じフォルダに先ほど作成したnode-rsa-js.js
を置きます。
早速、ブラウザから表示してみましょう。その時、各ブラウザのConsoleをのぞいてみてください。
以下のような感じで表示されれば成功です。
private= Uint8Array(609) [48, 130, 2, 93, 2, 1, 0, 2, 129, 129, 0, 145, 2, 86, 33, 88, 50, 157, 251, 230, 51, 254, 9, 56, 64, 126, 169, 20, 2, 90, 3, 220, 32, 203, 53, 160, 62, 57, 209, 184, 183, 216, 203, 55, 131, 173, 205, 41, 174, 161, 16, 8, 17, 240, 238, 164, 117, 161, 187, 111, 10, 85, 116, 163, 25, 228, 14, 233, 56, 203, 16, 14, 234, 62, 49, 183, 171, 82, 140, 225, 101, 207, 84, 92, 47, 222, 212, 69, 102, 196, 226, 197, 38, 106, 237, 73, 6, 228, 221, 36, …]
(index):10 public= Uint8Array(140) [48, 129, 137, 2, 129, 129, 0, 145, 2, 86, 33, 88, 50, 157, 251, 230, 51, 254, 9, 56, 64, 126, 169, 20, 2, 90, 3, 220, 32, 203, 53, 160, 62, 57, 209, 184, 183, 216, 203, 55, 131, 173, 205, 41, 174, 161, 16, 8, 17, 240, 238, 164, 117, 161, 187, 111, 10, 85, 116, 163, 25, 228, 14, 233, 56, 203, 16, 14, 234, 62, 49, 183, 171, 82, 140, 225, 101, 207, 84, 92, 47, 222, 212, 69, 102, 196, 226, 197, 38, 106, 237, 73, 6, 228, 221, 36, 198, 25, 31, 221, …]
(index):15 enc2= Uint8Array(128) [8, 132, 229, 218, 1, 167, 51, 85, 250, 19, 215, 124, 177, 59, 76, 160, 142, 151, 103, 170, 200, 101, 222, 190, 148, 79, 70, 88, 63, 92, 227, 244, 220, 185, 117, 131, 98, 113, 73, 130, 247, 156, 254, 244, 198, 154, 109, 12, 135, 158, 125, 88, 186, 121, 244, 194, 18, 1, 180, 236, 159, 169, 51, 170, 195, 172, 166, 178, 122, 230, 209, 78, 45, 122, 16, 126, 2, 156, 65, 215, 100, 22, 63, 211, 127, 220, 51, 173, 106, 232, 80, 109, 228, 250, 245, 32, 137, 217, 236, 96, …]
(index):17 OK? dec2= Uint8Array(10) [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
(index):20 -----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQCRAlYhWDKd++Yz/gk4QH6pFAJaA9wgyzWgPjnRuLfYyzeDrc0p
rqEQCBHw7qR1obtvClV0oxnkDuk4yxAO6j4xt6tSjOFlz1RcL97URWbE4sUmau1J
BuTdJMYZH91cLvedEwc5lc7SNHrmZytOaE4jxFACXdi9tHyu8fENhL4x1QIDAQAB
AoGAQassNDeL3K3J53vA0x+p/InaMseStasxIttrNcWQRHZrMo/P3HN/7xGohlKc
WcUfa77jSknenL//8D9Ni2ObPBstwhAIVu/n6UEOL19XSkTxGKExboI3zXXX8Uc5
5NTz6UQ12BPr4/cOmu19pAMuSRCA4QF/nfv3hpRGWNuWspUCQQDWcom+nT2DVzsx
B1mfPjV3d95049+p/WiWyRbxBh+G4MZs1PShT2cuchJNumrcgjZU6BFo9StSdSS8
DlVpa75rAkEArRtgpzBNLYqoJGBsq8bRZzgDlU72a7qYnxbmz/mBjQHIC4MUk3FD
rrrGfMBlQoK8poQBL9BZiPYNEB+N+JlgvwJBAJh8X3f8BUaEW6GUUWULbidiQ/uo
IV2VxK4blUWTjg1xfYbbsouVk5ASKvO8T8o2iP28+sxAMSr0A0f5hUBuDbsCQHbI
NHhEkpDPdjUP3UG5uXLkYsEPX9PoRFXV9yd6g8ToFgagOXw62kCJdS2hL1qGL0Dr
j4zpoKZ0f94yaM7PIC0CQQDL1VPh6oJ9MSDjoVGjVcjpxffXe/fNS/nnqRTn6lmZ
VT6KNQ+KKenp5RBqlYJjS2S9Jhnx1by3bdZOJlkIY34k
-----END RSA PRIVATE KEY-----
(index):22 Uint8Array(609) [48, 130, 2, 93, 2, 1, 0, 2, 129, 129, 0, 145, 2, 86, 33, 88, 50, 157, 251, 230, 51, 254, 9, 56, 64, 126, 169, 20, 2, 90, 3, 220, 32, 203, 53, 160, 62, 57, 209, 184, 183, 216, 203, 55, 131, 173, 205, 41, 174, 161, 16, 8, 17, 240, 238, 164, 117, 161, 187, 111, 10, 85, 116, 163, 25, 228, 14, 233, 56, 203, 16, 14, 234, 62, 49, 183, 171, 82, 140, 225, 101, 207, 84, 92, 47, 222, 212, 69, 102, 196, 226, 197, 38, 106, 237, 73, 6, 228, 221, 36, …]
(index):24 -----BEGIN RSA PUBLIC KEY-----
MIGJAoGBAJECViFYMp375jP+CThAfqkUAloD3CDLNaA+OdG4t9jLN4OtzSmuoRAI
EfDupHWhu28KVXSjGeQO6TjLEA7qPjG3q1KM4WXPVFwv3tRFZsTixSZq7UkG5N0k
xhkf3Vwu950TBzmVztI0euZnK05oTiPEUAJd2L20fK7x8Q2EvjHVAgMBAAE=
-----END RSA PUBLIC KEY-----
(index):26 Uint8Array(140) [48, 129, 137, 2, 129, 129, 0, 145, 2, 86, 33, 88, 50, 157, 251, 230, 51, 254, 9, 56, 64, 126, 169, 20, 2, 90, 3, 220, 32, 203, 53, 160, 62, 57, 209, 184, 183, 216, 203, 55, 131, 173, 205, 41, 174, 161, 16, 8, 17, 240, 238, 164, 117, 161, 187, 111, 10, 85, 116, 163, 25, 228, 14, 233, 56, 203, 16, 14, 234, 62, 49, 183, 171, 82, 140, 225, 101, 207, 84, 92, 47, 222, 212, 69, 102, 196, 226, 197, 38, 106, 237, 73, 6, 228, 221, 36, 198, 25, 31, 221, …]
(index):29 sig= Uint8Array(128) [80, 85, 255, 196, 23, 196, 40, 61, 23, 239, 13, 112, 221, 241, 216, 229, 45, 164, 241, 41, 212, 252, 55, 15, 86, 107, 42, 245, 86, 152, 155, 134, 37, 37, 255, 72, 93, 191, 63, 149, 144, 207, 72, 64, 80, 212, 92, 6, 74, 116, 62, 32, 25, 142, 155, 200, 42, 33, 31, 155, 163, 232, 219, 121, 99, 175, 164, 132, 140, 121, 145, 242, 147, 191, 90, 131, 195, 48, 193, 75, 3, 157, 98, 83, 161, 206, 221, 44, 132, 16, 144, 143, 103, 59, 233, 4, 87, 235, 15, 198, …]
(index):32 result= true
関数名を見れば、だいたい処理内容は理解いただけると思います。
1点だけ注意ですが、publicEncryptとprivateDecryptで指定する公開鍵はDER形式としています。
私がDER形式を使うことが多いためこのようにしていますが、PEM形式とすることは簡単だと思いますので、適宜修正してください。
(2019/3/2 追記)
言うのを忘れましたが、node-rsa-js.jsのファイルサイズは「でかい」です。768KBです。
(2019/3/9 追記)
鍵の形式にはPKCS#1形式とPKCS#8形式があります。PEM形式にすると違いが判ります。
-----BEGIN RSA PUBLIC KEY-----
こっちがPKCS#1形式
-----BEGIN PRIVATE KEY-----
こっちがPKCS#8形式。中にPKCS#1の鍵が包含されています。
OpenSSLとかで普通に作るとPKCS#8形式になるのかな?
der2derで変換してください。
以上