1use std::fmt;
7use std::io::{self, Read, Write};
8use std::net::{SocketAddr, TcpStream};
9use std::time::Duration;
10
11const SAM_VERSION: &str = "3.1";
13
14const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
16
17const READ_TIMEOUT: Duration = Duration::from_secs(30);
19
20const I2P_BASE64_ALPHABET: &[u8; 64] =
24 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~";
25
26fn i2p_base64_decode_table() -> [u8; 256] {
28 let mut table = [255u8; 256];
29 for (i, &ch) in I2P_BASE64_ALPHABET.iter().enumerate() {
30 table[ch as usize] = i as u8;
31 }
32 table[b'=' as usize] = 0;
34 table
35}
36
37pub fn i2p_base64_encode(data: &[u8]) -> String {
39 let mut out = String::with_capacity((data.len() + 2) / 3 * 4);
40 for chunk in data.chunks(3) {
41 let b0 = chunk[0] as u32;
42 let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
43 let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
44 let triple = (b0 << 16) | (b1 << 8) | b2;
45
46 out.push(I2P_BASE64_ALPHABET[((triple >> 18) & 0x3F) as usize] as char);
47 out.push(I2P_BASE64_ALPHABET[((triple >> 12) & 0x3F) as usize] as char);
48
49 if chunk.len() > 1 {
50 out.push(I2P_BASE64_ALPHABET[((triple >> 6) & 0x3F) as usize] as char);
51 } else {
52 out.push('=');
53 }
54
55 if chunk.len() > 2 {
56 out.push(I2P_BASE64_ALPHABET[(triple & 0x3F) as usize] as char);
57 } else {
58 out.push('=');
59 }
60 }
61 out
62}
63
64pub fn i2p_base64_decode(s: &str) -> Result<Vec<u8>, SamError> {
66 let table = i2p_base64_decode_table();
67 let bytes = s.as_bytes();
68
69 if bytes.len() % 4 != 0 {
70 return Err(SamError::InvalidResponse(format!(
71 "invalid I2P base64 length: {}",
72 bytes.len()
73 )));
74 }
75
76 let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
77
78 for chunk in bytes.chunks(4) {
79 let mut vals = [0u8; 4];
80 let mut pad_count = 0;
81 for (i, &b) in chunk.iter().enumerate() {
82 if b == b'=' {
83 pad_count += 1;
84 vals[i] = 0;
85 } else {
86 let v = table[b as usize];
87 if v == 255 {
88 return Err(SamError::InvalidResponse(format!(
89 "invalid I2P base64 character: {:?}",
90 b as char
91 )));
92 }
93 vals[i] = v;
94 }
95 }
96
97 let triple = (vals[0] as u32) << 18
98 | (vals[1] as u32) << 12
99 | (vals[2] as u32) << 6
100 | (vals[3] as u32);
101
102 out.push((triple >> 16) as u8);
103 if pad_count < 2 {
104 out.push((triple >> 8) as u8);
105 }
106 if pad_count < 1 {
107 out.push(triple as u8);
108 }
109 }
110
111 Ok(out)
112}
113
114#[derive(Debug)]
118pub enum SamError {
119 Io(io::Error),
121 Protocol(String),
123 InvalidResponse(String),
125}
126
127impl fmt::Display for SamError {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 match self {
130 SamError::Io(e) => write!(f, "SAM I/O error: {}", e),
131 SamError::Protocol(msg) => write!(f, "SAM protocol error: {}", msg),
132 SamError::InvalidResponse(msg) => write!(f, "SAM invalid response: {}", msg),
133 }
134 }
135}
136
137impl From<io::Error> for SamError {
138 fn from(e: io::Error) -> Self {
139 SamError::Io(e)
140 }
141}
142
143#[derive(Clone, Debug)]
147pub struct Destination {
148 pub data: Vec<u8>,
150}
151
152impl Destination {
153 pub fn to_i2p_base64(&self) -> String {
155 i2p_base64_encode(&self.data)
156 }
157
158 pub fn from_i2p_base64(s: &str) -> Result<Self, SamError> {
160 let data = i2p_base64_decode(s)?;
161 Ok(Destination { data })
162 }
163
164 pub fn base32_address(&self) -> String {
167 let hash = rns_crypto::sha256::sha256(&self.data);
168 let encoded = base32_encode(&hash);
169 format!("{}.b32.i2p", encoded)
170 }
171}
172
173#[derive(Clone, Debug)]
175pub struct KeyPair {
176 pub destination: Destination,
177 pub private_key: Vec<u8>,
179}
180
181const BASE32_ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
184
185fn base32_encode(data: &[u8]) -> String {
187 let mut out = String::with_capacity((data.len() * 8 + 4) / 5);
188 let mut buffer: u64 = 0;
189 let mut bits: u32 = 0;
190
191 for &byte in data {
192 buffer = (buffer << 8) | byte as u64;
193 bits += 8;
194 while bits >= 5 {
195 bits -= 5;
196 out.push(BASE32_ALPHABET[((buffer >> bits) & 0x1F) as usize] as char);
197 }
198 }
199 if bits > 0 {
200 out.push(BASE32_ALPHABET[((buffer << (5 - bits)) & 0x1F) as usize] as char);
201 }
202 out
203}
204
205fn parse_kv(token: &str) -> Option<(&str, &str)> {
209 let eq = token.find('=')?;
210 Some((&token[..eq], &token[eq + 1..]))
211}
212
213fn read_line(stream: &mut TcpStream) -> Result<String, SamError> {
220 let mut line = Vec::new();
221 let mut byte = [0u8; 1];
222 loop {
223 match stream.read_exact(&mut byte) {
224 Ok(()) => {
225 if byte[0] == b'\n' {
226 break;
227 }
228 line.push(byte[0]);
229 }
230 Err(e) => return Err(SamError::Io(e)),
231 }
232 }
233 String::from_utf8(line)
234 .map_err(|e| SamError::InvalidResponse(format!("non-UTF8 SAM response: {}", e)))
235}
236
237fn hello_connect(sam_addr: &SocketAddr) -> Result<TcpStream, SamError> {
239 let mut stream = TcpStream::connect_timeout(sam_addr, CONNECT_TIMEOUT)?;
240 stream.set_read_timeout(Some(READ_TIMEOUT))?;
241 stream.set_write_timeout(Some(READ_TIMEOUT))?;
242
243 write!(stream, "HELLO VERSION MIN={v} MAX={v}\n", v = SAM_VERSION)?;
245 stream.flush()?;
246
247 let line = read_line(&mut stream)?;
249 let resp = parse_sam_response(&line)?;
251 if resp.command != "HELLO" || resp.subcommand != "REPLY" {
252 return Err(SamError::InvalidResponse(format!(
253 "expected HELLO REPLY, got: {}",
254 line
255 )));
256 }
257 check_result(&resp)?;
258
259 Ok(stream)
260}
261
262struct SamResponse {
264 command: String,
265 subcommand: String,
266 params: Vec<(String, String)>,
267}
268
269impl SamResponse {
270 fn get(&self, key: &str) -> Option<&str> {
271 for (k, v) in &self.params {
272 if k == key {
273 return Some(v);
274 }
275 }
276 None
277 }
278}
279
280fn parse_sam_response(line: &str) -> Result<SamResponse, SamError> {
284 let mut parts = line.splitn(3, ' ');
285 let command = parts
286 .next()
287 .ok_or_else(|| SamError::InvalidResponse("empty response".into()))?
288 .to_string();
289 let subcommand = parts
290 .next()
291 .unwrap_or("")
292 .to_string();
293 let rest = parts.next().unwrap_or("");
294
295 let mut params = Vec::new();
296 for token in rest.split_whitespace() {
299 if let Some((k, v)) = parse_kv(token) {
300 params.push((k.to_string(), v.to_string()));
301 }
302 }
303
304 Ok(SamResponse {
305 command,
306 subcommand,
307 params,
308 })
309}
310
311fn check_result(resp: &SamResponse) -> Result<(), SamError> {
313 match resp.get("RESULT") {
314 Some("OK") => Ok(()),
315 Some(result) => {
316 let message = resp.get("MESSAGE").unwrap_or("(no message)");
317 Err(SamError::Protocol(format!(
318 "RESULT={} MESSAGE={}",
319 result, message
320 )))
321 }
322 None => Ok(()), }
324}
325
326pub fn dest_generate(sam_addr: &SocketAddr) -> Result<KeyPair, SamError> {
331 let mut stream = hello_connect(sam_addr)?;
332
333 write!(stream, "DEST GENERATE SIGNATURE_TYPE=7\n")?;
334 stream.flush()?;
335
336 let line = read_line(&mut stream)?;
337 let resp = parse_sam_response(&line)?;
338
339 if resp.command != "DEST" || resp.subcommand != "REPLY" {
340 return Err(SamError::InvalidResponse(format!(
341 "expected DEST REPLY, got: {}",
342 line
343 )));
344 }
345
346 let pub_b64 = resp.get("PUB").ok_or_else(|| {
347 SamError::InvalidResponse("DEST REPLY missing PUB".into())
348 })?;
349 let priv_b64 = resp.get("PRIV").ok_or_else(|| {
350 SamError::InvalidResponse("DEST REPLY missing PRIV".into())
351 })?;
352
353 let dest_data = i2p_base64_decode(pub_b64)?;
354 let priv_data = i2p_base64_decode(priv_b64)?;
355
356 Ok(KeyPair {
357 destination: Destination { data: dest_data },
358 private_key: priv_data,
359 })
360}
361
362pub fn session_create(
365 sam_addr: &SocketAddr,
366 session_id: &str,
367 private_key_b64: &str,
368) -> Result<TcpStream, SamError> {
369 let mut stream = hello_connect(sam_addr)?;
370
371 write!(
372 stream,
373 "SESSION CREATE STYLE=STREAM ID={} DESTINATION={} SIGNATURE_TYPE=7\n",
374 session_id, private_key_b64,
375 )?;
376 stream.flush()?;
377
378 let line = read_line(&mut stream)?;
379 let resp = parse_sam_response(&line)?;
380
381 if resp.command != "SESSION" || resp.subcommand != "STATUS" {
382 return Err(SamError::InvalidResponse(format!(
383 "expected SESSION STATUS, got: {}",
384 line
385 )));
386 }
387 check_result(&resp)?;
388
389 Ok(stream)
391}
392
393pub fn stream_connect(
396 sam_addr: &SocketAddr,
397 session_id: &str,
398 destination: &str,
399) -> Result<TcpStream, SamError> {
400 let mut stream = hello_connect(sam_addr)?;
401
402 write!(
403 stream,
404 "STREAM CONNECT ID={} DESTINATION={} SILENT=false\n",
405 session_id, destination,
406 )?;
407 stream.flush()?;
408
409 let line = read_line(&mut stream)?;
410 let resp = parse_sam_response(&line)?;
411
412 if resp.command != "STREAM" || resp.subcommand != "STATUS" {
413 return Err(SamError::InvalidResponse(format!(
414 "expected STREAM STATUS, got: {}",
415 line
416 )));
417 }
418 check_result(&resp)?;
419
420 stream.set_read_timeout(None)?;
423 stream.set_write_timeout(None)?;
424
425 Ok(stream)
426}
427
428pub fn stream_accept(
431 sam_addr: &SocketAddr,
432 session_id: &str,
433) -> Result<(TcpStream, Destination), SamError> {
434 let mut stream = hello_connect(sam_addr)?;
435
436 write!(
437 stream,
438 "STREAM ACCEPT ID={} SILENT=false\n",
439 session_id,
440 )?;
441 stream.flush()?;
442
443 let line = read_line(&mut stream)?;
444 let resp = parse_sam_response(&line)?;
445
446 if resp.command != "STREAM" || resp.subcommand != "STATUS" {
447 return Err(SamError::InvalidResponse(format!(
448 "expected STREAM STATUS, got: {}",
449 line
450 )));
451 }
452 check_result(&resp)?;
453
454 let dest_line = read_line(&mut stream)?;
457 let remote_dest = Destination::from_i2p_base64(dest_line.trim())?;
458
459 stream.set_read_timeout(None)?;
461 stream.set_write_timeout(None)?;
462
463 Ok((stream, remote_dest))
464}
465
466pub fn naming_lookup(
469 sam_addr: &SocketAddr,
470 name: &str,
471) -> Result<Destination, SamError> {
472 let mut stream = hello_connect(sam_addr)?;
473 naming_lookup_on(&mut stream, name)
474}
475
476pub fn naming_lookup_on(
480 stream: &mut TcpStream,
481 name: &str,
482) -> Result<Destination, SamError> {
483 write!(stream, "NAMING LOOKUP NAME={}\n", name)?;
484 stream.flush()?;
485
486 let line = read_line(stream)?;
487 let resp = parse_sam_response(&line)?;
488
489 if resp.command != "NAMING" || resp.subcommand != "REPLY" {
490 return Err(SamError::InvalidResponse(format!(
491 "expected NAMING REPLY, got: {}",
492 line
493 )));
494 }
495 check_result(&resp)?;
496
497 let value = resp.get("VALUE").ok_or_else(|| {
498 SamError::InvalidResponse("NAMING REPLY missing VALUE".into())
499 })?;
500
501 Destination::from_i2p_base64(value)
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
511 fn base64_encode_empty() {
512 assert_eq!(i2p_base64_encode(b""), "");
513 }
514
515 #[test]
516 fn base64_roundtrip() {
517 let data: Vec<u8> = (0..=255).collect();
518 let encoded = i2p_base64_encode(&data);
519 let decoded = i2p_base64_decode(&encoded).unwrap();
520 assert_eq!(decoded, data);
521 }
522
523 #[test]
524 fn base64_known_value() {
525 let encoded = i2p_base64_encode(b"Hello");
528 assert_eq!(encoded, "SGVsbG8=");
529 let decoded = i2p_base64_decode(&encoded).unwrap();
530 assert_eq!(decoded, b"Hello");
531 }
532
533 #[test]
534 fn base64_i2p_specific_chars() {
535 let data = [0xFB, 0xEF, 0xBE];
538 let encoded = i2p_base64_encode(&data);
539 assert!(encoded.contains('-') || encoded.contains('~'));
540 let decoded = i2p_base64_decode(&encoded).unwrap();
542 assert_eq!(decoded, data);
543 }
544
545 #[test]
546 fn base64_all_alphabet_chars_roundtrip() {
547 let data: Vec<u8> = (0..48).collect();
549 let encoded = i2p_base64_encode(&data);
550 let decoded = i2p_base64_decode(&encoded).unwrap();
551 assert_eq!(decoded, data);
552 }
553
554 #[test]
555 fn base64_padding_1() {
556 let encoded = i2p_base64_encode(&[0xFF]);
558 assert_eq!(encoded.len(), 4);
559 assert!(encoded.ends_with("=="));
560 let decoded = i2p_base64_decode(&encoded).unwrap();
561 assert_eq!(decoded, vec![0xFF]);
562 }
563
564 #[test]
565 fn base64_padding_2() {
566 let encoded = i2p_base64_encode(&[0xFF, 0xFE]);
568 assert_eq!(encoded.len(), 4);
569 assert!(encoded.ends_with('='));
570 let decoded = i2p_base64_decode(&encoded).unwrap();
571 assert_eq!(decoded, vec![0xFF, 0xFE]);
572 }
573
574 #[test]
575 fn base64_no_padding() {
576 let encoded = i2p_base64_encode(&[0xFF, 0xFE, 0xFD]);
578 assert_eq!(encoded.len(), 4);
579 assert!(!encoded.contains('='));
580 let decoded = i2p_base64_decode(&encoded).unwrap();
581 assert_eq!(decoded, vec![0xFF, 0xFE, 0xFD]);
582 }
583
584 #[test]
585 fn base64_decode_invalid_char() {
586 let result = i2p_base64_decode("!!!=");
587 assert!(result.is_err());
588 }
589
590 #[test]
591 fn base64_decode_invalid_length() {
592 let result = i2p_base64_decode("ABC");
593 assert!(result.is_err());
594 }
595
596 #[test]
599 fn base32_encode_empty() {
600 assert_eq!(base32_encode(&[]), "");
601 }
602
603 #[test]
604 fn base32_encode_known() {
605 let result = base32_encode(b"Hello");
607 assert_eq!(result, "jbswy3dp");
608 }
609
610 #[test]
611 fn base32_encode_sha256() {
612 let hash = rns_crypto::sha256::sha256(b"");
614 let encoded = base32_encode(&hash);
615 assert_eq!(encoded.len(), 52);
617 assert!(encoded.chars().all(|c| c.is_ascii_lowercase() || ('2'..='7').contains(&c)));
619 }
620
621 #[test]
624 fn destination_base32_address() {
625 let dest = Destination {
626 data: vec![0x42; 387], };
628 let addr = dest.base32_address();
629 assert!(addr.ends_with(".b32.i2p"));
630 assert_eq!(addr.len(), 60);
632 }
633
634 #[test]
635 fn destination_roundtrip_base64() {
636 let data: Vec<u8> = (0..=255).cycle().take(387).collect();
637 let dest = Destination { data: data.clone() };
638 let b64 = dest.to_i2p_base64();
639 let dest2 = Destination::from_i2p_base64(&b64).unwrap();
640 assert_eq!(dest2.data, data);
641 }
642
643 #[test]
646 fn parse_hello_reply() {
647 let line = "HELLO REPLY RESULT=OK VERSION=3.1";
648 let resp = parse_sam_response(line).unwrap();
649 assert_eq!(resp.command, "HELLO");
650 assert_eq!(resp.subcommand, "REPLY");
651 assert_eq!(resp.get("RESULT"), Some("OK"));
652 assert_eq!(resp.get("VERSION"), Some("3.1"));
653 }
654
655 #[test]
656 fn parse_session_status_ok() {
657 let line = "SESSION STATUS RESULT=OK DESTINATION=AAAA";
658 let resp = parse_sam_response(line).unwrap();
659 assert_eq!(resp.command, "SESSION");
660 assert_eq!(resp.subcommand, "STATUS");
661 assert_eq!(resp.get("RESULT"), Some("OK"));
662 assert_eq!(resp.get("DESTINATION"), Some("AAAA"));
663 }
664
665 #[test]
666 fn parse_session_status_error() {
667 let line = "SESSION STATUS RESULT=DUPLICATED_ID";
668 let resp = parse_sam_response(line).unwrap();
669 assert_eq!(resp.get("RESULT"), Some("DUPLICATED_ID"));
670 let err = check_result(&resp);
671 assert!(err.is_err());
672 }
673
674 #[test]
675 fn parse_stream_status_error() {
676 let line = "STREAM STATUS RESULT=CANT_REACH_PEER MESSAGE=unreachable";
677 let resp = parse_sam_response(line).unwrap();
678 assert_eq!(resp.get("RESULT"), Some("CANT_REACH_PEER"));
679 assert_eq!(resp.get("MESSAGE"), Some("unreachable"));
680 let err = check_result(&resp);
681 assert!(err.is_err());
682 if let Err(SamError::Protocol(msg)) = err {
683 assert!(msg.contains("CANT_REACH_PEER"));
684 }
685 }
686
687 #[test]
688 fn parse_naming_reply() {
689 let line = "NAMING REPLY RESULT=OK NAME=test.b32.i2p VALUE=AAAA";
690 let resp = parse_sam_response(line).unwrap();
691 assert_eq!(resp.command, "NAMING");
692 assert_eq!(resp.subcommand, "REPLY");
693 assert_eq!(resp.get("NAME"), Some("test.b32.i2p"));
694 assert_eq!(resp.get("VALUE"), Some("AAAA"));
695 }
696
697 #[test]
698 fn parse_naming_not_found() {
699 let line = "NAMING REPLY RESULT=KEY_NOT_FOUND";
700 let resp = parse_sam_response(line).unwrap();
701 let err = check_result(&resp);
702 assert!(err.is_err());
703 }
704
705 #[test]
706 fn parse_dest_reply() {
707 let line = "DEST REPLY PUB=AAAA PRIV=BBBB";
708 let resp = parse_sam_response(line).unwrap();
709 assert_eq!(resp.command, "DEST");
710 assert_eq!(resp.subcommand, "REPLY");
711 assert_eq!(resp.get("PUB"), Some("AAAA"));
712 assert_eq!(resp.get("PRIV"), Some("BBBB"));
713 }
714
715 #[test]
716 fn parse_stream_status_timeout() {
717 let line = "STREAM STATUS RESULT=TIMEOUT";
718 let resp = parse_sam_response(line).unwrap();
719 let err = check_result(&resp);
720 assert!(err.is_err());
721 if let Err(SamError::Protocol(msg)) = err {
722 assert!(msg.contains("TIMEOUT"));
723 }
724 }
725
726 #[test]
727 fn check_result_ok() {
728 let line = "TEST REPLY RESULT=OK";
729 let resp = parse_sam_response(line).unwrap();
730 assert!(check_result(&resp).is_ok());
731 }
732
733 #[test]
734 fn check_result_no_result_field() {
735 let line = "TEST REPLY FOO=BAR";
736 let resp = parse_sam_response(line).unwrap();
737 assert!(check_result(&resp).is_ok());
739 }
740
741 #[test]
742 fn sam_error_display() {
743 let io_err = SamError::Io(io::Error::new(io::ErrorKind::Other, "test"));
744 assert!(format!("{}", io_err).contains("test"));
745
746 let proto_err = SamError::Protocol("CANT_REACH_PEER".into());
747 assert!(format!("{}", proto_err).contains("CANT_REACH_PEER"));
748
749 let inv_err = SamError::InvalidResponse("bad".into());
750 assert!(format!("{}", inv_err).contains("bad"));
751 }
752}