如何加载 PEM 格式的 public 密钥进行加密?
How to load a public key in PEM format for encryption?
到目前为止,我使用的是 JSEncrypt,它能够从 PEM 格式的字符串中加载 public 密钥。然后将其与 RSA 一起使用以加密字符串。例如:
<textarea id="pubkey">-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+iOltdDtDdUq6u67L2Mb4HW5j
7E1scmYtg2mnnQD85LxFICZv3I3rQ4wMulfcH+n9VCrifdu4vN89lRLKgsb9Kzim
GUrbOWEZdKZ9D5Sfo90EXocM5NtHou14aN8xkRWbN7x/RK5o9jfJwKmrC1fCm6tx
2Qwvx5kypWQUN6UpCQIDAQAB
-----END PUBLIC KEY-----
</textarea>
然后:
var encrypt = new JSEncrypt();
encrypt.setPublicKey($('#pubkey').val());
我想对 WebCrypto 做同样的事情,但我不知道该怎么做。我尝试了以下步骤:
- 删除 PEM header
- 删除 PEM 页脚
- 删除CR/LF
- Trim 字符串
- 解码Base64字符串
- 将结果转换为 ArrayBuffer
然后我尝试导入密钥:
cryptoSubtle.importKey("spki", publicKey, {name: "RSA-OAEP", hash: {name: "SHA-256"}}, false, ["encrypt"]);
我尝试了很多方法(解压 ASN/DER 格式等),但出现各种错误(DOMException 数据等)。我不知道 PEM 格式是否可以作为受支持的格式接受,或者我是否必须将密钥转换为 JSON Web 密钥格式等
有没有没有第三方 JS 库的简单方法?
经过一些测试,我找到了答案。就我而言,我将 JSEncrypt 与 PHP/openssl 或 phpseclib 一起用作后备。
使用 JSEncrypt,您无法选择加密算法。这对PHP解密加密值时使用的padding有影响。 JSEncrypt 使用:
- RSASSA-PKCS1-v1_5
- SHA-1 作为哈希方法
如果你想破译一条消息,你必须使用默认的填充选项:
openssl_private_decrypt(base64_decode($_POST['CipheredValue']), $ouput, $privateKey, OPENSSL_PKCS1_PADDING);
但是 WebCrypto 与 JSEncrypt 不兼容(我们无法使用 PHP 使用相同的选项解密消息),因为:
- WebCrypto 可以使用 SHA-1 作为哈希方法,即使不推荐。
- 但是 WebCrypto 禁止您使用 RSASSA-PKCS1-v1_5 进行加密(仅允许用于签名目的)。您应该改用 RSA-OAEP。
如果您尝试使用默认选项解码加密值,您将收到此消息:
RSA_EAY_PRIVATE_DECRYPT:padding check failed
因此,您必须如下更改填充选项(在 PHP 中):
openssl_private_decrypt(base64_decode($_POST['CipheredValue']), $ouput, $privateKey, OPENSSL_PKCS1_OAEP_PADDING);
关于我最初的问题,是的,如果您按照我在 post
中提到的步骤操作,您可以导入 PEM 格式的密钥
- 删除 PEM header
- 删除 PEM 页脚
- 删除CR/LF
- Trim 字符串
- 解码Base64字符串
- 将结果转换为 ArrayBuffer
完整代码:
var crypto = window.crypto || window.msCrypto;
var encryptAlgorithm = {
name: "RSA-OAEP",
hash: {
name: "SHA-1"
}
};
function arrayBufferToBase64String(arrayBuffer) {
var byteArray = new Uint8Array(arrayBuffer)
var byteString = '';
for (var i=0; i<byteArray.byteLength; i++) {
byteString += String.fromCharCode(byteArray[i]);
}
return btoa(byteString);
}
function base64StringToArrayBuffer(b64str) {
var byteStr = atob(b64str);
var bytes = new Uint8Array(byteStr.length);
for (var i = 0; i < byteStr.length; i++) {
bytes[i] = byteStr.charCodeAt(i);
}
return bytes.buffer;
}
function textToArrayBuffer(str) {
var buf = unescape(encodeURIComponent(str)); // 2 bytes for each char
var bufView = new Uint8Array(buf.length);
for (var i=0; i < buf.length; i++) {
bufView[i] = buf.charCodeAt(i);
}
return bufView;
}
function convertPemToBinary(pem) {
var lines = pem.split('\n');
var encoded = '';
for(var i = 0;i < lines.length;i++){
if (lines[i].trim().length > 0 &&
lines[i].indexOf('-BEGIN RSA PRIVATE KEY-') < 0 &&
lines[i].indexOf('-BEGIN RSA PUBLIC KEY-') < 0 &&
lines[i].indexOf('-BEGIN PUBLIC KEY-') < 0 &&
lines[i].indexOf('-END PUBLIC KEY-') < 0 &&
lines[i].indexOf('-END RSA PRIVATE KEY-') < 0 &&
lines[i].indexOf('-END RSA PUBLIC KEY-') < 0) {
encoded += lines[i].trim();
}
}
return base64StringToArrayBuffer(encoded);
}
function importPublicKey(pemKey) {
return new Promise(function(resolve) {
var importer = crypto.subtle.importKey("spki", convertPemToBinary(pemKey), encryptAlgorithm, false, ["encrypt"]);
importer.then(function(key) {
resolve(key);
});
});
}
if (crypto.subtle) {
start = new Date().getTime();
importPublicKey($('#pubkey').val()).then(function(key) {
crypto.subtle.encrypt(encryptAlgorithm, key, textToArrayBuffer($('#txtClear').val())).then(function(cipheredData) {
cipheredValue = arrayBufferToBase64String(cipheredData);
console.log(cipheredValue);
});
});
}
首先选择你喜欢的Base64-to-ArrayBuffer和String-to-ArrayBuffer方法,例如
function b64ToArrayBuffer(b64) {
return new Promise((res, rej) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'data:application/octet-stream;base64,' + b64);
xhr.responseType = 'arraybuffer';
xhr.addEventListener('load', e => res(xhr.response));
xhr.addEventListener('error', e => rej(xhr));
xhr.send();
});
}
function stringToArrayBuffer(str) {
return new Promise((res, rej) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'data:text/plain,' + str);
xhr.responseType = 'arraybuffer';
xhr.addEventListener('load', e => res(xhr.response));
xhr.addEventListener('error', e => rej(xhr));
xhr.send();
});
}
(这些有机会给Uncaught (in promise) DOMException: Only secure origins are allowed (see: https://goo.gl/Y0ZkNV).
)
然后你可以在Promise风格
中写一个encrypt
函数
function encrypt(b64_key, clear_text) {
return b64ToArrayBuffer(b64_key)
.then(buffer => window.crypto.subtle.importKey("spki", buffer, {name: "RSA-OAEP", hash: {name: "SHA-256"}}, false, ["encrypt"]))
.then(key => new Promise((res, rej) => stringToArrayBuffer(clear_text).then(buffer => res({key, buffer}))))
.then(data => window.crypto.subtle.encrypt({name: "RSA-OAEP", hash: {name: "SHA-256"}}, data.key, data.buffer));
}
最后,使用它
encrypt('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+iOltdDtDdUq6u67L2Mb4HW5j\
7E1scmYtg2mnnQD85LxFICZv3I3rQ4wMulfcH+n9VCrifdu4vN89lRLKgsb9Kzim\
GUrbOWEZdKZ9D5Sfo90EXocM5NtHou14aN8xkRWbN7x/RK5o9jfJwKmrC1fCm6tx\
2Qwvx5kypWQUN6UpCQIDAQAB', 'Hello World')
.then(result => console.log(String.fromCharCode.apply(null, new Uint16Array(result))));
// 䍞鸵즱ය㥬ᬍ㖆淓䛿⫵�ɪꤿᮌ怀跰届쇎偌诔락曶락ه͌쥻쨋沶碅姮갣ꤠ퉥�ﮕ컙郞ꦨꉣ茱닦ꥋ༈쿵⇲蟌赅龙Ⲯ偼幱䋚⫛Ɂౖ勍
到目前为止,我使用的是 JSEncrypt,它能够从 PEM 格式的字符串中加载 public 密钥。然后将其与 RSA 一起使用以加密字符串。例如:
<textarea id="pubkey">-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+iOltdDtDdUq6u67L2Mb4HW5j
7E1scmYtg2mnnQD85LxFICZv3I3rQ4wMulfcH+n9VCrifdu4vN89lRLKgsb9Kzim
GUrbOWEZdKZ9D5Sfo90EXocM5NtHou14aN8xkRWbN7x/RK5o9jfJwKmrC1fCm6tx
2Qwvx5kypWQUN6UpCQIDAQAB
-----END PUBLIC KEY-----
</textarea>
然后:
var encrypt = new JSEncrypt();
encrypt.setPublicKey($('#pubkey').val());
我想对 WebCrypto 做同样的事情,但我不知道该怎么做。我尝试了以下步骤:
- 删除 PEM header
- 删除 PEM 页脚
- 删除CR/LF
- Trim 字符串
- 解码Base64字符串
- 将结果转换为 ArrayBuffer
然后我尝试导入密钥:
cryptoSubtle.importKey("spki", publicKey, {name: "RSA-OAEP", hash: {name: "SHA-256"}}, false, ["encrypt"]);
我尝试了很多方法(解压 ASN/DER 格式等),但出现各种错误(DOMException 数据等)。我不知道 PEM 格式是否可以作为受支持的格式接受,或者我是否必须将密钥转换为 JSON Web 密钥格式等
有没有没有第三方 JS 库的简单方法?
经过一些测试,我找到了答案。就我而言,我将 JSEncrypt 与 PHP/openssl 或 phpseclib 一起用作后备。
使用 JSEncrypt,您无法选择加密算法。这对PHP解密加密值时使用的padding有影响。 JSEncrypt 使用:
- RSASSA-PKCS1-v1_5
- SHA-1 作为哈希方法
如果你想破译一条消息,你必须使用默认的填充选项:
openssl_private_decrypt(base64_decode($_POST['CipheredValue']), $ouput, $privateKey, OPENSSL_PKCS1_PADDING);
但是 WebCrypto 与 JSEncrypt 不兼容(我们无法使用 PHP 使用相同的选项解密消息),因为:
- WebCrypto 可以使用 SHA-1 作为哈希方法,即使不推荐。
- 但是 WebCrypto 禁止您使用 RSASSA-PKCS1-v1_5 进行加密(仅允许用于签名目的)。您应该改用 RSA-OAEP。
如果您尝试使用默认选项解码加密值,您将收到此消息:
RSA_EAY_PRIVATE_DECRYPT:padding check failed
因此,您必须如下更改填充选项(在 PHP 中):
openssl_private_decrypt(base64_decode($_POST['CipheredValue']), $ouput, $privateKey, OPENSSL_PKCS1_OAEP_PADDING);
关于我最初的问题,是的,如果您按照我在 post
中提到的步骤操作,您可以导入 PEM 格式的密钥- 删除 PEM header
- 删除 PEM 页脚
- 删除CR/LF
- Trim 字符串
- 解码Base64字符串
- 将结果转换为 ArrayBuffer
完整代码:
var crypto = window.crypto || window.msCrypto;
var encryptAlgorithm = {
name: "RSA-OAEP",
hash: {
name: "SHA-1"
}
};
function arrayBufferToBase64String(arrayBuffer) {
var byteArray = new Uint8Array(arrayBuffer)
var byteString = '';
for (var i=0; i<byteArray.byteLength; i++) {
byteString += String.fromCharCode(byteArray[i]);
}
return btoa(byteString);
}
function base64StringToArrayBuffer(b64str) {
var byteStr = atob(b64str);
var bytes = new Uint8Array(byteStr.length);
for (var i = 0; i < byteStr.length; i++) {
bytes[i] = byteStr.charCodeAt(i);
}
return bytes.buffer;
}
function textToArrayBuffer(str) {
var buf = unescape(encodeURIComponent(str)); // 2 bytes for each char
var bufView = new Uint8Array(buf.length);
for (var i=0; i < buf.length; i++) {
bufView[i] = buf.charCodeAt(i);
}
return bufView;
}
function convertPemToBinary(pem) {
var lines = pem.split('\n');
var encoded = '';
for(var i = 0;i < lines.length;i++){
if (lines[i].trim().length > 0 &&
lines[i].indexOf('-BEGIN RSA PRIVATE KEY-') < 0 &&
lines[i].indexOf('-BEGIN RSA PUBLIC KEY-') < 0 &&
lines[i].indexOf('-BEGIN PUBLIC KEY-') < 0 &&
lines[i].indexOf('-END PUBLIC KEY-') < 0 &&
lines[i].indexOf('-END RSA PRIVATE KEY-') < 0 &&
lines[i].indexOf('-END RSA PUBLIC KEY-') < 0) {
encoded += lines[i].trim();
}
}
return base64StringToArrayBuffer(encoded);
}
function importPublicKey(pemKey) {
return new Promise(function(resolve) {
var importer = crypto.subtle.importKey("spki", convertPemToBinary(pemKey), encryptAlgorithm, false, ["encrypt"]);
importer.then(function(key) {
resolve(key);
});
});
}
if (crypto.subtle) {
start = new Date().getTime();
importPublicKey($('#pubkey').val()).then(function(key) {
crypto.subtle.encrypt(encryptAlgorithm, key, textToArrayBuffer($('#txtClear').val())).then(function(cipheredData) {
cipheredValue = arrayBufferToBase64String(cipheredData);
console.log(cipheredValue);
});
});
}
首先选择你喜欢的Base64-to-ArrayBuffer和String-to-ArrayBuffer方法,例如
function b64ToArrayBuffer(b64) {
return new Promise((res, rej) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'data:application/octet-stream;base64,' + b64);
xhr.responseType = 'arraybuffer';
xhr.addEventListener('load', e => res(xhr.response));
xhr.addEventListener('error', e => rej(xhr));
xhr.send();
});
}
function stringToArrayBuffer(str) {
return new Promise((res, rej) => {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'data:text/plain,' + str);
xhr.responseType = 'arraybuffer';
xhr.addEventListener('load', e => res(xhr.response));
xhr.addEventListener('error', e => rej(xhr));
xhr.send();
});
}
(这些有机会给Uncaught (in promise) DOMException: Only secure origins are allowed (see: https://goo.gl/Y0ZkNV).
)
然后你可以在Promise风格
中写一个encrypt
函数
function encrypt(b64_key, clear_text) {
return b64ToArrayBuffer(b64_key)
.then(buffer => window.crypto.subtle.importKey("spki", buffer, {name: "RSA-OAEP", hash: {name: "SHA-256"}}, false, ["encrypt"]))
.then(key => new Promise((res, rej) => stringToArrayBuffer(clear_text).then(buffer => res({key, buffer}))))
.then(data => window.crypto.subtle.encrypt({name: "RSA-OAEP", hash: {name: "SHA-256"}}, data.key, data.buffer));
}
最后,使用它
encrypt('MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC+iOltdDtDdUq6u67L2Mb4HW5j\
7E1scmYtg2mnnQD85LxFICZv3I3rQ4wMulfcH+n9VCrifdu4vN89lRLKgsb9Kzim\
GUrbOWEZdKZ9D5Sfo90EXocM5NtHou14aN8xkRWbN7x/RK5o9jfJwKmrC1fCm6tx\
2Qwvx5kypWQUN6UpCQIDAQAB', 'Hello World')
.then(result => console.log(String.fromCharCode.apply(null, new Uint16Array(result))));
// 䍞鸵즱ය㥬ᬍ㖆淓䛿⫵�ɪꤿᮌ怀跰届쇎偌诔락曶락ه͌쥻쨋沶碅姮갣ꤠ퉥�ﮕ컙郞ꦨꉣ茱닦ꥋ༈쿵⇲蟌赅龙Ⲯ偼幱䋚⫛Ɂౖ勍