Web Crypto API - IndexedDB 中的不可精确 CryptoKey 是否足够安全,不会从一个设备传递到下一个设备?

Ste*_*e06 7 javascript security cryptography rsa webcrypto-api

Web Crypto API 提供了将私钥或公钥保存为客户端 IndexedDB 数据库中特殊的、不透明类型的对象的可能性,即客户端和 JS 运行时可以使用 CryptoKey,但无法拼写出来。另外,在生成或导入所述密钥时,可以规定该密钥是不可提取的。

我的目标是将个人私钥保存在用户的客户端设备上,并将其用作他的数字签名。对我来说,重要的是要知道在设备之间传递此加密密钥有多困难或容易,我的用户将此加密密钥提供给他的朋友或将其复制到他的另一台设备有多困难。

JEY*_*JEY 2

可以以不同的格式导出密钥(但是并非所有类型的密钥都支持所有格式,不知道为什么!)。为了在生成/导入密钥时实现这一点,您需要指定该密钥可以按照您所说的那样提取。Web 加密 API说道:

\n\n
\n

如果 key 的 [[extractable]] 内部槽为 false,则抛出 InvalidAccessError。

\n
\n\n

但是您可以安全地导出密钥(但是您的页面中的一些恶意js也可以提取它)。

\n\n

例如,如果您希望能够导出 ECDSA 密钥:

