| |

Dart 84 🎯 Security Best Practices: Obfuscation, Cryptography, and Key Storage

⏱️ Estimated read: 17 min | |

Dart’s security model forces a distinction that many developers learn the hard way: obfuscation is not encryption, and client-side secrets are never truly secret. Flutter’s obfuscation tool renames symbols to make reverse engineering tedious, but it explicitly does not encrypt resources or protect against a determined attacker . Any key compiled into a binary—even an obfuscated one—can be extracted. Security in Dart therefore depends on understanding which problems each tool actually solves: obfuscation raises the cost of casual inspection, cryptography protects data in transit and at rest, and platform secure storage protects runtime credentials from other apps on the device.

The practical architecture separates these concerns. Secrets that must never reach a client—server API keys, signing keys, database credentials—belong on a backend, not in a Flutter app. Runtime tokens obtained after authentication belong in platform keychain or keystore via flutter_secure_storage. Data that must be encrypted before storage or transmission belongs in a cryptographic package like cryptography, which provides AES-GCM, Argon2id, and PBKDF2. Obfuscation sits on top of all of this as a deterrent, never as a primary defense.

This chapter covers the three layers: what obfuscation does and does not do, how to use the cryptography package for authenticated encryption and password hashing, and how to store runtime secrets with flutter_secure_storage while avoiding the traps that leak data on Android and iOS. It assumes familiarity with Flutter release builds and basic symmetric encryption concepts.

Key point: Obfuscation renames symbols; it does not encrypt data or protect secrets. A key in a client binary is compromised by definition. Use obfuscation to slow reverse engineering, cryptography to protect data, and secure storage for runtime credentials—never confuse the three.


Why client-side security requires layered defenses

The binary is public. Every Android APK and iOS IPA can be downloaded, unpacked, and inspected. Dart AOT compiles to machine code, but the data section contains strings, constants, and class metadata that remain readable. Flutter’s --obfuscate flag replaces function and class names with short meaningless identifiers, but the Flutter documentation is explicit: it “does not encrypt resources nor does it protect against reverse engineering. It only renames symbols with more obscure names” . An attacker with a decompiler and patience can still trace logic. Obfuscation raises the cost of casual analysis; it does not make secrets safe.

Secrets in code are compromised by design. A Stripe secret key, a Firebase server key, or a database password embedded in a Dart constant is extractable regardless of obfuscation. The strings command on an APK reveals plaintext constants; deobfuscation maps recover original names. The only safe place for a truly secret credential is a server the client communicates with. Client-side keys—publishable Stripe keys, Google Maps API keys restricted by package and SHA-1—are not secrets; they are identifiers that must be restricted at the provider level, not hidden in the binary.

Platform secure storage protects runtime credentials. After a user logs in, the returned access token must be stored somewhere. shared_preferences stores plaintext. flutter_secure_storage delegates to iOS Keychain, Android Keystore-backed EncryptedSharedPreferences, and platform equivalents on desktop . The operating system encrypts the data and, on modern Android, binds the encryption key to the device’s hardware keystore. This protects tokens from other apps and from offline extraction of the app’s data directory. It does not protect against a rooted device or a runtime attacker with debugging access, but it is the correct default for any credential that must persist.

Cryptography packages fill the gaps the SDK leaves. Dart’s core libraries do not include AES, PBKDF2, or Argon2. The cryptography package provides these with a consistent API: AES-GCM for authenticated encryption, Argon2id for password hashing, PBKDF2 for key derivation, and SHA-256 for hashing . Authenticated encryption is essential: it ensures not only that data cannot be read without the key, but also that it cannot be modified without detection. A cipher without authentication (AES-CBC without HMAC, for example) is vulnerable to padding oracle attacks. The cryptography package’s AesGcm returns a SecretBox containing the nonce, ciphertext, and MAC, and decryption fails if any part is tampered with.

The trade-off. Every security layer adds complexity. Obfuscation complicates stack traces and requires symbol files. Cryptography requires key management, nonce generation, and careful algorithm selection. Secure storage requires platform configuration (Keychain entitlements, Android backup exclusion). The alternative—leaving tokens in plaintext or keys in code—is not a trade-off; it is a vulnerability waiting for the first interested attacker.


a. Obfuscation: what it does and what it does not

Flutter’s obfuscation runs only in release builds and only for supported targets (apk, appbundle, ios, ipa, linux, macos, windows, aar, and their framework variants) . Web builds do not support obfuscation; they are minified instead, which produces a similar but distinct result.

