Skip to main content

kanade_shared/
secrets.rs

1//! Registry-backed secret store for production credentials.
2//!
3//! Windows services run as LocalSystem and inherit Machine-scope env
4//! vars, but those vars are readable by any logged-in user. Storing
5//! the credential under HKLM with a hardened ACL (SYSTEM +
6//! Administrators only) keeps it out of low-privilege reach.
7//!
8//! Layout in use across kanade:
9//!
10//! ```text
11//! HKLM\SOFTWARE\kanade\
12//!   agent\
13//!     NatsToken      — shared NATS bearer token (agent + backend + CLI)
14//!   backend\
15//!     StaticToken    — KANADE_AUTH_STATIC_TOKEN counterpart
16//!     JwtSecret      — KANADE_JWT_SECRET counterpart
17//!     MailPassword   — KANADE_MAIL_PASSWORD counterpart (SMTP AUTH)
18//! ```
19//!
20//! `deploy-agent.ps1` / `deploy-backend.ps1` provision these keys and
21//! apply the ACL. Non-Windows builds get an empty stub so the
22//! workspace still cross-compiles for the CLI's Linux / macOS release
23//! artifacts.
24
25/// Read a `REG_SZ` value from `HKLM\<subkey>` and return it when
26/// non-empty. Returns `None` for missing keys, missing values, empty
27/// strings, or non-Windows targets.
28#[cfg(windows)]
29pub fn read_hklm_value(subkey: &str, value: &str) -> Option<String> {
30    use winreg::RegKey;
31    use winreg::enums::HKEY_LOCAL_MACHINE;
32
33    let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
34    let key = hklm.open_subkey(subkey).ok()?;
35    let s: String = key.get_value(value).ok()?;
36    if s.is_empty() { None } else { Some(s) }
37}
38
39#[cfg(not(windows))]
40pub fn read_hklm_value(_subkey: &str, _value: &str) -> Option<String> {
41    None
42}
43
44/// Write a `REG_SZ` value into an **existing** `HKLM\<subkey>`.
45///
46/// Deliberately opens rather than creates. Registry ACLs are per-key, and the
47/// deploy scripts are what harden `HKLM\SOFTWARE\kanade\*` to SYSTEM +
48/// Administrators. Creating a missing key here would produce an unhardened one
49/// and leave whatever secret is being written readable by any logged-in user —
50/// the exact thing this module exists to prevent. A missing key means the host
51/// was never deployed properly, which is worth failing loudly over.
52#[cfg(windows)]
53pub fn write_hklm_value(subkey: &str, value: &str, data: &str) -> Result<(), String> {
54    use winreg::RegKey;
55    use winreg::enums::{HKEY_LOCAL_MACHINE, KEY_WRITE};
56
57    let hklm = RegKey::predef(HKEY_LOCAL_MACHINE);
58    let key = hklm
59        .open_subkey_with_flags(subkey, KEY_WRITE)
60        .map_err(|e| match e.kind() {
61            // The two failures need opposite responses, so they must not share
62            // a message. Asserting "not present, deploy this host" at an
63            // operator whose real problem is an unelevated shell sends them
64            // re-running a deploy that was already fine.
65            std::io::ErrorKind::NotFound => format!(
66                "HKLM\\{subkey} is not present — refusing to create it. A key created here \
67                 would not carry the SYSTEM+Administrators ACL the deploy scripts apply, \
68                 leaving the value readable by any logged-in user. Deploy this host first."
69            ),
70            std::io::ErrorKind::PermissionDenied => format!(
71                "HKLM\\{subkey} exists but could not be opened for writing: {e}. It is \
72                 restricted to SYSTEM + Administrators — run this elevated."
73            ),
74            _ => format!("HKLM\\{subkey} could not be opened for writing: {e}"),
75        })?;
76    key.set_value(value, &data.to_string())
77        .map_err(|e| format!("writing HKLM\\{subkey}\\{value}: {e}"))
78}
79
80#[cfg(not(windows))]
81pub fn write_hklm_value(_subkey: &str, _value: &str, _data: &str) -> Result<(), String> {
82    Err("registry secrets are Windows-only".to_string())
83}