使用 WebCrypto API 到 encrypt/decrypt 数据以及从字符串生成的加密密钥

Use WebCrypto API to encrypt/decrypt data with encryption key generated from string

在我的网络应用程序中,我试图在用户注销我的应用程序时将数据存储在本地存储中,并在再次登录后恢复它。此数据是私有的,因此需要在保存前对其进行加密。由于该要求,程序如下所示:

加密:

  1. 从后端请求唯一字符串(密钥)(当前用户名和日期时间是参数)。
  2. 使用 window.crypto.subtle.importKey()
  3. 从该字符串生成 AES-GCM 加密密钥
  4. 加密数据并将其放入本地存储(以及用于从后端获取密钥的初始化向量和日期时间)。

解密:

  1. 等到用户再次登录。
  2. 从后端请求唯一字符串(密钥)(当前用户名和日期时间是参数)。
  3. 使用 window.crypto.subtle.importKey()
  4. 从该字符串生成 AES-GCM 加密密钥
  5. 从本地存储中获取数据并解密。

这是代码 (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 位数据。我不确定这是否正确,因为开头的字符可能不是唯一的。散列可能是一个很好的答案吗?

任何 help/guidance 将不胜感激,尤其是验证我的方法。我正在努力寻找任何此类问题的例子。谢谢!

Cautionary Note:

另外,我不是专业的安全专家。综上所述...


一种方法是在客户端生成密钥,而无需从后端服务器请求唯一的字符串。使用该密钥加密,将密钥保存到您的后端服务器,然后再次获取密钥进行解密。

这在 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 method might be appropriate with the PBKDF2 algorithm.

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