Page cover image

Salt

Password Security in Flutter Apps: AES Encryption with Salt and IV

Understanding AES, Salt, and IV

  • AES (Advanced Encryption Standard):

    • A powerful and widely used symmetric encryption standard.

    • Uses the same key for both encryption and decryption.

    • Secure and efficient for mobile applications.

  • What is Salt, and why is it needed?

    • Salt is a random string added before encryption.

    • Ensures that encrypting the same data with the same passphrase produces different results.

    • Prevents dictionary attacks and lookup table attacks.

  • IV (Initialization Vector):

    • IV is a randomly generated initialization value used in encryption modes like CBC.

    • Ensures that the same plaintext encrypts into different ciphertexts.

Applying Security to Password Encryption in the App

The encryptAESCryptoJS Function

encryptAESCryptoJS
  • Functionality:

    • Encrypts a plaintext string (plainText) using a passphrase.

  String encryptAESCryptoJS(String plainText, String passphrase) {
    try {
      final salt = _genRandomWithNonZero(8);
      final keyndIV = _deriveKeyAndIV(passphrase, salt);
      final key = e.Key(keyndIV[0]);
      final iv = e.IV(keyndIV[1]);

      final encrypter = e.Encrypter(e.AES(key, mode: e.AESMode.cbc));
      final encrypted = encrypter.encrypt(plainText, iv: iv);
      final Uint8List encryptedBytesWithSalt = Uint8List.fromList(_createUint8ListFromString("Salted__") + salt + encrypted.bytes);
      return base64.encode(encryptedBytesWithSalt);
    } catch (error) {
      rethrow;
    }
  }
  • How It Works:

    1. Generate a Random Salt:

      • Uses _genRandomWithNonZero(8) to create an 8-byte random salt.

      • The salt ensures randomness and security in the encryption process.

        Uint8List _genRandomWithNonZero(int seedLength) {
            final random = Random.secure();
            const int randomMax = 245;
            final Uint8List uint8list = Uint8List(seedLength);
            for (int i = 0; i < seedLength; i++) {
              uint8list[i] = random.nextInt(randomMax) + 1;
            }
            return uint8list;
        }
    2. Derive Key and IV:

      • _deriveKeyAndIV(passphrase, salt) generates a key and IV from the passphrase and salt.

      • Uses multiple MD5 hash iterations to generate enough data for both the key and IV.

           List<Uint8List> _deriveKeyAndIV(String passphrase, Uint8List salt) {
              final password = _createUint8ListFromString(passphrase);
              Uint8List concatenatedHashes = Uint8List(0);
              Uint8List currentHash = Uint8List(0);
              bool enoughBytesForKey = false;
              Uint8List preHash = Uint8List(0);
           
              while (!enoughBytesForKey) {
              final int preHashLength = currentHash.length + password.length + salt.length;
              if (currentHash.isNotEmpty) {
               preHash = Uint8List.fromList(currentHash + password + salt);
              } else {
               preHash = Uint8List.fromList(password + salt);
              }
           
                  currentHash = preHash.myMd5;
                  concatenatedHashes = Uint8List.fromList(concatenatedHashes + currentHash);
                  if (concatenatedHashes.length >= 48) enoughBytesForKey = true;
              }
           
              final keyBytes = concatenatedHashes.sublist(0, 32);
              final ivBytes = concatenatedHashes.sublist(32, 48);
              return [keyBytes, ivBytes];
          }
    3. Encrypt the Data:

      • Uses the key and IV to encrypt plainText with the AES algorithm in CBC mode.

      • Produces an encrypted byte sequence.

    4. Prepare the Final Encrypted Data:

      • Concatenates "Salted__" + salt + encrypted bytes.

      • Encodes the entire sequence in base64 for easy storage or transmission.

        Uint8List _createUint8ListFromString(String s) {
          final ret = Uint8List(s.length);
          for (var i = 0; i < s.length; i++) {
            ret[i] = s.codeUnitAt(i);
          }
          return ret;
       }
  • Why Does the Encrypted Output Differ Each Time?

    • Since a new random salt is generated every time, even if the same plainText and passphrase are used, the encrypted result will always be different.

    • This enhances security and prevents attacks based on comparing encrypted outputs.

decryptAESCryptoJS Function

