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

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

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

Ответы [ 2 ]

0 голосов
/ 12 сентября 2018

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

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

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

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

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

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

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

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

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

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

0 голосов
/ 11 сентября 2018

Можно экспортировать ключ в другом формате (однако не все типы ключей поддерживают все форматы, не знаю почему!). Чтобы это было возможно, когда вы генерируете / импортируете ключ, вам нужно указать, что ключ можно извлечь, как вы сказали. 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, но вот быстрый и DIRTY пример ( 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));
          });
      });
  });
});
...