Encrypt secrets from a gitignored .secure_env plaintext file at build time into Rust + Dart embeds (AES-256-GCM). At runtime your app resolves values through dart:ffi so plaintext never lives as Dart constants and is much harder to lift with naive strings / decompilation.
| Platforms | Android (JNI .so), iOS (XCFramework .a), macOS (.dylib) |
| Not supported | Flutter Web — use server-held secrets instead |
Replace placeholder homepage, repository, issue_tracker, badge URLs, and CI shield targets in pubspec.yaml before publishing under your own org.
- Build-time codegen (
dart run secure_env:generate): parses.secure_env, writes ciphertext intorust/src/generated/and metadata intolib/src/generated/secure_env_data.g.dart. - Rust FFI: stripped release binaries, AES-GCM decrypt,
zeroizefor sensitive buffers. - Small Dart surface:
SecureEnvwithget/require,availableKeys,nativeAbiVersion,isAvailable. - Scripts under
tool/for NDK/Xcode builds and optional APK plaintext checks.
dependencies:
secure_env: ^1.0.0The package registers the generate executable:
dart run secure_env:generate --help-
Keep
.secure_envout of version control (see.gitignorepatterns). -
Define secrets as
KEY=valuelines (quotes and#comments supported — see codegen help). -
From your app / monorepo root (must contain
lib/+rust/as expected by the generator):dart run secure_env:generate
Default env file path is
<root>/.secure_env. Override with--env path/to/fileand--root .when your shell cwd is elsewhere. -
Compile natives into the embedding plugin (see below), then
flutter pub getand use the Dart API. -
Read at runtime:
import 'package:secure_env/secure_env.dart'; if (SecureEnv.isAvailable) { final apiKey = SecureEnv.require('MY_API_KEY'); // throws if missing/undecodable after FFI loads final optional = SecureEnv.get('OTHER'); // nullable final keys = SecureEnv.availableKeys(); // sorted key names only (plaintext values never listed) }
Codegen updates Dart + Rust sources only. You still ship platform binaries:
| Target | Script / output |
|---|---|
| Android (arm64‑v8a, armeabi‑v7a, x86_64, x86) | bash tool/build_android.sh → android/src/main/jniLibs/<abi>/libsecure_env.so |
| arm64‑only shortcut | bash tool/build_android_arm64_manual.sh |
| iOS (device + simulators, XCFramework) | bash tool/build_ios.sh → ios/Frameworks/secure_env.xcframework |
| macOS | bash tool/build_macos.sh → macos/libsecure_env.dylib |
| One-shot local build | bash tool/build_all.sh (needs ANDROID_NDK_HOME for Android) |
Prerequisites: Rust with the triples listed in script output/README comments, ANDROID_NDK_HOME for Android, Xcode for Apple targets. See Flutter’s CocoaPods docs for pod install on iOS/macOS example apps.
FFI loaders: Android opens libsecure_env.so; iOS uses DynamicLibrary.process() after static linking; macOS prefers opening libsecure_env.dylib.
dart run secure_env:generate [options]
--env Path to plaintext env file (default: .secure_env)
--root Repository root containing rust/ + lib/
--verbose Log key names only (never values)
--dry-run Validate without writing files
--master-key-hex Optional 64-hex deterministic key for reproducible CI (never ship real secrets)
- Secrets are stored as ciphertext + obfuscated key material inside native/Dart codegen outputs; plaintext is not emitted into Dart literals.
- This is meaningful hardening, not proof against a dedicated attacker with unlimited time, physical device access, memory scraping on a compromised device, or nation-state budgets.
- Key names appear in generated Dart metadata; values are ciphertext.
SecureEnv.getreturnsnullfor missing keys or failed decrypt paths;SecureEnvDecryptionExceptionis reserved for API evolution (secure_env.dart).- Web: not supported (
dart:ffi).
Bundled example/ demos every runtime call. Secrets there resolve from the parent plugin’s lib/ + rust/ embeds unless you regenerate with --env example/.secure_env --root . from the repo root. See example/README.md.
Golden fixture notes live in [doc/fixture_testing.md](doc/fixture_testing.md).
Developer checks:
dart analyze && flutter analyze
flutter test
(cd rust && cargo test)
bash tool/check_apk_plaintext_leak.sh <apk> <secret-substring> # optional release hygieneCI configuration: .github/workflows/ci.yml.
Substitute real URLs first, then:
dart format . --set-exit-if-changed
dart pub publish --dry-run
dart pub global run pana .Scores around 140 / 160 are typical until Flutter’s Swift Package Manager lane is adopted alongside CocoaPods; this package documents the CocoaPods + XCFramework path first.
Keep CHANGELOG.md updated per release.