//! The key a receipt is signed with. //! //! - `--demo-key`: the published demo key, for receipts that repeat byte for //! byte everywhere. Anyone can sign with it, so it proves nothing about who //! signed. //! - `--key FILE`, or `$WAI_QUANTUM_KEY` naming a file: that key. //! - Otherwise this user's key, `$XDG_CONFIG_HOME/wai/quantum-key` (or //! `~/.config/wai/quantum-key`), made from the operating system's random //! source on first use and readable by its owner only. //! //! A key file holds the 32-byte Ed25519 seed as 64 hex digits. use std::path::{Path, PathBuf}; use wai_quantum::ed25519_dalek::SigningKey; /// The published demo key's seed. pub const DEMO_SEED: [u8; 32] = [42; 32]; /// The key to sign with, and where it came from. pub struct Key { pub key: SigningKey, /// The file it was read from or made in; `None` for the demo key. pub path: Option, /// Whether this call made the file. pub made: bool, } /// This user's key file: `$XDG_CONFIG_HOME/wai/quantum-key`, or /// `~/.config/wai/quantum-key`. pub fn user_path() -> Option { let base = std::env::var_os("XDG_CONFIG_HOME") .map(PathBuf::from) .or_else(|| std::env::var_os("HOME").map(|h| Path::new(&h).join(".config")))?; Some(base.join("wai").join("quantum-key")) } fn parse(text: &str) -> Option<[u8; 32]> { let t = text.trim(); if t.len() != 64 { return None; } let mut out = [0u8; 32]; for (i, b) in out.iter_mut().enumerate() { *b = u8::from_str_radix(t.get(2 * i..2 * i + 2)?, 16).ok()?; } Some(out) } /// The key `demo` and `given` (`--demo-key`, `--key FILE`) name: a named file /// must exist; this user's own is made when there is none. pub fn load(given: Option<&Path>, demo: bool) -> Result { if demo { return Ok(Key { key: SigningKey::from_bytes(&DEMO_SEED), path: None, made: false }); } let named = given.map(Path::to_path_buf).or_else(|| std::env::var_os("WAI_QUANTUM_KEY").map(PathBuf::from)); let path = match &named { Some(p) => p.clone(), None => user_path().ok_or("no HOME to keep a key in: pass --key FILE")?, }; match std::fs::read_to_string(&path) { Ok(text) => { let seed = parse(&text).ok_or_else(|| format!("{}: a key file holds 64 hex digits", path.display()))?; Ok(Key { key: SigningKey::from_bytes(&seed), path: Some(path), made: false }) } Err(e) if e.kind() == std::io::ErrorKind::NotFound && named.is_none() => { let mut seed = [0u8; 32]; getrandom::fill(&mut seed).map_err(|e| format!("no random source for a new key: {e}"))?; if let Some(dir) = path.parent() { std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?; } let text: String = seed.iter().map(|b| format!("{b:02x}")).collect(); write_private(&path, &format!("{text}\n")).map_err(|e| format!("{}: {e}", path.display()))?; Ok(Key { key: SigningKey::from_bytes(&seed), path: Some(path), made: true }) } Err(e) => Err(format!("{}: {e}", path.display())), } } fn write_private(path: &Path, text: &str) -> std::io::Result<()> { use std::io::Write as _; let mut f = std::fs::OpenOptions::new(); f.write(true).create_new(true); #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; f.mode(0o600); } f.open(path)?.write_all(text.as_bytes()) } /// What `--demo-key` and a new key say on standard error. pub fn notes(k: &Key) { match &k.path { None => eprintln!("note: signed with the published demo key; the receipt shows the result is unaltered, not who signed it"), Some(p) if k.made => eprintln!("note: made a signing key at {}", p.display()), Some(_) => {} } } #[cfg(test)] mod tests { use super::*; #[test] fn a_key_is_made_once_private_then_read_and_a_named_file_must_exist() { let dir = std::env::temp_dir().join(format!("wai-quantum-cli-key-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); let path = dir.join("k"); // A named file that is not there is refused, never made. assert!(load(Some(&path), false).is_err()); assert!(!path.exists()); // The demo key is the published one. let demo = load(None, true).unwrap(); assert_eq!(demo.key.to_bytes(), DEMO_SEED); assert!(demo.path.is_none()); // A written key reads back; a malformed one is refused. std::fs::create_dir_all(&dir).unwrap(); let seed: String = (0..32).map(|i| format!("{:02x}", i * 7)).collect(); std::fs::write(&path, format!("{seed}\n")).unwrap(); let k = load(Some(&path), false).unwrap(); assert_eq!(k.key.to_bytes()[1], 7); assert!(!k.made); std::fs::write(&path, "not a key").unwrap(); assert!(load(Some(&path), false).is_err()); // A made key is the owner's alone. let fresh = dir.join("fresh"); write_private(&fresh, "x").unwrap(); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; assert_eq!(std::fs::metadata(&fresh).unwrap().permissions().mode() & 0o777, 0o600); } let _ = std::fs::remove_dir_all(&dir); } }