In my web application, I am trying to store data in local storage when user logs out of my application and restore it after logging in again. This data is private so it needs to be encrypted before saving. Because of that requirement, the procedure looks as follows:

Encryption:

  1. Request unique string (key) from backend (current username and datetime are parameters).
  2. Generate AES-GCM encryption key from that string using window.crypto.subtle.importKey()
  3. Encrypt the data and put it into local storage (along with initialization vector and datetime used to get key from backend).

Decryption:

  1. Wait until user is logged in again.
  2. Request unique string (key) from backend (current username and datetime are parameters).
  3. Generate AES-GCM encryption key from that string using window.crypto.subtle.importKey()
  4. Get the data from local storage and decrypt it.

Here is the code (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));
  });
}

The first problem is that I receive DOMException after decryption. This is almost impossible to debug, because the actual error is hidden by the browser due to security issues:

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

The other thing is that I am questioning my approach - is it even correct to generate encryption key like that? I suspect that may be the root of the problem, but I was unable to find any way to generate encryption key from string using Web Crypto API.

Also, the string which is the source for encryption key is 128-characters long, so far I am just taking first 32 characters to get 256 bits of data. I am not sure if this is correct, because the characters at the beginning may be not unique. May hashing be a good answer here?

Any help/guidance will be hugely appreciated, especially verifying my approach. I am struggling to find any examples of problems like that. Thank you!

1 Answers

1
Shaun Luttin On Best Solutions

Cautionary Note:

enter image description here

Also, I am not a specialist security expert. All that being said...


One approach is to generate the key on the client-side without requesting a unique string from the back-end server. Encrypt with that key, save the key to your back-end server, and then fetch the key again to decrypt.

This is in JavaScript and will work just as well in 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();

On the other hand, if the requirements need the encryption key to derive from a unique, low entropy string from the back-end, then the 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();