The build command combines --obfuscate with --split-debug-info. The symbol file must be preserved: without it, obfuscated stack traces from crash reports are unreadable. The flutter symbolize command consumes the stack trace and the matching symbol file to produce a human-readable trace .

# Build an obfuscated release APK.
# The symbol file is written to out/android for later symbolization.
flutter build apk \
  --obfuscate \
  --split-debug-info=out/android

Obfuscation has a concrete limitation beyond its non-encryption: code that relies on symbol names breaks. expect(foo.runtimeType.toString(), equals('Foo')) fails because runtimeType.toString() returns the obfuscated name . Enum names are not obfuscated. Any logic that matches on class names, function names, or type strings will behave differently in release builds. This is a correctness issue, not merely a security one.

The obfuscation map can be exported as JSON using --extra-gen-snapshot-options=--save-obfuscation-map=<file>, which records original-to-obfuscated name pairs . This is useful for debugging release-only issues but does not change the fundamental reality: the binary remains inspectable, and secrets within it remain extractable.


b. Cryptography: authenticated encryption and password hashing

The cryptography package provides two categories of operations relevant to application security: symmetric encryption for data at rest or in transit, and password hashing for credential storage. The package’s AesGcm.with256bits() is the recommended cipher for most use cases: it provides authenticated encryption, meaning tampering is detected at decryption time .

Encryption requires a secret key and a nonce. The nonce must be unique for every encryption under the same key; reusing a nonce with AES-GCM destroys both confidentiality and authentication. The cryptography package generates a random nonce automatically when one is not supplied, which is the correct default. The output SecretBox contains the nonce, ciphertext, and message authentication code. These three values must be stored or transmitted together; SecretBox.concatenation() produces a single byte sequence for storage .

import 'package:cryptography/cryptography.dart';

Future<void> encryptData() async {
  final algorithm = AesGcm.with256bits();
  final secretKey = await algorithm.newSecretKey();
  final keyBytes = await secretKey.extractBytes();

  // Encrypt. Nonce is auto-generated and stored in the SecretBox.
  final secretBox = await algorithm.encryptString(
    'sensitive payload',
    secretKey: secretKey,
  );

  // Persist nonce + ciphertext + MAC as a single value.
  final stored = secretBox.concatenation();

  // Decrypt: reconstruct the SecretBox from stored bytes.
  final restored = SecretBox.fromConcatenation(
    stored,
    nonceLength: secretBox.nonce.length,
    macLength: secretBox.mac.bytes.length,
  );
  final clear = await algorithm.decryptString(
    restored,
    secretKey: secretKey,
  );
  print(clear); // 'sensitive payload'
}

Password hashing is a different problem. Passwords must not be encrypted reversibly; they must be hashed with a slow, salted algorithm that makes brute-force attacks expensive. Argon2id is the current recommendation; PBKDF2-HMAC-SHA256 with a high iteration count (OWASP recommends 600,000) is acceptable when Argon2 is unavailable or interoperability demands it . The cryptography package provides both. The critical implementation details: use a random salt per password, store the salt and algorithm parameters alongside the hash, and compare hashes in constant time .

import 'package:cryptography/cryptography.dart';

Future<String> hashPassword(String password) async {
  final algorithm = Argon2id(
    memory: 10000,      // 10 MB of memory.
    parallelism: 2,      // Use up to 2 cores.
    iterations: 1,       // Raise memory before raising iterations.
    hashLength: 32,      // 256-bit output.
  );

  // Nonce is the salt; store it with the hash.
  final salt = List<int>.generate(16, (_) => Random.secure().nextInt(256));
  final secretKey = await algorithm.deriveKeyFromPassword(
    password: password,
    nonce: salt,
  );
  final hashBytes = await secretKey.extractBytes();

  // Store algorithm, parameters, salt, and hash together.
  return 'argon2id\$10MB\$2\$1\$${base64Encode(salt)}\$${base64Encode(hashBytes)}';
}

The cryptography package delegates to platform-native implementations where available—Web Crypto in browsers, hardware-accelerated paths on mobile—which improves performance and reduces timing side-channel exposure compared to pure Dart implementations . For Flutter apps, cryptography_flutter adds native bindings for Android and iOS.


c. Key storage: flutter_secure_storage and its platform traps

