Secret Management
This document covers credential security practices, secret storage, and rotation recommendations.
Key Files
crates/bf_auth/src/credentials.rs- Credential generationcrates/clerk-auth/src/service_auth.rs- Service auth secrets.env.local- Local development secrets (gitignored)- Cloudflare Dashboard - Production secret management
Overview
Secrets are managed across multiple systems depending on environment and purpose:
┌─────────────────────────────────────────────────────────────────────┐
│ Secret Management Architecture │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Development Environment │ │
│ │ │ │
│ │ .env.local ─────► Local secrets (gitignored) │ │
│ │ .env ───────────► Non-sensitive config (committed) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Production Environment │ │
│ │ │ │
│ │ Cloudflare Secrets Manager │ │
│ │ │ │ │
│ │ ├─► env.secret("SECRET_NAME") ─► Worker access │ │
│ │ │ │ │
│ │ └─► Encrypted at rest, decrypted at runtime │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ Mobile Application │ │
│ │ │ │
│ │ dotenv_codegen! ─► Compile-time embedded │ │
│ │ lynx::store_credential() ─► Native secure storage │ │
│ │ WebView localStorage ─► OS-sandboxed cache (see below) │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
Secret Categories
Authentication Secrets
| Secret | Purpose | Storage |
|---|---|---|
CLERK_SECRET_KEY | Clerk API authentication | Cloudflare Secret |
CLERK_PUBLISHABLE_KEY | Client-side Clerk init | Environment variable |
SERVICE_AUTH_SECRET | Inter-service JWT signing | Cloudflare Secret |
Credential Generation Secrets
| Secret | Purpose | Storage |
|---|---|---|
BF_CREDENTIAL_SEED_PROD | Production credential derivation | Cloudflare Secret |
BF_CREDENTIAL_SEED_STAGING | Staging credential derivation | Cloudflare Secret |
External Service Secrets
| Secret | Purpose | Storage |
|---|---|---|
YALE_API_KEY | Yale API authentication | Cloudflare Secret |
YALE_WEBHOOK_TOKEN | Yale webhook verification | Cloudflare Secret |
FIREBASE_SERVICE_ACCOUNT | FCM push notifications | Cloudflare Secret |
Mobile OTA Secrets
| Secret | Purpose | Storage |
|---|---|---|
BF_MOBILE_OTA_PRIVATE_KEY | OTA bundle signing | Build machine only |
BF_MOBILE_OTA_PUBLIC_KEY | OTA signature verification | Embedded in app |
MOBILE_SERVICE_ROLE_KEY | OTA deployment auth | CI/CD secret |
Cloudflare Secrets Manager
Accessing Secrets in Workers
// crates/bf_auth/src/credentials.rs
pub fn get_credential_seed(
env: &worker::Env,
credential_env: CredentialEnvironment,
) -> Result<[u8; 32], AuthError> {
let secret_name = match credential_env {
CredentialEnvironment::Production => "BF_CREDENTIAL_SEED_PROD",
_ => "BF_CREDENTIAL_SEED_STAGING",
};
let seed_hex = env
.secret(secret_name)
.map_err(|_| AuthError::ConfigError(format!("{} secret not found", secret_name)))?
.to_string();
// Decode and validate
let seed_bytes = hex::decode(seed_hex.trim())?;
// ...
}
Adding Secrets via Wrangler
# Set a secret
wrangler secret put SECRET_NAME
# List secrets (names only, not values)
wrangler secret list
Environment Variable Patterns
Non-Sensitive Configuration
Stored in .env (committed):
# Backend URLs
VITE_BF_RESIDENT_URL=https://api.broadfordlife.com
VITE_BF_AUTH_URL=https://auth.broadfordlife.com
# Clerk publishable key (public)
VITE_CLERK_PUBLISHABLE_KEY=pk_live_...
Sensitive Configuration
Stored in .env.local (gitignored):
# Never commit these
CLERK_SECRET_KEY=sk_live_...
SERVICE_AUTH_SECRET=your-256-bit-secret
BF_CREDENTIAL_SEED_PROD=64-hex-chars
Mobile Build-Time Secrets
Embedded via dotenv_codegen:
// Only public key embedded in app
let public_key = dotenv_codegen::dotenv!("BF_MOBILE_OTA_PUBLIC_KEY");
Credential Derivation
Deterministic Generation
Service credentials derived from seed + inputs:
// crates/bf_auth/src/credentials.rs
pub fn generate_email(
env: CredentialEnvironment,
service: Service,
unique_id: &str,
secret_seed: &[u8; 32], // From Cloudflare Secret
) -> String {
let input = format!("{}:{}:{}", service.name(), env.short_code(), unique_id);
let mut mac = HmacSha256::new_from_slice(secret_seed)?;
mac.update(input.as_bytes());
let hash_hex = hex::encode(&mac.finalize().into_bytes()[..4]);
format!("service+{}.{}.{}@it.broadfordlife.com",
service.short_code(),
env.short_code(),
hash_hex
)
}
Benefits
- Recovery: Lost credentials can be regenerated
- Consistency: Same inputs always produce same output
- Audit: Credential origin is traceable
- No random state: No need to store individual credentials
OAuth Token Storage
Encrypted Storage in bf_auth
Yale and Hive OAuth tokens stored encrypted:
pub struct YaleTokenData {
pub access_token: String, // Encrypted
pub refresh_token: String, // Encrypted
pub token_type: String,
pub expires_at: i64,
}
Token Refresh
DO Alarm API handles automatic refresh:
// Scheduled before expiration
// Exponential backoff on failures
// Max retry interval: 6 hours
Native Secure Storage
iOS Keychain
#[tauri::command]
fn store_credential(key: String, value: String) -> Result<(), String> {
lynx::store_credential(&key, &value) // iOS Keychain
}
Android Keystore
// Hardware-backed storage when available
// Encrypted SharedPreferences fallback
WebView localStorage Cache
Disclaimer — security posture depends on OS sandboxing.
The Tauri shell in
crates/bf_mobile/loads its frontend fromapps/mobile/(seetauri.conf.json→frontendDist). That Svelte frontend caches a non-trivial amount of state in the WebView'slocalStorage, including:
- Leased supplier credentials (
installer/stores/credentials.ts, 24h TTL)- Door PIN codes (
resident/stores/pinCodes.ts)- Guest data, property metadata, devices, documents, energy, notifications (
resident/stores/*.ts, all keyed by property ID)- Push notification tokens (
resident/stores/pushNotifications.ts)- Preferred guest property, selected role, theme, onboarding state
localStorageis not encrypted at the application layer. The data is plaintext JSON in the WebView's per-app storage directory, and the platform uses none of the protections that apply tolynx::store_credential()(iOS Keychain / Android Keystore).Confidentiality of this cache therefore depends entirely on:
- OS-level app sandboxing — iOS Data Protection (file is unreadable while the device is locked, given a passcode is set) and Android per-app data directory permissions (encrypted on modern Android when the device has a lockscreen credential).
- Device integrity — a jailbroken iOS device or rooted Android device can read another app's container, defeating the sandbox.
- No XSS in the WebView — the bundled JS has full access to
localStorage. Note thattauri.conf.jsoncurrently sets"csp": null, so there is no Content Security Policy mitigating an injected-script attack should one occur.- Backups being correctly scoped — items in
localStoragemay be included in iCloud / Google backups depending on platform defaults.Treat any value placed in
localStorageas "protected only by the phone OS". Anything that warrants a stronger guarantee (long-lived secrets, high-value credentials that should survive device compromise, anything outside the per-property data set) must go throughlynx::store_credential()into the native Keychain / Keystore instead.
Secret Rotation Recommendations
High Priority Rotation
| Secret | Rotation Frequency | Notes |
|---|---|---|
| Service Auth Secret | Quarterly | Requires coordinated deployment |
| Clerk Secret Key | On compromise | Via Clerk dashboard |
| Yale/Hive API Keys | Annually | Coordinate with providers |
Credential Seed Rotation
Warning: Rotating credential seeds invalidates ALL generated credentials.
Rotation process:
- Generate new seed
- Update Cloudflare secret
- Regenerate all service credentials
- Update credentials in SmartThings/Yale/Hive
- Update stored credentials in bf_auth
OTA Key Rotation
- Generate new Ed25519 keypair
- Update public key in app (requires app store release)
- Update private key on build machine
- Sign new bundles with new key
- Old app versions won't accept new bundles (intentional)
Git Security
.gitignore Patterns
# Secrets
.env.local
.env.*.local
# Platform-specific secrets
*.keystore
*.p12
*.pem
secrets/
Pre-commit Checks
Consider adding:
- Secret scanning (e.g.,
gitleaks,trufflehog) - Prevent commits containing API keys
Deployment Secret Management
CI/CD Variables
| Platform | Secret Storage | Notes |
|---|---|---|
| GitHub Actions | Repository secrets | Encrypted, masked in logs |
| Cloudflare | Workers secrets | Set via API or dashboard |
Deployment Flow
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ CI/CD │────►│ Wrangler │────►│ Cloudflare │
│ Secrets │ │ Deploy │ │ Workers │
└──────────────┘ └──────────────┘ └──────────────┘
│ │
│ ▼
│ ┌──────────────────┐
│ │ env.secret(name) │
│ │ (Runtime access) │
│ └──────────────────┘
│
▼
┌──────────────────┐
│ Secrets never in │
│ source code or │
│ build artifacts │
└──────────────────┘
Best Practices Summary
- Never commit secrets - Use
.env.localand.gitignore - Use Cloudflare Secrets - For production Workers
- Rotate regularly - Especially after personnel changes
- Minimize scope - Each service only accesses needed secrets
- Audit access - Track who can view/modify secrets
- Use derivation - Prefer HMAC derivation over random secrets
- Encrypt at rest - OAuth tokens stored encrypted
- Short-lived tokens - 5-minute service auth expiration
See Also
- Security Audit - Overall security assessment
- Credential Management - Generation details
- Authentication - Auth implementation
- Service-to-Service Auth - Internal auth