Web Crypto API提供了在客户端的IndexedDB数据库中将私钥或公钥保存为特殊的,不透明的对象类型的可能性,即客户端和JS运行时可以与CryptoKey一起使用,但是它们无法拼写出来。另外,在生成或导入所述密钥时,可以规定密钥是不可提取的。
我的目标是在用户的客户端设备上保存个人私钥,我将其用作他的数字签名。对我来说,知道在设备之间传递这个CryptoKey有多么难或多么难,我的用户将这个CryptoKey交给他的朋友或将其复制到他的另一个设备有多难。
标记为不可提取的密钥无法导出
WebCrypto规范绝对清晰。见exportKey
definition的第6节
14.3.10。 exportKey方法调用时,exportKey方法必须执行以下步骤:
- 让format和key分别是传递给exportKey方法的格式和关键参数。
- 让诺言成为新的承诺。
- 返回承诺并异步执行其余步骤。
- 如果以下步骤或引用的过程表示抛出错误,请拒绝承诺并返回错误,然后终止算法。
- 如果key的[[algorithm]]内部槽的名称成员未标识支持导出键操作的已注册算法,则抛出NotSupportedError。
- 如果key的[[extractable]]内部插槽为false,则抛出InvalidAccessError。
密钥材料必须是隐藏的,即使它存储在IndexedDB中,如果密钥不可提取也无法导出,因此您可以认为此密钥无法在其他设备中复制
可以以不同的格式导出密钥(但是并非所有类型的密钥支持所有格式都不知道为什么!)。为了在生成/导入密钥时可以实现这一点,您需要指定密钥是可提取的,如上所述。 The Web Cryptography API说:
如果key的[[extractable]]内部插槽为false,则抛出InvalidAccessError。
但是你可以安全地导出密钥(但是你的页面也可以提取它的一些恶意js)。
例如,如果您希望能够导出ECDSA密钥:
window.crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-256", // the curve name
},
true, // <== Here if you want it to be exportable !!
["sign", "verify"] // usage
)
.then(function(key){
//returns a keypair object
console.log(key);
console.log(key.publicKey);
console.log(key.privateKey);
})
.catch(function(err){
console.error(err);
});
然后,您可以在JWT中导出公钥和私钥。私钥示例:
window.crypto.subtle.exportKey(
"jwk", // here you can change the format but i think that only jwk is supported for both public and private key. JWK is easier to use later
privateKey
)
.then(function(keydata){
//returns the exported key data
console.log(keydata);
})
.catch(function(err){
console.error(err);
});
然后你可以将它保存在json文件中,让用户下载并稍后导入。要添加额外的安全性,您可以要求输入密码以加密AES中的json文件。用户导入密钥后禁止导出。他/她已经拥有它,所以再次出口它是没用的。
要导入密钥,只需加载文件并导入私钥或/和公钥。
window.crypto.subtle.importKey(
"jwk",
{
kty: myKetPubOrPrivateFromJson.kty,
crv: myKetPubOrPrivateFromJson.crv,
x: myKetPubOrPrivateFromJson.x,
y: myKetPubOrPrivateFromJson.y,
ext: myKetPubOrPrivateFromJson.ext,
},
{
name: "ECDSA",
namedCurve: "P-256", // i think you can change it by myKetPubOrPrivateFromJson.crv not sure about that
},
false, // <== it's useless to be able to export the key again
myKetPubOrPrivateFromJson.key_ops
)
.then(function(publicKey){
//returns a publicKey (or privateKey if you are importing a private key)
console.log(publicKey);
})
.catch(function(err){
console.error(err);
});
也可以使用wrap / unwrap函数,但似乎不可能将它与ECDSA和ECDH键一起使用,但这里有一个快速且简单的例子(live):
function str2Buffer(data) {
const utf8Str = decodeURI(encodeURIComponent(data));
const len = utf8Str.length;
const arr = new Uint8Array(len);
for (let i = 0; i < len; i++) {
arr[i] = utf8Str.charCodeAt(i);
}
return arr.buffer;
}
function buffer2Hex(buffer) {
return Array.from(new Uint8Array(buffer)).map(b => ('00' + b.toString(16)).slice(-2)).join('');
}
function hex2Buffer(data) {
if (data.length % 2 === 0) {
const bytes = [];
for (let i = 0; i < data.length; i += 2) {
bytes.push(parseInt(data.substr(i, 2), 16));
}
return new Uint8Array(bytes).buffer;
} else {
throw new Error('Wrong string format');
}
}
function createAesKey(password, salt) {
const passwordBuf = typeof password === 'string' ? str2Buffer(password) : password;
return window.crypto.subtle.importKey(
'raw',
passwordBuf,
'PBKDF2',
false,
['deriveKey', 'deriveBits']
).then(derivedKey =>
window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt: str2Buffer(salt),
iterations: 1000,
hash: { name: 'SHA-512' }
},
derivedKey,
{name: 'AES-CBC', length: 256},
false,
['wrapKey', 'unwrapKey']
)
);
}
function genKeyPair() {
return window.crypto.subtle.generateKey(
{
name: "RSA-PSS",
modulusLength: 2048, //can be 1024, 2048, or 4096
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
},
true, // <== Here exportable
["sign", "verify"] // usage
)
}
function exportKey(keyToWrap, wrappingKey) {
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const promise = new Promise(function(resolve, reject) {
window.crypto.subtle.wrapKey(
"jwk",
keyToWrap, //the key you want to wrap, must be able to export to above format
wrappingKey, //the AES-CBC key with "wrapKey" usage flag
{ //these are the wrapping key's algorithm options
name: "AES-CBC",
//Don't re-use initialization vectors!
//Always generate a new iv every time your encrypt!
iv: iv,
}
).then(result => {
const wrap = { key: buffer2Hex(result), iv: buffer2Hex(iv) };
resolve(wrap);
});
});
return promise;
}
function importKey(key, unwrappingKey, iv, usages) {
return window.crypto.subtle.unwrapKey(
"jwk",
key, //the key you want to unwrap
unwrappingKey, //the AES-CBC key with "unwrapKey" usage flag
{ //these are the wrapping key's algorithm options
name: "AES-CBC",
iv: iv, //The initialization vector you used to encrypt
},
{ //this what you want the wrapped key to become (same as when wrapping)
name: "RSA-PSS",
modulusLength: 2048, //can be 1024, 2048, or 4096
publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"
},
false, //whether the key is extractable (i.e. can be used in exportKey)
usages //the usages you want the unwrapped key to have
);
}
createAesKey("password", "usernameassalt").then(aesKey => {
genKeyPair().then(keyPair => {
exportKey(keyPair.publicKey, aesKey)
.then(publicKey => {
exportKey(keyPair.privateKey, aesKey)
.then(privateKey => {
const exportKeys = {publicKey: publicKey, privateKey: privateKey };
appDiv.innerHTML = `AesKey = ${aesKey}<br />
KeyPair: <ul>
<li>publicKey: ${keyPair.publicKey}</li><li>privateKey: ${keyPair.privateKey}</li>
</ul>
Exported: <ul>
<li>publicKey:
<ul>
<li>key: ${exportKeys.publicKey.key}</li>
<li>iv: ${exportKeys.publicKey.iv}</li>
</ul>
</li>
<li>privateKey:
<ul>
<li>key: ${exportKeys.privateKey.key}</li>
<li>iv: ${exportKeys.privateKey.iv}</li>
</ul>
</li>
<ul>`;
importKey(hex2Buffer(exportKeys.privateKey.key), aesKey, hex2Buffer(exportKeys.privateKey.iv), ["sign"]).then(key => console.log(key)).catch(error => console.log(error.message));
});
});
});
});