Reputation: 19288
I'm trying to encrypt something in a webextension with SubtleCrypto and decrypt it in flutter with cryptography. I want to use a password to encrypt a message, send it to a app and decrypt it with the same password. For this I use AES GCM with pbkdf2
I was able to find an encryption snippet on the Mozilla documentation page. However, I struggle decrypting it in flutter.
I'm also having problems with terminology. SubtleCrypto uses iv, salt and tags while flutter cryptography uses nonce and mac.
Javascript code:
test(){
// const salt = window.crypto.getRandomValues(new Uint8Array(16));
// const iv = window.crypto.getRandomValues(new Uint8Array(12));
const salt = new Uint8Array([0, 72, 16, 170, 232, 145, 179, 47, 241, 92, 75, 146, 25, 0, 193, 176]);
const iv = new Uint8Array([198, 0, 92, 253, 0, 245, 140, 79, 236, 215, 255, 0]);
console.log('salt: ', salt);
console.log('iv: ', iv);
console.log('salt: ', btoa(String.fromCharCode(...salt)));
console.log('iv: ', btoa(String.fromCharCode(...iv)));
this.encrypt('value', salt, iv).then(x => console.log('got encrypted: ', x));
}
getKeyMaterial(): Promise<CryptoKey> {
const password = 'key';
const enc = new TextEncoder();
return window.crypto.subtle.importKey(
'raw',
enc.encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
}
async encrypt(plaintext: string, salt: Uint8Array, iv: Uint8Array): Promise<string> {
const keyMaterial = await this.getKeyMaterial();
const key = await window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256},
true,
[ 'encrypt', 'decrypt' ]
);
const encoder = new TextEncoder();
const tes = await window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv
},
key,
encoder.encode(plaintext)
);
return btoa(String.fromCharCode(...new Uint8Array(tes)));
}
flutter dart code:
void decrypt(){
final algorithm = AesGcm.with256bits();
final encrypted = base64Decode('1MdEsqwqh4bUTlfpIk12SeziA9Pw');
final secretBox = SecretBox.fromConcatenation(encrypted, nonceLength: 12, macLength: 0);
// // Encrypt
final data = await algorithm.decrypt(
secretBox,
secretKey: await getKey(),
);
String res = utf8.decode(data);
}
Future<SecretKey> getKey() async{
final pbkdf2 = Pbkdf2(
macAlgorithm: Hmac.sha256(),
iterations: 100000,
bits: 128,
);
// Password we want to hash
final secretKey = SecretKey(utf8.encode('key'));
// A random salt
final salt = [0, 72, 16, 170, 232, 145, 179, 47, 241, 92, 75, 146, 25, 0, 193, 176];
// Calculate a hash that can be stored in the database
final newSecretKey = await pbkdf2.deriveKey(
secretKey: secretKey,
nonce: salt,
);
return Future<SecretKey>.value(newSecretKey);
}
What am I doing wrong?
Upvotes: 1
Views: 2277
Reputation: 49276
The following issues exist in the Dart code:
decrypt()
is: //final secretBox = SecretBox.fromConcatenation(encrypted, nonceLength: 12, macLength: 0);
Uint8List ciphertext = encrypted.sublist(0, encrypted.length - 16);
Uint8List mac = encrypted.sublist(encrypted.length - 16);
Uint8List iv = base64Decode('xgBc/QD1jE/s1/8A'); // should als be concatenated, e.g. iv | ciphertext | tag
SecretBox secretBox = new SecretBox(ciphertext, nonce: iv, mac: new Mac(mac));
In addition, the WebCryptoAPI code uses AES-256, so in the Dart code in getKey()
, 256 bits must be applied as the key size in the PBKDF2 call accordingly.
Also, since decrypt()
contains asynchronous method calls, it must be marked with the async
keyword.
With these changes, decrypt()
works on my machine and returns value
for the data from the WebCryptoAPI code:
function test(){
// const salt = window.crypto.getRandomValues(new Uint8Array(16));
// const iv = window.crypto.getRandomValues(new Uint8Array(12));
const salt = new Uint8Array([0, 72, 16, 170, 232, 145, 179, 47, 241, 92, 75, 146, 25, 0, 193, 176]);
const iv = new Uint8Array([198, 0, 92, 253, 0, 245, 140, 79, 236, 215, 255, 0]);
console.log('salt: ', salt);
console.log('iv: ', iv);
console.log('salt: ', btoa(String.fromCharCode(...salt)));
console.log('iv: ', btoa(String.fromCharCode(...iv)));
encrypt('value', salt, iv).then(x => console.log('got encrypted:', x));
}
function getKeyMaterial() {
const password = 'key';
const enc = new TextEncoder();
return window.crypto.subtle.importKey(
'raw',
enc.encode(password),
'PBKDF2',
false,
['deriveBits', 'deriveKey']
);
}
async function encrypt(plaintext, salt, iv) {
const keyMaterial = await getKeyMaterial();
const key = await window.crypto.subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations: 100000,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256},
true,
[ 'encrypt', 'decrypt' ]
);
const encoder = new TextEncoder();
const tes = await window.crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv
},
key,
encoder.encode(plaintext)
);
return btoa(String.fromCharCode(...new Uint8Array(tes)));
}
test();
salt: AEgQquiRsy/xXEuSGQDBsA==
iv: xgBc/QD1jE/s1/8A
got encrypted: 1MdEsqwqh4bUTlfpIk12SeziA9Pw
Note that a static nonce/IV and salt are generally insecure (for testing purposes it's fine, of course). Usually, they are randomly generated for each encryption/key derivation. Since salt and nonce/IV are not secret, they are typically concatenated with the ciphertext and tag, e.g. salt | nonce | ciphertext | tag, and separated on the recipient side.
Actually SecretBox
provides the method fromConcatenation()
which is supposed to separate a concatenation of nonce, ciphertext and tag. However, this implementation returns (at least in earlier versions) a corrupted ciphertext, which is probably a bug.
Regarding the terms nonce/IV, salt and MAC/tag in the context of GCM and PBKDF2:
The GCM mode uses a 12 bytes nonce, which is called an IV in WebCryptoAPI (and sometimes in other libraries), s. here. PBKDF2 applies a salt in the key derivation, which is called a nonce in Dart.
The naming nonce is appropriate in that, an IV (in combination with the same key) and a salt (in combination with the same password) may only be used once. The former is essential for the GCM security in particular, s. here.
MAC and tag are synonyms for the GCM authentication tag.
Upvotes: 1