1use crate::encryption::ObjectKey;
10use crate::types::Hash;
11use thiserror::Error;
12
13mod tlv {
15 pub const HASH: u8 = 0;
17 pub const PATH: u8 = 4;
19 pub const DECRYPT_KEY: u8 = 5;
21}
22
23#[derive(Debug, Error)]
25pub enum NHashError {
26 #[error("Bech32 error: {0}")]
27 Bech32(String),
28 #[error("Invalid prefix: expected {expected}, got {got}")]
29 InvalidPrefix { expected: String, got: String },
30 #[error("Invalid hash length: expected 32 bytes, got {0}")]
31 InvalidHashLength(usize),
32 #[error("Invalid key length: expected 32 or 33 bytes, got {0}")]
33 InvalidKeyLength(usize),
34 #[error("Missing required field: {0}")]
35 MissingField(String),
36 #[error("TLV error: {0}")]
37 TlvError(String),
38 #[error("Hex error: {0}")]
39 HexError(#[from] hex::FromHexError),
40}
41
42#[derive(Debug, Clone, PartialEq)]
44pub struct NHashData {
45 pub hash: Hash,
47 pub decrypt_key: Option<ObjectKey>,
49}
50
51#[derive(Debug, Clone, PartialEq)]
53pub enum DecodeResult {
54 NHash(NHashData),
55}
56
57fn parse_tlv(data: &[u8]) -> Result<std::collections::HashMap<u8, Vec<Vec<u8>>>, NHashError> {
59 let mut result: std::collections::HashMap<u8, Vec<Vec<u8>>> = std::collections::HashMap::new();
60 let mut offset = 0;
61
62 while offset < data.len() {
63 if offset + 2 > data.len() {
64 return Err(NHashError::TlvError("unexpected end of data".into()));
65 }
66 let t = data[offset];
67 let l = data[offset + 1] as usize;
68 offset += 2;
69
70 if offset + l > data.len() {
71 return Err(NHashError::TlvError(format!(
72 "not enough data for type {}, need {} bytes",
73 t, l
74 )));
75 }
76 let v = data[offset..offset + l].to_vec();
77 offset += l;
78
79 result.entry(t).or_default().push(v);
80 }
81
82 Ok(result)
83}
84
85fn encode_tlv(tlv: &std::collections::HashMap<u8, Vec<Vec<u8>>>) -> Result<Vec<u8>, NHashError> {
87 let mut entries: Vec<u8> = Vec::new();
88
89 let mut keys: Vec<u8> = tlv.keys().copied().collect();
91 keys.sort();
92
93 for t in keys {
94 if let Some(values) = tlv.get(&t) {
95 for v in values {
96 if v.len() > 255 {
97 return Err(NHashError::TlvError(format!(
98 "value too long for type {}: {} bytes",
99 t,
100 v.len()
101 )));
102 }
103 entries.push(t);
104 entries.push(v.len() as u8);
105 entries.extend_from_slice(v);
106 }
107 }
108 }
109
110 Ok(entries)
111}
112
113fn encode_bech32(hrp: &str, data: &[u8]) -> Result<String, NHashError> {
116 use bech32::{Bech32, Hrp};
117
118 let hrp = Hrp::parse(hrp).map_err(|e| NHashError::Bech32(e.to_string()))?;
119 bech32::encode::<Bech32>(hrp, data).map_err(|e| NHashError::Bech32(e.to_string()))
120}
121
122fn decode_bech32(s: &str) -> Result<(String, Vec<u8>), NHashError> {
124 let (hrp, data) = bech32::decode(s).map_err(|e| NHashError::Bech32(e.to_string()))?;
125
126 Ok((hrp.to_string(), data))
127}
128
129pub fn nhash_encode(hash: &Hash) -> Result<String, NHashError> {
135 nhash_encode_full(&NHashData {
136 hash: *hash,
137 decrypt_key: None,
138 })
139}
140
141pub fn nhash_encode_full(data: &NHashData) -> Result<String, NHashError> {
147 let mut tlv: std::collections::HashMap<u8, Vec<Vec<u8>>> = std::collections::HashMap::new();
148 tlv.insert(tlv::HASH, vec![data.hash.to_vec()]);
149
150 if let Some(key) = &data.decrypt_key {
151 tlv.insert(tlv::DECRYPT_KEY, vec![key.as_bytes().to_vec()]);
152 }
153
154 encode_bech32("nhash", &encode_tlv(&tlv)?)
155}
156
157pub fn nhash_decode(code: &str) -> Result<NHashData, NHashError> {
159 let code = code.strip_prefix("hashtree:").unwrap_or(code);
161
162 let (prefix, data) = decode_bech32(code)?;
163
164 if prefix != "nhash" {
165 return Err(NHashError::InvalidPrefix {
166 expected: "nhash".into(),
167 got: prefix,
168 });
169 }
170
171 if data.len() == 32 {
173 let mut hash = [0u8; 32];
174 hash.copy_from_slice(&data);
175 return Ok(NHashData {
176 hash,
177 decrypt_key: None,
178 });
179 }
180
181 let tlv = parse_tlv(&data)?;
183
184 let hash_bytes = tlv
185 .get(&tlv::HASH)
186 .and_then(|v| v.first())
187 .ok_or_else(|| NHashError::MissingField("hash".into()))?;
188
189 if hash_bytes.len() != 32 {
190 return Err(NHashError::InvalidHashLength(hash_bytes.len()));
191 }
192
193 let mut hash = [0u8; 32];
194 hash.copy_from_slice(hash_bytes);
195
196 let _ = tlv.get(&tlv::PATH);
198
199 let decrypt_key = if let Some(keys) = tlv.get(&tlv::DECRYPT_KEY) {
200 if let Some(key_bytes) = keys.first() {
201 Some(
202 ObjectKey::from_bytes(key_bytes)
203 .map_err(|e| NHashError::TlvError(e.to_string()))?,
204 )
205 } else {
206 None
207 }
208 } else {
209 None
210 };
211
212 Ok(NHashData { hash, decrypt_key })
213}
214
215pub fn decode(code: &str) -> Result<DecodeResult, NHashError> {
221 let code = code.strip_prefix("hashtree:").unwrap_or(code);
222
223 if code.starts_with("nhash1") {
224 return Ok(DecodeResult::NHash(nhash_decode(code)?));
225 }
226
227 Err(NHashError::InvalidPrefix {
228 expected: "nhash1".into(),
229 got: code.chars().take(10).collect(),
230 })
231}
232
233pub fn is_nhash(value: &str) -> bool {
239 value.starts_with("nhash1")
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn test_nhash_hash_only_uses_tlv_encoding() {
248 let hash: Hash = [
249 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
250 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
251 0x1d, 0x1e, 0x1f, 0x20,
252 ];
253
254 let encoded = nhash_encode(&hash).unwrap();
255 assert!(encoded.starts_with("nhash1"));
256
257 let (_prefix, payload) = decode_bech32(&encoded).unwrap();
258 assert_ne!(payload.len(), 32, "hash-only nhash must use TLV payload");
259
260 let decoded = nhash_decode(&encoded).unwrap();
261 assert_eq!(decoded.hash, hash);
262 assert!(decoded.decrypt_key.is_none());
263 }
264
265 #[test]
266 fn test_nhash_decode_legacy_simple_hash_payload() {
267 let hash: Hash = [0x42; 32];
268 let encoded = encode_bech32("nhash", &hash).unwrap();
269
270 let decoded = nhash_decode(&encoded).unwrap();
271 assert_eq!(decoded.hash, hash);
272 assert!(decoded.decrypt_key.is_none());
273 }
274
275 #[test]
276 fn test_nhash_with_key() {
277 let hash: Hash = [0xaa; 32];
278 let key: [u8; 32] = [0xbb; 32];
279
280 let data = NHashData {
281 hash,
282 decrypt_key: Some(key.into()),
283 };
284
285 let encoded = nhash_encode_full(&data).unwrap();
286 assert!(encoded.starts_with("nhash1"));
287
288 let decoded = nhash_decode(&encoded).unwrap();
289 assert_eq!(decoded.hash, hash);
290 assert_eq!(decoded.decrypt_key, Some(key.into()));
291 }
292
293 #[test]
294 fn test_nhash_encode_full_matches_nhash_encode_when_no_key() {
295 let hash: Hash = [0xaa; 32];
296 let encoded_a = nhash_encode(&hash).unwrap();
297 let encoded_b = nhash_encode_full(&NHashData {
298 hash,
299 decrypt_key: None,
300 })
301 .unwrap();
302 assert_eq!(encoded_a, encoded_b);
303 }
304
305 #[test]
306 fn test_nhash_decode_ignores_embedded_path_tags() {
307 let mut tlv: std::collections::HashMap<u8, Vec<Vec<u8>>> = std::collections::HashMap::new();
308 tlv.insert(tlv::HASH, vec![vec![0x11; 32]]);
309 tlv.insert(tlv::PATH, vec![b"nested".to_vec(), b"file.txt".to_vec()]);
310
311 let payload = encode_tlv(&tlv).unwrap();
312 let encoded = encode_bech32("nhash", &payload).unwrap();
313
314 let decoded = nhash_decode(&encoded).unwrap();
315 assert_eq!(decoded.hash, [0x11; 32]);
316 assert!(decoded.decrypt_key.is_none());
317 }
318
319 #[test]
320 fn test_decode_generic() {
321 let hash: Hash = [0x11; 32];
322 let nhash = nhash_encode(&hash).unwrap();
323
324 match decode(&nhash).unwrap() {
325 DecodeResult::NHash(data) => assert_eq!(data.hash, hash),
326 }
327 }
328
329 #[test]
330 fn test_decode_rejects_non_nhash_prefix() {
331 let err = decode("nref1abc").unwrap_err();
332 match err {
333 NHashError::InvalidPrefix { expected, .. } => assert_eq!(expected, "nhash1"),
334 _ => panic!("expected InvalidPrefix"),
335 }
336 }
337
338 #[test]
339 fn test_is_nhash() {
340 assert!(is_nhash("nhash1abc"));
341 assert!(!is_nhash("nref1abc"));
342 assert!(!is_nhash("npub1abc"));
343 }
344}