Hi! I’m new to cryptography. I’m trying to write a provider that encrypts data (text or JSON) before it is sent to a server, such that the server cannot read the data. For now only the user himself should be able to decrypt the data, but he should be able to do so on different devices (and possibly in a PWA in the browser).
The private key is created from a password with PBKDF2. This key is then directly used to encrypt the data with AES-GCM. According to here there is no real benefit of using an intermediate content encryption key. Is this correct?
The encryption is done with js-jose and WebCryptoAPI. The encrypted data will be sent to the server as a JWE string together with the password salt. On the next device the user can get the JWE and the salt, create the same key with the password and decrypt the data.
Are there any major flaws in this design? Any comment/feedback will be appreciated.
Update for future visitors: key storage removed.
Here is the code I have so far:
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage';
import { TextEncoder } from 'text-encoding';
import { Jose, JoseJWE, Encrypter, Decrypter } from 'jose-jwe-jws';
@Injectable()
export class CryptoProvider {
private encoder = new TextEncoder();
private encrypter: Encrypter;
private decrypter: Decrypter;
private alg: string = 'AES-GCM';
private keyUsage: string[] = ['encrypt', 'decrypt'];
constructor(private storage: Storage) {
console.log('Hello CryptoProvider Provider');
}
encodeString(text: string): Uint8Array {
return this.encoder.encode(text);
}
generateKey(password: string, salt: ArrayBufferView): PromiseLike<CryptoKey> {
let passwordUtf8 = this.encodeString(password);
return crypto.subtle.importKey('raw', passwordUtf8, {name: 'PBKDF2'}, false, ['deriveKey']).then((baseKey) => {
let params = {
name: 'PBKDF2',
salt: salt,
iterations: 5000,
hash: 'SHA-256'
}
return crypto.subtle.deriveKey(params, baseKey, {name: this.alg, length: 256}, false, this.keyUsage)
});
}
createNewKey(password: string): Promise<void> {
let salt = crypto.getRandomValues(new Uint8Array(16));
return this.storage.set('salt', salt).then(() => {
return this.generateKey(password, salt).then((key) => {
this.initCryptographer(key);
});
});
}
recreateKeyFromPassword(password: string): Promise<void> {
return this.storage.get('salt').then((salt) => {
return this.generateKey(password, salt).then((key) => {
this.initCryptographer(key);
});
})
}
initCryptographer(key: CryptoKey): void {
let cryptographer = new Jose.WebCryptographer();
cryptographer.setKeyEncryptionAlgorithm('dir');
this.encrypter = new JoseJWE.Encrypter(cryptographer,key);
this.decrypter = new JoseJWE.Decrypter(cryptographer,key);
}
removeKey(): void {
this.encrypter = null;
this.decrypter = null;
}
// returns JWE: HEADER.KEY.IV.TEXT.INTEGRITY
encryptText(plaintext: string): PromiseLike<string> {
return this.encrypter.encrypt(plaintext);
}
decryptText(ciphertext: string): PromiseLike<string> {
return this.decrypter.decrypt(ciphertext);
}
}
Sorry for the long post. Thanks for reading!