decryptAESCryptoJS
  • Functionality:

    • Decrypts a string that was encrypted using encryptAESCryptoJS.

 String decryptAESCryptoJS(String encrypted, String passphrase) {
  try {
    final Uint8List encryptedBytesWithSalt = base64.decode(encrypted);

    final Uint8List encryptedBytes = encryptedBytesWithSalt.sublist(16, encryptedBytesWithSalt.length);
    final salt = encryptedBytesWithSalt.sublist(8, 16);
    final keyndIV = _deriveKeyAndIV(passphrase, salt);
    final key = e.Key(keyndIV[0]);
    final iv = e.IV(keyndIV[1]);

    final encrypter = e.Encrypter(e.AES(key, mode: e.AESMode.cbc));
    final decrypted = encrypter.decrypt64(base64.encode(encryptedBytes), iv: iv);
    return decrypted;
  } catch (error) {
    rethrow;
  }
}
  • How It Works:

    1. Decode Base64:

      • Converts the encrypted string from base64 format back to its original byte form.

    2. Extract Salt and Encrypted Data:

      • Discards the first 8 bytes ("Salted__").

      • Extracts the next 8 bytes as the salt.

      • Retrieves the remaining bytes as the encrypted data.

    3. Derive Key and IV Again:

      • Uses the _deriveKeyAndIV function with the extracted salt and passphrase to regenerate the encryption key and IV.

    4. Decrypt the Data:

      • Uses the key and IV to decrypt the encrypted data using AES in CBC mode.

      • The result is the original plainText.

verify and verifyEncrypted Functions

  • Purpose:

    • Ensures the integrity and correctness of the encryption and decryption process.

  • How It Works:

    • verify: Compares the original plainText with the decrypted result of an encrypted string.

            bool verify({
              required String text,
              required String encrypted,
              required String passphrase,
            }) {
              try {
               return text == decryptAESCryptoJS(encrypted, passphrase);
              } catch (e) {
               return false;
              }
           }
    • verifyEncrypted: Decrypts two encrypted strings and compares their results.

              bool verifyEncrypted({
              required String encrypted1,
              required String encrypted2,
              required String passphrase,
              }) {
                  if (encrypted1 == encrypted2) return false;
                  try {
                    return decryptAESCryptoJS(encrypted1, passphrase) == decryptAESCryptoJS(encrypted2, passphrase);
                  } catch (e) {
                    return false;
                  }
              }

How to Use the Code in a Flutter Project

Install Dependencies

  • Add the following to pubspec.yaml:

    dependencies:
      encrypt: ^5.0.1
  • Run the command:

    flutter pub get

Using Encryption and Decryption Functions

  • Combine the previous code snippets into a MyEncrypt class.

import 'dart:convert';
import 'dart:math';
import 'dart:typed_data';

import 'package:dart_core/dart_core.dart';
import 'package:encrypt/encrypt.dart' as e;
// import 'package:flutter/services.dart';

class MyEncrypt {
    const MyEncrypt();
    
    /*
    Learn more | https://pub.dev/packages/encrypt
    
    Generate password
    Run |
    flutter pub global activate encrypt
    secure-random
    
    */
    
    // #TESTED
    String encryptAESCryptoJS(String plainText, String passphrase) {
    try {
        final salt = _genRandomWithNonZero(8);
        final keyndIV = _deriveKeyAndIV(passphrase, salt);
        final key = e.Key(keyndIV[0]);
        final iv = e.IV(keyndIV[1]);
    
          final encrypter = e.Encrypter(e.AES(key, mode: e.AESMode.cbc));
          final encrypted = encrypter.encrypt(plainText, iv: iv);
          final Uint8List encryptedBytesWithSalt = Uint8List.fromList(_createUint8ListFromString("Salted__") + salt + encrypted.bytes);
          return base64.encode(encryptedBytesWithSalt);
        } catch (error) {
          rethrow;
        }
    }
    
    // #TESTED
    String decryptAESCryptoJS(String encrypted, String passphrase) {
    try {
    final Uint8List encryptedBytesWithSalt = base64.decode(encrypted);
    
          final Uint8List encryptedBytes = encryptedBytesWithSalt.sublist(16, encryptedBytesWithSalt.length);
          final salt = encryptedBytesWithSalt.sublist(8, 16);
          final keyndIV = _deriveKeyAndIV(passphrase, salt);
          final key = e.Key(keyndIV[0]);
          final iv = e.IV(keyndIV[1]);
    
          final encrypter = e.Encrypter(e.AES(key, mode: e.AESMode.cbc));
          final decrypted = encrypter.decrypt64(base64.encode(encryptedBytes), iv: iv);
          return decrypted;
        } catch (error) {
          rethrow;
        }
    }
    
