use std::fmt;
use std::io::{self, Read, Write};
use std::net::{SocketAddr, TcpStream};
use std::time::Duration;
const SAM_VERSION: &str = "3.1";
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
const READ_TIMEOUT: Duration = Duration::from_secs(30);
const I2P_BASE64_ALPHABET: &[u8; 64] =
b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~";
fn i2p_base64_decode_table() -> [u8; 256] {
let mut table = [255u8; 256];
for (i, &ch) in I2P_BASE64_ALPHABET.iter().enumerate() {
table[ch as usize] = i as u8;
}
table[b'=' as usize] = 0;
table
}
pub fn i2p_base64_encode(data: &[u8]) -> String {
let mut out = String::with_capacity((data.len() + 2) / 3 * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
let triple = (b0 << 16) | (b1 << 8) | b2;
out.push(I2P_BASE64_ALPHABET[((triple >> 18) & 0x3F) as usize] as char);
out.push(I2P_BASE64_ALPHABET[((triple >> 12) & 0x3F) as usize] as char);
if chunk.len() > 1 {
out.push(I2P_BASE64_ALPHABET[((triple >> 6) & 0x3F) as usize] as char);
} else {
out.push('=');
}
if chunk.len() > 2 {
out.push(I2P_BASE64_ALPHABET[(triple & 0x3F) as usize] as char);
} else {
out.push('=');
}
}
out
}
pub fn i2p_base64_decode(s: &str) -> Result<Vec<u8>, SamError> {
let table = i2p_base64_decode_table();
let bytes = s.as_bytes();
if bytes.len() % 4 != 0 {
return Err(SamError::InvalidResponse(format!(
"invalid I2P base64 length: {}",
bytes.len()
)));
}
let mut out = Vec::with_capacity(bytes.len() / 4 * 3);
for chunk in bytes.chunks(4) {
let mut vals = [0u8; 4];
let mut pad_count = 0;
for (i, &b) in chunk.iter().enumerate() {
if b == b'=' {
pad_count += 1;
vals[i] = 0;
} else {
let v = table[b as usize];
if v == 255 {
return Err(SamError::InvalidResponse(format!(
"invalid I2P base64 character: {:?}",
b as char
)));
}
vals[i] = v;
}
}
let triple = (vals[0] as u32) << 18
| (vals[1] as u32) << 12
| (vals[2] as u32) << 6
| (vals[3] as u32);
out.push((triple >> 16) as u8);
if pad_count < 2 {
out.push((triple >> 8) as u8);
}
if pad_count < 1 {
out.push(triple as u8);
}
}
Ok(out)
}
#[derive(Debug)]
pub enum SamError {
Io(io::Error),
Protocol(String),
InvalidResponse(String),
}
impl fmt::Display for SamError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SamError::Io(e) => write!(f, "SAM I/O error: {}", e),
SamError::Protocol(msg) => write!(f, "SAM protocol error: {}", msg),
SamError::InvalidResponse(msg) => write!(f, "SAM invalid response: {}", msg),
}
}
}
impl From<io::Error> for SamError {
fn from(e: io::Error) -> Self {
SamError::Io(e)
}
}
#[derive(Clone, Debug)]
pub struct Destination {
pub data: Vec<u8>,
}
impl Destination {
pub fn to_i2p_base64(&self) -> String {
i2p_base64_encode(&self.data)
}
pub fn from_i2p_base64(s: &str) -> Result<Self, SamError> {
let data = i2p_base64_decode(s)?;
Ok(Destination { data })
}
pub fn base32_address(&self) -> String {
let hash = rns_crypto::sha256::sha256(&self.data);
let encoded = base32_encode(&hash);
format!("{}.b32.i2p", encoded)
}
}
#[derive(Clone, Debug)]
pub struct KeyPair {
pub destination: Destination,
pub private_key: Vec<u8>,
}
const BASE32_ALPHABET: &[u8; 32] = b"abcdefghijklmnopqrstuvwxyz234567";
fn base32_encode(data: &[u8]) -> String {
let mut out = String::with_capacity((data.len() * 8 + 4) / 5);
let mut buffer: u64 = 0;
let mut bits: u32 = 0;
for &byte in data {
buffer = (buffer << 8) | byte as u64;
bits += 8;
while bits >= 5 {
bits -= 5;
out.push(BASE32_ALPHABET[((buffer >> bits) & 0x1F) as usize] as char);
}
}
if bits > 0 {
out.push(BASE32_ALPHABET[((buffer << (5 - bits)) & 0x1F) as usize] as char);
}
out
}
fn parse_kv(token: &str) -> Option<(&str, &str)> {
let eq = token.find('=')?;
Some((&token[..eq], &token[eq + 1..]))
}
fn read_line(stream: &mut TcpStream) -> Result<String, SamError> {
let mut line = Vec::new();
let mut byte = [0u8; 1];
loop {
match stream.read_exact(&mut byte) {
Ok(()) => {
if byte[0] == b'\n' {
break;
}
line.push(byte[0]);
}
Err(e) => return Err(SamError::Io(e)),
}
}
String::from_utf8(line)
.map_err(|e| SamError::InvalidResponse(format!("non-UTF8 SAM response: {}", e)))
}
fn hello_connect(sam_addr: &SocketAddr) -> Result<TcpStream, SamError> {
let mut stream = TcpStream::connect_timeout(sam_addr, CONNECT_TIMEOUT)?;
stream.set_read_timeout(Some(READ_TIMEOUT))?;
stream.set_write_timeout(Some(READ_TIMEOUT))?;
write!(stream, "HELLO VERSION MIN={v} MAX={v}\n", v = SAM_VERSION)?;
stream.flush()?;
let line = read_line(&mut stream)?;
let resp = parse_sam_response(&line)?;
if resp.command != "HELLO" || resp.subcommand != "REPLY" {
return Err(SamError::InvalidResponse(format!(
"expected HELLO REPLY, got: {}",
line
)));
}
check_result(&resp)?;
Ok(stream)
}
struct SamResponse {
command: String,
subcommand: String,
params: Vec<(String, String)>,
}
impl SamResponse {
fn get(&self, key: &str) -> Option<&str> {
for (k, v) in &self.params {
if k == key {
return Some(v);
}
}
None
}
}
fn parse_sam_response(line: &str) -> Result<SamResponse, SamError> {
let mut parts = line.splitn(3, ' ');
let command = parts
.next()
.ok_or_else(|| SamError::InvalidResponse("empty response".into()))?
.to_string();
let subcommand = parts.next().unwrap_or("").to_string();
let rest = parts.next().unwrap_or("");
let mut params = Vec::new();
for token in rest.split_whitespace() {
if let Some((k, v)) = parse_kv(token) {
params.push((k.to_string(), v.to_string()));
}
}
Ok(SamResponse {
command,
subcommand,
params,
})
}
fn check_result(resp: &SamResponse) -> Result<(), SamError> {
match resp.get("RESULT") {
Some("OK") => Ok(()),
Some(result) => {
let message = resp.get("MESSAGE").unwrap_or("(no message)");
Err(SamError::Protocol(format!(
"RESULT={} MESSAGE={}",
result, message
)))
}
None => Ok(()), }
}
pub fn dest_generate(sam_addr: &SocketAddr) -> Result<KeyPair, SamError> {
let mut stream = hello_connect(sam_addr)?;
write!(stream, "DEST GENERATE SIGNATURE_TYPE=7\n")?;
stream.flush()?;
let line = read_line(&mut stream)?;
let resp = parse_sam_response(&line)?;
if resp.command != "DEST" || resp.subcommand != "REPLY" {
return Err(SamError::InvalidResponse(format!(
"expected DEST REPLY, got: {}",
line
)));
}
let pub_b64 = resp
.get("PUB")
.ok_or_else(|| SamError::InvalidResponse("DEST REPLY missing PUB".into()))?;
let priv_b64 = resp
.get("PRIV")
.ok_or_else(|| SamError::InvalidResponse("DEST REPLY missing PRIV".into()))?;
let dest_data = i2p_base64_decode(pub_b64)?;
let priv_data = i2p_base64_decode(priv_b64)?;
Ok(KeyPair {
destination: Destination { data: dest_data },
private_key: priv_data,
})
}
pub fn session_create(
sam_addr: &SocketAddr,
session_id: &str,
private_key_b64: &str,
) -> Result<TcpStream, SamError> {
let mut stream = hello_connect(sam_addr)?;
write!(
stream,
"SESSION CREATE STYLE=STREAM ID={} DESTINATION={} SIGNATURE_TYPE=7\n",
session_id, private_key_b64,
)?;
stream.flush()?;
let line = read_line(&mut stream)?;
let resp = parse_sam_response(&line)?;
if resp.command != "SESSION" || resp.subcommand != "STATUS" {
return Err(SamError::InvalidResponse(format!(
"expected SESSION STATUS, got: {}",
line
)));
}
check_result(&resp)?;
Ok(stream)
}
pub fn stream_connect(
sam_addr: &SocketAddr,
session_id: &str,
destination: &str,
) -> Result<TcpStream, SamError> {
let mut stream = hello_connect(sam_addr)?;
write!(
stream,
"STREAM CONNECT ID={} DESTINATION={} SILENT=false\n",
session_id, destination,
)?;
stream.flush()?;
let line = read_line(&mut stream)?;
let resp = parse_sam_response(&line)?;
if resp.command != "STREAM" || resp.subcommand != "STATUS" {
return Err(SamError::InvalidResponse(format!(
"expected STREAM STATUS, got: {}",
line
)));
}
check_result(&resp)?;
stream.set_read_timeout(None)?;
stream.set_write_timeout(None)?;
Ok(stream)
}
pub fn stream_accept(
sam_addr: &SocketAddr,
session_id: &str,
) -> Result<(TcpStream, Destination), SamError> {
let mut stream = hello_connect(sam_addr)?;
write!(stream, "STREAM ACCEPT ID={} SILENT=false\n", session_id,)?;
stream.flush()?;
let line = read_line(&mut stream)?;
let resp = parse_sam_response(&line)?;
if resp.command != "STREAM" || resp.subcommand != "STATUS" {
return Err(SamError::InvalidResponse(format!(
"expected STREAM STATUS, got: {}",
line
)));
}
check_result(&resp)?;
let dest_line = read_line(&mut stream)?;
let remote_dest = Destination::from_i2p_base64(dest_line.trim())?;
stream.set_read_timeout(None)?;
stream.set_write_timeout(None)?;
Ok((stream, remote_dest))
}
pub fn naming_lookup(sam_addr: &SocketAddr, name: &str) -> Result<Destination, SamError> {
let mut stream = hello_connect(sam_addr)?;
naming_lookup_on(&mut stream, name)
}
pub fn naming_lookup_on(stream: &mut TcpStream, name: &str) -> Result<Destination, SamError> {
write!(stream, "NAMING LOOKUP NAME={}\n", name)?;
stream.flush()?;
let line = read_line(stream)?;
let resp = parse_sam_response(&line)?;
if resp.command != "NAMING" || resp.subcommand != "REPLY" {
return Err(SamError::InvalidResponse(format!(
"expected NAMING REPLY, got: {}",
line
)));
}
check_result(&resp)?;
let value = resp
.get("VALUE")
.ok_or_else(|| SamError::InvalidResponse("NAMING REPLY missing VALUE".into()))?;
Destination::from_i2p_base64(value)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn base64_encode_empty() {
assert_eq!(i2p_base64_encode(b""), "");
}
#[test]
fn base64_roundtrip() {
let data: Vec<u8> = (0..=255).collect();
let encoded = i2p_base64_encode(&data);
let decoded = i2p_base64_decode(&encoded).unwrap();
assert_eq!(decoded, data);
}
#[test]
fn base64_known_value() {
let encoded = i2p_base64_encode(b"Hello");
assert_eq!(encoded, "SGVsbG8=");
let decoded = i2p_base64_decode(&encoded).unwrap();
assert_eq!(decoded, b"Hello");
}
#[test]
fn base64_i2p_specific_chars() {
let data = [0xFB, 0xEF, 0xBE];
let encoded = i2p_base64_encode(&data);
assert!(encoded.contains('-') || encoded.contains('~'));
let decoded = i2p_base64_decode(&encoded).unwrap();
assert_eq!(decoded, data);
}
#[test]
fn base64_all_alphabet_chars_roundtrip() {
let data: Vec<u8> = (0..48).collect();
let encoded = i2p_base64_encode(&data);
let decoded = i2p_base64_decode(&encoded).unwrap();
assert_eq!(decoded, data);
}
#[test]
fn base64_padding_1() {
let encoded = i2p_base64_encode(&[0xFF]);
assert_eq!(encoded.len(), 4);
assert!(encoded.ends_with("=="));
let decoded = i2p_base64_decode(&encoded).unwrap();
assert_eq!(decoded, vec![0xFF]);
}
#[test]
fn base64_padding_2() {
let encoded = i2p_base64_encode(&[0xFF, 0xFE]);
assert_eq!(encoded.len(), 4);
assert!(encoded.ends_with('='));
let decoded = i2p_base64_decode(&encoded).unwrap();
assert_eq!(decoded, vec![0xFF, 0xFE]);
}
#[test]
fn base64_no_padding() {
let encoded = i2p_base64_encode(&[0xFF, 0xFE, 0xFD]);
assert_eq!(encoded.len(), 4);
assert!(!encoded.contains('='));
let decoded = i2p_base64_decode(&encoded).unwrap();
assert_eq!(decoded, vec![0xFF, 0xFE, 0xFD]);
}
#[test]
fn base64_decode_invalid_char() {
let result = i2p_base64_decode("!!!=");
assert!(result.is_err());
}
#[test]
fn base64_decode_invalid_length() {
let result = i2p_base64_decode("ABC");
assert!(result.is_err());
}
#[test]
fn base32_encode_empty() {
assert_eq!(base32_encode(&[]), "");
}
#[test]
fn base32_encode_known() {
let result = base32_encode(b"Hello");
assert_eq!(result, "jbswy3dp");
}
#[test]
fn base32_encode_sha256() {
let hash = rns_crypto::sha256::sha256(b"");
let encoded = base32_encode(&hash);
assert_eq!(encoded.len(), 52);
assert!(encoded
.chars()
.all(|c| c.is_ascii_lowercase() || ('2'..='7').contains(&c)));
}
#[test]
fn destination_base32_address() {
let dest = Destination {
data: vec![0x42; 387], };
let addr = dest.base32_address();
assert!(addr.ends_with(".b32.i2p"));
assert_eq!(addr.len(), 60);
}
#[test]
fn destination_roundtrip_base64() {
let data: Vec<u8> = (0..=255).cycle().take(387).collect();
let dest = Destination { data: data.clone() };
let b64 = dest.to_i2p_base64();
let dest2 = Destination::from_i2p_base64(&b64).unwrap();
assert_eq!(dest2.data, data);
}
#[test]
fn parse_hello_reply() {
let line = "HELLO REPLY RESULT=OK VERSION=3.1";
let resp = parse_sam_response(line).unwrap();
assert_eq!(resp.command, "HELLO");
assert_eq!(resp.subcommand, "REPLY");
assert_eq!(resp.get("RESULT"), Some("OK"));
assert_eq!(resp.get("VERSION"), Some("3.1"));
}
#[test]
fn parse_session_status_ok() {
let line = "SESSION STATUS RESULT=OK DESTINATION=AAAA";
let resp = parse_sam_response(line).unwrap();
assert_eq!(resp.command, "SESSION");
assert_eq!(resp.subcommand, "STATUS");
assert_eq!(resp.get("RESULT"), Some("OK"));
assert_eq!(resp.get("DESTINATION"), Some("AAAA"));
}
#[test]
fn parse_session_status_error() {
let line = "SESSION STATUS RESULT=DUPLICATED_ID";
let resp = parse_sam_response(line).unwrap();
assert_eq!(resp.get("RESULT"), Some("DUPLICATED_ID"));
let err = check_result(&resp);
assert!(err.is_err());
}
#[test]
fn parse_stream_status_error() {
let line = "STREAM STATUS RESULT=CANT_REACH_PEER MESSAGE=unreachable";
let resp = parse_sam_response(line).unwrap();
assert_eq!(resp.get("RESULT"), Some("CANT_REACH_PEER"));
assert_eq!(resp.get("MESSAGE"), Some("unreachable"));
let err = check_result(&resp);
assert!(err.is_err());
if let Err(SamError::Protocol(msg)) = err {
assert!(msg.contains("CANT_REACH_PEER"));
}
}
#[test]
fn parse_naming_reply() {
let line = "NAMING REPLY RESULT=OK NAME=test.b32.i2p VALUE=AAAA";
let resp = parse_sam_response(line).unwrap();
assert_eq!(resp.command, "NAMING");
assert_eq!(resp.subcommand, "REPLY");
assert_eq!(resp.get("NAME"), Some("test.b32.i2p"));
assert_eq!(resp.get("VALUE"), Some("AAAA"));
}
#[test]
fn parse_naming_not_found() {
let line = "NAMING REPLY RESULT=KEY_NOT_FOUND";
let resp = parse_sam_response(line).unwrap();
let err = check_result(&resp);
assert!(err.is_err());
}
#[test]
fn parse_dest_reply() {
let line = "DEST REPLY PUB=AAAA PRIV=BBBB";
let resp = parse_sam_response(line).unwrap();
assert_eq!(resp.command, "DEST");
assert_eq!(resp.subcommand, "REPLY");
assert_eq!(resp.get("PUB"), Some("AAAA"));
assert_eq!(resp.get("PRIV"), Some("BBBB"));
}
#[test]
fn parse_stream_status_timeout() {
let line = "STREAM STATUS RESULT=TIMEOUT";
let resp = parse_sam_response(line).unwrap();
let err = check_result(&resp);
assert!(err.is_err());
if let Err(SamError::Protocol(msg)) = err {
assert!(msg.contains("TIMEOUT"));
}
}
#[test]
fn check_result_ok() {
let line = "TEST REPLY RESULT=OK";
let resp = parse_sam_response(line).unwrap();
assert!(check_result(&resp).is_ok());
}
#[test]
fn check_result_no_result_field() {
let line = "TEST REPLY FOO=BAR";
let resp = parse_sam_response(line).unwrap();
assert!(check_result(&resp).is_ok());
}
#[test]
fn sam_error_display() {
let io_err = SamError::Io(io::Error::new(io::ErrorKind::Other, "test"));
assert!(format!("{}", io_err).contains("test"));
let proto_err = SamError::Protocol("CANT_REACH_PEER".into());
assert!(format!("{}", proto_err).contains("CANT_REACH_PEER"));
let inv_err = SamError::InvalidResponse("bad".into());
assert!(format!("{}", inv_err).contains("bad"));
}
}