client_core/
auth_session.rs1use crate::auth::{load_multi_config, save_multi_config, CredentialError};
22use crate::config::RelayProfile;
23use crate::credstore;
24use crate::crypto;
25
26pub struct InstallParams<'a> {
29 pub user_id: &'a str,
30 pub device_id: &'a str,
31 pub token: &'a str,
32 pub relay_url: &'a str,
33 pub hostname: &'a str,
34 pub device_private_key: Option<&'a str>,
38 pub email: &'a str,
40 pub identity_provider: &'a str,
42}
43
44#[derive(Debug, Clone)]
48pub struct InstallOutcome {
49 pub active_relay_id: String,
51 pub credential_version: u64,
53 pub encryption_backend: &'static str,
55 pub generated_encryption_key: bool,
57 pub generated_device_private_key: bool,
59}
60
61pub fn install_credentials(p: InstallParams<'_>) -> Result<InstallOutcome, CredentialError> {
70 if p.user_id.is_empty() || p.device_id.is_empty() || p.token.is_empty() {
71 return Err(CredentialError::BadConfig(
72 "user_id, device_id, token are required".into(),
73 ));
74 }
75
76 let mut generated_encryption_key = false;
78 let encryption_backend: &'static str = "plaintext";
79 if credstore::read_encryption_key(p.user_id).is_none() {
80 let key = crypto::generate_aes_key();
81 credstore::write_encryption_key(p.user_id, &key)
82 .map_err(|e| CredentialError::Io(format!("encryption key: {}", e)))?;
83 generated_encryption_key = true;
84 }
85
86 let mut generated_device_private_key = false;
88 let device_priv_b64: String = if let Some(provided) = p.device_private_key {
89 if !provided.is_empty() {
90 credstore::write_device_privkey(p.user_id, p.device_id, provided)
91 .map_err(|e| CredentialError::Io(format!("device key: {}", e)))?;
92 provided.to_string()
93 } else {
94 install_or_reuse_device_privkey(
95 p.user_id,
96 p.device_id,
97 &mut generated_device_private_key,
98 )?
99 }
100 } else {
101 install_or_reuse_device_privkey(p.user_id, p.device_id, &mut generated_device_private_key)?
102 };
103
104 let mut mc = load_multi_config()?;
107 let next_version = mc
108 .relays
109 .iter()
110 .map(|r| r.credential_version)
111 .max()
112 .unwrap_or(0)
113 .checked_add(1)
114 .ok_or_else(|| CredentialError::BadConfig("credential_version overflow".into()))?;
115
116 let machine_id = crate::machine::stable_machine_id();
117
118 if let Some(profile) = mc.active_profile_mut() {
119 profile.token = p.token.to_string();
120 profile.user_id = p.user_id.to_string();
121 profile.device_id = p.device_id.to_string();
122 profile.relay_url = p.relay_url.to_string();
123 profile.hostname = p.hostname.to_string();
124 profile.device_private_key = device_priv_b64;
125 profile.machine_id = machine_id;
126 profile.credential_version = next_version;
127 if !p.email.is_empty() {
128 profile.email = p.email.to_string();
129 }
130 if !p.identity_provider.is_empty() {
131 profile.identity_provider = p.identity_provider.to_string();
132 }
133 } else {
134 use ulid::Ulid;
135 let label = url::Url::parse(p.relay_url)
136 .ok()
137 .and_then(|u| u.host_str().map(|h| h.to_string()))
138 .unwrap_or_else(|| p.relay_url.to_string());
139 let profile = RelayProfile {
140 id: Ulid::new().to_string(),
141 label,
142 relay_url: p.relay_url.to_string(),
143 user_id: p.user_id.to_string(),
144 device_id: p.device_id.to_string(),
145 hostname: p.hostname.to_string(),
146 encryption_key: String::new(),
147 device_private_key: device_priv_b64,
148 credential_version: next_version,
149 token: p.token.to_string(),
150 machine_id,
151 email: p.email.to_string(),
152 identity_provider: p.identity_provider.to_string(),
153 };
154 let id = profile.id.clone();
155 mc.relays.push(profile);
156 mc.active_relay_id = Some(id);
157 }
158
159 let active_relay_id = mc.active_relay_id.clone().unwrap_or_default();
160 save_multi_config(&mc)?;
161
162 Ok(InstallOutcome {
163 active_relay_id,
164 credential_version: next_version,
165 encryption_backend,
166 generated_encryption_key,
167 generated_device_private_key,
168 })
169}
170
171#[derive(Debug, thiserror::Error)]
173pub enum RequireKeyError {
174 #[error("encryption key not found for user")]
175 Missing,
176}
177
178pub fn require_encryption_key(user_id: &str) -> Result<[u8; 32], RequireKeyError> {
181 if user_id.is_empty() {
182 return Err(RequireKeyError::Missing);
183 }
184 credstore::read_encryption_key(user_id).ok_or(RequireKeyError::Missing)
185}
186
187fn install_or_reuse_device_privkey(
188 user_id: &str,
189 device_id: &str,
190 generated_flag: &mut bool,
191) -> Result<String, CredentialError> {
192 let existing = load_multi_config()
195 .ok()
196 .and_then(|mc| mc.active_profile().cloned())
197 .filter(|p| {
198 p.user_id == user_id && p.device_id == device_id && !p.device_private_key.is_empty()
199 })
200 .map(|p| p.device_private_key);
201
202 if let Some(priv_b64) = existing {
203 return Ok(priv_b64);
204 }
205
206 let (priv_b64, _pub_b64) = crypto::generate_device_keypair();
207 credstore::write_device_privkey(user_id, device_id, &priv_b64)
208 .map_err(|e| CredentialError::Io(format!("device key: {}", e)))?;
209 *generated_flag = true;
210 Ok(priv_b64)
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn rejects_empty_required_fields() {
219 let p = InstallParams {
220 user_id: "",
221 device_id: "d1",
222 token: "tok",
223 relay_url: "http://localhost:8080",
224 hostname: "h",
225 device_private_key: None,
226 email: "",
227 identity_provider: "",
228 };
229 assert!(install_credentials(p).is_err());
230 }
231
232 #[test]
233 fn require_encryption_key_errors_when_missing() {
234 let err = require_encryption_key("test-no-key-x7k9q").unwrap_err();
235 assert!(matches!(err, RequireKeyError::Missing));
236 }
237
238 #[test]
239 fn require_encryption_key_errors_on_empty_user_id() {
240 let err = require_encryption_key("").unwrap_err();
241 assert!(matches!(err, RequireKeyError::Missing));
242 }
243}