Web Crypto API - IndexedDB中的不可纠正的CryptoKey是否足以安全地防止从一个设备传递到下一个设备?

问题描述 投票:0回答:2

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

我的目标是在用户的客户端设备上保存个人私钥,我将其用作他的数字签名。对我来说,知道在设备之间传递这个CryptoKey有多么难或多么难,我的用户将这个CryptoKey交给他的朋友或将其复制到他的另一个设备有多难。

javascript security cryptography rsa webcryptoapi
2个回答
2
投票

标记为不可提取的密钥无法导出

WebCrypto规范绝对清晰。见exportKey definition的第6节

14.3.10。 exportKey方法调用时,exportKey方法必须执行以下步骤:

  1. 让format和key分别是传递给exportKey方法的格式和关键参数。
  2. 让诺言成为新的承诺。
  3. 返回承诺并异步执行其余步骤。
  4. 如果以下步骤或引用的过程表示抛出错误,请拒绝承诺并返回错误,然后终止算法。
  5. 如果key的[[algorithm]]内部槽的名称成员未标识支持导出键操作的已注册算法,则抛出NotSupportedError。
  6. 如果key的[[extractable]]内部插槽为false,则抛出InvalidAccessError。

密钥材料必须是隐藏的,即使它存储在IndexedDB中,如果密钥不可提取也无法导出,因此您可以认为此密钥无法在其他设备中复制


1
投票

可以以不同的格式导出密钥(但是并非所有类型的密钥支持所有格式都不知道为什么!)。为了在生成/导入密钥时可以实现这一点,您需要指定密钥是可提取的,如上所述。 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));
          });
      });
  });
});
© www.soinside.com 2019 - 2024. All rights reserved.