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