nordisk-pairing 0.5.25

The boxes of an estate find each other: a seed each box is installed with, a one-time join against the front, a tunnel (WireGuard by default) they meet inside, roles decided once, a pairing loop each box runs itself, and the signal it reports. Shared by gunnar and holger; the laptop's xtask only lays machines, writes seeds and reads the signal. Design: .nornir/pairing-design.md (2026-09-24).
Documentation
//! **What a collected log line may never carry** (0.3.0). The installer
//! stores every VM's boot log on the operator's machine; a line that carries
//! a secret is withheld there — replaced by its class and its size, never a
//! byte of the content. The rule moved here from private-gunnar-ops'
//! `xtask/src/bootnarr.rs` (the UDP boot narration's filter, retired with the
//! narration on 2026-09-24), so gunnar and holger share one filter.
//!
//! The classes, and why each one is here:
//!
//! * `cmdline` — the kernel command line carries `korp.args` with the
//!   monetize public key and `clock_key=` on it, so ONE echoed cmdline
//!   discloses the medium's whole configuration.
//! * `token` — any admin or API token, any `Bearer`, a password, a secret.
//! * `key` — any signing or private key material, and any SSH key line.
//! * `pubkey` — a PUBLIC verifying key. It discloses nothing; it is withheld
//!   anyway, on the owner's rule, because it is not progress.
//! * `env` — the shape of a secrets file being read out.
//! * `envrecord` — an environment record (`NAME=value`, an upper-case variable):
//!   a kernel `with environment:` line, a process printing its environment.
//! * `highentropy` — a long unbroken base64/hex run: what a key looks like
//!   when nobody labelled it.

/// The class a line must be withheld under, or `None` when it may be kept.
pub fn secret_class(line: &str) -> Option<&'static str> {
    let lower = line.to_ascii_lowercase();
    if lower.contains("kernel command line") || lower.contains("/proc/cmdline") || lower.contains("korp.args") || lower.contains("korp.log_udp=") || lower.contains("korp.env.") || lower.contains("clock_key=") {
        return Some("cmdline");
    }
    if lower.contains("token") || lower.contains("bearer ") || lower.contains("password") || lower.contains("secret") || stripe_secret(&lower) {
        return Some("token");
    }
    if lower.contains("private key") || lower.contains("begin openssh") || lower.contains("ssh-ed25519") || lower.contains("ssh-rsa") || lower.contains("signing.key") || lower.contains("time_ed25519") {
        return Some("key");
    }
    if lower.contains("pubkey") || lower.contains("public key") || lower.contains("sha256:") {
        return Some("pubkey");
    }
    if lower.contains(".env") {
        return Some("env");
    }
    if has_env_record(line) {
        return Some("envrecord");
    }
    if longest_opaque_run(line) >= 40 {
        return Some("highentropy");
    }
    None
}

/// A Stripe secret by its prefix (2026-09-28): a secret or restricted API key
/// (`sk_live_`, `sk_test_`, `rk_live_`, `rk_test_`) or a webhook signing
/// secret (`whsec_`). A real `whsec_` value is 32 base64 characters —
/// shorter than the `highentropy` run — and a test key can be shorter still,
/// so the prefix is the rule: the front logs monetize's lines now
/// (gunnar-front stderr capture), and monetize talks to Stripe.
fn stripe_secret(lower: &str) -> bool {
    ["sk_live_", "sk_test_", "rk_live_", "rk_test_", "whsec_"].iter().any(|p| lower.contains(p))
}

/// **An environment record**: a `NAME=value` token whose name is an upper-case
/// variable (3+ of `A-Z0-9_`, a letter first) — a kernel `with environment:` record,
/// or a process printing its environment. The value is what must not travel; a short
/// secret-shaped value escapes the `highentropy` run, so the shape is the rule
/// (the same rule gunnar-server's narration door and nordisk-narrate's kmsg follower keep).
fn has_env_record(line: &str) -> bool {
    line.split_whitespace().any(|tok| {
        let Some((name, value)) = tok.split_once('=') else { return false };
        !value.is_empty() && name.len() >= 3 && name.starts_with(|c: char| c.is_ascii_uppercase()) && name.chars().all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
    })
}

/// The longest unbroken run of base64/hex alphabet.
fn longest_opaque_run(line: &str) -> usize {
    let mut best = 0usize;
    let mut run = 0usize;
    for c in line.chars() {
        if c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=' {
            run += 1;
            best = best.max(run);
        } else {
            run = 0;
        }
    }
    best
}

