使用WebCrypto API通过从字符串生成的加密密钥来加密/解密数据

时间:2019-04-25 12:42:59

标签: typescript encryption cryptography webcryptoapi

在我的Web应用程序中,当用户注销我的应用程序时,我试图将数据存储在本地存储中,并在再次登录后将其还原。此数据是私有数据,因此在保存之前需要进行加密。由于这一要求,该过程如下所示:

加密:

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

解密:

  1. 等待直到用户再次登录。
  2. 从后端请求唯一的字符串(键)(当前用户名和日期时间是参数)。
  3. 使用window.crypto.subtle.importKey()从该字符串生成AES-GCM加密密钥
  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 个答案:

答案 0 :(得分:1)

Cautionary Note

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 algorithm。 / p>

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