use base64::Engine;
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
use crate::audit::{self, AuditEvent, EventType, EventResult};
use crate::keystore;
use crate::localhost;
use crate::webauthn;
use crate::protocol::{Request, Response, ResponseData};
use crate::session::SharedSession;
use crate::subprocess::run_with_secrets;
const WA_VERIFY_WINDOW_SECS: u64 = 120;
static SESSION: std::sync::OnceLock<SharedSession> = std::sync::OnceLock::new();
pub fn init_session(session: SharedSession) {
SESSION.set(session).expect("Session already initialized");
}
fn get_session() -> &'static SharedSession {
SESSION.get().expect("Session not initialized")
}
pub async fn handle_request_string(json: &str) -> String {
let response = handle_request(json).await;
serde_json::to_string(&response).unwrap_or_else(|_| {
r#"{"success":false,"error":"Serialization failed"}"#.to_string()
})
}
pub async fn handle_connection<S>(stream: S)
where
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
{
let (reader, mut writer) = tokio::io::split(stream);
let mut reader = BufReader::new(reader);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => break, Ok(_) => {
let response = handle_request(&line).await;
let json = serde_json::to_string(&response).unwrap_or_else(|_| {
r#"{"success":false,"error":"Serialization failed"}"#.to_string()
});
if let Err(e) = writer.write_all(format!("{}\n", json).as_bytes()).await {
tracing::error!("Write error: {}", e);
break;
}
}
Err(e) => {
tracing::error!("Read error: {}", e);
break;
}
}
}
}
async fn handle_request(line: &str) -> Response {
let request: Request = match serde_json::from_str(line) {
Ok(r) => r,
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::InvalidRequest, EventResult::Failure)
.with_error(&e.to_string())
);
return Response::error(format!("Invalid request: {}", e));
}
};
match request {
Request::Store { token, secrets, ttl } => handle_store(token, secrets, ttl).await,
Request::Clear => handle_clear().await,
Request::Status => handle_status().await,
Request::List => handle_list().await,
Request::Run { command, working_dir, scope } => handle_run(command, working_dir, scope).await,
Request::Reveal { name } => handle_reveal(name).await,
Request::RevealConfirm { challenge, code } => handle_reveal_confirm(challenge, code).await,
Request::RevealAll => handle_reveal_all().await,
Request::RevealAllConfirm { challenge, code } => handle_reveal_all_confirm(challenge, code).await,
Request::AddSecrets { secrets } => handle_add_secrets(secrets).await,
Request::UnlockWebauthn { ttl } => handle_unlock_webauthn(ttl).await,
Request::SetupWebauthn => handle_setup_webauthn_init().await,
Request::SetupWebauthnComplete { encrypted_payload, wrapping_key, prf_salt_b64 } =>
handle_setup_webauthn_complete(encrypted_payload, wrapping_key, prf_salt_b64).await,
Request::UnlockWebauthnComplete { encrypted_payload, wrapping_key, ttl } =>
handle_unlock_webauthn_complete(encrypted_payload, wrapping_key, ttl).await,
Request::Extend { ttl } => handle_extend(ttl).await,
Request::BackupKey => handle_backup_key().await,
Request::InitializeKeysWebauthn => handle_initialize_keys_webauthn().await,
Request::CheckWaState => handle_check_wa_state().await,
Request::DisableWa => handle_disable_wa().await,
Request::EnableWa => handle_enable_wa().await,
Request::DisableWaUnlock => handle_disable_wa_unlock().await,
Request::EnableWaUnlock => handle_enable_wa_unlock().await,
Request::UnlockLocal { ttl } => handle_unlock_local(ttl).await,
Request::UnlockLocalComplete { ttl } => handle_unlock_local_complete(ttl).await,
Request::SetupLocal => handle_setup_local().await,
Request::SetupLocalComplete => handle_setup_local_complete().await,
Request::RotateVault => handle_rotate_vault().await,
Request::RegisterEncrypted { path, folder_name, file_count, archive_size } =>
handle_register_encrypted(path, folder_name, file_count, archive_size).await,
Request::UnregisterEncrypted { id } => handle_unregister_encrypted(id).await,
Request::MarkDecrypted { path } => handle_mark_decrypted(path).await,
Request::ListEncrypted => handle_list_encrypted().await,
Request::CleanupEncrypted { remove_missing } => handle_cleanup_encrypted(remove_missing).await,
}
}
async fn handle_store(
token_b64: String,
secrets: std::collections::HashMap<String, String>,
ttl: u64,
) -> Response {
let token = match base64::engine::general_purpose::STANDARD.decode(&token_b64) {
Ok(t) => t,
Err(_) => return Response::error("Invalid base64 token"),
};
let mut session = get_session().write().await;
let saved_master_key = session.master_key().map(|k| k.to_string());
match session.store(token, secrets, ttl) {
Ok(()) => {
if let Some(key) = saved_master_key {
session.set_master_key(key);
}
if let (Some(all_secrets), Some(master_key)) = (session.secrets(), session.master_key()) {
if let Err(e) = keystore::save_encrypted_env(all_secrets, master_key) {
tracing::error!("Failed to persist secrets to disk: {}", e);
}
}
Response::ok()
}
Err(e) => Response::error(e),
}
}
async fn handle_add_secrets(
secrets: std::collections::HashMap<String, String>,
) -> Response {
let count = secrets.len();
let mut session = get_session().write().await;
match session.add_secrets(secrets) {
Ok(added) => {
if let (Some(all_secrets), Some(master_key)) = (session.secrets(), session.master_key()) {
if let Err(e) = keystore::save_encrypted_env(all_secrets, master_key) {
tracing::error!("Failed to persist secrets to disk: {}", e);
}
}
audit::log_event(
AuditEvent::new(EventType::SecretsAdded, EventResult::Success)
.with_secret_count(added)
);
Response::ok_with_data(ResponseData::Unlocked { count })
}
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::SecretsAdded, EventResult::Failure)
.with_error(&e)
);
Response::error(e)
}
}
}
async fn handle_clear() -> Response {
let mut session = get_session().write().await;
session.clear();
audit::log_simple(EventType::SessionEnd, EventResult::Success);
Response::ok()
}
async fn handle_status() -> Response {
let session = get_session().read().await;
Response::ok_with_data(ResponseData::Status {
active: session.is_active(),
remaining: session.remaining_secs(),
})
}
async fn handle_list() -> Response {
let session = get_session().read().await;
match session.secret_names() {
Some(names) => {
audit::log_event(
AuditEvent::new(EventType::SecretList, EventResult::Success)
.with_secret_count(names.len())
);
Response::ok_with_data(ResponseData::List { names })
}
None => {
audit::log_event(
AuditEvent::new(EventType::SecretList, EventResult::Failure)
.with_error("No active session")
);
Response::error("No active session")
}
}
}
async fn handle_run(command: String, working_dir: Option<String>, scope: Option<Vec<String>>) -> Response {
let session = get_session().read().await;
let all_secrets = match session.secrets() {
Some(s) => s.clone(),
None => {
audit::log_event(
AuditEvent::new(EventType::CommandRun, EventResult::Failure)
.with_command(&command)
.with_error("No active session")
);
return Response::error("No active session - authenticate first");
}
};
drop(session);
let secrets = if let Some(ref allowed_names) = scope {
let mut filtered = std::collections::HashMap::new();
for name in allowed_names {
if let Some(value) = all_secrets.get(name) {
filtered.insert(name.clone(), value.clone());
}
}
filtered
} else {
all_secrets
};
let secret_count = secrets.len();
match run_with_secrets(&command, working_dir.as_deref(), &secrets).await {
Ok(result) => {
audit::log_event(
AuditEvent::new(EventType::CommandRun, EventResult::Success)
.with_command(&command)
.with_secret_count(secret_count)
.with_exit_code(result.exit_code)
);
Response::ok_with_data(ResponseData::Run {
exit_code: result.exit_code,
output: result.output,
})
}
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::CommandError, EventResult::Failure)
.with_command(&command)
.with_error(&e)
);
Response::error(e)
}
}
}
async fn handle_reveal(name: String) -> Response {
let mut session = get_session().write().await;
if !keystore::is_dev_mode() && !session.consume_wa_verification(WA_VERIFY_WINDOW_SECS) {
audit::log_event(
AuditEvent::new(EventType::RevealChallengeIssued, EventResult::Failure)
.with_secret_name(&name)
.with_error("WebAuthn verification required")
);
return Response::error("WebAuthn verification required for reveal operations");
}
match session.create_challenge(&name) {
Ok((nonce, display_code)) => {
audit::log_event(
AuditEvent::new(EventType::RevealChallengeIssued, EventResult::Pending)
.with_secret_name(&name)
);
Response::ok_with_data(ResponseData::Challenge {
challenge: nonce,
prompt: format!("Reveal secret: {}", name),
code: display_code,
})
}
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::RevealChallengeFailed, EventResult::Failure)
.with_secret_name(&name)
.with_error(&e)
);
Response::error(e)
}
}
}
async fn handle_reveal_confirm(challenge: String, code: String) -> Response {
let mut session = get_session().write().await;
match session.validate_challenge(&challenge, &code) {
Ok(value) => {
audit::log_event(
AuditEvent::new(EventType::RevealChallengeCompleted, EventResult::Success)
);
Response::ok_with_data(ResponseData::Reveal { value })
}
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::RevealChallengeFailed, EventResult::Failure)
.with_error(&e)
);
Response::error(e)
}
}
}
async fn handle_reveal_all() -> Response {
let mut session = get_session().write().await;
if !keystore::is_dev_mode() && !session.consume_wa_verification(WA_VERIFY_WINDOW_SECS) {
audit::log_event(
AuditEvent::new(EventType::BulkRevealChallengeIssued, EventResult::Failure)
.with_error("WebAuthn verification required")
);
return Response::error("WebAuthn verification required for reveal operations");
}
match session.create_bulk_challenge() {
Ok((nonce, display_code)) => {
audit::log_event(
AuditEvent::new(EventType::BulkRevealChallengeIssued, EventResult::Pending)
);
Response::ok_with_data(ResponseData::Challenge {
challenge: nonce,
prompt: "Reveal all secrets".to_string(),
code: display_code,
})
}
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::RevealChallengeFailed, EventResult::Failure)
.with_error(&e)
);
Response::error(e)
}
}
}
async fn handle_reveal_all_confirm(challenge: String, code: String) -> Response {
let mut session = get_session().write().await;
match session.validate_bulk_challenge(&challenge, &code) {
Ok(secrets) => {
let count = secrets.len();
audit::log_event(
AuditEvent::new(EventType::BulkRevealChallengeCompleted, EventResult::Success)
.with_secret_count(count)
);
Response::ok_with_data(ResponseData::RevealAll { secrets })
}
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::RevealChallengeFailed, EventResult::Failure)
.with_error(&e)
);
Response::error(e)
}
}
}
async fn handle_unlock_webauthn(_ttl: Option<u64>) -> Response {
audit::log_simple(EventType::AuthAttempt, EventResult::Pending);
if !webauthn::is_wa_configured() {
audit::log_event(
AuditEvent::new(EventType::AuthFailure, EventResult::Failure)
.with_error("WebAuthn not configured — run setup first")
);
return Response::error("WebAuthn not configured. Run 'scrt4 setup' first.");
}
let credential = match webauthn::load_credential() {
Ok(c) => c,
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::AuthFailure, EventResult::Failure)
.with_error(&e)
);
return Response::error(format!("Failed to load credential: {}", e));
}
};
let prf_salt = match keystore::load_prf_salt() {
Ok(s) => s,
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::AuthFailure, EventResult::Failure)
.with_error(&e)
);
return Response::error(format!("Failed to load PRF salt: {}", e));
}
};
let params = match webauthn::generate_auth_params(&credential, &prf_salt) {
Ok(p) => p,
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::AuthFailure, EventResult::Failure)
.with_error(&e)
);
return Response::error(format!("Failed to generate auth params: {}", e));
}
};
tracing::info!("Unlock phase 1: relay URL generated");
let qr = webauthn::render_qr_string(¶ms.url);
Response::ok_with_data(ResponseData::RelaySetup {
url: params.url,
session_id: params.session_id,
wrapping_key: params.wrapping_key,
prf_salt_b64: params.prf_salt_b64,
qr,
})
}
async fn handle_extend(ttl: Option<u64>) -> Response {
let mut session = get_session().write().await;
match session.extend(ttl) {
Ok(remaining) => {
audit::log_event(
AuditEvent::new(EventType::SessionExtend, EventResult::Success)
.with_ttl(ttl.unwrap_or(0))
);
Response::ok_with_data(ResponseData::Extended { remaining })
}
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::SessionExtend, EventResult::Failure)
.with_error(&e)
);
Response::error(e)
}
}
}
async fn handle_check_wa_state() -> Response {
let configured = webauthn::is_wa_configured();
let enabled = webauthn::is_wa_enabled();
let unlock_enabled = webauthn::is_wa_unlock_enabled();
Response::ok_with_data(ResponseData::WaState { configured, enabled, unlock_enabled })
}
async fn handle_setup_webauthn_init() -> Response {
let params = match webauthn::generate_register_params() {
Ok(p) => p,
Err(e) => return Response::error(format!("Failed to generate setup params: {}", e)),
};
tracing::info!("Setup phase 1: relay URL generated");
let qr = webauthn::render_qr_string(¶ms.url);
Response::ok_with_data(ResponseData::RelaySetup {
url: params.url,
session_id: params.session_id,
wrapping_key: params.wrapping_key,
prf_salt_b64: params.prf_salt_b64,
qr,
})
}
async fn handle_setup_webauthn_complete(
encrypted_payload: String,
wrapping_key: String,
prf_salt_b64: String,
) -> Response {
audit::log_event(
AuditEvent::new(EventType::KeysInitialized, EventResult::Pending)
);
let engine = base64::engine::general_purpose::STANDARD;
let reg_result = match webauthn::complete_registration(&encrypted_payload, &wrapping_key) {
Ok(r) => r,
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::KeysInitialized, EventResult::Failure)
.with_error(&e)
);
return Response::error(format!("WebAuthn registration failed: {}", e));
}
};
let prf_salt_bytes = match engine.decode(&prf_salt_b64) {
Ok(b) if b.len() == 32 => {
let mut arr = [0u8; 32];
arr.copy_from_slice(&b);
arr
}
_ => {
return Response::error("Invalid PRF salt");
}
};
if let Err(e) = webauthn::save_credential(®_result.credential) {
audit::log_event(
AuditEvent::new(EventType::KeysInitialized, EventResult::Failure)
.with_error(&e)
);
return Response::error(format!("Failed to save credential: {}", e));
}
let master_key = match keystore::generate_new_master_key() {
Ok(k) => k,
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::KeysInitialized, EventResult::Failure)
.with_error(&e)
);
return Response::error(format!("Key generation failed: {}", e));
}
};
tracing::info!("Setup: PRF output received ({} bytes)", reg_result.prf_output.len());
tracing::info!("Setup: master key generated ({} chars, base64)", master_key.len());
if let Err(e) = keystore::save_master_key_webauthn(
&master_key,
®_result.prf_output,
&prf_salt_bytes,
Some(®_result.credential.credential_id),
) {
audit::log_event(
AuditEvent::new(EventType::KeysInitialized, EventResult::Failure)
.with_error(&e)
);
return Response::error(format!("Failed to save master key: {}", e));
}
if let Err(e) = keystore::reset_encrypted_env(&master_key) {
audit::log_event(
AuditEvent::new(EventType::KeysInitialized, EventResult::Failure)
.with_error(&e)
);
return Response::error(format!("Failed to initialize secret store: {}", e));
}
{
let mut session = get_session().write().await;
session.clear();
tracing::info!("Cleared old session after setup (prevents stale master key usage)");
}
audit::log_event(
AuditEvent::new(EventType::KeysInitialized, EventResult::Success)
);
Response::ok()
}
async fn handle_unlock_webauthn_complete(
encrypted_payload: String,
wrapping_key: String,
ttl: Option<u64>,
) -> Response {
let auth_result = match webauthn::complete_authentication(&encrypted_payload, &wrapping_key) {
Ok(r) => r,
Err(e) => return Response::error(format!("WebAuthn auth failed: {}", e)),
};
tracing::info!("PRF output received ({} bytes)", auth_result.prf_output.len());
let master_key = match keystore::load_master_key_webauthn(&auth_result.prf_output) {
Ok(k) => {
tracing::info!("Master key unwrapped OK ({} chars, base64)", k.len());
k
}
Err(e) => return Response::error(format!("Failed to decrypt master key: {}", e)),
};
{
let vault_path = keystore::secrets_path();
if let Ok(contents) = std::fs::read_to_string(&vault_path) {
tracing::info!("Vault file ({} bytes): {}", contents.len(),
&contents[..contents.len().min(80)]);
} else {
tracing::warn!("Cannot read vault file at {:?}", vault_path);
}
}
let secrets = match keystore::decrypt_secrets(&master_key) {
Ok(s) => s,
Err(e) => {
tracing::error!("Vault decryption failed with master key len={}: {}", master_key.len(), e);
return Response::error(format!("Failed to load secrets: {}", e));
}
};
let count = secrets.len();
let ttl_secs = ttl.unwrap_or(7200);
let token: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
let mut session = get_session().write().await;
match session.store(token, secrets, ttl_secs) {
Ok(()) => {
session.set_master_key(master_key);
session.set_wa_verified();
audit::log_event(
AuditEvent::new(EventType::AuthSuccess, EventResult::Success)
);
audit::log_event(
AuditEvent::new(EventType::SessionStart, EventResult::Success)
.with_ttl(ttl_secs)
.with_secret_count(count)
);
Response::ok_with_data(ResponseData::Unlocked { count })
}
Err(e) => Response::error(e),
}
}
async fn handle_backup_key() -> Response {
let mut session = get_session().write().await;
if !keystore::is_dev_mode() && !session.consume_wa_verification(WA_VERIFY_WINDOW_SECS) {
audit::log_event(
AuditEvent::new(EventType::BackupKeyRequested, EventResult::Failure)
.with_error("WebAuthn verification required")
);
return Response::error("WebAuthn verification required for backup key access");
}
match session.master_key() {
Some(key) => {
audit::log_event(
AuditEvent::new(EventType::BackupKeyRequested, EventResult::Success)
);
Response::ok_with_data(ResponseData::BackupKey { key: key.to_string() })
}
None => {
audit::log_event(
AuditEvent::new(EventType::BackupKeyRequested, EventResult::Failure)
.with_error("No active session or master key not available")
);
Response::error("No active session. Run 'scrt4 unlock' first to access the master key.")
}
}
}
async fn handle_initialize_keys_webauthn() -> Response {
handle_setup_webauthn_init().await
}
async fn handle_disable_wa() -> Response {
if !keystore::is_dev_mode() {
let mut session = get_session().write().await;
if !session.is_active() {
return Response::error("Active session required to change WebAuthn settings");
}
if !session.consume_wa_verification(WA_VERIFY_WINDOW_SECS) {
audit::log_event(
AuditEvent::new(EventType::WaDisabled, EventResult::Failure)
.with_error("WebAuthn verification required")
);
return Response::error("WebAuthn verification required to disable WebAuthn 2FA");
}
}
match webauthn::set_wa_state(false) {
Ok(()) => {
audit::log_event(
AuditEvent::new(EventType::WaDisabled, EventResult::Success)
);
Response::ok()
}
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::WaDisabled, EventResult::Failure)
.with_error(&e)
);
Response::error(e)
}
}
}
async fn handle_enable_wa() -> Response {
match webauthn::set_wa_state(true) {
Ok(()) => {
audit::log_event(
AuditEvent::new(EventType::WaEnabled, EventResult::Success)
);
Response::ok()
}
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::WaEnabled, EventResult::Failure)
.with_error(&e)
);
Response::error(e)
}
}
}
async fn handle_disable_wa_unlock() -> Response {
if !keystore::is_dev_mode() {
let mut session = get_session().write().await;
if !session.is_active() {
return Response::error("Active session required to change WebAuthn settings");
}
if !session.consume_wa_verification(WA_VERIFY_WINDOW_SECS) {
audit::log_event(
AuditEvent::new(EventType::WaUnlockDisabled, EventResult::Failure)
.with_error("WebAuthn verification required")
);
return Response::error("WebAuthn verification required to disable WebAuthn unlock");
}
}
match webauthn::set_wa_unlock_state(false) {
Ok(()) => {
audit::log_event(
AuditEvent::new(EventType::WaUnlockDisabled, EventResult::Success)
);
Response::ok()
}
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::WaUnlockDisabled, EventResult::Failure)
.with_error(&e)
);
Response::error(e)
}
}
}
async fn handle_enable_wa_unlock() -> Response {
match webauthn::set_wa_unlock_state(true) {
Ok(()) => {
audit::log_event(
AuditEvent::new(EventType::WaUnlockEnabled, EventResult::Success)
);
Response::ok()
}
Err(e) => {
audit::log_event(
AuditEvent::new(EventType::WaUnlockEnabled, EventResult::Failure)
.with_error(&e)
);
Response::error(e)
}
}
}
async fn handle_unlock_local(_ttl: Option<u64>) -> Response {
audit::log_simple(EventType::AuthAttempt, EventResult::Pending);
if !webauthn::is_local_configured() {
return Response::error("No localhost credential. Run 'scrt4 setup --local' after unlocking via phone.");
}
let credential = match webauthn::load_local_credential() {
Ok(c) => c,
Err(e) => return Response::error(format!("Failed to load localhost credential: {}", e)),
};
let prf_salt = match keystore::load_prf_salt_local() {
Ok(s) => s,
Err(e) => return Response::error(format!("Failed to load local PRF salt: {}", e)),
};
let engine = base64::engine::general_purpose::STANDARD;
let salt_b64 = base64::Engine::encode(&engine, &prf_salt);
let wrapping_key = webauthn::generate_hex_public(32);
let mut challenge_bytes = [0u8; 32];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut challenge_bytes);
let challenge_b64 = base64::Engine::encode(&engine, challenge_bytes);
let server = match localhost::start(
"auth",
&challenge_b64,
&salt_b64,
&wrapping_key,
Some(&credential.credential_id),
).await {
Ok(s) => s,
Err(e) => return Response::error(format!("Failed to start local server: {}", e)),
};
let url = server.url.clone();
localhost::set_pending(localhost::PendingLocalAuth {
server,
wrapping_key,
prf_salt_b64: salt_b64,
}).await;
tracing::info!("Unlock local phase 1: server started at {}", url);
Response::ok_with_data(ResponseData::LocalUrl { url })
}
async fn handle_unlock_local_complete(ttl: Option<u64>) -> Response {
let mut pending = match localhost::take_pending().await {
Some(p) => p,
None => return Response::error("No pending local auth — call unlock_local first"),
};
let encrypted_payload = match pending.server.wait_for_callback(120).await {
Ok(p) => p,
Err(e) => {
pending.server.shutdown();
return Response::error(e);
}
};
pending.server.shutdown();
let auth_result = match webauthn::complete_authentication(&encrypted_payload, &pending.wrapping_key) {
Ok(r) => r,
Err(e) => return Response::error(format!("WebAuthn auth failed: {}", e)),
};
tracing::info!("Local auth: PRF output received ({} bytes)", auth_result.prf_output.len());
let master_key = match keystore::load_master_key_local(&auth_result.prf_output) {
Ok(k) => k,
Err(e) => return Response::error(format!("Failed to decrypt master key: {}", e)),
};
let secrets = match keystore::decrypt_secrets(&master_key) {
Ok(s) => s,
Err(e) => return Response::error(format!("Failed to load secrets: {}", e)),
};
let count = secrets.len();
let ttl_secs = ttl.unwrap_or(7200);
let token: Vec<u8> = (0..32).map(|_| rand::random::<u8>()).collect();
let mut session = get_session().write().await;
match session.store(token, secrets, ttl_secs) {
Ok(()) => {
session.set_master_key(master_key);
session.set_wa_verified();
audit::log_event(AuditEvent::new(EventType::AuthSuccess, EventResult::Success));
audit::log_event(
AuditEvent::new(EventType::SessionStart, EventResult::Success)
.with_ttl(ttl_secs)
.with_secret_count(count)
);
Response::ok_with_data(ResponseData::Unlocked { count })
}
Err(e) => Response::error(e),
}
}
async fn handle_setup_local() -> Response {
{
let session = get_session().read().await;
if !session.is_active() {
return Response::error(
"Session not active. Unlock via phone first ('scrt4 unlock --remote'), then run 'scrt4 setup --local'."
);
}
if session.master_key().is_none() {
return Response::error("No master key in session. Unlock via phone first.");
}
}
let engine = base64::engine::general_purpose::STANDARD;
let prf_salt = webauthn::generate_prf_salt();
let prf_salt_b64 = base64::Engine::encode(&engine, &prf_salt);
let wrapping_key = webauthn::generate_hex_public(32);
let mut challenge_bytes = [0u8; 32];
rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut challenge_bytes);
let challenge_b64 = base64::Engine::encode(&engine, &challenge_bytes);
let server = match localhost::start(
"register",
&challenge_b64,
&prf_salt_b64,
&wrapping_key,
None,
).await {
Ok(s) => s,
Err(e) => return Response::error(format!("Failed to start local server: {}", e)),
};
let url = server.url.clone();
localhost::set_pending(localhost::PendingLocalAuth {
server,
wrapping_key,
prf_salt_b64,
}).await;
tracing::info!("Setup local phase 1: server started at {}", url);
Response::ok_with_data(ResponseData::LocalUrl { url })
}
async fn handle_setup_local_complete() -> Response {
let master_key = {
let session = get_session().read().await;
match session.master_key() {
Some(k) => k.to_string(),
None => return Response::error("No master key in session — unlock via phone first"),
}
};
let mut pending = match localhost::take_pending().await {
Some(p) => p,
None => return Response::error("No pending local auth — call setup_local first"),
};
let encrypted_payload = match pending.server.wait_for_callback(120).await {
Ok(p) => p,
Err(e) => {
pending.server.shutdown();
return Response::error(e);
}
};
pending.server.shutdown();
let engine = base64::engine::general_purpose::STANDARD;
let reg_result = match webauthn::complete_registration_local(&encrypted_payload, &pending.wrapping_key) {
Ok(r) => r,
Err(e) => return Response::error(format!("WebAuthn registration failed: {}", e)),
};
let prf_salt_bytes = match base64::Engine::decode(&engine, &pending.prf_salt_b64) {
Ok(b) if b.len() == 32 => {
let mut arr = [0u8; 32];
arr.copy_from_slice(&b);
arr
}
_ => return Response::error("Invalid PRF salt"),
};
if let Err(e) = webauthn::save_local_credential(®_result.credential) {
return Response::error(format!("Failed to save localhost credential: {}", e));
}
tracing::info!("Setup local: wrapping existing master key with localhost PRF");
if let Err(e) = keystore::save_master_key_local(
&master_key,
®_result.prf_output,
&prf_salt_bytes,
Some(®_result.credential.credential_id),
) {
return Response::error(format!("Failed to save local master key: {}", e));
}
audit::log_event(AuditEvent::new(EventType::KeysInitialized, EventResult::Success));
tracing::info!("Localhost credential registered, master key wrapped");
Response::ok()
}
async fn handle_rotate_vault() -> Response {
let mut session = get_session().write().await;
if !session.is_active() {
return Response::error("No active session");
}
if !keystore::is_dev_mode() && !session.consume_wa_verification(WA_VERIFY_WINDOW_SECS) {
return Response::error(
"WebAuthn step-up required before vault rotation — unlock again first",
);
}
let current_secrets = match session.secrets() {
Some(s) => s.clone(),
None => return Response::error("No secrets in session"),
};
let secret_count = current_secrets.len();
let new_master_key = match keystore::generate_new_master_key() {
Ok(k) => k,
Err(e) => return Response::error(format!("Failed to generate new master key: {}", e)),
};
if let Err(e) = keystore::save_encrypted_env(¤t_secrets, &new_master_key) {
return Response::error(format!("Failed to write rotated vault: {}", e));
}
session.set_master_key(new_master_key.clone());
drop(session);
let wrapper_stale = !keystore::is_dev_mode();
audit::log_event(
AuditEvent::new(EventType::SecretsAdded, EventResult::Success)
.with_error(&format!(
"VaultRotated secret_count={} wrapper_stale={}",
secret_count, wrapper_stale
))
);
tracing::warn!(
"Vault rotated: secret_count={} wrapper_stale={}",
secret_count,
wrapper_stale
);
Response::ok_with_data(ResponseData::VaultRotated {
new_master_key_b64: new_master_key,
secret_count,
wrapper_stale,
})
}
async fn handle_register_encrypted(
path: String,
folder_name: String,
file_count: u32,
archive_size: u64,
) -> Response {
let session = get_session().read().await;
if !session.is_active() {
return Response::error("No active session");
}
drop(session);
match crate::encrypted_inventory::register(&path, &folder_name, file_count, archive_size) {
Ok(entry) => Response::ok_with_data(ResponseData::EncryptedRegistered {
id: entry.id,
path: entry.path,
}),
Err(e) => Response::error(format!("register_encrypted: {}", e)),
}
}
async fn handle_unregister_encrypted(id: String) -> Response {
let session = get_session().read().await;
if !session.is_active() {
return Response::error("No active session");
}
drop(session);
match crate::encrypted_inventory::unregister(&id) {
Ok(removed) => Response::ok_with_data(ResponseData::EncryptedUnregistered { removed, id }),
Err(e) => Response::error(format!("unregister_encrypted: {}", e)),
}
}
async fn handle_mark_decrypted(path: String) -> Response {
let session = get_session().read().await;
if !session.is_active() {
return Response::error("No active session");
}
drop(session);
match crate::encrypted_inventory::mark_decrypted(&path) {
Ok(()) => Response::ok_with_data(ResponseData::EncryptedMarkedDecrypted { path }),
Err(e) => Response::error(format!("mark_decrypted: {}", e)),
}
}
async fn handle_list_encrypted() -> Response {
let session = get_session().read().await;
if !session.is_active() {
return Response::error("No active session");
}
drop(session);
let rows = crate::encrypted_inventory::list_with_existence();
let entries: Vec<serde_json::Value> = rows
.into_iter()
.map(|(entry, exists)| {
serde_json::json!({
"id": entry.id,
"path": entry.path,
"folder_name": entry.folder_name,
"file_count": entry.file_count,
"archive_size": entry.archive_size,
"created_at": entry.created_at,
"last_decrypted_at": entry.last_decrypted_at,
"exists": exists,
})
})
.collect();
Response::ok_with_data(ResponseData::EncryptedList { entries })
}
async fn handle_cleanup_encrypted(remove_missing: bool) -> Response {
let session = get_session().read().await;
if !session.is_active() {
return Response::error("No active session");
}
drop(session);
match crate::encrypted_inventory::cleanup(remove_missing) {
Ok(summary) => Response::ok_with_data(ResponseData::EncryptedCleanup {
present_count: summary.present_count,
missing_count: summary.missing_count,
removed_count: summary.removed_count,
missing_paths: summary.missing_paths,
}),
Err(e) => Response::error(format!("cleanup_encrypted: {}", e)),
}
}