/// `line` with every opaque base64/hex run of the `highentropy` length (40)
/// replaced by [`VALUE_WITHHELD`].
fn withhold_runs(line: &str) -> String {
    let mut out = String::with_capacity(line.len());
    let mut run = String::new();
    let flush = |run: &mut String, out: &mut String| {
        if run.chars().count() >= 40 {
            out.push_str(VALUE_WITHHELD);
        } else {
            out.push_str(run);
        }
        run.clear();
    };
    for c in line.chars() {
        if c.is_ascii_alphanumeric() || c == '+' || c == '/' || c == '=' {
            run.push(c);
        } else {
            flush(&mut run, &mut out);
            out.push(c);
        }
    }
    flush(&mut run, &mut out);
    out
}

/// The text that stands in for a withheld line.
pub fn withheld(class: &str, bytes: usize) -> String {
    format!("<<WITHHELD: a {class}-class line of {bytes} bytes was not recorded>>")
}

/// **The text that stands in for a RUN of withheld lines** of one class, in
/// a row (0.5.5): one line with the count, never one line each. A run of one
/// is [`withheld`] exactly.
pub fn withheld_run(class: &str, count: u64, bytes: u64) -> String {
    if count == 1 {
        return withheld(class, bytes as usize);
    }
    format!("<<WITHHELD: {count} {class}-class lines in a row, {bytes} bytes, were not recorded>>")
}

/// What stands in for a public key's VALUE inside a kept line (0.5.5).
pub const VALUE_WITHHELD: &str = "<withheld>";

/// Is `line` this module's own marker? A marker is kept as it is: cleaning
/// is idempotent, so the VM and the installer may both clean.
pub fn is_marker(line: &str) -> bool {
    line.starts_with("<<WITHHELD: ")
}

/// What cleaning makes of one line.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Cleaned<'a> {
    /// The line may be stored: itself, or itself with its public-key values
    /// withheld in place.
    Kept(std::borrow::Cow<'a, str>),
    /// The whole line is withheld: only its class and its size may be.
    Withheld { class: &'static str, bytes: usize },
}

/// **What may be stored of `line`** (0.5.5). A line of the `pubkey` class
/// is KEPT with every public-key value withheld inside it; a line of any
/// other class is withheld whole; a marker is kept as it is.
///
/// Why `pubkey` is redacted inside the line (MEASURED 2026-09-28, test2's
/// main VM: 8 502 of 20 008 held lines read back as a pubkey-class marker —
/// every ssh login and every audited RPC names the key's fingerprint): the
/// line that says "a key logged in" is progress; only the key is not. A
/// whole-line marker per login made the log unreadable and said nothing a
/// withheld value does not.
pub fn cleaned(line: &str) -> Cleaned<'_> {
    if is_marker(line) {
        return Cleaned::Kept(std::borrow::Cow::Borrowed(line));
    }
    match secret_class(line) {
        None => Cleaned::Kept(std::borrow::Cow::Borrowed(line)),
        Some("pubkey") => match redact_values(line) {
            Some(kept) => Cleaned::Kept(std::borrow::Cow::Owned(kept)),
            None => Cleaned::Kept(std::borrow::Cow::Borrowed(line)),
        },
        // An unlabelled opaque run (a hash, a digest — or a key nobody
        // labelled) is withheld IN PLACE: the run never stored, the line kept.
        Some("highentropy") => Cleaned::Kept(std::borrow::Cow::Owned(withhold_runs(line))),
        Some(class) => Cleaned::Withheld { class, bytes: line.len() },
    }
}

/// **The line as it may be stored**: itself, itself with its public-key
/// values withheld in place, or the withheld marker. Idempotent.
pub fn clean(line: &str) -> std::borrow::Cow<'_, str> {
    match cleaned(line) {
        Cleaned::Kept(kept) => kept,
        Cleaned::Withheld { class, bytes } => std::borrow::Cow::Owned(withheld(class, bytes)),
    }
}

