secure_key_manager 1.1.0 copy "secure_key_manager: ^1.1.0" to clipboard
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 #

pub package License: MIT

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:

  1. 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 appId exists.
  2. Copy the four values shown under SDK setup and configuration and pass them straight to initialize via firebaseOptions. 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. region must match where you deployed the function. If you deployed to the default us-central1, omit it.

Where do the values come from? Firebase Console β†’ βš™οΈ Project settings β†’ Your apps β†’ SDK setup and configuration. apiKey, appId, messagingSenderId, and projectId are 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.json and GoogleService-Info.plist contain 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

0
likes
150
points
13
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

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.

Topics

#firebase #secrets #security #secret-manager #cloud-functions

License

MIT (license)

Dependencies

cloud_functions, firebase_auth, firebase_core, flutter

More

Packages that depend on secure_key_manager