Credential Management
This document covers deterministic credential generation for external service accounts and OAuth token management.
Key Files
crates/bf_auth/src/credentials.rs- Credential generation utilitiescrates/bf_auth/src/handlers/yale_oauth.rs- Yale OAuth flowcrates/bf_auth/src/handlers/verification.rs- Email verification code handlingcrates/bf_resident_db/src/operations/credentials.rs- Encrypted credential storage
Overview
The system generates deterministic, obfuscated credentials for service accounts:
- SmartThings - Samsung account for SmartThings Enterprise
- Yale - Yale Access account for smart lock control
- Hive - Hive account for thermostat control
- Daikin - Daikin Onecta account for Altherma heat pump control
┌─────────────────────────────────────────────────────────────────────┐
│ Credential Generation Flow │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ IT Admin ─► Initialize IT Setup │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ bf_auth Service │ │
│ │ │ │
│ │ POST /credentials │ │
│ │ { service: "yale", │ │
│ │ unique_id: "..." }│ │
│ └──────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────┐ │
│ │ Deterministic Gen │ │
│ │ │ │
│ │ HMAC-SHA256( │ │
│ │ seed, │ │
│ │ service:env:id │ │
│ │ ) │ │
│ └──────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐│
│ │ Email: service+yale.staging.a1b2c3d4@it.broadfordlife.com││
│ │ Password: Xy7k@L9mN2pQ4rS6 (16 chars, mixed) ││
│ └──────────────────────────────────────────────────────────┘│
│ │
└─────────────────────────────────────────────────────────────────────┘
Email Generation
Format
service+{service_short}.{env_short}.{hash}@it.broadfordlife.com
Examples
| Service | Environment | Unique ID | |
|---|---|---|---|
| SmartThings | staging | property-123 | service+st.staging.a1b2c3d4@it.broadfordlife.com |
| Yale | production | property-456 | service+yale.prod.b2c3d4e5@it.broadfordlife.com |
| Hive | staging | property-789 | service+hive.staging.c3d4e5f6@it.broadfordlife.com |
| Daikin | production | property-012 | service+daikin.prod.d4e5f6g7@it.broadfordlife.com |
Implementation
// crates/bf_auth/src/credentials.rs:126
pub fn generate_email(
env: CredentialEnvironment,
service: Service,
unique_id: &str,
secret_seed: &[u8; 32],
) -> String {
// Create HMAC input: "{service}:{env}:{unique_id}"
let input = format!("{}:{}:{}", service.name(), env.short_code(), unique_id);
// Compute HMAC-SHA256
let mut mac = HmacSha256::new_from_slice(secret_seed)?;
mac.update(input.as_bytes());
let result = mac.finalize();
let hash_bytes = result.into_bytes();
// Take first 4 bytes and convert to hex (8 characters)
let hash_hex = hex::encode(&hash_bytes[..4]);
format!(
"service+{}.{}.{}@{}",
service.short_code(),
env.short_code(),
hash_hex,
EMAIL_DOMAIN
)
}
Service Short Codes
impl Service {
pub fn short_code(&self) -> &'static str {
match self {
Service::SmartThings => "st",
Service::Yale => "yale",
Service::Hive => "hive",
Service::Daikin => "daikin",
}
}
}
Environment Short Codes
impl CredentialEnvironment {
pub fn short_code(&self) -> &'static str {
match self {
CredentialEnvironment::Development => "dev",
CredentialEnvironment::Staging => "staging",
CredentialEnvironment::Production => "prod",
}
}
}
Password Generation
Requirements
Passwords meet all common service requirements:
- 16 characters (exceeds most 8-char minimums)
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
- At least one symbol
Character Sets
const UPPERCASE: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ"; // Excluding I, O
const LOWERCASE: &[u8] = b"abcdefghjkmnpqrstuvwxyz"; // Excluding i, l, o
const DIGITS: &[u8] = b"23456789"; // Excluding 0, 1
const SYMBOLS: &[u8] = b"!@#$%^&*";
const SYMBOLS_HIVE: &[u8] = b"!@$%&?"; // Hive-specific allowed symbols
Implementation
// crates/bf_auth/src/credentials.rs:180
pub fn generate_password_for_service(
email: &str,
secret_seed: &[u8; 32],
service: Option<Service>,
) -> String {
// Select symbol set based on service
let symbols = match service {
Some(Service::Hive) => SYMBOLS_HIVE,
_ => SYMBOLS,
};
// Compute HMAC-SHA256 of the email
let mut mac = HmacSha256::new_from_slice(secret_seed)?;
mac.update(email.as_bytes());
let hash_bytes = mac.finalize().into_bytes();
// Guaranteed characters from each set (positions 0-3)
let mut password = Vec::with_capacity(16);
password.push(UPPERCASE[hash_bytes[0] as usize % UPPERCASE.len()]);
password.push(LOWERCASE[hash_bytes[1] as usize % LOWERCASE.len()]);
password.push(DIGITS[hash_bytes[2] as usize % DIGITS.len()]);
password.push(symbols[hash_bytes[3] as usize % symbols.len()]);
// Fill remaining 12 characters from mixed set
let mixed: Vec<u8> = [UPPERCASE, LOWERCASE, DIGITS, symbols].concat();
for i in 4..16 {
password.push(mixed[hash_bytes[i] as usize % mixed.len()]);
}
// Fisher-Yates shuffle with hash bytes as random source
for i in (1..16).rev() {
let j = hash_bytes[16 + (i % 16)] as usize % (i + 1);
password.swap(i, j);
}
String::from_utf8(password).expect("ASCII only")
}
Secret Seed Management
Environment Variables
| Environment | Secret Name |
|---|---|
| Production | BF_CREDENTIAL_SEED_PROD |
| Staging/Dev | BF_CREDENTIAL_SEED_STAGING |
Format
32-byte value stored as 64-character hex string.
Retrieval
// crates/bf_auth/src/credentials.rs:248
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)?.to_string();
let seed_bytes = hex::decode(seed_hex.trim())?;
if seed_bytes.len() != 32 {
return Err(AuthError::ConfigError("Seed must be 32 bytes"));
}
let mut seed = [0u8; 32];
seed.copy_from_slice(&seed_bytes);
Ok(seed)
}
OAuth Token Storage
OAuth tokens for Yale, Hive, and Daikin are encrypted and stored in bf_auth. Token refresh is service-aware — the bf_auth DO dispatches to the correct refresh handler (yale::refresh_token or daikin::refresh_token) based on the stored service type.
Yale Token Structure
pub struct YaleTokenData {
pub access_token: String,
pub refresh_token: String,
pub token_type: String,
pub expires_at: i64, // Unix timestamp
}
Token Refresh
The DO Alarm API schedules automatic token refresh before expiration:
// Exponential backoff for failed refreshes
const INITIAL_RETRY_INTERVAL_MS: i64 = 5 * 60 * 1000; // 5 minutes
const MAX_RETRY_INTERVAL_MS: i64 = 6 * 60 * 60 * 1000; // 6 hours
fn calculate_refresh_time(expires_at: i64, consecutive_errors: i64) -> i64 {
let now = now_timestamp_millis();
if consecutive_errors > 0 {
// Exponential backoff
let backoff = INITIAL_RETRY_INTERVAL_MS * (2_i64.pow(consecutive_errors as u32 - 1));
let backoff = backoff.min(MAX_RETRY_INTERVAL_MS);
return now + backoff;
}
// Normal refresh: 5 minutes before expiration
let refresh_buffer = 5 * 60 * 1000;
(expires_at - refresh_buffer).max(now + refresh_buffer)
}
Email Verification Code Handling
Verification codes from Samsung/Yale/Hive emails are captured and made available to IT Admins.
Service Detection
// crates/bf_auth/src/credentials.rs:272
pub fn detect_service_from_sender(sender: &str, subject: Option<&str>) -> Option<Service> {
let sender_lower = sender.to_lowercase();
if sender_lower.contains("samsung") {
Some(Service::SmartThings)
} else if sender_lower.contains("yale") {
Some(Service::Yale)
} else if sender_lower.contains("hive-home") || sender_lower.contains("hive") {
Some(Service::Hive)
} else if sender_lower.contains("eu.mailgun.net") {
// Yale emails may be forwarded via Mailgun - check subject
subject.and_then(|s| {
if s.to_lowercase().contains("yale") {
Some(Service::Yale)
} else {
None
}
})
} else {
subject.and_then(|s| {
if s.to_lowercase().contains("hive") {
Some(Service::Hive)
} else {
None
}
})
}
}
Verification Code API
GET /verification/{email}
Returns the most recent verification code for a generated email address.
Credential API Endpoints
Generate Credentials
POST /credentials
pub struct GenerateCredentialsRequest {
pub service: String, // "yale", "smartthings", "hive"
pub unique_id: String, // Usually property_id
pub environment: String, // "staging", "production"
}
pub struct GenerateCredentialsResponse {
pub email: String,
pub password: String,
}
Get Yale OAuth Token
GET /yale/token/{email}
Returns encrypted Yale OAuth token for the specified email.
Store Yale OAuth Token
POST /yale/token/{email}
Stores OAuth token after successful Yale OAuth flow.
Security Properties
- Deterministic: Same inputs always produce same credentials
- Obfuscated: Hash prevents reverse-engineering of unique_id
- Environment-scoped: Different credentials per environment
- Service-scoped: Different credentials per service
- Seed-protected: Requires secret seed to generate
- Encrypted storage: OAuth tokens encrypted at rest
Determinism Benefits
Deterministic generation enables:
- Recovery: Regenerate credentials if lost
- Consistency: Same credential across deployments
- Audit: Traceable credential source
- Simplicity: No random state to manage
See Also
- IT Setup Flow - When credentials are generated
- Service-to-Service Auth - Internal auth
- Secret Management - Secret storage