use crate::auth::{load_multi_config, save_multi_config, CredentialError};
use crate::config::RelayProfile;
use crate::credstore;
use crate::crypto;
pub struct InstallParams<'a> {
pub user_id: &'a str,
pub device_id: &'a str,
pub token: &'a str,
pub relay_url: &'a str,
pub hostname: &'a str,
pub device_private_key: Option<&'a str>,
pub email: &'a str,
pub identity_provider: &'a str,
}
#[derive(Debug, Clone)]
pub struct InstallOutcome {
pub active_relay_id: String,
pub credential_version: u64,
pub encryption_backend: &'static str,
pub generated_encryption_key: bool,
pub generated_device_private_key: bool,
}
pub fn install_credentials(p: InstallParams<'_>) -> Result<InstallOutcome, CredentialError> {
if p.user_id.is_empty() || p.device_id.is_empty() || p.token.is_empty() {
return Err(CredentialError::BadConfig(
"user_id, device_id, token are required".into(),
));
}
let mut generated_encryption_key = false;
let encryption_backend: &'static str = "plaintext";
if credstore::read_encryption_key(p.user_id).is_none() {
let key = crypto::generate_aes_key();
credstore::write_encryption_key(p.user_id, &key)
.map_err(|e| CredentialError::Io(format!("encryption key: {}", e)))?;
generated_encryption_key = true;
}
let mut generated_device_private_key = false;
let device_priv_b64: String = if let Some(provided) = p.device_private_key {
if !provided.is_empty() {
credstore::write_device_privkey(p.user_id, p.device_id, provided)
.map_err(|e| CredentialError::Io(format!("device key: {}", e)))?;
provided.to_string()
} else {
install_or_reuse_device_privkey(
p.user_id,
p.device_id,
&mut generated_device_private_key,
)?
}
} else {
install_or_reuse_device_privkey(p.user_id, p.device_id, &mut generated_device_private_key)?
};
let mut mc = load_multi_config()?;
let next_version = mc
.relays
.iter()
.map(|r| r.credential_version)
.max()
.unwrap_or(0)
.checked_add(1)
.ok_or_else(|| CredentialError::BadConfig("credential_version overflow".into()))?;
let machine_id = crate::machine::stable_machine_id();
if let Some(profile) = mc.active_profile_mut() {
profile.token = p.token.to_string();
profile.user_id = p.user_id.to_string();
profile.device_id = p.device_id.to_string();
profile.relay_url = p.relay_url.to_string();
profile.hostname = p.hostname.to_string();
profile.device_private_key = device_priv_b64;
profile.machine_id = machine_id;
profile.credential_version = next_version;
if !p.email.is_empty() {
profile.email = p.email.to_string();
}
if !p.identity_provider.is_empty() {
profile.identity_provider = p.identity_provider.to_string();
}
} else {
use ulid::Ulid;
let label = url::Url::parse(p.relay_url)
.ok()
.and_then(|u| u.host_str().map(|h| h.to_string()))
.unwrap_or_else(|| p.relay_url.to_string());
let profile = RelayProfile {
id: Ulid::new().to_string(),
label,
relay_url: p.relay_url.to_string(),
user_id: p.user_id.to_string(),
device_id: p.device_id.to_string(),
hostname: p.hostname.to_string(),
encryption_key: String::new(),
device_private_key: device_priv_b64,
credential_version: next_version,
token: p.token.to_string(),
machine_id,
email: p.email.to_string(),
identity_provider: p.identity_provider.to_string(),
};
let id = profile.id.clone();
mc.relays.push(profile);
mc.active_relay_id = Some(id);
}
let active_relay_id = mc.active_relay_id.clone().unwrap_or_default();
save_multi_config(&mc)?;
Ok(InstallOutcome {
active_relay_id,
credential_version: next_version,
encryption_backend,
generated_encryption_key,
generated_device_private_key,
})
}
#[derive(Debug, thiserror::Error)]
pub enum RequireKeyError {
#[error("encryption key not found for user")]
Missing,
}
pub fn require_encryption_key(user_id: &str) -> Result<[u8; 32], RequireKeyError> {
if user_id.is_empty() {
return Err(RequireKeyError::Missing);
}
credstore::read_encryption_key(user_id).ok_or(RequireKeyError::Missing)
}
fn install_or_reuse_device_privkey(
user_id: &str,
device_id: &str,
generated_flag: &mut bool,
) -> Result<String, CredentialError> {
let existing = load_multi_config()
.ok()
.and_then(|mc| mc.active_profile().cloned())
.filter(|p| {
p.user_id == user_id && p.device_id == device_id && !p.device_private_key.is_empty()
})
.map(|p| p.device_private_key);
if let Some(priv_b64) = existing {
return Ok(priv_b64);
}
let (priv_b64, _pub_b64) = crypto::generate_device_keypair();
credstore::write_device_privkey(user_id, device_id, &priv_b64)
.map_err(|e| CredentialError::Io(format!("device key: {}", e)))?;
*generated_flag = true;
Ok(priv_b64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_empty_required_fields() {
let p = InstallParams {
user_id: "",
device_id: "d1",
token: "tok",
relay_url: "http://localhost:8080",
hostname: "h",
device_private_key: None,
email: "",
identity_provider: "",
};
assert!(install_credentials(p).is_err());
}
#[test]
fn require_encryption_key_errors_when_missing() {
let err = require_encryption_key("test-no-key-x7k9q").unwrap_err();
assert!(matches!(err, RequireKeyError::Missing));
}
#[test]
fn require_encryption_key_errors_on_empty_user_id() {
let err = require_encryption_key("").unwrap_err();
assert!(matches!(err, RequireKeyError::Missing));
}
}