AES GCM Encrypt With Web Subtlecrypto And Decrypt With Flutter Cryptography
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?
Answer
The following issues exist in the Dart code:
- The WebCryptoAPI code concatenates the GCM tag with the ciphertext in the order ciphertext | tag. In the Dart code, both parts have to be separated accordingly.
Also, in the Dart code, the nonce/IV is not taken into account. A possible fix ofdecrypt()
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 theasync
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.
Related Questions
- → How do you create a 12 or 24 mnemonics code for multiple cryptocurrencies (ETH, BTC and so on..)
- → Flutter: input text field don't work properly in a simple example..... where am I wrong?
- → Can I customize the code formatting of Dart code in Atom?
- → Is it possible to develop iOS apps with Flutter on a Linux virtual machine?
- → Display SnackBar in Flutter
- → JSON ObjectMapper in Flutter
- → Material flutter app source code
- → TabBarSelection No such method error
- → How do I set the animation color of a LinearProgressIndicator?
- → Add different routes/screens to Flutter app
- → Is there a way to get the size of an existing widget?
- → How to share a file using flutter
- → Is there an easy way to find particular text built from RichText in a Flutter test?