1

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?

Robin Dijkhof
  • 18,665
  • 11
  • 65
  • 116
  • *"However, I struggle decrypting it in flutter."* How do you struggle with it? What are the symptoms? – Artjom B. Apr 05 '21 at 11:29
  • If aiming for compatibility with `crypto.subtle` it might be worth taking a look at: https://pub.dev/packages/webcrypto, it specifically aims at compatibility with `crypto.subtle`. – jonasfj Aug 24 '22 at 15:55

1 Answers1

1

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 of 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.

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • That all makes a lot of sense. If it wasn't for that bug, this should work as well?: `final encrypted = base64Decode('xgBc/QD1jE/s1/8A1MdEsqwqh4bUTlfpIk12SeziA9Pw'); final secretBox = SecretBox.fromConcatenation(encrypted, nonceLength: 12, macLength: 16);` – Robin Dijkhof Apr 05 '21 at 15:57
  • @RobinDijkhof - Correct, without the bug this would work. In the current version Cryptography 2.0.1 the bug is still present (tested). This can even be seen in the source code given in the [description](https://pub.dev/documentation/cryptography/latest/cryptography/SecretBox/fromConcatenation.html): In `final cipherText = List.unmodifiable(...` a wrong offset is used. – Topaco Apr 05 '21 at 16:32