/// **Many lines as they may be stored** (0.5.5): each [`clean`]ed, and a run
/// of withheld lines of one class in a row folded into ONE counted marker
/// ([`withheld_run`]) — so redaction never multiplies lines, also for a VM
/// that holds its lines raw (before 0.5.5).
pub fn clean_lines<'a, I: IntoIterator<Item = &'a str>>(lines: I) -> Vec<String> {
    let mut out: Vec<String> = Vec::new();
    let mut run: Option<(&'static str, u64, u64)> = None;
    for line in lines {
        match cleaned(line) {
            Cleaned::Kept(kept) => {
                out.push(kept.into_owned());
                run = None;
            }
            Cleaned::Withheld { class, bytes } => {
                if let Some((c, n, b)) = run.as_mut() {
                    if *c == class {
                        *n += 1;
                        *b += bytes as u64;
                        let text = withheld_run(class, *n, *b);
                        if let Some(last) = out.last_mut() {
                            *last = text;
                        }
                        continue;
                    }
                }
                out.push(withheld(class, bytes));
                run = Some((class, 1, bytes as u64));
            }
        }
    }
    out
}

/// A word that names a public key (`pubkey`, `public_key`, `monetize_pubkey`,
/// `fingerprint`).
fn names_a_key(word: &str) -> bool {
    let w = word.to_ascii_lowercase();
    w.contains("pubkey") || w.contains("public_key") || w.contains("public-key") || w.contains("fingerprint")
}

/// A value that looks like key material: a `SHA256:` fingerprint or an
/// opaque base64/hex run of 16 or more.
fn looks_like_key(value: &str) -> bool {
    value.to_ascii_lowercase().contains("sha256:") || longest_opaque_run(value) >= 16
}

/// A short word that may be a key's value when a key-naming word precedes
/// it: the base64/hex alphabet only, with at least one digit (`eb16`, not
/// `revoked`).
fn short_value(word: &str) -> bool {
    !word.is_empty()
        && word.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '+' | '/' | '=' | ':'))
        && word.chars().any(|c| c.is_ascii_digit())
}

/// The line with every public-key value replaced by [`VALUE_WITHHELD`]: a
/// `SHA256:` fingerprint, any long opaque run, the value of a key-naming
/// `k=v`, and a value-shaped word after a key-naming word or `public key`.
/// `None` when nothing needed withholding.
fn redact_values(line: &str) -> Option<String> {
    let words: Vec<&str> = line.split(' ').collect();
    let mut out: Vec<String> = Vec::with_capacity(words.len());
    let mut after_name = false;
    for (i, w) in words.iter().enumerate() {
        let word = if *w == VALUE_WITHHELD {
            (*w).to_string()
        } else if let Some((k, v)) = w.split_once('=') {
            let bare = v.trim_matches('"');
            if !bare.is_empty() && bare != VALUE_WITHHELD && (names_a_key(k) || looks_like_key(bare)) {
                format!("{k}={VALUE_WITHHELD}")
            } else {
                (*w).to_string()
            }
        } else if looks_like_key(w) || (after_name && short_value(w)) {
            VALUE_WITHHELD.to_string()
        } else {
            (*w).to_string()
        };
        let public_key = i > 0 && words[i - 1].eq_ignore_ascii_case("public") && w.eq_ignore_ascii_case("key");
        after_name = !w.contains('=') && (names_a_key(w.trim_end_matches(':')) || public_key);
        out.push(word);
    }
    let kept = out.join(" ");
    (kept != line).then_some(kept)
}

#[cfg(test)]
mod tests {

    /// **A hash is not a secret, and a line is not its hash** (MEASURED
    /// estate-box3 2026-09-30): every line with a 40-character run was
    /// withheld WHOLE — every git push naming a commit, gunnar's
    /// self-registration naming its plugin's blake3 ("67 highentropy-class
    /// lines in a row" on main), so the installer never saw the step, and an
    /// ERROR naming a hash would never reach the log gate. The run is withheld
    /// IN PLACE; the line — its time, level, target and words — is kept.
    #[test]
    fn an_opaque_run_is_withheld_in_place_and_its_line_kept() {
        let sha = "0123456789abcdef0123456789abcdef01234567";
        let l = format!("2026-09-30T07:00:00.1Z ERROR gunnar::store: push rejected for {sha}: object missing");
        match cleaned(&l) {
            Cleaned::Kept(k) => {
                assert!(!k.contains(sha), "{k}");
                assert!(k.contains("ERROR gunnar::store: push rejected for <withheld>: object missing"), "{k}");
            }
            w => panic!("the line was withheld whole: {w:?}"),
        }
        let digest = "dba8e30fe8af4a881572f52144afa386df347e80f5029066f695810784e751dd";
        let reg = format!("2026-09-30T07:00:00.1Z  INFO gunnar_server::self_register: plugin: ADDED — gunnar blake3 {digest} event=\"plugin.self_register\"");
        let kept = clean(&reg);
        assert!(kept.contains("plugin: ADDED — gunnar blake3 <withheld>") && !kept.contains(digest), "{kept}");
        // An unlabelled key run is still never stored, whatever surrounds it.
        let key = "AAAAC3NzaC1lZDI1NTE5AAAAIGx8r8b4v8v5r7q2Zk1pQ3aQ";
        assert!(!clean(&format!("x {key} y")).contains(key));
        assert_eq!(clean(&clean(&reg)), clean(&reg), "idempotent");
    }
    use super::*;