    bool verify({
    required String text,
    required String encrypted,
    required String passphrase,
    }) {
        try {
          return text == decryptAESCryptoJS(encrypted, passphrase);
        } catch (e) {
          return false;
        }
    }
    
    bool verifyEncrypted({
    required String encrypted1,
    required String encrypted2,
    required String passphrase,
    }) {
        if (encrypted1 == encrypted2) return false;
        try {
          return decryptAESCryptoJS(encrypted1, passphrase) == decryptAESCryptoJS(encrypted2, passphrase);
        } catch (e) {
          return false;
        }
    }
    
    List<Uint8List> _deriveKeyAndIV(String passphrase, Uint8List salt) {
    final password = _createUint8ListFromString(passphrase);
    Uint8List concatenatedHashes = Uint8List(0);
    Uint8List currentHash = Uint8List(0);
    bool enoughBytesForKey = false;
    Uint8List preHash = Uint8List(0);
    
        while (!enoughBytesForKey) {
          final int preHashLength = currentHash.length + password.length + salt.length;
          if (currentHash.isNotEmpty) {
            preHash = Uint8List.fromList(currentHash + password + salt);
          } else {
            preHash = Uint8List.fromList(password + salt);
          }
    
          currentHash = preHash.myMd5;
          concatenatedHashes = Uint8List.fromList(concatenatedHashes + currentHash);
          if (concatenatedHashes.length >= 48) enoughBytesForKey = true;
        }
    
        final keyBytes = concatenatedHashes.sublist(0, 32);
        final ivBytes = concatenatedHashes.sublist(32, 48);
        return [keyBytes, ivBytes];
    }
    
    Uint8List _createUint8ListFromString(String s) {
        final ret = Uint8List(s.length);
        for (var i = 0; i < s.length; i++) {
          ret[i] = s.codeUnitAt(i);
        }
        return ret;
    }
    
    Uint8List _genRandomWithNonZero(int seedLength) {
        final random = Random.secure();
        const int randomMax = 245;
        final Uint8List uint8list = Uint8List(seedLength);
        for (int i = 0; i < seedLength; i++) {
          uint8list[i] = random.nextInt(randomMax) + 1;
        }
        return uint8list;
    }
}
  • Encrypt Data:

    final encryptHelper = MyEncrypt();
    final plainText = "Data to be encrypted";
    final passphrase = "Secret password";
    
    final encryptedText = encryptHelper.encryptAESCryptoJS(plainText, passphrase);
  • Decrypt Data:

    final decryptedText = encryptHelper.decryptAESCryptoJS(encryptedText, passphrase);
  • Verify the Result:

    final isVerified = encryptHelper.verify(
      text: plainText,
      encrypted: encryptedText,
      passphrase: passphrase,
    );

Notes on Usage

  • Protecting the passphrase:

    • Do not store the passphrase as plain text in the source code or database.

    • Use secure methods such as environment variables or secret management services.

  • Managing Keys and Sensitive Data:

    • Restrict access to encryption-related code.

    • Follow data security regulations and best practices.

Why Are Salt and IV Important?

  • Salt:

    • Prevents dictionary and rainbow table attacks.

    • Ensures unique encryption results for the same plaintext.

    • Without Salt, attackers can compare ciphertexts to detect patterns.

    • This is particularly dangerous if multiple users share the same passphrase or sensitive data.

    • Using Salt is like adding a unique spice to a dish, making each encryption process different.

    • This prevents attackers from guessing your "recipe."

  • IV (Initialization Vector):

    • Ensures security in CBC mode encryption.

    • Prevents repeating patterns in encrypted data.

Conclusion

  • Security is not optional; it's a mandatory requirement in app development.

  • Using AES encryption with Salt and IV effectively protects user data.

  • With this guide, you and your team can implement a secure encryption solution for your Flutter app.

  • If you want to skip the complex parts and use it right away, check out my package: https://pub.dev/packages/my_salt.

References:

Buy Me a Coffee | Support Me on Ko-fi

Last updated