1use std::path::{Path, PathBuf};
15
16fn keypair_checksum(public_key: &[u8; 32], private_key: &[u8]) -> [u8; 32] {
25 *blake3::keyed_hash(public_key, private_key).as_bytes()
26}
27
28pub fn generate_keypair() -> core_types::Result<ZeroizingKeypair> {
37 let builder = snow::Builder::new(
38 crate::noise::NOISE_PARAMS
39 .parse()
40 .map_err(|e| core_types::Error::Platform(format!("invalid Noise params: {e}")))?,
41 );
42 let keypair = builder
43 .generate_keypair()
44 .map_err(|e| core_types::Error::Platform(format!("keypair generation failed: {e}")))?;
45 Ok(ZeroizingKeypair::new(keypair))
46}
47
48pub struct ZeroizingKeypair {
58 inner: snow::Keypair,
59}
60
61impl ZeroizingKeypair {
62 #[must_use]
64 pub fn new(keypair: snow::Keypair) -> Self {
65 Self { inner: keypair }
66 }
67
68 #[must_use]
70 pub fn public(&self) -> &[u8] {
71 &self.inner.public
72 }
73
74 #[must_use]
76 pub fn private(&self) -> &[u8] {
77 &self.inner.private
78 }
79
80 #[must_use]
82 pub fn as_inner(&self) -> &snow::Keypair {
83 &self.inner
84 }
85
86 #[must_use]
92 pub fn into_inner(mut self) -> snow::Keypair {
93 let private = std::mem::take(&mut self.inner.private);
94 let public = std::mem::take(&mut self.inner.public);
95 snow::Keypair { private, public }
96 }
97}
98
99impl Drop for ZeroizingKeypair {
100 fn drop(&mut self) {
101 zeroize::Zeroize::zeroize(&mut self.inner.private);
102 }
103}
104
105impl From<snow::Keypair> for ZeroizingKeypair {
106 fn from(keypair: snow::Keypair) -> Self {
107 Self::new(keypair)
108 }
109}
110
111static RUNTIME_DIR_OVERRIDE: std::sync::Mutex<Option<PathBuf>> = std::sync::Mutex::new(None);
114
115#[doc(hidden)]
118pub fn set_runtime_dir_override(path: PathBuf) {
119 *RUNTIME_DIR_OVERRIDE.lock().unwrap() = Some(path);
120}
121
122fn runtime_dir() -> core_types::Result<PathBuf> {
124 if let Some(ref override_dir) = *RUNTIME_DIR_OVERRIDE.lock().unwrap() {
125 return Ok(override_dir.clone());
126 }
127 let runtime = std::env::var("XDG_RUNTIME_DIR")
128 .map_err(|_| core_types::Error::Platform("XDG_RUNTIME_DIR is not set".into()))?;
129 Ok(PathBuf::from(runtime).join("pds"))
130}
131
132pub async fn write_bus_keypair(keypair: &snow::Keypair) -> core_types::Result<()> {
141 let dir = runtime_dir()?;
142 tokio::fs::create_dir_all(&dir).await.map_err(|e| {
143 core_types::Error::Platform(format!(
144 "failed to create runtime dir {}: {e}",
145 dir.display()
146 ))
147 })?;
148
149 let pub_path = dir.join("bus.pub");
150 let key_path = dir.join("bus.key");
151
152 tokio::fs::write(&pub_path, &keypair.public)
153 .await
154 .map_err(|e| core_types::Error::Platform(format!("failed to write bus.pub: {e}")))?;
155 #[cfg(unix)]
156 {
157 use std::os::unix::fs::PermissionsExt;
158 tokio::fs::set_permissions(&pub_path, std::fs::Permissions::from_mode(0o644))
159 .await
160 .map_err(|e| {
161 core_types::Error::Platform(format!("failed to set bus.pub permissions: {e}"))
162 })?;
163 }
164
165 #[cfg(unix)]
168 {
169 use std::io::Write;
170 use std::os::unix::fs::OpenOptionsExt;
171
172 let tmp_key_path = dir.join("bus.key.tmp");
173 let private_key = zeroize::Zeroizing::new(keypair.private.clone());
176 let tmp_path = tmp_key_path.clone();
177 let final_path = key_path.clone();
178 tokio::task::spawn_blocking(move || -> std::io::Result<()> {
179 let mut f = std::fs::OpenOptions::new()
180 .write(true)
181 .create(true)
182 .truncate(true)
183 .mode(0o600)
184 .open(&tmp_path)?;
185 f.write_all(&private_key)?;
186 f.sync_all()?;
187 std::fs::rename(&tmp_path, &final_path)?;
188 Ok(())
189 })
190 .await
191 .map_err(|e| core_types::Error::Platform(format!("bus.key write task failed: {e}")))?
192 .map_err(|e| core_types::Error::Platform(format!("failed to write bus.key: {e}")))?;
193 }
194
195 #[cfg(not(unix))]
196 {
197 compile_error!(
198 "bus.key writing requires Unix file permissions (mode 0600). Non-Unix platforms are not supported in MVP."
199 );
200 }
201
202 {
204 let pub_array: [u8; 32] =
205 keypair.public.clone().try_into().map_err(|_| {
206 core_types::Error::Platform("bus public key is not 32 bytes".into())
207 })?;
208 let checksum = keypair_checksum(&pub_array, &keypair.private);
209 let checksum_path = dir.join("bus.checksum");
210 tokio::fs::write(&checksum_path, checksum)
211 .await
212 .map_err(|e| {
213 core_types::Error::Platform(format!("failed to write bus.checksum: {e}"))
214 })?;
215 }
216
217 tracing::info!(
218 pub_path = %pub_path.display(),
219 key_path = %key_path.display(),
220 "bus keypair written"
221 );
222 Ok(())
223}
224
225fn keys_dir() -> core_types::Result<PathBuf> {
227 Ok(runtime_dir()?.join("keys"))
228}
229
230pub async fn create_keys_dir() -> core_types::Result<()> {
236 let dir = keys_dir()?;
237 tokio::fs::create_dir_all(&dir).await.map_err(|e| {
238 core_types::Error::Platform(format!("failed to create keys dir {}: {e}", dir.display()))
239 })?;
240 #[cfg(unix)]
243 {
244 use std::os::unix::fs::PermissionsExt;
245 tokio::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700))
246 .await
247 .map_err(|e| {
248 core_types::Error::Platform(format!(
249 "failed to set keys dir permissions on {}: {e}",
250 dir.display()
251 ))
252 })?;
253 }
254 Ok(())
255}
256
257pub async fn write_daemon_keypair(
265 daemon_name: &str,
266 keypair: &snow::Keypair,
267) -> core_types::Result<()> {
268 let dir = keys_dir()?;
269
270 let pub_path = dir.join(format!("{daemon_name}.pub"));
271 let key_path = dir.join(format!("{daemon_name}.key"));
272
273 tokio::fs::write(&pub_path, &keypair.public)
275 .await
276 .map_err(|e| {
277 core_types::Error::Platform(format!("failed to write {daemon_name}.pub: {e}"))
278 })?;
279 #[cfg(unix)]
280 {
281 use std::os::unix::fs::PermissionsExt;
282 tokio::fs::set_permissions(&pub_path, std::fs::Permissions::from_mode(0o644))
283 .await
284 .map_err(|e| {
285 core_types::Error::Platform(format!(
286 "failed to set {daemon_name}.pub permissions: {e}"
287 ))
288 })?;
289 }
290
291 #[cfg(unix)]
293 {
294 use std::io::Write;
295 use std::os::unix::fs::OpenOptionsExt;
296
297 let tmp_path = dir.join(format!("{daemon_name}.key.tmp"));
298 let private_key = zeroize::Zeroizing::new(keypair.private.clone());
299 let tmp = tmp_path.clone();
300 let final_path = key_path.clone();
301 tokio::task::spawn_blocking(move || -> std::io::Result<()> {
302 let mut f = std::fs::OpenOptions::new()
303 .write(true)
304 .create(true)
305 .truncate(true)
306 .mode(0o600)
307 .open(&tmp)?;
308 f.write_all(&private_key)?;
309 f.sync_all()?;
310 std::fs::rename(&tmp, &final_path)?;
311 Ok(())
312 })
313 .await
314 .map_err(|e| {
315 core_types::Error::Platform(format!("{daemon_name}.key write task failed: {e}"))
316 })?
317 .map_err(|e| {
318 core_types::Error::Platform(format!("failed to write {daemon_name}.key: {e}"))
319 })?;
320 }
321
322 #[cfg(not(unix))]
323 {
324 compile_error!("daemon keypair writing requires Unix file permissions (mode 0600)");
325 }
326
327 {
329 let pub_array: [u8; 32] = keypair.public.clone().try_into().map_err(|_| {
330 core_types::Error::Platform(format!("{daemon_name} public key is not 32 bytes"))
331 })?;
332 let checksum = keypair_checksum(&pub_array, &keypair.private);
333 let checksum_path = dir.join(format!("{daemon_name}.checksum"));
334 tokio::fs::write(&checksum_path, checksum)
335 .await
336 .map_err(|e| {
337 core_types::Error::Platform(format!("failed to write {daemon_name}.checksum: {e}"))
338 })?;
339 }
340
341 tracing::debug!(
342 daemon = daemon_name,
343 pub_path = %pub_path.display(),
344 key_path = %key_path.display(),
345 "daemon keypair written"
346 );
347 Ok(())
348}
349
350pub async fn read_daemon_keypair(
356 daemon_name: &str,
357) -> core_types::Result<(zeroize::Zeroizing<Vec<u8>>, [u8; 32])> {
358 let dir = keys_dir()?;
359
360 let key_path = dir.join(format!("{daemon_name}.key"));
361 let pub_path = dir.join(format!("{daemon_name}.pub"));
362
363 let private_bytes = tokio::fs::read(&key_path).await.map_err(|e| {
364 core_types::Error::Platform(format!(
365 "failed to read {daemon_name}.key at {}: {e}",
366 key_path.display()
367 ))
368 })?;
369
370 let public_bytes = tokio::fs::read(&pub_path).await.map_err(|e| {
371 core_types::Error::Platform(format!(
372 "failed to read {daemon_name}.pub at {}: {e}",
373 pub_path.display()
374 ))
375 })?;
376
377 let public_key: [u8; 32] = public_bytes.try_into().map_err(|v: Vec<u8>| {
378 core_types::Error::Platform(format!(
379 "{daemon_name}.pub has wrong size: expected 32 bytes, got {}",
380 v.len()
381 ))
382 })?;
383
384 let checksum_path = dir.join(format!("{daemon_name}.checksum"));
386 match tokio::fs::read(&checksum_path).await {
387 Ok(stored_checksum) => {
388 let expected = keypair_checksum(&public_key, &private_bytes);
389 if stored_checksum.len() != 32 || stored_checksum[..] != expected[..] {
390 return Err(core_types::Error::Platform(format!(
391 "TAMPER DETECTED: {daemon_name} keypair checksum mismatch. \
392 The private key or public key file may have been modified. \
393 Delete $XDG_RUNTIME_DIR/pds/keys/{daemon_name}.* and restart daemon-profile."
394 )));
395 }
396 tracing::debug!(daemon = daemon_name, "keypair integrity verified");
397 }
398 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
399 tracing::warn!(
401 daemon = daemon_name,
402 "no checksum file found — keypair integrity cannot be verified. \
403 Restart daemon-profile to generate checksums."
404 );
405 }
406 Err(e) => {
407 return Err(core_types::Error::Platform(format!(
408 "failed to read {daemon_name}.checksum: {e}"
409 )));
410 }
411 }
412
413 Ok((zeroize::Zeroizing::new(private_bytes), public_key))
414}
415
416pub async fn read_daemon_public_key(daemon_name: &str) -> core_types::Result<[u8; 32]> {
422 let dir = keys_dir()?;
423 let pub_path = dir.join(format!("{daemon_name}.pub"));
424 let bytes = tokio::fs::read(&pub_path).await.map_err(|e| {
425 core_types::Error::Platform(format!(
426 "failed to read {daemon_name}.pub at {}: {e}",
427 pub_path.display()
428 ))
429 })?;
430 let key: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
431 core_types::Error::Platform(format!(
432 "{daemon_name}.pub has wrong size: expected 32 bytes, got {}",
433 v.len()
434 ))
435 })?;
436 Ok(key)
437}
438
439pub async fn read_bus_public_key() -> core_types::Result<[u8; 32]> {
448 read_bus_public_key_from(&runtime_dir()?).await
449}
450
451pub async fn read_bus_public_key_from(dir: &Path) -> core_types::Result<[u8; 32]> {
459 let pub_path = dir.join("bus.pub");
460 let bytes = tokio::fs::read(&pub_path).await.map_err(|e| {
461 core_types::Error::Platform(format!(
462 "failed to read bus.pub at {}: {e}",
463 pub_path.display()
464 ))
465 })?;
466 let key: [u8; 32] = bytes.try_into().map_err(|v: Vec<u8>| {
467 core_types::Error::Platform(format!(
468 "bus.pub has wrong size: expected 32 bytes, got {}",
469 v.len()
470 ))
471 })?;
472 Ok(key)
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[test]
480 fn keypair_checksum_consistency() {
481 let kp = generate_keypair().unwrap();
482 let pub_array: [u8; 32] = kp.public().try_into().unwrap();
483 let checksum1 = keypair_checksum(&pub_array, kp.private());
484 let checksum2 = keypair_checksum(&pub_array, kp.private());
485 assert_eq!(checksum1, checksum2);
486 }
487
488 #[test]
489 fn keypair_checksum_detects_tampering() {
490 let kp = generate_keypair().unwrap();
491 let pub_array: [u8; 32] = kp.public().try_into().unwrap();
492 let checksum = keypair_checksum(&pub_array, kp.private());
493
494 let mut tampered = kp.private().to_vec();
496 tampered[0] ^= 0xFF;
497 let tampered_checksum = keypair_checksum(&pub_array, &tampered);
498 assert_ne!(checksum, tampered_checksum);
499 }
500
501 #[test]
502 fn generate_keypair_produces_32_byte_keys() {
503 let kp = generate_keypair().unwrap();
504 assert_eq!(kp.private().len(), 32);
505 assert_eq!(kp.public().len(), 32);
506 }
507
508 #[test]
512 fn zeroizing_keypair_into_inner_zeroes_source() {
513 let kp = generate_keypair().unwrap();
514 let private_copy = kp.private().to_vec();
515 assert!(
516 !private_copy.iter().all(|&b| b == 0),
517 "generated key must be non-zero"
518 );
519
520 let extracted = kp.into_inner();
521 assert_eq!(extracted.private, private_copy);
523 }
529
530 }