    #[test]
    fn progress_is_kept_and_every_secret_class_is_withheld_without_its_content() {
        assert_eq!(clean("korp-start: stage net up at 6.34 s"), "korp-start: stage net up at 6.34 s");
        for (line, class) in [
            ("[    0.000000] Kernel command line: korp.args=monetize_pubkey=abc", "cmdline"),
            ("GUNNAR_UI_ADMIN_TOKEN=xyz", "token"),
            ("-----BEGIN OPENSSH PRIVATE KEY-----", "key"),
            ("reading /etc/monetize/monetize.env", "env"),
            // Stripe's secrets by prefix: a whsec_ value is shorter than the
            // highentropy run, a test key shorter still.
            ("monetize-server: stripe: POST /v1/checkout/sessions with sk_live_51Habc", "token"),
            ("monetize-server: hmac key whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH", "token"),
            ("monetize-server: vendor key rk_test_abc", "token"),
        ] {
            assert_eq!(secret_class(line), Some(class), "{line}");
            let c = clean(line);
            assert!(c.starts_with("<<WITHHELD: a ") && c.contains(class) && !c.contains(line), "{c}");
            assert_eq!(clean(&c), c, "a marker cleans to itself");
        }
        // An unlabelled key run: classed highentropy, and withheld IN PLACE —
        // nothing of it stored, the line (here: nothing else) kept.
        let run = "AAAAC3NzaC1lZDI1NTE5AAAAIGx8r8b4v8v5r7q2Zk1pQ3a";
        assert_eq!(secret_class(run), Some("highentropy"));
        assert_eq!(clean(run), VALUE_WITHHELD);
    }

    /// **An environment record is withheld by the SHARED filter** (release-3 DEV-NARRATE
    /// review, redact-no-envrecord): gunnar-server's own narration door withheld
    /// `envrecord` and `korp.env.` lines, this filter — the one the log door and every
    /// other path use — kept them: a kernel `with environment:` record, or any line that
    /// prints `NAME=value` for an upper-case variable, went out with its value. A short,
    /// secret-shaped value passes the `highentropy` run, so the shape is the rule.
    #[test]
    fn an_environment_record_is_withheld_and_names_no_value() {
        for (line, class) in [
            ("kmsg 6,368,90210,-;with environment: HOME=/ TERM=linux KORP_INSTALLER_ROOT=/run/installer", "envrecord"),
            ("kmsg 6,369,90211,-;with environment: MONETIZE_VERIFY_KEY=MCowBQYDK2VwAyEA", "envrecord"),
            ("box: STRIPE_ACCOUNT=acct_1Qx9 MONETIZE_SEAL=q7Zp2", "envrecord"),
            ("app @4s korp.env.GUNNAR_DATA_ROOT applied", "cmdline"),
        ] {
            assert_eq!(secret_class(line), Some(class), "{line}");
            let c = clean(line);
            assert!(c.starts_with("<<WITHHELD: a ") && c.contains(class) && !c.contains("q7Zp2") && !c.contains("MCowBQ"), "{c}");
        }
        // Lower-case structured fields are a tracing line's own, not an environment.
        assert_eq!(secret_class(r#"INFO gunnar::audit: served event="grpc.call" decision="allow" elapsed_ms=3"#), None);
        assert_eq!(secret_class("HTTP/1.1 200 OK"), None);
    }

    #[test]
    fn a_pubkey_line_is_kept_with_its_values_withheld_in_place() {
        let w = VALUE_WITHHELD;
        for (line, kept) in [
            ("fed monetize pubkey eb16".to_string(), format!("fed monetize pubkey {w}")),
            (
                "WARN gunnar::audit: event=\"ssh.auth.accepted\" fingerprint=SHA256:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU principal=admin".to_string(),
                format!("WARN gunnar::audit: event=\"ssh.auth.accepted\" fingerprint={w} principal=admin"),
            ),
            ("feed: monetize_pubkey=abc public key 1f2e3d4c".to_string(), format!("feed: monetize_pubkey={w} public key {w}")),
            ("public key revoked".to_string(), "public key revoked".to_string()),
        ] {
            assert_eq!(secret_class(&line), Some("pubkey"), "{line}");
            let c = clean(&line);
            assert_eq!(c, kept);
            assert_eq!(clean(&c), c, "idempotent: the VM and the installer both clean");
        }
    }

    #[test]
    fn a_run_of_withheld_lines_is_one_counted_marker() {
        let lines = ["boot", "a token here", "the token again", "reading x.env", "up", "token", "token"];
        let out = clean_lines(lines);
        assert_eq!(
            out,
            vec![
                "boot".to_string(),
                withheld_run("token", 2, 27),
                withheld("env", 13),
                "up".to_string(),
                withheld_run("token", 2, 10),
            ]
        );
        assert!(out.iter().all(|l| clean(l) == l.as_str()), "markers clean to themselves");
        assert_eq!(withheld_run("key", 1, 7), withheld("key", 7));
    }
}