\n\n
window.crypto.subtle.generateKey(\n    {\n        name: "ECDSA",\n        namedCurve: "P-256", // the curve name\n    },\n    true, // <== Here if you want it to be exportable !!\n    ["sign", "verify"] // usage\n)\n.then(function(key){\n    //returns a keypair object\n    console.log(key);\n    console.log(key.publicKey);\n    console.log(key.privateKey);\n})\n.catch(function(err){\n    console.error(err);\n});\n
Run Code Online (Sandbox Code Playgroud)\n\n

然后就可以导出JWT中的公钥和私钥了。私钥示例:

\n\n
window.crypto.subtle.exportKey(\n    "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\n    privateKey\n)\n.then(function(keydata){\n    //returns the exported key data\n    console.log(keydata);\n})\n.catch(function(err){\n    console.error(err);\n});\n
Run Code Online (Sandbox Code Playgroud)\n\n

然后你可以将其保存在json文件中并让用户下载并稍后导入。要增加额外的安全性,您可以要求输入密码以 AES 加密 json 文件。并且一旦用户导入密钥就禁止导出。他/她已经拥有了,所以他再次导出是没有用的。

\n\n

要导入密钥,只需加载文件并导入私钥或/和公钥。

\n\n
window.crypto.subtle.importKey(\n    "jwk", \n    {\n        kty: myKetPubOrPrivateFromJson.kty,\n        crv: myKetPubOrPrivateFromJson.crv,\n        x: myKetPubOrPrivateFromJson.x,\n        y: myKetPubOrPrivateFromJson.y,\n        ext: myKetPubOrPrivateFromJson.ext,\n    },\n    {   \n        name: "ECDSA",\n        namedCurve: "P-256", // i think you can change it by myKetPubOrPrivateFromJson.crv not sure about that\n    },\n    false, // <== it\'s useless to be able to export the key again\n    myKetPubOrPrivateFromJson.key_ops\n)\n.then(function(publicKey){\n    //returns a publicKey (or privateKey if you are importing a private key)\n    console.log(publicKey);\n})\n.catch(function(err){\n    console.error(err);\n});\n
Run Code Online (Sandbox Code Playgroud)\n\n

也可以使用包装/解开功能,但是似乎不可能将其与 ECDSA 和 ECDH 密钥一起使用,但这里有一个快速而肮脏的示例(实时):

\n\n
function str2Buffer(data) {\n  const utf8Str = decodeURI(encodeURIComponent(data));\n  const len = utf8Str.length;\n  const arr = new Uint8Array(len);\n  for (let i = 0; i < len; i++) {\n    arr[i] = utf8Str.charCodeAt(i);\n  }\n  return arr.buffer;\n}\n\nfunction buffer2Hex(buffer) {\n    return Array.from(new Uint8Array(buffer)).map(b => (\'00\' + b.toString(16)).slice(-2)).join(\'\');\n}\n\nfunction hex2Buffer(data) {\n  if (data.length % 2 === 0) {\n    const bytes = [];\n    for (let i = 0; i < data.length; i += 2) {\n      bytes.push(parseInt(data.substr(i, 2), 16));\n    }\n    return new Uint8Array(bytes).buffer;\n  } else\xc2\xa0{\n    throw new Error(\'Wrong string format\');\n  }\n}\n\nfunction createAesKey(password, salt) {\n  const passwordBuf = typeof password === \'string\' ? str2Buffer(password) : password;\n  return window.crypto.subtle.importKey(\n        \'raw\',\n        passwordBuf,\n        \'PBKDF2\',\n        false,\n        [\'deriveKey\', \'deriveBits\']\n      ).then(derivedKey =>\n        window.crypto.subtle.deriveKey(\n          {\n            name: \'PBKDF2\',\n            salt: str2Buffer(salt),\n            iterations: 1000,\n            hash: { name: \'SHA-512\' }\n          },\n          derivedKey,\n          {name: \'AES-CBC\', length: 256},\n          false,\n          [\'wrapKey\', \'unwrapKey\']\n        )\n     );\n}\n\nfunction genKeyPair() {\n  return window.crypto.subtle.generateKey(\n    {\n        name: "RSA-PSS",\n        modulusLength: 2048, //can be 1024, 2048, or 4096\n        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),\n        hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"\n    },\n    true, // <== Here exportable\n    ["sign", "verify"] // usage\n  )\n}\n\nfunction exportKey(keyToWrap, wrappingKey) {\n  const iv = window.crypto.getRandomValues(new Uint8Array(16));\n  const promise = new Promise(function(resolve, reject) {\n    window.crypto.subtle.wrapKey(\n      "jwk",\n      keyToWrap, //the key you want to wrap, must be able to export to above format\n      wrappingKey, //the AES-CBC key with "wrapKey" usage flag\n      {   //these are the wrapping key\'s algorithm options\n          name: "AES-CBC",\n          //Don\'t re-use initialization vectors!\n          //Always generate a new iv every time your encrypt!\n          iv: iv,\n      }\n    ).then(result => {\n      const wrap = { key: buffer2Hex(result), iv: buffer2Hex(iv) };\n      resolve(wrap);\n    });\n  });\n  return promise;\n}\n\nfunction importKey(key, unwrappingKey, iv, usages) {\n  return window.crypto.subtle.unwrapKey(\n    "jwk",\n    key, //the key you want to unwrap\n    unwrappingKey, //the AES-CBC key with "unwrapKey" usage flag\n    {   //these are the wrapping key\'s algorithm options\n        name: "AES-CBC",\n        iv: iv, //The initialization vector you used to encrypt\n    },\n    {   //this what you want the wrapped key to become (same as when wrapping)\n        name: "RSA-PSS",\n        modulusLength: 2048, //can be 1024, 2048, or 4096\n        publicExponent: new Uint8Array([0x01, 0x00, 0x01]),\n        hash: {name: "SHA-256"}, //can be "SHA-1", "SHA-256", "SHA-384", or "SHA-512"\n    },\n    false, //whether the key is extractable (i.e. can be used in exportKey)\n    usages //the usages you want the unwrapped key to have\n  );\n}\n\ncreateAesKey("password", "usernameassalt").then(aesKey => {\n  genKeyPair().then(keyPair => {\n    exportKey(keyPair.publicKey, aesKey)\n      .then(publicKey => {\n        exportKey(keyPair.privateKey, aesKey)\n          .then(privateKey => {\n            const exportKeys = {publicKey: publicKey, privateKey: privateKey };\n            appDiv.innerHTML = `AesKey = ${aesKey}<br />\n            KeyPair:  <ul>\n              <li>publicKey: ${keyPair.publicKey}</li><li>privateKey: ${keyPair.privateKey}</li>\n            </ul>\n            Exported: <ul>\n              <li>publicKey:\n                <ul>\n                  <li>key: ${exportKeys.publicKey.key}</li>\n                  <li>iv: ${exportKeys.publicKey.iv}</li>\n                </ul>\n              </li>\n              <li>privateKey:\n                <ul>\n                  <li>key: ${exportKeys.privateKey.key}</li>\n                  <li>iv: ${exportKeys.privateKey.iv}</li>\n                </ul>\n              </li>\n            <ul>`;\n            importKey(hex2Buffer(exportKeys.privateKey.key), aesKey, hex2Buffer(exportKeys.privateKey.iv), ["sign"]).then(key => console.log(key)).catch(error => console.log(error.message));\n          });\n      });\n  });\n});\n
Run Code Online (Sandbox Code Playgroud)\n