Skip to main content

Secret Management

This document covers credential security practices, secret storage, and rotation recommendations.

Key Files

  • crates/bf_auth/src/credentials.rs - Credential generation
  • crates/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

SecretPurposeStorage
CLERK_SECRET_KEYClerk API authenticationCloudflare Secret
CLERK_PUBLISHABLE_KEYClient-side Clerk initEnvironment variable
SERVICE_AUTH_SECRETInter-service JWT signingCloudflare Secret

Credential Generation Secrets

SecretPurposeStorage
BF_CREDENTIAL_SEED_PRODProduction credential derivationCloudflare Secret
BF_CREDENTIAL_SEED_STAGINGStaging credential derivationCloudflare Secret

External Service Secrets

SecretPurposeStorage
YALE_API_KEYYale API authenticationCloudflare Secret
YALE_WEBHOOK_TOKENYale webhook verificationCloudflare Secret
FIREBASE_SERVICE_ACCOUNTFCM push notificationsCloudflare Secret

Mobile OTA Secrets

SecretPurposeStorage
BF_MOBILE_OTA_PRIVATE_KEYOTA bundle signingBuild machine only
BF_MOBILE_OTA_PUBLIC_KEYOTA signature verificationEmbedded in app
MOBILE_SERVICE_ROLE_KEYOTA deployment authCI/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 from apps/mobile/ (see tauri.conf.jsonfrontendDist). That Svelte frontend caches a non-trivial amount of state in the WebView's localStorage, 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

localStorage is 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 to lynx::store_credential() (iOS Keychain / Android Keystore).

Confidentiality of this cache therefore depends entirely on:

  1. 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).
  2. Device integrity — a jailbroken iOS device or rooted Android device can read another app's container, defeating the sandbox.
  3. No XSS in the WebView — the bundled JS has full access to localStorage. Note that tauri.conf.json currently sets "csp": null, so there is no Content Security Policy mitigating an injected-script attack should one occur.
  4. Backups being correctly scoped — items in localStorage may be included in iCloud / Google backups depending on platform defaults.

Treat any value placed in localStorage as "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 through lynx::store_credential() into the native Keychain / Keystore instead.

Secret Rotation Recommendations

High Priority Rotation

SecretRotation FrequencyNotes
Service Auth SecretQuarterlyRequires coordinated deployment
Clerk Secret KeyOn compromiseVia Clerk dashboard
Yale/Hive API KeysAnnuallyCoordinate with providers

Credential Seed Rotation

Warning: Rotating credential seeds invalidates ALL generated credentials.

Rotation process:

  1. Generate new seed
  2. Update Cloudflare secret
  3. Regenerate all service credentials
  4. Update credentials in SmartThings/Yale/Hive
  5. Update stored credentials in bf_auth

OTA Key Rotation

  1. Generate new Ed25519 keypair
  2. Update public key in app (requires app store release)
  3. Update private key on build machine
  4. Sign new bundles with new key
  5. 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

PlatformSecret StorageNotes
GitHub ActionsRepository secretsEncrypted, masked in logs
CloudflareWorkers secretsSet 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

  1. Never commit secrets - Use .env.local and .gitignore
  2. Use Cloudflare Secrets - For production Workers
  3. Rotate regularly - Especially after personnel changes
  4. Minimize scope - Each service only accesses needed secrets
  5. Audit access - Track who can view/modify secrets
  6. Use derivation - Prefer HMAC derivation over random secrets
  7. Encrypt at rest - OAuth tokens stored encrypted
  8. Short-lived tokens - 5-minute service auth expiration

See Also