flutter_secure_storage stores key-value pairs in the platform’s secure storage: iOS Keychain, Android EncryptedSharedPreferences (Keystore-backed), Linux libsecret, macOS Keychain, and Windows Credential Manager . The API is deliberately simple: write, read, delete, and containsKey.

The Android configuration has two traps that leak data or cause runtime crashes. First, Android’s auto-backup feature backs up SharedPreferences to Google Drive by default. The encryption key lives in the hardware Keystore and is not backed up. Restoring the backed-up preferences on a new device produces java.security.InvalidKeyException: Failed to unwrap key because the ciphertext exists but the key does not . The fix is to disable auto-backup or exclude the secure storage preferences file:

<!-- android/app/src/main/AndroidManifest.xml -->
<application
    android:allowBackup="false"
    ...>

Second, on iOS and macOS, Keychain Sharing must be configured in the entitlements files. Without it, writes appear to succeed but values are never actually stored . The entitlement requires an empty keychain-access-groups array in both DebugProfile and Release entitlements.

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

final storage = FlutterSecureStorage();

// Store an access token after login.
await storage.write(key: 'auth_token', value: token);

// Retrieve it on subsequent launches.
final token = await storage.read(key: 'auth_token');

// Remove on logout.
await storage.delete(key: 'auth_token');

For biometric-gated access on Android, AndroidOptions.biometric() enables Keystore-backed keys that require device authentication before decryption . The enforceBiometrics parameter controls whether authentication is required or optional. On iOS, the accessibility option controls when Keychain items are readable—unlocked (default), first_unlock, or first_unlock_this_device . These options trade security for usability: first_unlock allows background access after the first unlock following a reboot; unlocked requires the device to be actively unlocked.

For compile-time secrets that must be encrypted at build time, packages like envault generate encrypted Dart files and derive the master key at runtime via PBKDF2, avoiding plaintext in the binary . This is a meaningful improvement over flutter_dotenv, which ships a plaintext .env asset inside the APK, but it does not eliminate the fundamental problem: the master password must be supplied at build time via --dart-define, which is recoverable from the binary if the attacker is determined. The best practice remains: do not ship secrets you cannot afford to lose.


Complete Example Session

// ============================================
// PART 1: OBFUSCATION BUILD CONFIGURATION
// ============================================
// Build with obfuscation and symbol preservation.
// The symbol file is required to decode crash reports.
//
// flutter build apk \
//   --obfuscate \
//   --split-debug-info=build/symbols
//
// Backup build/symbols. Without it, obfuscated
// stack traces are unreadable.

// ============================================
// PART 2: AUTHENTICATED ENCRYPTION
// ============================================
import 'package:cryptography/cryptography.dart';

class DataVault {
  final _algorithm = AesGcm.with256bits();
  late final SecretKey _key;

  Future<void> initialize(List<int> keyBytes) async {
    // Key should come from secure storage, not a constant.
    _key = SecretKey(keyBytes);
  }

  Future<List<int>> encrypt(String plaintext) async {
    final box = await _algorithm.encryptString(
      plaintext,
      secretKey: _key,
    );
    // concatenation() = nonce + ciphertext + MAC.
    return box.concatenation();
  }

  Future<String> decrypt(List<int> stored) async {
    final box = SecretBox.fromConcatenation(
      stored,
      nonceLength: 12,  // AES-GCM standard nonce length.
      macLength: 16,    // AES-GCM standard MAC length.
    );
    return _algorithm.decryptString(box, secretKey: _key);
  }
}

// ============================================
// PART 3: PASSWORD HASHING WITH ARGON2ID
// ============================================
Future<Map<String, dynamic>> hashPassword(String password) async {
  final algorithm = Argon2id(
    memory: 10000,
    parallelism: 2,
    iterations: 1,
    hashLength: 32,
  );

  final salt = List<int>.generate(16, (_) => Random.secure().nextInt(256));
  final key = await algorithm.deriveKeyFromPassword(
    password: password,
    nonce: salt,
  );
  final hash = await key.extractBytes();

  // Return all parameters needed for verification.
  return {
    'algorithm': 'argon2id',
    'memory': 10000,
    'parallelism': 2,
    'iterations': 1,
    'salt': base64Encode(salt),
    'hash': base64Encode(hash),
  };
}

// ============================================
// PART 4: SECURE STORAGE FOR RUNTIME TOKENS
// ============================================
import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class TokenStore {
  final _storage = const FlutterSecureStorage();

  Future<void> saveToken(String token) async {
    await _storage.write(key: 'auth_token', value: token);
  }

  Future<String?> loadToken() async {
    return _storage.read(key: 'auth_token');
  }

  Future<void> clearToken() async {
    await _storage.delete(key: 'auth_token');
  }
}

