Dart 85 🎯 Secure Data Hashing and Encryption using package:crypto and package:encrypt
Dart’s cryptographic ecosystem splits along a clean line: hashing and encryption are different operations with different packages, and confusing them produces insecure code. The crypto package provides one-way hash functions—SHA-256, HMAC—that cannot be reversed. The encrypt package provides two-way symmetric encryption built on PointyCastle, primarily AES with PKCS7 padding . Neither package is a complete security solution on its own, and neither replaces the cryptography package for authenticated encryption. But together they cover the two most common needs in Dart applications: verifying data integrity and protecting data at rest.
The critical distinction is directionality. A hash is a fingerprint; you can verify that data matches a fingerprint, but you cannot recover data from it. Encryption is a lockbox; with the key, you can open it. Using a hash where encryption is needed means the data is gone. Using encryption where a hash is needed means you have introduced key management for no reason. The crypto package handles HMAC for message authentication and SHA-256 for integrity checks . The encrypt package handles AES-CBC for reversible confidentiality . Understanding which to reach for—and when neither is sufficient—is the core skill.
This chapter covers the crypto package’s hashing and HMAC APIs, the encrypt package’s AES encryption with modes and padding, the correct pattern for combining them (AES-CBC + HMAC for authenticated encryption when GCM is unavailable), and the pitfalls that make encrypted data insecure despite using strong algorithms. It assumes familiarity with basic symmetric encryption concepts and the distinction between confidentiality and integrity.
Key point: crypto hashes; encrypt encrypts. A hash cannot be decrypted, and encryption without authentication is vulnerable to tampering. When you need both confidentiality and integrity, either use AES-GCM (via cryptography) or combine AES-CBC with a separate HMAC—never rely on encryption alone.
Why two packages exist for different problems
Hashing is one-way by design. The crypto package implements SHA-1, SHA-256, SHA-512, MD5, and HMAC variants . These functions take arbitrary input and produce a fixed-size digest. There is no key to decrypt, no reverse operation. The digest proves that some data matches the original input—if an attacker changes even one byte, the digest changes completely. This is the foundation of integrity verification: store the digest, recompute it later, and compare.
HMAC adds a secret to hashing. A plain SHA-256 hash of a message can be recomputed by anyone. HMAC-SHA256 requires a secret key, so only parties possessing the key can produce a valid digest. This makes HMAC a message authentication code: it proves both that the message is unchanged and that it came from someone with the key . The crypto package’s Hmac class accepts a hash algorithm and a key, then produces digests that verify authenticity.
Encryption is two-way and requires a key. The encrypt package wraps PointyCastle to provide AES encryption with a simpler API . You supply a key and an initialization vector (IV), and receive ciphertext that can be decrypted back to plaintext with the same key and IV. The package defaults to AES in SIC mode with PKCS7 padding, but supports CBC, CFB, CTR, ECB, OFB, and GCM modes . CBC is the most commonly used mode for general encryption when combined with a separate integrity check.
The gap: authenticated encryption. AES-CBC provides confidentiality but not integrity. An attacker who can modify ciphertext can potentially manipulate the decrypted plaintext without detection (padding oracle attacks). The correct mitigation is either AES-GCM (which authenticates as it encrypts) or AES-CBC combined with HMAC (encrypt-then-MAC). The encrypt package does not provide authenticated encryption natively; the cryptography package does. When encrypt is used for CBC, the HMAC from crypto must be applied separately .
The trade-off. crypto is pure Dart, has no native dependencies, and works everywhere. encrypt depends on PointyCastle and is slightly heavier but provides a straightforward AES API. Neither package handles key storage, key derivation, or secure random generation comprehensively. They are building blocks, not solutions. The cryptography package exists because these two packages together still leave the authenticated encryption problem to the developer.
a. Hashing and HMAC with package:crypto
The crypto package’s API is minimal: import the library, call sha256.convert(bytes), receive a Digest. The digest exposes both bytes (the raw digest) and toString() (the hex representation) .
import 'package:crypto/crypto.dart';
import 'dart:convert';
void main() {
final data = utf8.encode('sensitive payload');
// SHA-256 hash: one-way, no key, deterministic.
final shaDigest = sha256.convert(data);
print('SHA-256 hex: $shaDigest');
print('SHA-256 bytes: ${shaDigest.bytes}');
// HMAC-SHA256: keyed hash, proves authenticity.
final key = utf8.encode('my-secret-key');
final hmac = Hmac(sha256, key);
final hmacDigest = hmac.convert(data);
print('HMAC-SHA256 hex: $hmacDigest');
}
HMAC is not encryption. An HMAC digest cannot be reversed to recover the original message, even with the key. The key is used during digest computation, not during a decryption step . The only operation HMAC supports is verification: given a message, a key, and a claimed digest, recompute the digest and compare. If they match, the message is authentic and unmodified.
A critical detail for HMAC verification: the comparison must be constant-time to prevent timing attacks. The crypto package does not provide a constant-time comparison function. For high-security applications, compare digests byte-by-byte using a constant-time algorithm rather than == on strings or lists.
b. AES encryption with package:encrypt
The encrypt package provides Key, IV, and Encrypter classes. The key must be the correct length for the AES variant: 16 bytes for AES-128, 24 for AES-192, 32 for AES-256. Key.fromUtf8 accepts a string and uses its UTF-8 bytes; Key.fromSecureRandom(32) generates a random 256-bit key .
import 'package:encrypt/encrypt.dart';
void main() {
// 32-byte key for AES-256.
final key = Key.fromUtf8('my 32 length key................');
// 16-byte IV; must be unique per encryption under the same key.
final iv = IV.fromSecureRandom(16);
// CBC mode with PKCS7 padding (the default padding).
final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
final plainText = 'confidential message';
final encrypted = encrypter.encrypt(plainText, iv: iv);
print('Ciphertext (base64): ${encrypted.base64}');
print('IV (base64): ${iv.base64}');
// Decryption requires the same key, mode, and IV.
final decrypted = encrypter.decrypt(encrypted, iv: iv);
print('Decrypted: $decrypted');
}
The IV is not secret, but it must be unique for every encryption under the same key. Reusing an IV with CBC mode leaks information about the plaintext: identical plaintext blocks produce identical ciphertext blocks. IV.fromSecureRandom(16) generates a cryptographically random IV, which is correct for storage and transmission alongside the ciphertext .
The default mode is SIC (Segmented Integer Counter), not CBC . For most applications, CBC is the conventional choice when combined with HMAC, but GCM is superior when available. The encrypt package supports CBC, CFB, CTR, ECB, OFB, and SIC . ECB should never be used for data longer than one block because identical plaintext blocks produce identical ciphertext blocks, revealing patterns.
// Persisting encrypted data: store IV + ciphertext together.
final stored = '${iv.base64}:${encrypted.base64}';
// On read, split and reconstruct.
final parts = stored.split(':');
final storedIv = IV.fromBase64(parts[0]);
final storedEncrypted = Encrypted.fromBase64(parts[1]);
final recovered = encrypter.decrypt(storedEncrypted, iv: storedIv);
The Encrypted object is not interchangeable with a String. When retrieving encrypted data from storage, it must be reconstructed with Encrypted.fromBase64 or Encrypted.fromUtf8, not cast from a string .
c. Combining AES-CBC and HMAC for authenticated encryption
AES-CBC alone does not detect tampering. An attacker can flip bits in the ciphertext, and decryption will produce altered plaintext without raising an error until padding validation fails (and even then, the error may leak information). The solution is encrypt-then-MAC: encrypt the plaintext with AES-CBC, then compute an HMAC over the IV and ciphertext, and transmit all three values .
import 'package:crypto/crypto.dart';
import 'package:encrypt/encrypt.dart';
import 'dart:convert';
String encryptWithHmac(String plaintext, String keyString) {
final key = Key.fromUtf8(keyString);
final iv = IV.fromSecureRandom(16);
final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
final encrypted = encrypter.encrypt(plaintext, iv: iv);
// HMAC over IV + ciphertext (encrypt-then-MAC).
final hmac = Hmac(sha256, utf8.encode(keyString));
final macInput = utf8.encode(iv.base64 + encrypted.base64);
final mac = hmac.convert(macInput);
// Format: iv:ciphertext:hmac
return '${iv.base64}:${encrypted.base64}:${mac.toString()}';
}
String decryptWithHmac(String stored, String keyString) {
final parts = stored.split(':');
if (parts.length != 3) throw FormatException('Invalid format');
final ivBase64 = parts[0];
final cipherBase64 = parts[1];
final receivedMac = parts[2];
// Verify HMAC before decrypting.
final hmac = Hmac(sha256, utf8.encode(keyString));
final macInput = utf8.encode(ivBase64 + cipherBase64);
final expectedMac = hmac.convert(macInput).toString();
if (receivedMac != expectedMac) {
throw Exception('Integrity check failed: data was tampered with');
}
final iv = IV.fromBase64(ivBase64);
final encrypted = Encrypted.fromBase64(cipherBase64);
final encrypter = Encrypter(AES(Key.fromUtf8(keyString), mode: AESMode.cbc));
return encrypter.decrypt(encrypted, iv: iv);
}
The order matters: encrypt, then MAC. MAC-then-encrypt and encrypt-and-MAC are vulnerable to different attacks. Encrypt-then-MAC ensures that the MAC covers the ciphertext, so any modification is detected before decryption is attempted . Using the same key for both encryption and HMAC is not ideal but is acceptable for many applications; best practice is to derive separate keys from a master secret.
Complete Example Session
// ============================================
// PART 1: HASHING FOR INTEGRITY
// ============================================
import 'package:crypto/crypto.dart';
import 'dart:convert';
String sha256Hex(String input) {
return sha256.convert(utf8.encode(input)).toString();
}
// Store this digest alongside data to detect modification.
// Recompute later; if the digest differs, the data changed.
// ============================================
// PART 2: HMAC FOR AUTHENTICITY
// ============================================
String hmacHex(String message, String key) {
final hmac = Hmac(sha256, utf8.encode(key));
return hmac.convert(utf8.encode(message)).toString();
}
// Only parties with the key can produce a valid HMAC.
// Verification: recompute and compare in constant time.
// ============================================
// PART 3: AES-CBC ENCRYPTION
// ============================================
import 'package:encrypt/encrypt.dart';
Encrypted aesEncrypt(String plaintext, Key key, IV iv) {
final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
return encrypter.encrypt(plaintext, iv: iv);
}
String aesDecrypt(Encrypted ciphertext, Key key, IV iv) {
final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
return encrypter.decrypt(ciphertext, iv: iv);
}
// ============================================
// PART 4: THE ENCRYPT-THEN-MAC PATTERN
// ============================================
class SecureVault {
final Key _encKey;
final List<int> _macKey;
SecureVault(String keyString)
: _encKey = Key.fromUtf8(keyString),
_macKey = utf8.encode(keyString);
String encrypt(String plaintext) {
final iv = IV.fromSecureRandom(16);
final encrypter = Encrypter(AES(_encKey, mode: AESMode.cbc));
final encrypted = encrypter.encrypt(plaintext, iv: iv);
final hmac = Hmac(sha256, _macKey);
final mac = hmac.convert(utf8.encode(iv.base64 + encrypted.base64));
return '${iv.base64}:${encrypted.base64}:${mac.toString()}';
}
String decrypt(String stored) {
final parts = stored.split(':');
if (parts.length != 3) throw FormatException('Invalid vault format');
final iv = IV.fromBase64(parts[0]);
final encrypted = Encrypted.fromBase64(parts[1]);
final receivedMac = parts[2];
final hmac = Hmac(sha256, _macKey);
final expectedMac = hmac.convert(utf8.encode(parts[0] + parts[1])).toString();
if (receivedMac != expectedMac) {
throw Exception('Tampering detected');
}
final encrypter = Encrypter(AES(_encKey, mode: AESMode.cbc));
return encrypter.decrypt(encrypted, iv: iv);
}
}
// ============================================
// PART 5: KEY GENERATION
// ============================================
// NEVER hardcode keys. Generate randomly and store securely.
final randomKey = Key.fromSecureRandom(32); // 256-bit AES key.
// The key bytes must be persisted in platform secure storage,
// not in source code or shared_preferences.
// ============================================
// PART 6: IV MANAGEMENT
// ============================================
// IV is not secret but MUST be unique per encryption.
// Store IV with ciphertext; never reuse IV with same key.
final iv = IV.fromSecureRandom(16); // Correct.
// IV.fromLength(16) creates a zero-filled IV. Use only for testing.
// ============================================
// PART 7: WHAT CRYPTO DOES NOT DO
// ============================================
// crypto does not encrypt. A sha256 digest cannot be reversed.
// To recover plaintext, you need a cipher (encrypt package).
// To verify integrity without recovering plaintext, use HMAC.
// ============================================
// PART 8: WHAT ENCRYPT DOES NOT DO
// ============================================
// encrypt does not authenticate. AES-CBC ciphertext can be
// modified without detection. Always pair with HMAC or use GCM.
// ============================================
// PART 9: THE PREFERRED ALTERNATIVE
// ============================================
// package:cryptography provides AES-GCM, which combines
// encryption and authentication in one operation.
// Use it when possible; use encrypt+crypto only when
// constraining dependencies or maintaining legacy code.
// ============================================
// PART 10: COMPLETE FLOW
// ============================================
void main() {
final vault = SecureVault('my 32 length key................');
final ciphertext = vault.encrypt('secret data');
print('Encrypted: $ciphertext');
final decrypted = vault.decrypt(ciphertext);
print('Decrypted: $decrypted');
// Tamper detection:
try {
vault.decrypt(ciphertext.replaceFirst('A', 'B'));
} catch (e) {
print('Tampering detected: $e');
}
}
This session demonstrates the complete pattern: SHA-256 for integrity hashing, HMAC for keyed authenticity, AES-CBC for confidentiality, and encrypt-then-MAC to combine them securely. The SecureVault class encapsulates the correct sequence: generate a random IV, encrypt, compute HMAC over IV and ciphertext, transmit all three, verify HMAC before decrypting.
Quick Reference
Package Responsibilities
| Package | Operation | Reversible | Key Required |
|---|---|---|---|
crypto | SHA-256 hash | No | No |
crypto | HMAC-SHA256 | No | Yes |
encrypt | AES-CBC encrypt | Yes | Yes |
AES Modes in encrypt
| Mode | Name | Recommended | Notes |
|---|---|---|---|
| CBC | AESMode.cbc | Yes (with HMAC) | Requires IV |
| GCM | Not native | — | Use cryptography instead |
| ECB | AESMode.ecb | Never | Leaks patterns |
| SIC | AESMode.sic | Default | Counter mode |
Key and IV Sizes
| AES Variant | Key Size | IV Size (CBC) |
|---|---|---|
| AES-128 | 16 bytes | 16 bytes |
| AES-192 | 24 bytes | 16 bytes |
| AES-256 | 32 bytes | 16 bytes |
HMAC Verification Steps
| Step | Action |
|---|---|
| 1 | Receive message, key, claimed HMAC |
| 2 | Recompute HMAC over message with key |
| 3 | Compare in constant time |
| 4 | If match: authentic; else: reject |
Best Practices
✅ Do This:
// Use AES-256 with a 32-byte key. // ✅
final key = Key.fromSecureRandom(32);
// Generate a random IV for every encryption. // ✅
final iv = IV.fromSecureRandom(16);
// Pair AES-CBC with HMAC (encrypt-then-MAC). // ✅
final mac = hmac.convert(utf8.encode(iv.base64 + encrypted.base64));
// Store IV and HMAC alongside ciphertext. // ✅
return '${iv.base64}:${encrypted.base64}:${mac}';
// Use HMAC when you need keyed authenticity. // ✅
final hmac = Hmac(sha256, secretKey);
// Verify HMAC before decrypting. // ✅
if (receivedMac != expectedMac) throw Exception('tampered');
❌ Don’t Do This:
// Reuse the same IV across encryptions. // ❌
final iv = IV.fromLength(16); // Zero-filled, leaked patterns.
// Use AES-ECB for data longer than 16 bytes. // ❌
final encrypter = Encrypter(AES(key, mode: AESMode.ecb));
// Treat a hash as encryption. // ❌
// sha256.convert(data) cannot be reversed. Use AES.
// Use AES-CBC without HMAC for sensitive data. // ❌
// Tampering goes undetected until padding validation.
// Hardcode encryption keys in source. // ❌
final key = Key.fromUtf8('hardcoded-key-here......');
// Cast a String to Encrypted. // ❌
// Use Encrypted.fromBase64(storedString).
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| HMAC treated as decryption | HMAC is one-way | Use encrypt for reversible operations |
| IV reused | IV.fromLength creates zeros | Use IV.fromSecureRandom |
String cast to Encrypted | Type mismatch | Encrypted.fromBase64(stored) |
| No integrity check | CBC alone does not authenticate | Add HMAC (encrypt-then-MAC) |
| Key too short | AES key size mismatch | 16/24/32 bytes for 128/192/256 |
| ECB used for multi-block | Default mode confusion | Specify AESMode.cbc |
Real-World Examples
1. SHA-256 File Fingerprint
final digest = sha256.convert(fileBytes);
print(digest.toString()); // Compare to detect changes.
2. HMAC API Request Signing
final mac = Hmac(sha256, apiSecret).convert(utf8.encode(requestBody));
// Send mac alongside request; server recomputes.
3. AES-CBC Database Field Encryption
final iv = IV.fromSecureRandom(16);
final encrypter = Encrypter(AES(key, mode: AESMode.cbc));
final cipher = encrypter.encrypt(ssn, iv: iv);
4. Encrypt-Then-MAC Storage
final stored = '${iv.base64}:${cipher.base64}:${mac.toString()}';
// Split on read, verify MAC, then decrypt.
5. Reconstructing Encrypted from Storage
final encrypted = Encrypted.fromBase64(storedCipherBase64);
6. Generating a Random AES Key
final key = Key.fromSecureRandom(32); // 256-bit.
// Store key bytes in flutter_secure_storage.
7. Verifying HMAC Before Use
if (receivedMac != expectedMac) throw Exception('integrity');
// Only decrypt after MAC verification passes.
8. IV from Secure Random
final iv = IV.fromSecureRandom(16); // Never fromLength in prod.
9. SHA-256 Password Hashing (Wrong)
// WRONG: SHA-256 is too fast for passwords.
// Use Argon2id or PBKDF2 instead.
10. AES Key Length Validation
if (keyBytes.length != 32) throw ArgumentError('Need 32 bytes');
Visual
Hash vs Encryption
┌──────────────────────────────────────────────┐
│ HASH (crypto) ENCRYPT (encrypt) │
│ │
│ One-way. Two-way. │
│ │
│ Input → Digest Input + Key + IV │
│ ↓ │
│ Cannot reverse. Ciphertext │
│ ↓ │
│ Verify by Decrypt with same │
│ recomputing. Key + IV → Plaintext. │
│ │
│ SHA-256, HMAC AES-CBC │
│ │
│ Integrity. Confidentiality. │
│ │
│ Use for: checksums, Use for: data at rest, │
│ signatures, MACs. transmitted secrets. │
│ │
└──────────────────────────────────────────────┘
Encrypt-Then-MAC Flow
┌──────────────────────────────────────────────┐
│ AUTHENTICATED ENCRYPTION │
│ │
│ Plaintext │
│ │ │
│ ▼ │
│ AES-CBC + Random IV │
│ │ │
│ ▼ │
│ Ciphertext │
│ │ │
│ ├──────────────────────┐ │
│ │ │ │
│ ▼ ▼ │
│ Store IV HMAC(IV + Cipher) │
│ │ │ │
│ └──────────┬───────────┘ │
│ ▼ │
│ Transmit all three: │
│ IV : Cipher : HMAC │
│ │
│ On read: verify HMAC FIRST, then decrypt. │
│ HMAC mismatch → reject before any │
│ decryption attempt. │
│ │
└──────────────────────────────────────────────┘
AES-CBC Block Chaining
┌──────────────────────────────────────────────┐
│ CBC MODE (why IV matters) │
│ │
│ Block 1: Plaintext XOR IV → AES → C1 │
│ Block 2: Plaintext XOR C1 → AES → C2 │
│ Block 3: Plaintext XOR C2 → AES → C3 │
│ │
│ Same plaintext + same IV = same C1. │
│ Same plaintext + different IV = different C1│
│ │
│ IV reuse → identical first blocks reveal │
│ that plaintexts share a prefix. │
│ │
│ Fix: IV.fromSecureRandom(16) every time. │
│ │
└──────────────────────────────────────────────┘
Package Decision Tree
┌──────────────────────────────────────────────┐
│ WHICH PACKAGE? │
│ │
│ Need to verify data unchanged? │
│ ├── Yes, no secret → crypto sha256 │
│ └── Yes, with secret → crypto HMAC │
│ │
│ Need to recover original data? │
│ ├── Yes, one block → encrypt ECB (test)│
│ ├── Yes, multi-block → encrypt CBC │
│ │ + crypto HMAC │
│ └── Yes, authenticated → cryptography │
│ AES-GCM (preferred) │
│ │
│ Need password hashing? │
│ └── Use Argon2id, NOT sha256/crypto │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
crypto operations | SHA-256, HMAC-SHA256 |
encrypt operations | AES-CBC, AES-SIC, AES-ECB |
| Hash reversibility | Never |
| Encryption reversibility | With key and IV |
| IV uniqueness | Required per encryption |
| IV secrecy | Not secret, store with ciphertext |
| Authenticated encryption | AES-GCM (cryptography) or CBC + HMAC |
| Encrypt-then-MAC order | MAC covers IV + ciphertext |
| Key storage | Platform secure storage, never source |
Encrypted from storage | Encrypted.fromBase64(string) |
Key takeaways:
cryptohashes;encryptencrypts. A SHA-256 digest cannot be decrypted. If you need the original data back, use AES .- HMAC proves authenticity, not confidentiality. It requires a shared key and produces a digest that cannot be reversed. Use it to verify that data came from someone with the key and was not modified .
- AES-CBC without HMAC is incomplete. Confidentiality without integrity is vulnerable to tampering. Pair CBC with HMAC using encrypt-then-MAC, or use AES-GCM via
cryptography. - The IV must be random and unique per encryption. Reusing an IV with the same key leaks patterns.
IV.fromSecureRandom(16)is correct;IV.fromLength(16)creates zeros and must not be used in production. - The
encryptpackage is not authenticated encryption. It provides AES modes but does not include a MAC. The developer must add HMAC or choose a different package . Encryptedobjects cannot be cast from strings. When retrieving ciphertext from storage, reconstruct withEncrypted.fromBase64(stored).- The
cryptographypackage is the modern alternative. AES-GCM combines encryption and authentication in one operation. Useencrypt+cryptowhen constraints require it or when maintaining existing code.
Remember: The two packages solve different problems and must be composed correctly to achieve security. crypto provides one-way integrity and authenticity; encrypt provides two-way confidentiality. Neither is sufficient alone for sensitive data. The correct pattern for reversible storage is AES-CBC for confidentiality plus HMAC for integrity, applied in encrypt-then-MAC order, with a random IV per encryption and the key stored outside the source code. When the choice is available, AES-GCM via cryptography provides both properties in a single, harder-to-misuse API.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!