使用 WebCrypto API 导入 pem 格式 public RSA 密钥时出现 DOMException
DOMException when importing a pem formatted public RSA key using the WebCrypto API
我可能遗漏了一些阻止浏览器将 public 密钥文件导入 CryptoKey 对象的安全问题,但我看不到文档中提到的任何内容。浏览器产生一个没有附加消息的“DOMException”。
我正在使用帮助程序库“OpenCrypto”来简化密钥管理过程,但是当我直接使用 WebCrypto APIs 时收到同样的错误(提示我使用帮助程序库,以确保不只是我滥用 API).
async importKey() {
try {
let pem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3GvsQ5+vT+lkuGb7PP6
slV5mNyOAQo5rhInuDMFkyJOnwIDLzOQ7tLe4ApDPt2CmuRG+qpMaul+IYPBk6Ts
9YPdvvVh5lohiDRN7ny3Sd5uwUy4Ea/NkY62lui4zDFnUDMH8pAUcJWQW4zKloRI
k2EsXR5A5dqOq4wv2+I76Ax9lK2qYkQBZ8ZqeePPMYU1N0lETzCgDW/FqQEk6m81
2c8LnF2bhnrjFJ2k0lTDVx4TwvEUOEg6TbFah+PNe8CFN/cJsHMxlr4StV6nwpZu
n62YSXo9KskLmSRNhGKUS+oNEzTeLRyNfpZb3WQFOjqlgqJFW1xp1KfEdqFk+37z
HwIDAQAB
-----END PUBLIC KEY-----`;
let key = await this.#crypt.pemPublicToCrypto(pem, { name: 'RSA-PSS', usages: ['verify'], isExtractable: true });
} catch (error) {
console.log(`Error importing key`, error);
}
}
public密钥是使用Node JS实现的WebCryptoAPI(crypto.webcrypto)生成的,然后我简单的把内容复制粘贴到这个函数里试试. 运行 节点中的这个函数导入正常。 运行 它在浏览器中产生 DOMException。
以下是 OpenCrypto 来源的相关代码:
const cryptoLib = window.crypto || window.msCrypto
const cryptoApi = cryptoLib.subtle || cryptoLib.webkitSubtle
//... other code removed for clarity
/**
* Method that converts asymmetric public key from PEM to CryptoKey format
* @param {String} publicKey default: "undefined"
* @param {Object} options default: depends on algorithm below
* -- ECDH: { name: 'ECDH', usages: [], isExtractable: true }
* -- ECDSA: { name: 'ECDSA', usages: ['verify'], isExtractable: true }
* -- RSA-OAEP: { name: 'RSA-OAEP', hash: { name: 'SHA-512' }, usages: ['encrypt', 'wrapKey'], isExtractable: true }
* -- RSA-PSS: { name: 'RSA-PSS', hash: { name: 'SHA-512' }, usages: ['verify'], isExtractable: true }
*/
pemPublicToCrypto (pem, options) {
const self = this
if (typeof options === 'undefined') {
options = {}
}
options.isExtractable = (typeof options.isExtractable !== 'undefined') ? options.isExtractable : true
return new Promise((resolve, reject) => {
if (typeof pem !== 'string') {
throw new TypeError('Expected input of pem to be a String')
}
if (typeof options.isExtractable !== 'boolean') {
throw new TypeError('Expected input of options.isExtractable to be a Boolean')
}
pem = pem.replace('-----BEGIN PUBLIC KEY-----', '')
pem = pem.replace('-----END PUBLIC KEY-----', '')
const b64 = self.removeLines(pem)
const arrayBuffer = self.base64ToArrayBuffer(b64)
const hex = self.arrayBufferToHexString(arrayBuffer)
const keyOptions = {}
if (hex.includes(EC_OID)) {
options.name = (typeof options.name !== 'undefined') ? options.name : 'ECDH'
if (typeof options.name !== 'string') {
throw new TypeError('Expected input of options.name to be a String')
}
let curve = null
if (hex.includes(P256_OID)) {
curve = 'P-256'
} else if (hex.includes(P384_OID)) {
curve = 'P-384'
} else if (hex.includes(P521_OID)) {
curve = 'P-521'
}
if (options.name === 'ECDH') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : []
} else if (options.name === 'ECDSA') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : ['verify']
} else {
throw new TypeError('Expected input of options.name is not a valid algorithm name')
}
if (typeof options.usages !== 'object') {
throw new TypeError('Expected input of options.usages to be an Array')
}
keyOptions.name = options.name
keyOptions.namedCurve = curve
} else if (hex.includes(RSA_OID)) {
options.name = (typeof options.name !== 'undefined') ? options.name : 'RSA-OAEP'
options.hash = (typeof options.hash !== 'undefined') ? options.hash : 'SHA-512'
if (typeof options.name !== 'string') {
throw new TypeError('Expected input of options.name to be a String')
}
if (typeof options.hash !== 'string') {
throw new TypeError('Expected input of options.hash to be a String')
}
if (options.name === 'RSA-OAEP') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : ['encrypt', 'wrapKey']
} else if (options.name === 'RSA-PSS') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : ['verify']
} else {
throw new TypeError('Expected input of options.name is not a valid algorithm name')
}
if (typeof options.usages !== 'object') {
throw new TypeError('Expected input of options.usages to be an Array')
}
keyOptions.name = options.name
keyOptions.hash = {}
keyOptions.hash.name = options.hash
} else {
throw new TypeError('Expected input of pem is not a valid public key')
}
cryptoApi.importKey(
'spki',
arrayBuffer,
keyOptions,
options.isExtractable,
options.usages
).then(importedPublicKey => {
resolve(importedPublicKey)
}).catch(err => {
reject(err)
})
})
}
错误是由 SubtleCrypto.importKey()
函数引发的。
我已经尝试过 ECDSA 和 RSA-PSS 密钥以防两者不兼容,但两者都会产生相同的错误。
如有任何帮助,我将不胜感激。
谢谢
问题仅仅是因为键的缩进造成的。
这会导致从 PEM 到 DER 的转换过程中出现损坏:在 pemPublicToCrypto()
中,行 const b64 = self.removeLines(pem)
中的换行符被删除。但是,这不会删除可能的空格或制表符,因此在随后转换为带有 const arrayBuffer = self.base64ToArrayBuffer(b64)
的 ArrayBuffer
期间,它们会包含在数据中,这会损坏数据。
所以解决方案是省略缩进或删除缩进,例如pem.replace(/(\s)[ \t]+/g, '')
:
async function importKey() {
var c = new OpenCrypto();
try {
//
// Without indentation: Works
//
var pem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3GvsQ5+vT+lkuGb7PP6
slV5mNyOAQo5rhInuDMFkyJOnwIDLzOQ7tLe4ApDPt2CmuRG+qpMaul+IYPBk6Ts
9YPdvvVh5lohiDRN7ny3Sd5uwUy4Ea/NkY62lui4zDFnUDMH8pAUcJWQW4zKloRI
k2EsXR5A5dqOq4wv2+I76Ax9lK2qYkQBZ8ZqeePPMYU1N0lETzCgDW/FqQEk6m81
2c8LnF2bhnrjFJ2k0lTDVx4TwvEUOEg6TbFah+PNe8CFN/cJsHMxlr4StV6nwpZu
n62YSXo9KskLmSRNhGKUS+oNEzTeLRyNfpZb3WQFOjqlgqJFW1xp1KfEdqFk+37z
HwIDAQAB
-----END PUBLIC KEY-----`;
console.log("Key without indentation:")
var key = await c.pemPublicToCrypto(pem, { name: 'RSA-PSS', usages: ['verify'], isExtractable: true });
console.log(key);
//
// With indentation and space/tab removal: Works
//
var pem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3GvsQ5+vT+lkuGb7PP6
slV5mNyOAQo5rhInuDMFkyJOnwIDLzOQ7tLe4ApDPt2CmuRG+qpMaul+IYPBk6Ts
9YPdvvVh5lohiDRN7ny3Sd5uwUy4Ea/NkY62lui4zDFnUDMH8pAUcJWQW4zKloRI
k2EsXR5A5dqOq4wv2+I76Ax9lK2qYkQBZ8ZqeePPMYU1N0lETzCgDW/FqQEk6m81
2c8LnF2bhnrjFJ2k0lTDVx4TwvEUOEg6TbFah+PNe8CFN/cJsHMxlr4StV6nwpZu
n62YSXo9KskLmSRNhGKUS+oNEzTeLRyNfpZb3WQFOjqlgqJFW1xp1KfEdqFk+37z
HwIDAQAB
-----END PUBLIC KEY-----`;
console.log("Key with indentation and space/tab removal:")
var key = await c.pemPublicToCrypto(pem.replace(/(\s)[ \t]+/g, ''), { name: 'RSA-PSS', usages: ['verify'], isExtractable: true });
console.log(key);
//
// With indentation: Doesn't work
//
var pem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3GvsQ5+vT+lkuGb7PP6
slV5mNyOAQo5rhInuDMFkyJOnwIDLzOQ7tLe4ApDPt2CmuRG+qpMaul+IYPBk6Ts
9YPdvvVh5lohiDRN7ny3Sd5uwUy4Ea/NkY62lui4zDFnUDMH8pAUcJWQW4zKloRI
k2EsXR5A5dqOq4wv2+I76Ax9lK2qYkQBZ8ZqeePPMYU1N0lETzCgDW/FqQEk6m81
2c8LnF2bhnrjFJ2k0lTDVx4TwvEUOEg6TbFah+PNe8CFN/cJsHMxlr4StV6nwpZu
n62YSXo9KskLmSRNhGKUS+oNEzTeLRyNfpZb3WQFOjqlgqJFW1xp1KfEdqFk+37z
HwIDAQAB
-----END PUBLIC KEY-----`;
console.log("Key with indentation:");
var key = await c.pemPublicToCrypto(pem, { name: 'RSA-PSS', usages: ['verify'], isExtractable: true });
console.log(key);
} catch (error) {
console.log(`Error importing key`, error);
}
}
// --------------------------------------------------------------------------------------------------
/**
*
* Copyright (c) 2016 SafeBash
* Cryptography consultant: Andrew Kozlik, Ph.D.
*
*/
/**
* MIT License
*
* Copyright (c) 2016 SafeBash
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
* to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const cryptoLib = window.crypto || window.msCrypto
const cryptoApi = cryptoLib.subtle || cryptoLib.webkitSubtle
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
const lookup = new Uint8Array(256)
const RSA_OID = '06092a864886f70d010101'
const EC_OID = '06072a8648ce3d0201'
const P256_OID = '06082a8648ce3d030107'
const P384_OID = '06052b81040022'
const P521_OID = '06052b81040023'
class OpenCrypto {
constructor () {
for (let i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i
}
}
/**
* BEGIN
* base64-arraybuffer
* GitHub @niklasvh
* Copyright (c) 2012 Niklas von Hertzen
* MIT License
*/
decodeAb (base64) {
const len = base64.length
let bufferLength = base64.length * 0.75
let p = 0
let encoded1
let encoded2
let encoded3
let encoded4
if (base64[base64.length - 1] === '=') {
bufferLength--
if (base64[base64.length - 2] === '=') {
bufferLength--
}
}
const arrayBuffer = new ArrayBuffer(bufferLength)
let bytes = new Uint8Array(arrayBuffer)
for (let i = 0; i < len; i += 4) {
encoded1 = lookup[base64.charCodeAt(i)]
encoded2 = lookup[base64.charCodeAt(i + 1)]
encoded3 = lookup[base64.charCodeAt(i + 2)]
encoded4 = lookup[base64.charCodeAt(i + 3)]
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4)
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2)
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63)
}
return arrayBuffer
}
/**
* END
* base64-arraybuffer
*/
/**
* Method for encoding ArrayBuffer to hexadecimal String
*/
arrayBufferToHexString (arrayBuffer) {
if (typeof arrayBuffer !== 'object') {
throw new TypeError('Expected input of arrayBuffer to be an ArrayBuffer Object')
}
const byteArray = new Uint8Array(arrayBuffer)
let hexString = ''
let nextHexByte
for (let i = 0; i < byteArray.byteLength; i++) {
nextHexByte = byteArray[i].toString(16)
if (nextHexByte.length < 2) {
nextHexByte = '0' + nextHexByte
}
hexString += nextHexByte
}
return hexString
}
/**
* Method for decoding base64 String to ArrayBuffer
*/
base64ToArrayBuffer (b64) {
if (typeof b64 !== 'string') {
throw new TypeError('Expected input of b64 to be a Base64 String')
}
return this.decodeAb(b64)
}
/**
* Method that removes lines from PEM encoded key
*/
removeLines (str) {
return str.replace(/\r?\n|\r/g, '')
}
/**
* Method that converts asymmetric public key from PEM to CryptoKey format
* @param {String} publicKey default: "undefined"
* @param {Object} options default: depends on algorithm below
* -- ECDH: { name: 'ECDH', usages: [], isExtractable: true }
* -- ECDSA: { name: 'ECDSA', usages: ['verify'], isExtractable: true }
* -- RSA-OAEP: { name: 'RSA-OAEP', hash: { name: 'SHA-512' }, usages: ['encrypt', 'wrapKey'], isExtractable: true }
* -- RSA-PSS: { name: 'RSA-PSS', hash: { name: 'SHA-512' }, usages: ['verify'], isExtractable: true }
*/
pemPublicToCrypto (pem, options) {
const self = this
if (typeof options === 'undefined') {
options = {}
}
options.isExtractable = (typeof options.isExtractable !== 'undefined') ? options.isExtractable : true
return new Promise((resolve, reject) => {
if (typeof pem !== 'string') {
throw new TypeError('Expected input of pem to be a String')
}
if (typeof options.isExtractable !== 'boolean') {
throw new TypeError('Expected input of options.isExtractable to be a Boolean')
}
pem = pem.replace('-----BEGIN PUBLIC KEY-----', '')
pem = pem.replace('-----END PUBLIC KEY-----', '')
const b64 = self.removeLines(pem)
const arrayBuffer = self.base64ToArrayBuffer(b64)
const hex = self.arrayBufferToHexString(arrayBuffer)
const keyOptions = {}
if (hex.includes(EC_OID)) {
options.name = (typeof options.name !== 'undefined') ? options.name : 'ECDH'
if (typeof options.name !== 'string') {
throw new TypeError('Expected input of options.name to be a String')
}
let curve = null
if (hex.includes(P256_OID)) {
curve = 'P-256'
} else if (hex.includes(P384_OID)) {
curve = 'P-384'
} else if (hex.includes(P521_OID)) {
curve = 'P-521'
}
if (options.name === 'ECDH') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : []
} else if (options.name === 'ECDSA') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : ['verify']
} else {
throw new TypeError('Expected input of options.name is not a valid algorithm name')
}
if (typeof options.usages !== 'object') {
throw new TypeError('Expected input of options.usages to be an Array')
}
keyOptions.name = options.name
keyOptions.namedCurve = curve
} else if (hex.includes(RSA_OID)) {
options.name = (typeof options.name !== 'undefined') ? options.name : 'RSA-OAEP'
options.hash = (typeof options.hash !== 'undefined') ? options.hash : 'SHA-512'
if (typeof options.name !== 'string') {
throw new TypeError('Expected input of options.name to be a String')
}
if (typeof options.hash !== 'string') {
throw new TypeError('Expected input of options.hash to be a String')
}
if (options.name === 'RSA-OAEP') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : ['encrypt', 'wrapKey']
} else if (options.name === 'RSA-PSS') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : ['verify']
} else {
throw new TypeError('Expected input of options.name is not a valid algorithm name')
}
if (typeof options.usages !== 'object') {
throw new TypeError('Expected input of options.usages to be an Array')
}
keyOptions.name = options.name
keyOptions.hash = {}
keyOptions.hash.name = options.hash
} else {
throw new TypeError('Expected input of pem is not a valid public key')
}
cryptoApi.importKey(
'spki',
arrayBuffer,
keyOptions,
options.isExtractable,
options.usages
).then(importedPublicKey => {
resolve(importedPublicKey)
}).catch(err => {
reject(err)
})
})
}
}
// --------------------------------------------------------------------------------------------------
(async () => {
await importKey();
})();
我可能遗漏了一些阻止浏览器将 public 密钥文件导入 CryptoKey 对象的安全问题,但我看不到文档中提到的任何内容。浏览器产生一个没有附加消息的“DOMException”。
我正在使用帮助程序库“OpenCrypto”来简化密钥管理过程,但是当我直接使用 WebCrypto APIs 时收到同样的错误(提示我使用帮助程序库,以确保不只是我滥用 API).
async importKey() {
try {
let pem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3GvsQ5+vT+lkuGb7PP6
slV5mNyOAQo5rhInuDMFkyJOnwIDLzOQ7tLe4ApDPt2CmuRG+qpMaul+IYPBk6Ts
9YPdvvVh5lohiDRN7ny3Sd5uwUy4Ea/NkY62lui4zDFnUDMH8pAUcJWQW4zKloRI
k2EsXR5A5dqOq4wv2+I76Ax9lK2qYkQBZ8ZqeePPMYU1N0lETzCgDW/FqQEk6m81
2c8LnF2bhnrjFJ2k0lTDVx4TwvEUOEg6TbFah+PNe8CFN/cJsHMxlr4StV6nwpZu
n62YSXo9KskLmSRNhGKUS+oNEzTeLRyNfpZb3WQFOjqlgqJFW1xp1KfEdqFk+37z
HwIDAQAB
-----END PUBLIC KEY-----`;
let key = await this.#crypt.pemPublicToCrypto(pem, { name: 'RSA-PSS', usages: ['verify'], isExtractable: true });
} catch (error) {
console.log(`Error importing key`, error);
}
}
public密钥是使用Node JS实现的WebCryptoAPI(crypto.webcrypto)生成的,然后我简单的把内容复制粘贴到这个函数里试试. 运行 节点中的这个函数导入正常。 运行 它在浏览器中产生 DOMException。
以下是 OpenCrypto 来源的相关代码:
const cryptoLib = window.crypto || window.msCrypto
const cryptoApi = cryptoLib.subtle || cryptoLib.webkitSubtle
//... other code removed for clarity
/**
* Method that converts asymmetric public key from PEM to CryptoKey format
* @param {String} publicKey default: "undefined"
* @param {Object} options default: depends on algorithm below
* -- ECDH: { name: 'ECDH', usages: [], isExtractable: true }
* -- ECDSA: { name: 'ECDSA', usages: ['verify'], isExtractable: true }
* -- RSA-OAEP: { name: 'RSA-OAEP', hash: { name: 'SHA-512' }, usages: ['encrypt', 'wrapKey'], isExtractable: true }
* -- RSA-PSS: { name: 'RSA-PSS', hash: { name: 'SHA-512' }, usages: ['verify'], isExtractable: true }
*/
pemPublicToCrypto (pem, options) {
const self = this
if (typeof options === 'undefined') {
options = {}
}
options.isExtractable = (typeof options.isExtractable !== 'undefined') ? options.isExtractable : true
return new Promise((resolve, reject) => {
if (typeof pem !== 'string') {
throw new TypeError('Expected input of pem to be a String')
}
if (typeof options.isExtractable !== 'boolean') {
throw new TypeError('Expected input of options.isExtractable to be a Boolean')
}
pem = pem.replace('-----BEGIN PUBLIC KEY-----', '')
pem = pem.replace('-----END PUBLIC KEY-----', '')
const b64 = self.removeLines(pem)
const arrayBuffer = self.base64ToArrayBuffer(b64)
const hex = self.arrayBufferToHexString(arrayBuffer)
const keyOptions = {}
if (hex.includes(EC_OID)) {
options.name = (typeof options.name !== 'undefined') ? options.name : 'ECDH'
if (typeof options.name !== 'string') {
throw new TypeError('Expected input of options.name to be a String')
}
let curve = null
if (hex.includes(P256_OID)) {
curve = 'P-256'
} else if (hex.includes(P384_OID)) {
curve = 'P-384'
} else if (hex.includes(P521_OID)) {
curve = 'P-521'
}
if (options.name === 'ECDH') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : []
} else if (options.name === 'ECDSA') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : ['verify']
} else {
throw new TypeError('Expected input of options.name is not a valid algorithm name')
}
if (typeof options.usages !== 'object') {
throw new TypeError('Expected input of options.usages to be an Array')
}
keyOptions.name = options.name
keyOptions.namedCurve = curve
} else if (hex.includes(RSA_OID)) {
options.name = (typeof options.name !== 'undefined') ? options.name : 'RSA-OAEP'
options.hash = (typeof options.hash !== 'undefined') ? options.hash : 'SHA-512'
if (typeof options.name !== 'string') {
throw new TypeError('Expected input of options.name to be a String')
}
if (typeof options.hash !== 'string') {
throw new TypeError('Expected input of options.hash to be a String')
}
if (options.name === 'RSA-OAEP') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : ['encrypt', 'wrapKey']
} else if (options.name === 'RSA-PSS') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : ['verify']
} else {
throw new TypeError('Expected input of options.name is not a valid algorithm name')
}
if (typeof options.usages !== 'object') {
throw new TypeError('Expected input of options.usages to be an Array')
}
keyOptions.name = options.name
keyOptions.hash = {}
keyOptions.hash.name = options.hash
} else {
throw new TypeError('Expected input of pem is not a valid public key')
}
cryptoApi.importKey(
'spki',
arrayBuffer,
keyOptions,
options.isExtractable,
options.usages
).then(importedPublicKey => {
resolve(importedPublicKey)
}).catch(err => {
reject(err)
})
})
}
错误是由 SubtleCrypto.importKey()
函数引发的。
我已经尝试过 ECDSA 和 RSA-PSS 密钥以防两者不兼容,但两者都会产生相同的错误。
如有任何帮助,我将不胜感激。
谢谢
问题仅仅是因为键的缩进造成的。
这会导致从 PEM 到 DER 的转换过程中出现损坏:在 pemPublicToCrypto()
中,行 const b64 = self.removeLines(pem)
中的换行符被删除。但是,这不会删除可能的空格或制表符,因此在随后转换为带有 const arrayBuffer = self.base64ToArrayBuffer(b64)
的 ArrayBuffer
期间,它们会包含在数据中,这会损坏数据。
所以解决方案是省略缩进或删除缩进,例如pem.replace(/(\s)[ \t]+/g, '')
:
async function importKey() {
var c = new OpenCrypto();
try {
//
// Without indentation: Works
//
var pem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3GvsQ5+vT+lkuGb7PP6
slV5mNyOAQo5rhInuDMFkyJOnwIDLzOQ7tLe4ApDPt2CmuRG+qpMaul+IYPBk6Ts
9YPdvvVh5lohiDRN7ny3Sd5uwUy4Ea/NkY62lui4zDFnUDMH8pAUcJWQW4zKloRI
k2EsXR5A5dqOq4wv2+I76Ax9lK2qYkQBZ8ZqeePPMYU1N0lETzCgDW/FqQEk6m81
2c8LnF2bhnrjFJ2k0lTDVx4TwvEUOEg6TbFah+PNe8CFN/cJsHMxlr4StV6nwpZu
n62YSXo9KskLmSRNhGKUS+oNEzTeLRyNfpZb3WQFOjqlgqJFW1xp1KfEdqFk+37z
HwIDAQAB
-----END PUBLIC KEY-----`;
console.log("Key without indentation:")
var key = await c.pemPublicToCrypto(pem, { name: 'RSA-PSS', usages: ['verify'], isExtractable: true });
console.log(key);
//
// With indentation and space/tab removal: Works
//
var pem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3GvsQ5+vT+lkuGb7PP6
slV5mNyOAQo5rhInuDMFkyJOnwIDLzOQ7tLe4ApDPt2CmuRG+qpMaul+IYPBk6Ts
9YPdvvVh5lohiDRN7ny3Sd5uwUy4Ea/NkY62lui4zDFnUDMH8pAUcJWQW4zKloRI
k2EsXR5A5dqOq4wv2+I76Ax9lK2qYkQBZ8ZqeePPMYU1N0lETzCgDW/FqQEk6m81
2c8LnF2bhnrjFJ2k0lTDVx4TwvEUOEg6TbFah+PNe8CFN/cJsHMxlr4StV6nwpZu
n62YSXo9KskLmSRNhGKUS+oNEzTeLRyNfpZb3WQFOjqlgqJFW1xp1KfEdqFk+37z
HwIDAQAB
-----END PUBLIC KEY-----`;
console.log("Key with indentation and space/tab removal:")
var key = await c.pemPublicToCrypto(pem.replace(/(\s)[ \t]+/g, ''), { name: 'RSA-PSS', usages: ['verify'], isExtractable: true });
console.log(key);
//
// With indentation: Doesn't work
//
var pem = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq3GvsQ5+vT+lkuGb7PP6
slV5mNyOAQo5rhInuDMFkyJOnwIDLzOQ7tLe4ApDPt2CmuRG+qpMaul+IYPBk6Ts
9YPdvvVh5lohiDRN7ny3Sd5uwUy4Ea/NkY62lui4zDFnUDMH8pAUcJWQW4zKloRI
k2EsXR5A5dqOq4wv2+I76Ax9lK2qYkQBZ8ZqeePPMYU1N0lETzCgDW/FqQEk6m81
2c8LnF2bhnrjFJ2k0lTDVx4TwvEUOEg6TbFah+PNe8CFN/cJsHMxlr4StV6nwpZu
n62YSXo9KskLmSRNhGKUS+oNEzTeLRyNfpZb3WQFOjqlgqJFW1xp1KfEdqFk+37z
HwIDAQAB
-----END PUBLIC KEY-----`;
console.log("Key with indentation:");
var key = await c.pemPublicToCrypto(pem, { name: 'RSA-PSS', usages: ['verify'], isExtractable: true });
console.log(key);
} catch (error) {
console.log(`Error importing key`, error);
}
}
// --------------------------------------------------------------------------------------------------
/**
*
* Copyright (c) 2016 SafeBash
* Cryptography consultant: Andrew Kozlik, Ph.D.
*
*/
/**
* MIT License
*
* Copyright (c) 2016 SafeBash
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
* documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
* to whom the Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
* Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
* NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
* DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const cryptoLib = window.crypto || window.msCrypto
const cryptoApi = cryptoLib.subtle || cryptoLib.webkitSubtle
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
const lookup = new Uint8Array(256)
const RSA_OID = '06092a864886f70d010101'
const EC_OID = '06072a8648ce3d0201'
const P256_OID = '06082a8648ce3d030107'
const P384_OID = '06052b81040022'
const P521_OID = '06052b81040023'
class OpenCrypto {
constructor () {
for (let i = 0; i < chars.length; i++) {
lookup[chars.charCodeAt(i)] = i
}
}
/**
* BEGIN
* base64-arraybuffer
* GitHub @niklasvh
* Copyright (c) 2012 Niklas von Hertzen
* MIT License
*/
decodeAb (base64) {
const len = base64.length
let bufferLength = base64.length * 0.75
let p = 0
let encoded1
let encoded2
let encoded3
let encoded4
if (base64[base64.length - 1] === '=') {
bufferLength--
if (base64[base64.length - 2] === '=') {
bufferLength--
}
}
const arrayBuffer = new ArrayBuffer(bufferLength)
let bytes = new Uint8Array(arrayBuffer)
for (let i = 0; i < len; i += 4) {
encoded1 = lookup[base64.charCodeAt(i)]
encoded2 = lookup[base64.charCodeAt(i + 1)]
encoded3 = lookup[base64.charCodeAt(i + 2)]
encoded4 = lookup[base64.charCodeAt(i + 3)]
bytes[p++] = (encoded1 << 2) | (encoded2 >> 4)
bytes[p++] = ((encoded2 & 15) << 4) | (encoded3 >> 2)
bytes[p++] = ((encoded3 & 3) << 6) | (encoded4 & 63)
}
return arrayBuffer
}
/**
* END
* base64-arraybuffer
*/
/**
* Method for encoding ArrayBuffer to hexadecimal String
*/
arrayBufferToHexString (arrayBuffer) {
if (typeof arrayBuffer !== 'object') {
throw new TypeError('Expected input of arrayBuffer to be an ArrayBuffer Object')
}
const byteArray = new Uint8Array(arrayBuffer)
let hexString = ''
let nextHexByte
for (let i = 0; i < byteArray.byteLength; i++) {
nextHexByte = byteArray[i].toString(16)
if (nextHexByte.length < 2) {
nextHexByte = '0' + nextHexByte
}
hexString += nextHexByte
}
return hexString
}
/**
* Method for decoding base64 String to ArrayBuffer
*/
base64ToArrayBuffer (b64) {
if (typeof b64 !== 'string') {
throw new TypeError('Expected input of b64 to be a Base64 String')
}
return this.decodeAb(b64)
}
/**
* Method that removes lines from PEM encoded key
*/
removeLines (str) {
return str.replace(/\r?\n|\r/g, '')
}
/**
* Method that converts asymmetric public key from PEM to CryptoKey format
* @param {String} publicKey default: "undefined"
* @param {Object} options default: depends on algorithm below
* -- ECDH: { name: 'ECDH', usages: [], isExtractable: true }
* -- ECDSA: { name: 'ECDSA', usages: ['verify'], isExtractable: true }
* -- RSA-OAEP: { name: 'RSA-OAEP', hash: { name: 'SHA-512' }, usages: ['encrypt', 'wrapKey'], isExtractable: true }
* -- RSA-PSS: { name: 'RSA-PSS', hash: { name: 'SHA-512' }, usages: ['verify'], isExtractable: true }
*/
pemPublicToCrypto (pem, options) {
const self = this
if (typeof options === 'undefined') {
options = {}
}
options.isExtractable = (typeof options.isExtractable !== 'undefined') ? options.isExtractable : true
return new Promise((resolve, reject) => {
if (typeof pem !== 'string') {
throw new TypeError('Expected input of pem to be a String')
}
if (typeof options.isExtractable !== 'boolean') {
throw new TypeError('Expected input of options.isExtractable to be a Boolean')
}
pem = pem.replace('-----BEGIN PUBLIC KEY-----', '')
pem = pem.replace('-----END PUBLIC KEY-----', '')
const b64 = self.removeLines(pem)
const arrayBuffer = self.base64ToArrayBuffer(b64)
const hex = self.arrayBufferToHexString(arrayBuffer)
const keyOptions = {}
if (hex.includes(EC_OID)) {
options.name = (typeof options.name !== 'undefined') ? options.name : 'ECDH'
if (typeof options.name !== 'string') {
throw new TypeError('Expected input of options.name to be a String')
}
let curve = null
if (hex.includes(P256_OID)) {
curve = 'P-256'
} else if (hex.includes(P384_OID)) {
curve = 'P-384'
} else if (hex.includes(P521_OID)) {
curve = 'P-521'
}
if (options.name === 'ECDH') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : []
} else if (options.name === 'ECDSA') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : ['verify']
} else {
throw new TypeError('Expected input of options.name is not a valid algorithm name')
}
if (typeof options.usages !== 'object') {
throw new TypeError('Expected input of options.usages to be an Array')
}
keyOptions.name = options.name
keyOptions.namedCurve = curve
} else if (hex.includes(RSA_OID)) {
options.name = (typeof options.name !== 'undefined') ? options.name : 'RSA-OAEP'
options.hash = (typeof options.hash !== 'undefined') ? options.hash : 'SHA-512'
if (typeof options.name !== 'string') {
throw new TypeError('Expected input of options.name to be a String')
}
if (typeof options.hash !== 'string') {
throw new TypeError('Expected input of options.hash to be a String')
}
if (options.name === 'RSA-OAEP') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : ['encrypt', 'wrapKey']
} else if (options.name === 'RSA-PSS') {
options.usages = (typeof options.usages !== 'undefined') ? options.usages : ['verify']
} else {
throw new TypeError('Expected input of options.name is not a valid algorithm name')
}
if (typeof options.usages !== 'object') {
throw new TypeError('Expected input of options.usages to be an Array')
}
keyOptions.name = options.name
keyOptions.hash = {}
keyOptions.hash.name = options.hash
} else {
throw new TypeError('Expected input of pem is not a valid public key')
}
cryptoApi.importKey(
'spki',
arrayBuffer,
keyOptions,
options.isExtractable,
options.usages
).then(importedPublicKey => {
resolve(importedPublicKey)
}).catch(err => {
reject(err)
})
})
}
}
// --------------------------------------------------------------------------------------------------
(async () => {
await importKey();
})();