// ============================================
// PART 5: DERIVING AN ENCRYPTION KEY FROM A PASSWORD
// ============================================
Future<SecretKey> deriveKeyFromPassword(
  String password,
  List<int> salt,
) async {
  final pbkdf2 = Pbkdf2(
    macAlgorithm: Hmac.sha256(),
    iterations: 600000,  // OWASP recommendation for PBKDF2-SHA256.
    bits: 256,
  );
  return pbkdf2.deriveKeyFromPassword(
    password: password,
    nonce: salt,
  );
}

// ============================================
// PART 6: THE WRONG WAY — SECRETS IN CODE
// ============================================
// Never do this. The string is extractable
// from the binary regardless of obfuscation.
//
// const apiKey = 'sk_live_abc123...';  // COMPROMISED.

// ============================================
// PART 7: THE RIGHT WAY — BACKEND PROXY
// ============================================
// The client calls your server. The server holds
// the secret key and makes the third-party API call.
// The client never sees the secret.
//
// Future<Response> callApi() {
//   return http.post(
//     Uri.parse('https://your-backend.com/proxy'),
//     body: {'action': 'charge'},
//   );
// }

// ============================================
// PART 8: ANDROID BACKUP EXCLUSION
// ============================================
// In AndroidManifest.xml:
// <application android:allowBackup="false" ...>
//
// Without this, encrypted preferences are backed up,
// restored on a new device, and fail to decrypt
// because the Keystore key was not backed up.

// ============================================
// PART 9: iOS KEYCHAIN ENTITLEMENT
// ============================================
// In ios/Runner/DebugProfile.entitlements and
// ios/Runner/Release.entitlements:
// <key>keychain-access-groups</key>
// <array/>
//
// Without this, writes silently fail.

// ============================================
// PART 10: THE SECURITY LAYERS
// ============================================
// 1. Obfuscation: --obfuscate, --split-debug-info.
//    Deterrent only. Backup symbols.
// 2. Cryptography: cryptography package.
//    AES-GCM for data. Argon2id for passwords.
// 3. Secure storage: flutter_secure_storage.
//    Runtime tokens. Configure Android backup
//    exclusion and iOS Keychain entitlement.
// 4. Backend: secrets that must not leak.
//    Never ship them in the client.

This session demonstrates the complete stack: obfuscation as a build flag with symbol preservation, AES-GCM for authenticated encryption, Argon2id for password hashing, PBKDF2 for key derivation, and flutter_secure_storage for runtime tokens. The wrong-way example shows the anti-pattern; the right-way example shows the only correct architecture for true secrets.


Quick Reference

Obfuscation Flags

FlagEffectRequired
--obfuscateRenames symbolsRelease only
--split-debug-info=<dir>Writes symbol fileYes, for debugging
--save-obfuscation-map=<file>Exports name mapping JSONOptional

Cryptography Algorithms

OperationRecommendedAlternative
Symmetric encryptionAES-GCM 256ChaCha20-Poly1305
Password hashingArgon2idPBKDF2-SHA256 (600k)
Key derivationPBKDF2 or Argon2idHKDF (from shared secret)
HashingSHA-256SHA-512

Secure Storage Platform Mapping

PlatformBackendRequirement
AndroidEncryptedSharedPreferencesallowBackup="false"
iOSKeychainKeychain Sharing entitlement
macOSKeychainKeychain Sharing entitlement
Linuxlibsecretlibsecret-1-dev
WindowsCredential ManagerNone

Security Layer Responsibilities

LayerProtects AgainstDoes Not Protect Against
ObfuscationCasual inspectionDetermined reverse engineering
CryptographyData confidentiality, tamperingKey theft
Secure storageOther apps, offline extractionRooted device, runtime attacker
BackendAll client-side extractionServer compromise

Common Secure Storage Options

OptionPlatformEffect
allowBackup="false"AndroidPrevents Keystore mismatch on restore
KeychainAccessibility.unlockediOSRequires device unlocked
first_unlockiOSReadable after first unlock post-reboot
enforceBiometrics: trueAndroidRequires PIN/biometric

Best Practices

✅ Do This:

// Obfuscate release builds with symbol preservation.         // ✅
flutter build apk --obfuscate --split-debug-info=build/symbols

