之前的 前端加密 虽然提到了 Web Cryptography API,但没有演示如何使用 Web Cryptography API 实现 RSA 加密、解密,这里做个补充。
subtle.importKey()
webcrypto
的 API 相对于 Node 的 crypto
模块更加底层(low-level),Node 的 crypto.publicEncrypt
和 crypto.privateDecrypt
会自动识别传入的密钥格式,而 webcrypto
需要我们主动处理。
const cryptoKey = await subtle.importKey(
format,
keyData,
algorithm,
extractable,
keyUsages
);
- format: 待导入密钥的格式,目前支持
"raw"
,"spki"
,"pkcs8"
,"jwk"
4 种 - keyData: 密钥数据,支持
BufferSource
和JsonWebKey
类型 - algorithm: 字典对象,包含了待导入密钥的类型和算法指定的额外参数
- extractable: 布尔值,用于指定密钥是否可以导出
- keyUsages: 字符串数组,用于指定密钥的用法,和
SubtleCrypto
暴露的方法对应
具体使用参考 SubtleCrypto.importKey() 文档。
我们可以写出如下的 RSA 公钥、私钥导入代码:
const publicKey = await subtle.importKey(
'spki',
publicKeyData,
{
name: 'RSA-OAEP',
hash: 'SHA-256'
},
false,
['encrypt']
);
const privateKey = await subtle.importKey(
'pkcs8',
privateKeyData,
{
name: 'RSA-OAEP',
hash: 'SHA-256'
},
false,
['decrypt']
);
其中的 publicKeyData
和 privateKeyData
还需要进一步实现。
祭出前文生成的 RSA 私钥和公钥。
rsa_1024_priv.pem
:
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDKKqgnPzneyl2kz7G3yyBgeIKkUPk17DHfvU0ElcZt1tJpqFgG
8L6X4Xb3QyDagxqa/HKMSDrlpRakbzgJTs6kfFCofIvNhzQwQNfEnpKdWK7Im7di
zPIzIi58ogFHToDAEWfOtpaXMiqY6pknvtVSJHMNlbDJ3WDkPKj8KOUBGQIDAQAB
AoGAYuj73DfS2G2p4zi6enGnJYvQXxQ+2WL2A8FaLSQaMSMpTwhOCRdAKI7m5ZKy
QDZkje91G607I5/htBG2GNe1wWUNMgJmuPMuI738wYFRVj/VhFgfoB2XiKgzvA3q
CFyrOPt1d4H8XXMdAFEYpqMCc2bgcFrKWB+kZMQ715pboAECQQDxqXdbMFaIhB4m
Tg+wWnrUVUtWq1RrKxkZaf46hMKLPpdVrWjzgMY9DvMob8s3wTLcDXFmtBUHD5Zb
SVLgCaqJAkEA1ilOpHsuvXtByIWffB7oo65HQKwN5KegbG/kljE6Gz7rnjtEneFy
DCV02PTL9Jxf2NhlCTeSxwQcgInZpne+EQJADvHxDLWnlFe/WZUYSUq/L+R6fUip
Ntt6eOTiMRJGyb+8MjNAO1bqa5pCFW0cfz02fP9j1PssFby0Cr81Hd/bKQJBALzu
RAqnAVz319jmyQPe4K1FmmZbYwZNOyFutOIrG2/d2k8FSkteEBbXFHYxv5xUN9o9
TSUMedhIsDxVYEWTbYECQFF4KoYq2iBud98xNZoaMUToUNaF9WJGNYqCXyOUMk3A
Hohxi0kf+Nx1/+AVGFEPR1+FJgqFHEAaimgEL53mfU8=
-----END RSA PRIVATE KEY-----
rsa_1024_pub.pem
:
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDKKqgnPzneyl2kz7G3yyBgeIKk
UPk17DHfvU0ElcZt1tJpqFgG8L6X4Xb3QyDagxqa/HKMSDrlpRakbzgJTs6kfFCo
fIvNhzQwQNfEnpKdWK7Im7dizPIzIi58ogFHToDAEWfOtpaXMiqY6pknvtVSJHMN
lbDJ3WDkPKj8KOUBGQIDAQAB
-----END PUBLIC KEY-----
PEM 编码的密钥(PEM-encoded)可以通过特定的头部和尾部区分密钥实际格式(format),以下是常见的头部:
-----BEGIN RSA PRIVATE KEY-----
PKCS#1 RSA 私钥-----BEGIN RSA PUBLIC KEY-----
PKCS#1 RSA 公钥-----BEGIN PRIVATE KEY-----
PKCS#8 私钥,未加密-----BEGIN PUBLIC KEY-----
PKCS#8 公钥-----BEGIN ENCRYPTED PRIVATE KEY-----
PKCS#8 私钥,已加密-----BEGIN OPENSSH PRIVATE KEY-----
OpenSSH 私钥
rsa_1024_pub.pem
对应 PKCS#8 公钥格式,只需要将文本内容掐头去尾,base64 解码,最后处理成 BufferSource
类型。需要注意在 subtle.importKey()
中 RSA 私钥对应 pkcs8
,RSA 公钥对应 spki
,importPublicKey()
的完整实现如下:
/**
* PEM-encoded RSA public key string to CryptoKey
* @param {string} pem PEM-encoded RSA public key (SubjectPublicKeyInfo format)
* @returns {Promise<CryptoKey>}
*/
async function importPublicKey(pem) {
const pemHeader = `-----BEGIN PUBLIC KEY-----`;
const pemFooter = `-----END PUBLIC KEY-----`;
const pemContent = pem
.replace(pemHeader, '')
.replace(pemFooter, '')
.replace(/\n/g, '');
const publicKeyData = base64ToUint8Array(pemContent);
const publicKey = await subtle.importKey(
'spki',
publicKeyData,
{
name: 'RSA-OAEP',
hash: 'SHA-256'
},
false,
['encrypt']
);
return publicKey;
}
rsa_1024_priv.pem
对应 PKCS#1 RSA 私钥格式,和公钥处理流程类似。但需要注意 subtle.importKey()
只支持 PKCS#8 私钥格式,和 Node 原本的 crypto
模块 API 不一致。
我们需要使用 OpenSSL 的命令行工具,将 PKCS#1 转换成 PKCS#8 格式。
openssl pkcs8 -topk8 -inform PEM -outform PEM -in rsa_1024_priv.pem -out rsa_1024_priv2.pem -nocrypt
执行完后得到 rsa_1024_priv2.pem
:
-----BEGIN PRIVATE KEY-----
MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAMoqqCc/Od7KXaTP
sbfLIGB4gqRQ+TXsMd+9TQSVxm3W0mmoWAbwvpfhdvdDINqDGpr8coxIOuWlFqRv
OAlOzqR8UKh8i82HNDBA18Sekp1Yrsibt2LM8jMiLnyiAUdOgMARZ862lpcyKpjq
mSe+1VIkcw2VsMndYOQ8qPwo5QEZAgMBAAECgYBi6PvcN9LYbanjOLp6cacli9Bf
FD7ZYvYDwVotJBoxIylPCE4JF0AojublkrJANmSN73UbrTsjn+G0EbYY17XBZQ0y
Ama48y4jvfzBgVFWP9WEWB+gHZeIqDO8DeoIXKs4+3V3gfxdcx0AURimowJzZuBw
WspYH6RkxDvXmlugAQJBAPGpd1swVoiEHiZOD7BaetRVS1arVGsrGRlp/jqEwos+
l1WtaPOAxj0O8yhvyzfBMtwNcWa0FQcPlltJUuAJqokCQQDWKU6key69e0HIhZ98
HuijrkdArA3kp6Bsb+SWMTobPuueO0Sd4XIMJXTY9Mv0nF/Y2GUJN5LHBByAidmm
d74RAkAO8fEMtaeUV79ZlRhJSr8v5Hp9SKk223p45OIxEkbJv7wyM0A7VuprmkIV
bRx/PTZ8/2PU+ywVvLQKvzUd39spAkEAvO5ECqcBXPfX2ObJA97grUWaZltjBk07
IW604isbb93aTwVKS14QFtcUdjG/nFQ32j1NJQx52EiwPFVgRZNtgQJAUXgqhira
IG533zE1mhoxROhQ1oX1YkY1ioJfI5QyTcAeiHGLSR/43HX/4BUYUQ9HX4UmCoUc
QBqKaAQvneZ9Tw==
-----END PRIVATE KEY-----
可以看到 -----BEGIN PRIVATE KEY-----
正好对应 PKCS#8 私钥格式。subtle.importPublicKey()
的完整实现如下:
/**
* PEM-encoded RSA private key string to CryptoKey
* @param {string} pem PEM-encoded RSA private key (PKCS #8 format)
* @returns {Promise<CryptoKey>}
*/
async function importPrivateKey(pem) {
const pemHeader = `-----BEGIN PRIVATE KEY-----`;
const pemFooter = `-----END PRIVATE KEY-----`;
const pemContent = pem
.replace(pemHeader, '')
.replace(pemFooter, '')
.replace(/\n/g, '');
const privateKeyData = base64ToUint8Array(pemContent);
const privateKey = await subtle.importKey(
'pkcs8',
privateKeyData,
{
name: 'RSA-OAEP',
hash: 'SHA-256'
},
false,
['decrypt']
);
return privateKey;
}
到此可以顺利获取到 webcrypto
API 可用的公钥和私钥。
subtle.encrypt()
const encrypted = await subtle.encrypt(algorithm, key, data);
- algorithm: 字典对象,包含了加密算法和需要的额外参数
- key:
CryptoKey
对象,对应subtle.importKey()
返回值 - data:
BufferSource
类型,待加密明文
具体使用参考 SubtleCrypto.encrypt() 文档。
RSA 加密完整实现如下:
/**
* RSA encrypt
* @param {string} plaintext
* @param {string} pk PEM-encoded RSA public key
* @returns {Promise<string>}
*/
async function encrypt(plaintext, pk) {
const publicKey = await importPublicKey(pk);
const encryptedBuffer = await subtle.encrypt(
{
name: 'RSA-OAEP'
},
publicKey,
new TextEncoder().encode(plaintext)
);
const encrypted = arrayBufferToBase64(encryptedBuffer);
return encrypted;
}
subtle.decrypt()
解密函数的参数和加密类似,具体使用参考 SubtleCrypto.decrypt() 文档。
const decrypted = await subtle.decrypt(algorithm, key, data);
RSA 解密完整实现如下:
/**
* RSA decrypt
* @param {string} ciphertext
* @param {string} sk PEM-encoded RSA private key
* @returns {Promise<string>}
*/
async function decrypt(ciphertext, sk) {
const privateKey = await importPrivateKey(sk);
const decryptedBuffer = await subtle.decrypt(
{
name: 'RSA-OAEP'
},
privateKey,
base64ToUint8Array(ciphertext)
);
const decrypted = new TextDecoder().decode(decryptedBuffer);
return decrypted;
}
验证
在验证前,补齐两个简化 base64、类型数组转换的工具函数 base64ToUint8Array()
和 arrayBufferToBase64()
:
/**
* base64 string to Uint8Array
* @param {string} base64
*/
function base64ToUint8Array(base64) {
const byteString = atob(base64);
const bytes = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++) {
bytes[i] = byteString.charCodeAt(i);
}
return bytes;
}
/**
* ArrayBuffer to base64 string
* @param {ArrayBuffer} buffer
* @returns
*/
function arrayBufferToBase64(buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.byteLength; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
}
验证:
const plaintext = 'keqingrong@outlook.com';
/** @type {Crypto} */
const webcrypto =
typeof module === 'object' && typeof require === 'function'
? require('crypto').webcrypto
: window.crypto;
const { subtle } = webcrypto;
(async () => {
const encrypted = await encrypt(plaintext, publicKey);
console.log(encrypted);
const decrypted = await decrypt(encrypted, privateKey);
console.log(decrypted);
})();
相关链接
- SubtleCrypto.importKey()
- SubtleCrypto.encrypt()
- SubtleCrypto.decrypt()
- Web Cryptography API Specification
- Specification Draft https://w3c.github.io/webcrypto/
- Specification https://www.w3.org/TR/WebCryptoAPI/