secure_key_manager 1.1.0
secure_key_manager: ^1.1.0 copied to clipboard
Fetch secret keys by name from Firebase Cloud Functions backed by Google Secret Manager. No secrets ever ship in your app binary, repo, or .env.
secure_key_manager #
Fetch secret keys by name from Firebase Cloud Functions backed by Google
Secret Manager. No API key, token, or other secret ever ships in your app
binary, source repo, or .env. Your app passes Firebase config once at startup,
then reads keys anywhere:
final agoraId = await SecureKeyManager.get('AGORA_APP_ID');
Features #
- π Secrets never on device β values live only in Google Secret Manager and are fetched at runtime over an authenticated channel.
- π§Ύ Server-side whitelist β the Cloud Function only returns keys you explicitly register, never arbitrary secrets.
- β‘ In-memory TTL cache β the function isn't called on every
get(); nothing is written to disk. - π Rotate without a release β change a value in Secret Manager and every app picks it up. No app-store submission.
- π§© Tiny API β
initialize,get,clearCache,evict. - π± iOS + Android, pure Dart on top of the FlutterFire SDKs (no native code).
How it works #
App ββsignInAnonymouslyβββΆ Firebase Auth
App ββget('AGORA_APP_ID')βββΆ getSecretKey (Cloud Function)
β validates request.auth
β checks ALLOWED_KEYS whitelist
βΌ
Google Secret Manager βββΆ value βββΆ App (in-memory cache)
Getting started #
You need: a Firebase project (Blaze/pay-as-you-go plan β Secret Manager and v2 Cloud Functions require it), the Firebase CLI, and the FlutterFire CLI.
1. Add the dependency #
dependencies:
secure_key_manager: ^1.0.0
flutter pub add secure_key_manager
2. Set up Firebase #
You need a Firebase project and one app entry to obtain config values. You do
not need the FlutterFire CLI, firebase_options.dart, google-services.json,
GoogleService-Info.plist, or the native google-services Gradle/CocoaPods
plugins. Pick whichever path you prefer:
Option A β inline config (no CLI, no native files) β recommended for minimal setup
- In the Firebase Console, create (or
open) a project and register an app: Project settings β Your apps β Add app.
You don't need to download any config file β just register it so an
appIdexists. - Copy the four values shown under SDK setup and configuration and pass
them straight to
initializeviafirebaseOptions. That's the entire client setup.
These values (
apiKey,appId,messagingSenderId,projectId) are Firebase project identifiers, not application secrets β they're safe to ship.
Option B β FlutterFire CLI (if you also use other Firebase products)
dart pub global activate flutterfire_cli
flutterfire configure
This writes lib/firebase_options.dart plus the platform config files. Then call
Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform) yourself
before SecureKeyManager.initialize() and omit the firebaseOptions argument.
Then: enable Anonymous sign-in (both options)
Firebase Console β Build β Authentication β Sign-in method β Anonymous β Enable
This lets the plugin prove every call comes from your app without asking users to log in. (If your app already uses real user auth, that works too β the function accepts any authenticated caller.)
3. Deploy the Cloud Function #
The function source is in functions/. From your project root:
firebase init functions # only if you don't have a functions/ folder yet
cd functions
npm install
Copy functions/src/index.ts into your project's
functions/src/index.ts, then deploy:
firebase deploy --only functions
4. Initialize in your app #
Option A β inline config (no firebase_options.dart, no native files):
import 'package:secure_key_manager/secure_key_manager.dart'; // exports FirebaseOptions
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await SecureKeyManager.initialize(
firebaseOptions: const FirebaseOptions(
apiKey: 'AIza...', // from the Firebase Console
appId: '1:1234567890:android:abcdef',
messagingSenderId: '1234567890',
projectId: 'my-project',
),
region: 'asia-south1', // your Cloud Functions region
cacheTtl: const Duration(minutes: 30),
);
runApp(const MyApp());
}
SecureKeyManager calls Firebase.initializeApp(options: ...) for you when you
pass firebaseOptions, so no native config files or CLI generation are needed.
Option B β you already initialized Firebase yourself (e.g. via the FlutterFire
CLI): just omit firebaseOptions.
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
await SecureKeyManager.initialize(region: 'asia-south1');
Region matters.
regionmust match where you deployed the function. If you deployed to the defaultus-central1, omit it.
Where do the values come from? Firebase Console β βοΈ Project settings β Your apps β SDK setup and configuration.
apiKey,appId,messagingSenderId, andprojectIdare project identifiers (not secrets) and are safe to commit.
Adding a key to Firebase (Secret Manager) #
This is the only thing you do per secret. The value is typed into the CLI and sent straight to Google Secret Manager β it never touches your code or repo.
Step 1 β Store the value #
firebase functions:secrets:set AGORA_APP_ID
# ? Enter a value for AGORA_APP_ID [hidden] βΊ ********
Repeat for each secret:
firebase functions:secrets:set OPENAI_API_KEY
firebase functions:secrets:set STRIPE_PUBLISHABLE_KEY
firebase functions:secrets:set GOOGLE_MAPS_KEY
Step 2 β Whitelist the key in the function #
Add the name to the ALLOWED_KEYS array in
functions/src/index.ts. The function will only
return keys on this list β this is your server-side guard against a client
asking for an arbitrary secret:
const ALLOWED_KEYS = [
"AGORA_APP_ID",
"OPENAI_API_KEY",
"STRIPE_PUBLISHABLE_KEY",
"GOOGLE_MAPS_KEY",
"ONESIGNAL_APP_ID",
// π add your new key here
"NEW_KEY_NAME",
];
Step 3 β Redeploy #
firebase deploy --only functions
Step 4 β Read it in Flutter β no app release needed #
final value = await SecureKeyManager.get('NEW_KEY_NAME');
Rotating or removing a key #
firebase functions:secrets:set AGORA_APP_ID # set a new value
firebase functions:secrets:destroy AGORA_APP_ID --force # remove old versions
Then either wait out the cache TTL or call SecureKeyManager.evict('AGORA_APP_ID').
Usage examples #
Fetch keys anywhere #
final agoraAppId = await SecureKeyManager.get('AGORA_APP_ID');
final mapsKey = await SecureKeyManager.get('GOOGLE_MAPS_KEY');
final openAiKey = await SecureKeyManager.get('OPENAI_API_KEY');
In a service / repository #
class AgoraService {
Future<RtcEngine> create() async {
final appId = await SecureKeyManager.get('AGORA_APP_ID');
return createAgoraRtcEngine()..initialize(RtcEngineContext(appId: appId));
}
}
Load several keys at once #
final results = await Future.wait([
SecureKeyManager.get('OPENAI_API_KEY'),
SecureKeyManager.get('GOOGLE_MAPS_KEY'),
]);
final openAiKey = results[0];
final mapsKey = results[1];
Handle errors #
try {
final key = await SecureKeyManager.get('AGORA_APP_ID');
// use key
} catch (e) {
// not registered, no value set, network/timeout, or not initialized
debugPrint('Could not load key: $e');
}
Clear on logout #
await FirebaseAuth.instance.signOut();
SecureKeyManager.clearCache();
A full runnable app is in example/.
API reference #
| Method | Description |
|---|---|
initialize({String? region, Duration cacheTtl}) |
Call once in main(). Signs in anonymously and configures the region. |
get(String keyName) |
Returns the value β from cache if fresh, otherwise via the Cloud Function. |
clearCache() |
Drop all cached keys (e.g. on logout). |
evict(String keyName) |
Drop one key, forcing a re-fetch next call. |
Security properties #
| Property | Detail |
|---|---|
| Keys in app binary | Never β not in code, not in .env, not in assets |
| Keys in source repo | Never β functions:secrets:set writes straight to Secret Manager |
| Who can call the function | Only apps signed in with Firebase Auth (anonymous or user) |
| On-device storage | In-memory cache only, cleared on restart or clearCache() |
| Key rotation | Update the value in Secret Manager β no app release required |
| App Check | Set enforceAppCheck: true in the function once App Check is configured |
βΉοΈ
google-services.jsonandGoogleService-Info.plistcontain only Firebase project configuration (project ID, Firebase SDK API keys), not application secrets. They are safe to include in your project.
Troubleshooting #
| Symptom | Likely cause |
|---|---|
unauthenticated error |
Anonymous sign-in not enabled in the Firebase Console. |
not-found: Key 'β¦' is not registered |
Key missing from ALLOWED_KEYS, or function not redeployed. |
not-found: Key 'β¦' has no value set |
firebase functions:secrets:set <KEY> was never run. |
| Calls hang / time out | region in initialize() doesn't match the deployed function. |
License #
MIT