Skip to main content

hashtree_core/
visibility.rs

1//! Tree visibility helpers
2
3use std::str::FromStr;
4
5/// Tree visibility modes
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum TreeVisibility {
8    Public,
9    LinkVisible,
10    Private,
11}
12
13impl TreeVisibility {
14    pub fn as_str(&self) -> &'static str {
15        match self {
16            TreeVisibility::Public => "public",
17            TreeVisibility::LinkVisible => "link-visible",
18            TreeVisibility::Private => "private",
19        }
20    }
21}
22
23impl FromStr for TreeVisibility {
24    type Err = String;
25
26    fn from_str(s: &str) -> Result<Self, Self::Err> {
27        let normalized = s.trim().to_ascii_lowercase();
28        match normalized.as_str() {
29            "public" => Ok(TreeVisibility::Public),
30            "link-visible" | "link_visible" | "linkvisible" => Ok(TreeVisibility::LinkVisible),
31            "private" => Ok(TreeVisibility::Private),
32            _ => Err(format!("invalid visibility: {}", s)),
33        }
34    }
35}
36
37/// XOR two 32-byte keys (used for link-visible key masking)
38pub fn xor_keys(a: &[u8; 32], b: &[u8; 32]) -> [u8; 32] {
39    let mut out = [0u8; 32];
40    for i in 0..32 {
41        out[i] = a[i] ^ b[i];
42    }
43    out
44}
45
46/// Opaque link secret or masked key. Its first byte need not identify a suite.
47#[derive(Clone, Copy, PartialEq, Eq, Hash)]
48pub enum LinkKeyBytes {
49    Legacy([u8; 32]),
50    Versioned([u8; 33]),
51}
52
53impl std::fmt::Debug for LinkKeyBytes {
54    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55        f.write_str("LinkKeyBytes(..)")
56    }
57}
58
59impl LinkKeyBytes {
60    pub fn from_hex(value: &str) -> Result<Self, crate::CryptoError> {
61        Self::from_bytes(&hex::decode(value).map_err(|_| crate::CryptoError::InvalidKeyLength)?)
62    }
63    pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::CryptoError> {
64        match bytes.len() {
65            32 => Ok(Self::Legacy(bytes.try_into().unwrap())),
66            33 => Ok(Self::Versioned(bytes.try_into().unwrap())),
67            _ => Err(crate::CryptoError::InvalidKeyLength),
68        }
69    }
70}
71
72impl AsRef<[u8]> for LinkKeyBytes {
73    fn as_ref(&self) -> &[u8] {
74        match self {
75            Self::Legacy(bytes) => bytes,
76            Self::Versioned(bytes) => bytes,
77        }
78    }
79}
80
81/// Preserve an existing 32-byte share secret by encoding a CHK root key in its
82/// equivalent legacy form. Randomized roots always require a 33-byte secret.
83pub fn mask_object_key(
84    key: &crate::ObjectKey,
85    secret: &[u8],
86) -> Result<Vec<u8>, crate::CryptoError> {
87    match (key.mode(), secret.len()) {
88        (crate::EncryptionMode::ChkV1, 32) => xor_key_bytes(&key.raw_key(), secret),
89        (crate::EncryptionMode::LegacyChk, 33) => {
90            let mut encoded = [1u8; 33];
91            encoded[1..].copy_from_slice(key.as_bytes());
92            xor_key_bytes(&encoded, secret)
93        }
94        _ => xor_key_bytes(key.as_bytes(), secret),
95    }
96}
97
98/// Mask or unmask a legacy or versioned key with an equally sized link secret.
99/// The masked bytes and link secret are opaque, not parsed encryption keys.
100pub fn xor_key_bytes(a: &[u8], b: &[u8]) -> Result<Vec<u8>, crate::crypto::CryptoError> {
101    if a.len() != b.len() || !matches!(a.len(), 32 | 33) {
102        return Err(crate::crypto::CryptoError::InvalidKeyLength);
103    }
104    Ok(a.iter().zip(b).map(|(a, b)| a ^ b).collect())
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn test_visibility_parse() {
113        assert_eq!(
114            TreeVisibility::from_str("public").unwrap(),
115            TreeVisibility::Public
116        );
117        assert_eq!(
118            TreeVisibility::from_str("link-visible").unwrap(),
119            TreeVisibility::LinkVisible
120        );
121        assert_eq!(
122            TreeVisibility::from_str("link_visible").unwrap(),
123            TreeVisibility::LinkVisible
124        );
125        assert_eq!(
126            TreeVisibility::from_str("private").unwrap(),
127            TreeVisibility::Private
128        );
129        assert!(TreeVisibility::from_str("unknown").is_err());
130    }
131
132    #[test]
133    fn test_xor_keys_roundtrip() {
134        let a = [0x11u8; 32];
135        let b = [0x22u8; 32];
136        let masked = xor_keys(&a, &b);
137        let unmasked = xor_keys(&masked, &b);
138        assert_eq!(unmasked, a);
139    }
140}