Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 119 additions & 22 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,11 @@ chrono = { version = "0.4", features = ["serde"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
anyhow = "1"
sha2 = "0.10"
# Key issuance needs a CSPRNG; `secrets.token_hex(16)` in the Python CLI.
rand = "0.8"
sha2 = "0.11"
# The admin CLI (issue / show / revoke / suspend …) that replaced the
# Python scripts.
clap = { version = "4", features = ["derive", "env"] }
getrandom = "0.3"

[dev-dependencies]
tower = { version = "0.5", features = ["util"] }
Expand Down
21 changes: 17 additions & 4 deletions src/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
//! The raw value is generated once, shown once by `issue`, and never
//! persisted or logged.

use rand::RngCore;
use sha2::{Digest, Sha256};

pub const KEY_PREFIX: &str = "slk_";
Expand All @@ -18,16 +17,30 @@ pub const KEY_PREFIX: &str = "slk_";
/// `KEY_PREFIX + secrets.token_hex(16)`.
pub fn generate_key() -> String {
let mut bytes = [0u8; 16];
// OsRng, not a seeded PRNG — this is key material.
rand::rngs::OsRng.fill_bytes(&mut bytes);
// Straight from the OS CSPRNG. This used `rand::rngs::OsRng`, but
// rand 0.10 removed OsRng from that path entirely; rather than chase
// it across another rand reshuffle, `getrandom` is the thing rand was
// calling underneath anyway, and for key material the shorter, more
// obviously-correct call is the better dependency.
getrandom::fill(&mut bytes).expect("OS entropy is available");
let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
format!("{KEY_PREFIX}{hex}")
}

pub fn hash_key(raw_key: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(raw_key.as_bytes());
format!("{:x}", hasher.finalize())
// Explicit hex rather than `format!("{:x}", ..)`: sha2 0.11 changed
// finalize() to return a type that no longer implements LowerHex.
//
// This function's output IS the stored key_hash. A silently different
// encoding would orphan every license row rather than fail a build,
// which is why the fixed vector below is a test and not a comment.
hasher
.finalize()
.iter()
.map(|b| format!("{b:02x}"))
.collect()
}

/// The last four characters, stored alongside the hash so an operator can
Expand Down