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
Function
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:
Generate a Random Salt:
Uses
_genRandomWithNonZero(8)
to create an 8-byte randomsalt
.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; }
Derive Key and IV:
_deriveKeyAndIV(passphrase, salt)
generates akey
andIV
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]; }
Encrypt the Data:
Uses the
key
andIV
to encryptplainText
with the AES algorithm in CBC mode.Produces an encrypted byte sequence.
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 sameplainText
andpassphrase
are used, the encrypted result will always be different.This enhances security and prevents attacks based on comparing encrypted outputs.
decryptAESCryptoJS
Function
decryptAESCryptoJS
Function
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:
Decode Base64:
Converts the encrypted string from base64 format back to its original byte form.
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.
Derive Key and IV Again:
Uses the
_deriveKeyAndIV
function with the extractedsalt
andpassphrase
to regenerate the encryption key and IV.
Decrypt the Data:
Uses the
key
andIV
to decrypt the encrypted data using AES in CBC mode.The result is the original
plainText
.
verify
and verifyEncrypted
Functions
verify
and verifyEncrypted
FunctionsPurpose:
Ensures the integrity and correctness of the encryption and decryption process.
How It Works:
verify
: Compares the originalplainText
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:
Last updated