Skip to main content

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 utilities
  • crates/bf_auth/src/handlers/yale_oauth.rs - Yale OAuth flow
  • crates/bf_auth/src/handlers/verification.rs - Email verification code handling
  • crates/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

ServiceEnvironmentUnique IDEmail
SmartThingsstagingproperty-123service+st.staging.a1b2c3d4@it.broadfordlife.com
Yaleproductionproperty-456service+yale.prod.b2c3d4e5@it.broadfordlife.com
Hivestagingproperty-789service+hive.staging.c3d4e5f6@it.broadfordlife.com
Daikinproductionproperty-012service+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

EnvironmentSecret Name
ProductionBF_CREDENTIAL_SEED_PROD
Staging/DevBF_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

  1. Deterministic: Same inputs always produce same credentials
  2. Obfuscated: Hash prevents reverse-engineering of unique_id
  3. Environment-scoped: Different credentials per environment
  4. Service-scoped: Different credentials per service
  5. Seed-protected: Requires secret seed to generate
  6. 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