crypto
crypto模塊的目的是為了提供通用的加密和哈希算法。用純JavaScript代碼實現(xiàn)這些功能不是不可能,但速度會非常慢。Nodejs用C/C++實現(xiàn)這些算法后,通過cypto這個模塊暴露為JavaScript接口,這樣用起來方便,運行速度也快。
MD5和SHA1
MD5是一種常用的哈希算法,用于給任意數(shù)據(jù)一個“簽名”。這個簽名通常用一個十六進(jìn)制的字符串表示:
const crypto = require('crypto');
const hash = crypto.createHash('md5');
// 可任意多次調(diào)用update():
hash.update('Hello, world!');
hash.update('Hello, nodejs!');
//相當(dāng)於字符串疊加
console.log(hash.digest('hex')); // 7e1977739c748beac0c0fd14fd26a544
update()方法默認(rèn)字符串編碼為UTF-8,也可以傳入Buffer。
如果要計算SHA1,只需要把'md5'改成'sha1',就可以得到SHA1的結(jié)果1f32b9c9932c02227819a4151feed43e131aca40。
還可以使用更安全的sha256和sha512。
Hmac
Hmac算法也是一種哈希算法,它可以利用MD5或SHA1等哈希算法。不同的是,Hmac還需要一個密鑰:
const crypto = require('crypto');
const hmac = crypto.createHmac('sha256', 'secret-key');
hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');
console.log(hmac.digest('hex')); // 80f7e22570...
只要密鑰發(fā)生了變化,那么同樣的輸入數(shù)據(jù)也會得到不同的簽名,因此,可以把Hmac理解為用隨機(jī)數(shù)“增強(qiáng)”的哈希算法。
AES
AES是一種常用的對稱加密算法,加解密都用同一個密鑰。crypto模塊提供了AES支持,但是需要自己封裝好函數(shù),便于使用:
const crypto = require('crypto');
function aesEncrypt(data, key) {
const cipher = crypto.createCipher('aes192', key);
var crypted = cipher.update(data, 'utf8', 'hex');
crypted += cipher.final('hex');
return crypted;
}
function aesDecrypt(encrypted, key) {
const decipher = crypto.createDecipher('aes192', key);
var decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
var data = 'Hello, this is a secret message!';
var key = 'Password!';
var encrypted = aesEncrypt(data, key);
var decrypted = aesDecrypt(encrypted, key);
console.log('Plain text: ' + data);
console.log('Encrypted text: ' + encrypted);
console.log('Decrypted text: ' + decrypted);
運行結(jié)果如下:
Plain text: Hello, this is a secret message!
Encrypted text: 8a944d97bdabc157a5b7a40cb180e7...
Decrypted text: Hello, this is a secret message!
注意到AES有很多不同的算法,如aes192,aes-128-ecb,aes-256-cbc等,AES除了密鑰外還可以指定IV(Initial Vector),不同的系統(tǒng)只要IV不同,用相同的密鑰加密相同的數(shù)據(jù)得到的加密結(jié)果也是不同的。加密結(jié)果通常有兩種表示方法:hex和base64,這些功能Nodejs全部都支持,但是在應(yīng)用中要注意,如果加解密雙方一方用Nodejs,另一方用Java、PHP等其它語言,需要仔細(xì)測試。如果無法正確解密,要確認(rèn)雙方是否遵循同樣的AES算法,字符串密鑰和IV是否相同,加密后的數(shù)據(jù)是否統(tǒng)一為hex或base64格式。
Diffie-Hellman
DH算法是一種密鑰交換協(xié)議,它可以讓雙方在不泄漏密鑰的情況下協(xié)商出一個密鑰來。DH算法基于數(shù)學(xué)原理,比如小明和小紅想要協(xié)商一個密鑰,可以這么做:
小明先選一個素數(shù)和一個底數(shù),例如,素數(shù)p=23,底數(shù)g=5(底數(shù)可以任選),再選擇一個秘密整數(shù)a=6,計算A=g^a mod p=8,然后大聲告訴小紅:p=23,g=5,A=8;
小紅收到小明發(fā)來的p,g,A后,也選一個秘密整數(shù)b=15,然后計算B=g^b mod p=19,并大聲告訴小明:B=19;
小明自己計算出s=B^a mod p=2,小紅也自己計算出s=A^b mod p=2,因此,最終協(xié)商的密鑰s為2。
在這個過程中,密鑰2并不是小明告訴小紅的,也不是小紅告訴小明的,而是雙方協(xié)商計算出來的。第三方只能知道p=23,g=5,A=8,B=19,由于不知道雙方選的秘密整數(shù)a=6和b=15,因此無法計算出密鑰2。
用crypto模塊實現(xiàn)DH算法如下:
const crypto = require('crypto');
// xiaoming's keys:
var ming = crypto.createDiffieHellman(512);
var ming_keys = ming.generateKeys();
var prime = ming.getPrime();
var generator = ming.getGenerator();
console.log('Prime: ' + prime.toString('hex'));
console.log('Generator: ' + generator.toString('hex'));
// xiaohong's keys:
var hong = crypto.createDiffieHellman(prime, generator);
var hong_keys = hong.generateKeys();
// exchange and generate secret:
var ming_secret = ming.computeSecret(hong_keys);
var hong_secret = hong.computeSecret(ming_keys);
// print secret:
console.log('Secret of Xiao Ming: ' + ming_secret.toString('hex'));
console.log('Secret of Xiao Hong: ' + hong_secret.toString('hex'));
運行后,可以得到如下輸出:
Prime: 80f65a4455ecfc93b472be8a67fbe5edd2836de38885ae779db15a1e86449cd84138df156d5cb2dcdab680da8822431e5642d22614a55390bd665313a62b540b
Generator: 02
Secret of Xiao Ming: 5518afe72bde63dad7493804ae2e556f2e2df4aef34a761f089fd3c996e8d753fbc822d3e75a59530a4e28a55733f90f2f1d18f2eff3ef005a6ed3e643ba0853
Secret of Xiao Hong: 5518afe72bde63dad7493804ae2e556f2e2df4aef34a761f089fd3c996e8d753fbc822d3e75a59530a4e28a55733f90f2f1d18f2eff3ef005a6ed3e643ba0853
RSA
RSA算法是一種非對稱加密算法,即由一個私鑰和一個公鑰構(gòu)成的密鑰對,通過私鑰加密,公鑰解密,或者通過公鑰加密,私鑰解密。其中,公鑰可以公開,私鑰必須保密。
RSA算法是1977年由Ron Rivest、Adi Shamir和Leonard Adleman共同提出的,所以以他們?nèi)说男帐系念^字母命名。
當(dāng)小明給小紅發(fā)送信息時,可以用小明自己的私鑰加密,小紅用小明的公鑰解密,也可以用小紅的公鑰加密,小紅用她自己的私鑰解密,這就是非對稱加密。相比對稱加密,非對稱加密只需要每個人各自持有自己的私鑰,同時公開自己的公鑰,不需要像AES那樣由兩個人共享同一個密鑰。
在使用Node進(jìn)行RSA加密前,我們先要準(zhǔn)備好私鑰和公鑰。
首先,在命令行執(zhí)行以下命令以生成一個RSA密鑰對:
openssl genrsa -aes256 -out rsa-key.pem 2048
根據(jù)提示輸入密碼,這個密碼是用來加密RSA密鑰的,加密方式指定為AES256,生成的RSA的密鑰長度是2048位。執(zhí)行成功后,我們獲得了加密的rsa-key.pem文件。
第二步,通過上面的rsa-key.pem加密文件,我們可以導(dǎo)出原始的私鑰,命令如下:
openssl rsa -in rsa-key.pem -outform PEM -out rsa-prv.pem
輸入第一步的密碼,我們獲得了解密后的私鑰。
類似的,我們用下面的命令導(dǎo)出原始的公鑰:
openssl rsa -in rsa-key.pem -outform PEM -pubout -out rsa-pub.pem
這樣,我們就準(zhǔn)備好了原始私鑰文件rsa-prv.pem和原始公鑰文件rsa-pub.pem,編碼格式均為PEM。
下面,使用crypto模塊提供的方法,即可實現(xiàn)非對稱加解密。
首先,我們用私鑰加密,公鑰解密:
const
fs = require('fs'),
crypto = require('crypto');
// 從文件加載key:
function loadKey(file) {
// key實際上就是PEM編碼的字符串:
return fs.readFileSync(file, 'utf8');
}
let
prvKey = loadKey('./rsa-prv.pem'),
pubKey = loadKey('./rsa-pub.pem'),
message = 'Hello, world!';
// 使用私鑰加密:
let enc_by_prv = crypto.privateEncrypt(prvKey, Buffer.from(message, 'utf8'));
console.log('encrypted by private key: ' + enc_by_prv.toString('hex'));
let dec_by_pub = crypto.publicDecrypt(pubKey, enc_by_prv);
console.log('decrypted by public key: ' + dec_by_pub.toString('utf8'));
執(zhí)行后,可以得到解密后的消息,與原始消息相同。
接下來我們使用公鑰加密,私鑰解密:
// 使用公鑰加密:
let enc_by_pub = crypto.publicEncrypt(pubKey, Buffer.from(message, 'utf8'));
console.log('encrypted by public key: ' + enc_by_pub.toString('hex'));
// 使用私鑰解密:
let dec_by_prv = crypto.privateDecrypt(prvKey, enc_by_pub);
console.log('decrypted by private key: ' + dec_by_prv.toString('utf8'));
執(zhí)行得到的解密后的消息仍與原始消息相同。
如果我們把message字符串的長度增加到很長,例如1M,這時,執(zhí)行RSA加密會得到一個類似這樣的錯誤:data too large for key size,這是因為RSA加密的原始信息必須小于Key的長度。那如何用RSA加密一個很長的消息呢?實際上,RSA并不適合加密大數(shù)據(jù),而是先生成一個隨機(jī)的AES密碼,用AES加密原始信息,然后用RSA加密AES口令,這樣,實際使用RSA時,給對方傳的密文分兩部分,一部分是AES加密的密文,另一部分是RSA加密的AES口令。對方用RSA先解密出AES口令,再用AES解密密文,即可獲得明文。
證書
crypto模塊也可以處理數(shù)字證書。數(shù)字證書通常用在SSL連接,也就是Web的https連接。一般情況下,https連接只需要處理服務(wù)器端的單向認(rèn)證,如無特殊需求(例如自己作為Root給客戶發(fā)認(rèn)證證書),建議用反向代理服務(wù)器如Nginx等Web服務(wù)器去處理證書。