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 pub display_name: &'a str,
45}
46
47#[derive(Debug, Clone)]
51pub struct InstallOutcome {
52 pub active_relay_id: String,
54 pub credential_version: u64,
56 pub encryption_backend: &'static str,
58 pub generated_encryption_key: bool,
60 pub generated_device_private_key: bool,
62}
63
64pub fn install_credentials(p: InstallParams<'_>) -> Result<InstallOutcome, CredentialError> {
73 if p.user_id.is_empty() || p.device_id.is_empty() || p.token.is_empty() {
74 return Err(CredentialError::BadConfig(
75 "user_id, device_id, token are required".into(),
76 ));
77 }
78
79 let mut generated_encryption_key = false;
81 let encryption_backend: &'static str = "plaintext";
82 if credstore::read_encryption_key(p.user_id).is_none() {
83 let key = crypto::generate_aes_key();
84 credstore::write_encryption_key(p.user_id, &key)
85 .map_err(|e| CredentialError::Io(format!("encryption key: {}", e)))?;
86 generated_encryption_key = true;
87 }
88
89 let mut generated_device_private_key = false;
91 let device_priv_b64: String = if let Some(provided) = p.device_private_key {
92 if !provided.is_empty() {
93 credstore::write_device_privkey(p.user_id, p.device_id, provided)
94 .map_err(|e| CredentialError::Io(format!("device key: {}", e)))?;
95 provided.to_string()
96 } else {
97 install_or_reuse_device_privkey(
98 p.user_id,
99 p.device_id,
100 &mut generated_device_private_key,
101 )?
102 }
103 } else {
104 install_or_reuse_device_privkey(p.user_id, p.device_id, &mut generated_device_private_key)?
105 };
106
107 let mut mc = load_multi_config()?;
110 let next_version = mc
111 .relays
112 .iter()
113 .map(|r| r.credential_version)
114 .max()
115 .unwrap_or(0)
116 .checked_add(1)
117 .ok_or_else(|| CredentialError::BadConfig("credential_version overflow".into()))?;
118
119 let machine_id = crate::machine::stable_machine_id();
120
121 if let Some(profile) = mc.active_profile_mut() {
122 profile.token = p.token.to_string();
123 profile.user_id = p.user_id.to_string();
124 profile.device_id = p.device_id.to_string();
125 profile.relay_url = p.relay_url.to_string();
126 profile.hostname = p.hostname.to_string();
127 profile.device_private_key = device_priv_b64;
128 profile.machine_id = machine_id;
129 profile.credential_version = next_version;
130 if !p.email.is_empty() {
131 profile.email = p.email.to_string();
132 }
133 if !p.identity_provider.is_empty() {
134 profile.identity_provider = p.identity_provider.to_string();
135 }
136 if !p.display_name.is_empty() {
137 profile.display_name = p.display_name.to_string();
138 }
139 } else {
140 use ulid::Ulid;
141 let label = url::Url::parse(p.relay_url)
142 .ok()
143 .and_then(|u| u.host_str().map(|h| h.to_string()))
144 .unwrap_or_else(|| p.relay_url.to_string());
145 let profile = RelayProfile {
146 id: Ulid::new().to_string(),
147 label,
148 relay_url: p.relay_url.to_string(),
149 user_id: p.user_id.to_string(),
150 device_id: p.device_id.to_string(),
151 hostname: p.hostname.to_string(),
152 encryption_key: String::new(),
153 device_private_key: device_priv_b64,
154 credential_version: next_version,
155 token: p.token.to_string(),
156 machine_id,
157 email: p.email.to_string(),
158 identity_provider: p.identity_provider.to_string(),
159 display_name: p.display_name.to_string(),
160 };
161 let id = profile.id.clone();
162 mc.relays.push(profile);
163 mc.active_relay_id = Some(id);
164 }
165
166 let active_relay_id = mc.active_relay_id.clone().unwrap_or_default();
167 save_multi_config(&mc)?;
168
169 Ok(InstallOutcome {
170 active_relay_id,
171 credential_version: next_version,
172 encryption_backend,
173 generated_encryption_key,
174 generated_device_private_key,
175 })
176}
177
178#[derive(Debug, thiserror::Error)]
180pub enum RequireKeyError {
181 #[error("encryption key not found for user")]
182 Missing,
183}
184
185pub fn require_encryption_key(user_id: &str) -> Result<[u8; 32], RequireKeyError> {
188 if user_id.is_empty() {
189 return Err(RequireKeyError::Missing);
190 }
191 credstore::read_encryption_key(user_id).ok_or(RequireKeyError::Missing)
192}
193
194fn install_or_reuse_device_privkey(
195 user_id: &str,
196 device_id: &str,
197 generated_flag: &mut bool,
198) -> Result<String, CredentialError> {
199 let existing = load_multi_config()
202 .ok()
203 .and_then(|mc| mc.active_profile().cloned())
204 .filter(|p| {
205 p.user_id == user_id && p.device_id == device_id && !p.device_private_key.is_empty()
206 })
207 .map(|p| p.device_private_key);
208
209 if let Some(priv_b64) = existing {
210 return Ok(priv_b64);
211 }
212
213 let (priv_b64, _pub_b64) = crypto::generate_device_keypair();
214 credstore::write_device_privkey(user_id, device_id, &priv_b64)
215 .map_err(|e| CredentialError::Io(format!("device key: {}", e)))?;
216 *generated_flag = true;
217 Ok(priv_b64)
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn rejects_empty_required_fields() {
226 let p = InstallParams {
227 user_id: "",
228 device_id: "d1",
229 token: "tok",
230 relay_url: "http://localhost:8080",
231 hostname: "h",
232 device_private_key: None,
233 email: "",
234 identity_provider: "",
235 display_name: "",
236 };
237 assert!(install_credentials(p).is_err());
238 }
239
240 #[test]
241 fn install_writes_display_name_into_profile() {
242 let tmp = tempfile::tempdir().unwrap();
243 std::env::set_var("HOME", tmp.path());
244 let outcome = install_credentials(InstallParams {
245 user_id: "u1",
246 device_id: "d1",
247 token: "tok",
248 relay_url: "https://r",
249 hostname: "host",
250 device_private_key: None,
251 email: "alice@example.com",
252 identity_provider: "github",
253 display_name: "Alice Example",
254 })
255 .expect("install");
256 assert!(!outcome.active_relay_id.is_empty());
257
258 let cfg = crate::auth::load_config().expect("load");
259 assert_eq!(cfg.display_name, "Alice Example");
260 }
261
262 #[test]
263 fn require_encryption_key_errors_when_missing() {
264 let err = require_encryption_key("test-no-key-x7k9q").unwrap_err();
265 assert!(matches!(err, RequireKeyError::Missing));
266 }
267
268 #[test]
269 fn require_encryption_key_errors_on_empty_user_id() {
270 let err = require_encryption_key("").unwrap_err();
271 assert!(matches!(err, RequireKeyError::Missing));
272 }
273}