nodedb_cluster/auth/
bundle.rs1use hmac::{Hmac, Mac};
15use sha2::Sha256;
16
17use crate::wire_version::WireVersion;
18
19#[derive(Debug, Clone)]
21pub struct AuthenticatedJoinBundle {
22 pub version: WireVersion,
24 pub bundle: Vec<u8>,
27 pub mac: [u8; 32],
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
33pub enum BundleError {
34 #[error("bundle MAC verification failed")]
35 MacMismatch,
36 #[error("hmac key length invalid")]
37 HmacKeyLength,
38 #[error("bundle version mismatch: expected {expected:?}, got {got:?}")]
39 VersionMismatch {
40 expected: WireVersion,
41 got: WireVersion,
42 },
43}
44
45pub fn derive_mac_key(cluster_secret: &[u8; 32], token_hash: &[u8; 32]) -> [u8; 32] {
51 let mut key = [0u8; 32];
52 for i in 0..32 {
53 key[i] = cluster_secret[i] ^ token_hash[i];
54 }
55 key
56}
57
58pub fn seal_bundle(
60 bundle_bytes: Vec<u8>,
61 cluster_secret: &[u8; 32],
62 token_hash: &[u8; 32],
63) -> Result<AuthenticatedJoinBundle, BundleError> {
64 let key = derive_mac_key(cluster_secret, token_hash);
65 let mut mac = <Hmac<Sha256>>::new_from_slice(&key).map_err(|_| BundleError::HmacKeyLength)?;
66 mac.update(&bundle_bytes);
67 let tag: [u8; 32] = mac.finalize().into_bytes().into();
68 Ok(AuthenticatedJoinBundle {
69 version: WireVersion::CURRENT,
70 bundle: bundle_bytes,
71 mac: tag,
72 })
73}
74
75pub fn open_bundle<'a>(
79 sealed: &'a AuthenticatedJoinBundle,
80 cluster_secret: &[u8; 32],
81 token_hash: &[u8; 32],
82) -> Result<&'a [u8], BundleError> {
83 if sealed.version != WireVersion::CURRENT {
84 return Err(BundleError::VersionMismatch {
85 expected: WireVersion::CURRENT,
86 got: sealed.version,
87 });
88 }
89 let key = derive_mac_key(cluster_secret, token_hash);
90 let mut mac = <Hmac<Sha256>>::new_from_slice(&key).map_err(|_| BundleError::HmacKeyLength)?;
91 mac.update(&sealed.bundle);
92 mac.verify_slice(&sealed.mac)
93 .map_err(|_| BundleError::MacMismatch)?;
94 Ok(&sealed.bundle)
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn seal_and_open_roundtrip() {
103 let secret = [0x1Au8; 32];
104 let token_hash = [0x2Bu8; 32];
105 let payload = b"hello cluster".to_vec();
106 let sealed = seal_bundle(payload.clone(), &secret, &token_hash).unwrap();
107 let out = open_bundle(&sealed, &secret, &token_hash).unwrap();
108 assert_eq!(out, payload.as_slice());
109 }
110
111 #[test]
112 fn tampered_bundle_rejected() {
113 let secret = [0x3Cu8; 32];
114 let token_hash = [0x4Du8; 32];
115 let mut sealed = seal_bundle(b"real payload".to_vec(), &secret, &token_hash).unwrap();
116 sealed.bundle[0] ^= 0xFF;
118 assert_eq!(
119 open_bundle(&sealed, &secret, &token_hash).unwrap_err(),
120 BundleError::MacMismatch
121 );
122 }
123
124 #[test]
125 fn wrong_cluster_secret_rejected() {
126 let secret = [0x5Eu8; 32];
127 let wrong = [0x6Fu8; 32];
128 let token_hash = [0x70u8; 32];
129 let sealed = seal_bundle(b"payload".to_vec(), &secret, &token_hash).unwrap();
130 assert_eq!(
131 open_bundle(&sealed, &wrong, &token_hash).unwrap_err(),
132 BundleError::MacMismatch
133 );
134 }
135
136 #[test]
137 fn wrong_token_hash_rejected() {
138 let secret = [0x81u8; 32];
139 let token_hash = [0x92u8; 32];
140 let wrong_hash = [0xA3u8; 32];
141 let sealed = seal_bundle(b"payload".to_vec(), &secret, &token_hash).unwrap();
142 assert_eq!(
143 open_bundle(&sealed, &secret, &wrong_hash).unwrap_err(),
144 BundleError::MacMismatch
145 );
146 }
147
148 #[test]
149 fn version_mismatch_rejected() {
150 let secret = [0xB4u8; 32];
151 let token_hash = [0xC5u8; 32];
152 let mut sealed = seal_bundle(b"payload".to_vec(), &secret, &token_hash).unwrap();
153 sealed.version = WireVersion::V1;
154 assert!(matches!(
155 open_bundle(&sealed, &secret, &token_hash).unwrap_err(),
156 BundleError::VersionMismatch { .. }
157 ));
158 }
159
160 #[test]
161 fn derive_mac_key_xors_both_inputs() {
162 let secret = [0xFFu8; 32];
163 let hash = [0x0Fu8; 32];
164 let key = derive_mac_key(&secret, &hash);
165 assert!(key.iter().all(|&b| b == 0xF0));
167 }
168}