Используйте API WebCrypto для шифрования / дешифрования данных с помощью ключа шифрования, сгенерированного из строки - PullRequest
2 голосов
/ 25 апреля 2019

В моем веб-приложении я пытаюсь сохранить данные в локальном хранилище, когда пользователь выходит из моего приложения, и восстанавливать его после повторного входа. Эти данные являются частными, поэтому перед сохранением их необходимо зашифровать. Из-за этого требования процедура выглядит следующим образом:

Шифрование:

  1. Запросить уникальную строку (ключ) из бэкэнда (текущее имя пользователя и дата / время являются параметрами).
  2. Создать ключ шифрования AES-GCM из этой строки с помощью window.crypto.subtle.importKey ()
  3. Зашифруйте данные и поместите их в локальное хранилище (вместе с вектором инициализации и датой и временем, используемыми для получения ключа от бэкэнда).

дешифрование:

  1. Подождите, пока пользователь снова не войдет в систему.
  2. Запросить уникальную строку (ключ) из бэкэнда (текущее имя пользователя и дата / время являются параметрами).
  3. Создание ключа шифрования AES-GCM из этой строки с помощью window.crypto.subtle.importKey ()
  4. Получить данные из локального хранилища и расшифровать их.

Вот код (TypeScript):

interface Data {
  queue: string;
  initializationVector: string;
  date: string;
}

private getEncryptionKey(): void {
  const date: string = this.getDateParamForEncryptionKeyGeneration();
  const params = new HttpParams().set('date', date);
  this.encryptionKeyDate = DateSerializer.deserialize(date);
  this.http.get(this.ENCRYPTION_KEY_ENDPOINT, {params}).subscribe((response: {key: string}) => {
    const seed = response.key.slice(0, 32);
    window.crypto.subtle.importKey(
      'raw',
      new TextEncoder().encode(seed),
      'AES-GCM',
      true,
      ['encrypt', 'decrypt']
    ).then(
      (key: CryptoKey) => {
        this.encryptionKey = key;
        this.decrypt();
      }
    );
  });
}

private getDateParamForEncryptionKeyGeneration(): string {
  const dataAsString: string = this.localStorageService.getItem(...);
  const data: Data = dataAsString ? JSON.parse(dataAsString) : null;
  return data ? data.date : DateSerializer.serialize(moment());
}

private decrypt(data: Data): void {
  const encoder = new TextEncoder();
  const encryptionAlgorithm: AesGcmParams = {
    name: 'AES-GCM',
    iv: encoder.encode(data.initializationVector)
  };
  window.crypto.subtle.decrypt(
    encryptionAlgorithm,
    this.encryptionKey,
    encoder.encode(data.queue)
  ).then(
    (decryptedData: ArrayBuffer) => {
      const decoder = new TextDecoder();
      console.log(JSON.parse(decoder.decode(decryptedData)));
    }
  );
}

private encrypt(queue: any[]): void {
  const initializationVector: Uint8Array = window.crypto.getRandomValues(new Uint8Array(12));
  const encryptionAlgorithm: AesGcmParams = {
    name: 'AES-GCM',
    iv: initializationVector
  };
  window.crypto.subtle.encrypt(
    encryptionAlgorithm,
    this.encryptionKey,
    new TextEncoder().encode(JSON.stringify(queue))
  ).then((encryptedQueue: ArrayBuffer) => {
    const decoder = new TextDecoder();
    const newState: Data = {
      queue: decoder.decode(encryptedQueue),
      initializationVector: decoder.decode(initializationVector),
      date: DateSerializer.serialize(this.encryptionKeyDate)
    };
    this.localStorageService.setItem('...', JSON.stringify(newState));
  });
}

Первая проблема заключается в том, что я получаю DOMException после расшифровки. Это почти невозможно отладить, потому что настоящая ошибка скрыта браузером из-за проблем безопасности:

error: DOMException
code: 0
message: ""
name: "OperationError"

Другое дело, что я подвергаю сомнению свой подход - правильно ли вообще генерировать такой ключ шифрования? Я подозреваю, что это может быть причиной проблемы, но я не смог найти какой-либо способ генерирования ключа шифрования из строки с использованием Web Crypto API.

Кроме того, строка, которая является источником ключа шифрования, имеет длину 128 символов, поэтому пока я просто беру первые 32 символа, чтобы получить 256 бит данных. Я не уверен, что это правильно, потому что символы в начале могут быть не уникальными. Может ли хеширование быть хорошим ответом здесь?

Буду очень признателен за любую помощь / руководство, особенно проверяя мой подход. Я изо всех сил пытаюсь найти какие-либо примеры подобных проблем. Спасибо!

1 Ответ

1 голос
/ 03 мая 2019

Предупреждение. :

enter image description here

Кроме того, я не являюсь специалистом по безопасности.Все это, как говорится ...