// Encrypt data with AES-GCM (authenticated).                 // ✅
final box = await AesGcm.with256bits().encryptString(data, secretKey: key);

// Hash passwords with Argon2id or PBKDF2.                    // ✅
final key = await Argon2id().deriveKeyFromPassword(password: pw, nonce: salt);

// Store runtime tokens in flutter_secure_storage.            // ✅
await storage.write(key: 'token', value: accessToken);

// Disable Android auto-backup.                               // ✅
// <application android:allowBackup="false" ...>

// Keep true secrets on the backend.                          // ✅
// Client calls your proxy; server holds the key.

❌ Don’t Do This:

// Embed secrets in Dart constants.                           // ❌
const apiKey = 'sk_live_...'; // Extractable.

// Rely on obfuscation as encryption.                         // ❌
// Obfuscation renames; it does not encrypt.

// Use AES-CBC without authentication.                        // ❌
// Vulnerable to padding oracle attacks.

// Reuse a nonce with AES-GCM.                                // ❌
// Destroys confidentiality and authentication.

// Store tokens in shared_preferences.                        // ❌
// Plaintext; readable on rooted devices.

// Skip the iOS Keychain entitlement.                         // ❌
// Writes silently fail without it.

Common Pitfalls

PitfallWhy It HappensFix
Secrets extracted from APKObfuscation ≠ encryptionMove secrets to backend
Crash traces unreadableSymbols not preservedBackup --split-debug-info output
Android restore crashesKeystore key not backed upallowBackup="false"
iOS secure storage emptyMissing Keychain entitlementAdd keychain-access-groups
AES-GCM decryption failsNonce reused or tampered dataNever reuse nonce; verify MAC
Password hash weakLow iterations, no saltArgon2id or PBKDF2 600k + salt

Real-World Examples

1. Obfuscated Release Build

flutter build apk --obfuscate --split-debug-info=build/symbols

2. AES-GCM Encryption

final box = await AesGcm.with256bits().encryptString('data', secretKey: key);
final stored = box.concatenation(); // nonce + ciphertext + MAC

3. Argon2id Password Hash

final key = await Argon2id(memory: 10000, parallelism: 2, iterations: 1)
    .deriveKeyFromPassword(password: pw, nonce: salt);

4. Secure Token Storage

const storage = FlutterSecureStorage();
await storage.write(key: 'token', value: token);

5. Android Backup Exclusion

<application android:allowBackup="false" ...>

6. iOS Keychain Entitlement

<key>keychain-access-groups</key>
<array/>

7. PBKDF2 Key Derivation

final key = await Pbkdf2(macAlgorithm: Hmac.sha256(), iterations: 600000, bits: 256)
    .deriveKeyFromPassword(password: pw, nonce: salt);

8. Wrong: Hardcoded API Key

const apiKey = 'sk_live_abc'; // Extractable from binary.

9. Right: Backend Proxy

// Client calls your server. Server holds the secret.
await http.post(Uri.parse('https://your-api.com/charge'));

10. Biometric-Gated Storage

final storage = FlutterSecureStorage(
  aOptions: AndroidOptions.biometric(enforceBiometrics: true),
);

Visual

The Security Layer Stack

┌──────────────────────────────────────────────┐
│  DEFENSE IN DEPTH                            │
│                                              │
│  ┌────────────────────────────────────┐      │
│  │  BACKEND                           │      │
│  │  True secrets: API keys, DB creds  │      │
│  │  Never shipped to client.          │      │
│  └────────────────────────────────────┘      │
│         ▲                                    │
│         │ HTTPS                              │
│         ▼                                    │
│  ┌────────────────────────────────────┐      │
│  │  OBFUSCATION                       │      │
│  │  --obfuscate --split-debug-info    │      │
│  │  Deterrent only. Not encryption.   │      │
│  └────────────────────────────────────┘      │
│         ▲                                    │
│         │                                    │
│         ▼                                    │
│  ┌────────────────────────────────────┐      │
│  │  CRYPTOGRAPHY                      │      │
│  │  AES-GCM, Argon2id, PBKDF2         │      │
│  │  Protects data at rest/in transit. │      │
│  └────────────────────────────────────┘      │
│         ▲                                    │
│         │                                    │
│         ▼                                    │
│  ┌────────────────────────────────────┐      │
│  │  SECURE STORAGE                    │      │
│  │  Keychain / Keystore               │      │
│  │  Runtime tokens, session keys.     │      │
│  └────────────────────────────────────┘      │
│                                              │
└──────────────────────────────────────────────┘

