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:
- Request unique string (key) from backend (current username and datetime are parameters).
- Generate AES-GCM encryption key from that string using window.crypto.subtle.importKey()
- Encrypt the data and put it into local storage (along with initialization vector and datetime used to get key from backend).
Decryption:
- Wait until user is logged in again.
- Request unique string (key) from backend (current username and datetime are parameters).
- Generate AES-GCM encryption key from that string using window.crypto.subtle.importKey()
- 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!