Одним из подходов является генерация ключа на стороне клиента без запроса уникальной строки с внутреннего сервера.Зашифруйте этим ключом, сохраните ключ на своем внутреннем сервере, а затем снова извлеките ключ для расшифровки.

Это в JavaScript и будет работать так же хорошо в TypeScript.

const runDemo = async () => {

  const messageOriginalDOMString = 'Do the messages match?';

  //
  // Encode the original data
  //

  const encoder = new TextEncoder();
  const messageUTF8 = encoder.encode(messageOriginalDOMString);

  //
  // Configure the encryption algorithm to use
  //

  const iv = window.crypto.getRandomValues(new Uint8Array(12));
  const algorithm = {
    iv,
    name: 'AES-GCM',
  };

  //
  // Generate/fetch the cryptographic key
  //

  const key = await window.crypto.subtle.generateKey({
      name: 'AES-GCM',
      length: 256
    },
    true, [
      'encrypt',
      'decrypt'
    ]
  );

  //
  // Run the encryption algorithm with the key and data.
  //

  const messageEncryptedUTF8 = await window.crypto.subtle.encrypt(
    algorithm,
    key,
    messageUTF8,
  );

  //
  // Export Key
  //
  const exportedKey = await window.crypto.subtle.exportKey(
    'raw',
    key,
  );
  
  // This is where to save the exported key to the back-end server,
  // and then to fetch the exported key from the back-end server.

  //
  // Import Key
  //
  const importedKey = await window.crypto.subtle.importKey(
    'raw',
    exportedKey,
    "AES-GCM",
    true, [
      "encrypt",
      "decrypt"
    ]
  );

  //
  // Run the decryption algorithm with the key and cyphertext.
  //

  const messageDecryptedUTF8 = await window.crypto.subtle.decrypt(
    algorithm,
    importedKey,
    messageEncryptedUTF8,
  );

  //
  // Decode the decryped data.
  //

  const decoder = new TextDecoder();
  const messageDecryptedDOMString = decoder.decode(messageDecryptedUTF8);

  //
  // Assert
  //
  console.log(messageOriginalDOMString);
  console.log(messageDecryptedDOMString);

};

runDemo();

С другой стороны, если в соответствии с требованиями ключ шифрования должен быть получен из уникальной строки с низкой энтропией из серверной части, то deriveKey метод может быть подходящим с алгоритмом PBKDF2 .

const runDemo = async() => {

  const messageOriginalDOMString = 'Do the messages match?';

  //
  // Encode the original data
  //

  const encoder = new TextEncoder();
  const messageUTF8 = encoder.encode(messageOriginalDOMString);

  //
  // Configure the encryption algorithm to use
  //

  const iv = window.crypto.getRandomValues(new Uint8Array(12));
  const algorithm = {
    iv,
    name: 'AES-GCM',
  };

  //
  // Generate/fetch the cryptographic key
  //

  function getKeyMaterial() {
    let input = 'the-username' + new Date();
    let enc = new TextEncoder();
    return window.crypto.subtle.importKey(
      "raw",
      enc.encode(input), {
        name: "PBKDF2"
      },
      false, ["deriveBits", "deriveKey"]
    );
  }

  let keyMaterial = await getKeyMaterial();
  let salt = window.crypto.getRandomValues(new Uint8Array(16));

  let key = await window.crypto.subtle.deriveKey({
      "name": "PBKDF2",
      salt: salt,
      "iterations": 100000,
      "hash": "SHA-256"
    },
    keyMaterial, {
      "name": "AES-GCM",
      "length": 256
    },
    true, ["encrypt", "decrypt"]
  );

  //
  // Run the encryption algorithm with the key and data.
  //

  const messageEncryptedUTF8 = await window.crypto.subtle.encrypt(
    algorithm,
    key,
    messageUTF8,
  );

  //
  // Export Key
  //
  const exportedKey = await window.crypto.subtle.exportKey(
    'raw',
    key,
  );

  // This is where to save the exported key to the back-end server,
  // and then to fetch the exported key from the back-end server.

  //
  // Import Key
  //
  const importedKey = await window.crypto.subtle.importKey(
    'raw',
    exportedKey,
    "AES-GCM",
    true, [
      "encrypt",
      "decrypt"
    ]
  );

  //
  // Run the decryption algorithm with the key and cyphertext.
  //

  const messageDecryptedUTF8 = await window.crypto.subtle.decrypt(
    algorithm,
    importedKey,
    messageEncryptedUTF8,
  );

  //
  // Decode the decryped data.
  //

  const decoder = new TextDecoder();
  const messageDecryptedDOMString = decoder.decode(messageDecryptedUTF8);

  //
  // Assert
  //
  console.log(messageOriginalDOMString);
  console.log(messageDecryptedDOMString);

};

runDemo();
...