Obfuscation vs Encryption

┌──────────────────────────────────────────────┐
│  OBFUSCATION          ENCRYPTION             │
│                                              │
│  Renames symbols.     Transforms data.       │
│                                              │
│  Reversible with      Irreversible without   │
│  deobfuscation map.   the key.               │
│                                              │
│  Deters casual        Protects               │
│  inspection.          confidentiality.       │
│                                              │
│  Does NOT protect     DOES protect            │
│  secrets.             secrets (if key safe). │
│                                              │
│  Flutter flag:        Package:               │
│  --obfuscate          cryptography           │
│                                              │
│  Use for: code        Use for: data at rest, │
│  readability.         passwords, tokens.     │
│                                              │
└──────────────────────────────────────────────┘

Secure Storage Platform Traps

┌──────────────────────────────────────────────┐
│  ANDROID                          iOS       │
│                                              │
│  EncryptedSharedPrefs           Keychain    │
│  backed by Keystore.            backed.     │
│                                              │
│  Trap:                          Trap:        │
│  Auto-backup restores prefs     Missing      │
│  without Keystore key.          entitlement  │
│  Crash on decrypt.              → silent     │
│                                 write fail.  │
│  Fix:                                        │
│  android:allowBackup="false"    Fix:         │
│                                 Add          │
│                                 keychain-    │
│                                 access-      │
│                                 groups.      │
│                                              │
└──────────────────────────────────────────────┘

Authenticated Encryption Flow

┌──────────────────────────────────────────────┐
│  AES-GCM SECRETBOX                           │
│                                              │
│  Plaintext + Key + Random Nonce              │
│       │                                      │
│       ▼                                      │
│  ┌────────────────────────────────────┐      │
│  │  ENCRYPT                           │      │
│  │  nonce + ciphertext + MAC          │      │
│  └────────────────────────────────────┘      │
│       │                                      │
│       ▼                                      │
│  Store concatenation() as one value.         │
│                                              │
│  Decrypt:                                    │
│  Reconstruct SecretBox from stored bytes.    │
│  If MAC fails → tampered or wrong key.       │
│  If nonce reused → catastrophic failure.     │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
ObfuscationRenames symbols; does not encrypt
Required flags--obfuscate --split-debug-info
Symbol backupRequired for crash symbolization
Symmetric cipherAES-GCM 256 (authenticated)
Password hashArgon2id (preferred) or PBKDF2 600k
Secure storageflutter_secure_storage
Android trapallowBackup="false" required
iOS trapKeychain Sharing entitlement required
True secretsBackend only, never client
runtimeType with obfuscationReturns obfuscated names

Key takeaways:

  • Obfuscation is a deterrent, not encryption. It renames symbols to slow reverse engineering but does not hide strings, constants, or logic. Secrets in an obfuscated binary remain extractable .
  • Authenticated encryption is mandatory. AES-GCM ensures both confidentiality and integrity. AES-CBC without HMAC is vulnerable to tampering. The cryptography package’s SecretBox handles nonce, ciphertext, and MAC correctly .
  • Password hashing requires slow, salted algorithms. Argon2id is the current recommendation; PBKDF2-SHA256 with 600,000 iterations is acceptable. Never use plain SHA-256 for passwords .
  • Platform secure storage has configuration traps. Android auto-backup must be disabled; iOS Keychain Sharing must be entitled. Without these, storage fails silently or crashes on restore .
  • True secrets belong on the backend. Client-side keys are compromised by definition. The only safe architecture for a secret is a server the client communicates with over HTTPS.
  • Symbols must be preserved. An obfuscated binary without its symbol file produces unreadable crash reports. Back up the --split-debug-info output .
  • Compile-time encryption packages improve on plaintext. envault and similar tools encrypt secrets at build time and derive keys at runtime, but the master password must still be supplied via --dart-define, which is recoverable .

Remember: Dart security is layered because no single tool solves the problem. Obfuscation makes the binary harder to read but does not hide secrets. Cryptography protects data with mathematical guarantees, but only if keys are managed correctly. Secure storage protects runtime credentials from other apps, but only if platform configuration is correct. The only place a true secret is safe is a backend the client does not control. Build with obfuscation and symbol preservation, encrypt sensitive data with AES-GCM, hash passwords with Argon2id, store tokens in the platform keychain or keystore, and never ship a credential you cannot afford to have extracted.

Was this helpful?


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!