use crate::proto::FormatError;
use crate::proto::codec::{decode_node_key, encode_node_key};
use crate::proto::frame::label;
use crate::proto::path::Segment;
use zeroize::Zeroizing;
use crate::keys::kdf::{derive32, info};
use crate::keys::owner::OwnerKeys;
use crate::keys::random::random_bytes;
#[derive(Clone)]
pub struct NodeKey(Zeroizing<[u8; 32]>);
impl std::fmt::Debug for NodeKey {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("NodeKey(<redacted>)")
}
}
impl NodeKey {
pub fn generate() -> NodeKey {
NodeKey(random_bytes::<32>())
}
pub fn from_bytes(bytes: Zeroizing<[u8; 32]>) -> NodeKey {
NodeKey(bytes)
}
pub fn parse(s: &str) -> Result<NodeKey, FormatError> {
decode_node_key(s).map(NodeKey)
}
pub fn encode(&self) -> Zeroizing<String> {
encode_node_key(&self.0)
}
pub(crate) fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn child(&self, segment: &Segment) -> NodeKey {
NodeKey(derive32(
self.0.as_slice(),
&info(label::CHILD, segment.as_str().as_bytes()),
))
}
pub fn descend(&self, segments: &[Segment]) -> NodeKey {
segments
.iter()
.fold(self.clone(), |key, segment| key.child(segment))
}
pub fn owner(&self) -> OwnerKeys {
OwnerKeys::from_node(self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proto::path::EnvPath;
fn root() -> NodeKey {
let mut b = [0u8; 32];
for (i, x) in b.iter_mut().enumerate() {
*x = i as u8;
}
NodeKey::from_bytes(Zeroizing::new(b))
}
#[test]
fn derivation_is_deterministic_and_segment_specific() {
let prod = Segment::new("prod").unwrap();
let dev = Segment::new("dev").unwrap();
assert_eq!(
root().child(&prod).as_bytes(),
root().child(&prod).as_bytes()
);
assert_ne!(
root().child(&prod).as_bytes(),
root().child(&dev).as_bytes()
);
assert_ne!(root().child(&prod).as_bytes(), root().as_bytes());
}
#[test]
fn descend_equals_chained_children() {
let path = EnvPath::parse("acme/prod/eu").unwrap();
let tail = &path.segments()[1..];
let chained = root().child(&tail[0]).child(&tail[1]);
assert_eq!(root().descend(tail).as_bytes(), chained.as_bytes());
}
#[test]
fn encode_parse_roundtrip_and_redacted_debug() {
let k = NodeKey::generate();
let s = k.encode();
assert!(s.starts_with("gvk1_"));
assert_eq!(NodeKey::parse(&s).unwrap().as_bytes(), k.as_bytes());
assert_eq!(format!("{k:?}"), "NodeKey(<redacted>)");
}
#[test]
fn a_key_of_another_version_is_refused() {
let other = crate::proto::codec::encode_checked("gvk2_", &[9u8; 32]);
assert!(matches!(
NodeKey::parse(&other),
Err(FormatError::UnknownVersion { found: '2' })
));
}
#[test]
fn generated_keys_differ() {
assert_ne!(
NodeKey::generate().as_bytes(),
NodeKey::generate().as_bytes()
);
}
}