lean_ctx/core/context_package/
keys.rs1use std::path::PathBuf;
9
10use ed25519_dalek::SigningKey;
11
12pub const KEY_REL_PATH: &str = "keys/ctxpkg-ed25519.key";
13
14pub fn key_path() -> Result<PathBuf, String> {
15 let home = dirs::home_dir().ok_or("cannot resolve home directory")?;
16 Ok(home.join(".lean-ctx").join(KEY_REL_PATH))
17}
18
19pub fn load_or_create() -> Result<(SigningKey, bool), String> {
22 let path = key_path()?;
23 if path.exists() {
24 let hex_seed =
25 std::fs::read_to_string(&path).map_err(|e| format!("read signing key: {e}"))?;
26 let seed = parse_seed(hex_seed.trim())?;
27 return Ok((SigningKey::from_bytes(&seed), false));
28 }
29
30 let dir = path.parent().expect("key path has a parent");
31 std::fs::create_dir_all(dir).map_err(|e| format!("create key dir: {e}"))?;
32
33 let mut seed = [0u8; 32];
34 getrandom::fill(&mut seed).map_err(|e| format!("entropy source failed: {e}"))?;
35 let encoded = hex_encode(&seed);
36
37 std::fs::write(&path, &encoded).map_err(|e| format!("write signing key: {e}"))?;
38 #[cfg(unix)]
39 {
40 use std::os::unix::fs::PermissionsExt;
41 std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))
42 .map_err(|e| format!("chmod signing key: {e}"))?;
43 }
44 Ok((SigningKey::from_bytes(&seed), true))
45}
46
47pub fn public_key_hex(key: &SigningKey) -> String {
49 hex_encode(key.verifying_key().as_bytes())
50}
51
52fn parse_seed(s: &str) -> Result<[u8; 32], String> {
53 if s.len() != 64 || !s.chars().all(|c| c.is_ascii_hexdigit()) {
54 return Err(format!(
55 "signing key file is corrupt (expected 64 hex chars, got {} chars) — \
56 delete it to regenerate (this changes your publisher identity!)",
57 s.len()
58 ));
59 }
60 let mut seed = [0u8; 32];
61 for (i, byte) in seed.iter_mut().enumerate() {
62 *byte = u8::from_str_radix(&s[i * 2..i * 2 + 2], 16)
63 .map_err(|e| format!("signing key hex: {e}"))?;
64 }
65 Ok(seed)
66}
67
68fn hex_encode(bytes: &[u8]) -> String {
69 use std::fmt::Write;
70 let mut s = String::with_capacity(bytes.len() * 2);
71 for b in bytes {
72 let _ = write!(s, "{b:02x}");
73 }
74 s
75}
76
77#[cfg(test)]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn seed_roundtrip() {
83 let seed = [42u8; 32];
84 let encoded = hex_encode(&seed);
85 assert_eq!(parse_seed(&encoded).expect("parses"), seed);
86 }
87
88 #[test]
89 fn corrupt_seed_rejected() {
90 assert!(parse_seed("zz").is_err());
91 assert!(parse_seed(&"a".repeat(63)).is_err());
92 }
93
94 #[test]
95 fn public_key_is_stable_for_seed() {
96 let k1 = SigningKey::from_bytes(&[7u8; 32]);
97 let k2 = SigningKey::from_bytes(&[7u8; 32]);
98 assert_eq!(public_key_hex(&k1), public_key_hex(&k2));
99 assert_eq!(public_key_hex(&k1).len(), 64);
100 }
101}