Web Crypto API - Достаточно ли безопасен не точный CryptoKey в IndexedDB от передачи с одного устройства на другое?

Web Crypto API предлагает возможность сохранить закрытый или открытый ключ в виде специального непрозрачного типа объекта в клиентской базе данных IndexedDB, т. Е. Среда выполнения клиента и JS может работать с CryptoKey, но они не могут разобрать его. Кроме того, после генерации или импорта указанного ключа можно оговорить, что ключ не может быть извлечен.

Моя цель - сохранить индивидуальный закрытый ключ на клиентском устройстве пользователя, который я бы использовал в качестве его цифровой подписи. Для меня важно знать, насколько трудно или просто было бы передать этот CryptoKey между устройствами, как трудно моему пользователю передать этот CryptoKey своему другу или скопировать его на другое его устройство.

2 ответа

Ключ, помеченный как неизвлекаемый, не может быть экспортирован

Спецификация WebCrypto абсолютно ясна. Смотрите раздел 6 exportKey определение

14.3.10. Метод exportKey При вызове метод exportKey ДОЛЖЕН выполнять следующие шаги:

  1. Пусть format и key будут параметрами формата и key, передаваемыми в метод exportKey, соответственно.

  2. Пусть обещание будет новым обещанием.

  3. Верните обещание и выполните оставшиеся шаги асинхронно.

  4. Если в следующих шагах или процедурах, на которые имеются ссылки, указано "выбросить ошибку", отклоните обещание с возвращенной ошибкой и затем завершите алгоритм.

  5. Если имя члена внутреннего слота ключа [[алгоритма]] не идентифицирует зарегистрированный алгоритм, который поддерживает операцию экспорта ключа, то генерируется ошибка NotSupportedError.

  6. Если внутренний слот ключа [[extractable]] имеет значение false, выдается ошибка InvalidAccessError.

Материал ключа должен быть скрыт, даже если он хранится в IndexedDB и не может быть экспортирован, если ключ не может быть извлечен, поэтому вы можете считать, что этот ключ нельзя реплицировать на другое устройство

Можно экспортировать ключ в другом формате (однако не все типы ключей поддерживают все форматы, не знаю почему!). Чтобы это было возможно, когда вы генерируете / импортируете ключ, вам нужно указать, что ключ можно извлечь, как вы сказали. API веб-криптографии говорит:

Если внутренний слот ключа [[extractable]] имеет значение false, выдается ошибка InvalidAccessError.

Однако вы можете безопасно экспортировать ключ (но некоторые злонамеренные файлы также могут быть извлечены вами).

Например, если вы хотите иметь возможность экспортировать ключ 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 и позволить пользователю загрузить его и импортировать позже. Для дополнительной безопасности вы можете попросить пароль для шифрования файла JSON в AES. И запретить экспорт, как только пользователь импортировал ключ. Он / она уже имеет его, поэтому бесполезно для него снова экспортировать.

Чтобы импортировать ключ, просто загрузите файл и импортируйте закрытый или открытый ключ.

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);
});

Также можно использовать функцию обтекания / развёртывания, но, кажется, невозможно использовать её с клавишами 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));
          });
      });
  });
});
Другие вопросы по тегам