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