#[cfg(test)]
mod tests {
use std::process::Command;
struct StateDirGuard(std::path::PathBuf);
impl Drop for StateDirGuard {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn isolated_state_dir(name: &str) -> StateDirGuard {
let dir = std::env::temp_dir().join(format!("ffsync_tls_state_{name}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
unsafe {
std::env::set_var("APPDATA", &dir);
std::env::set_var("HOME", &dir);
}
StateDirGuard(dir)
}
fn generate_identity_in(name: &str) -> (ffsync::transport::Identity, StateDirGuard) {
let _guard = isolated_state_dir(name);
let certified =
rcgen::generate_simple_self_signed(vec!["ffsync-peer".to_string()]).unwrap();
let id = ffsync::transport::Identity {
cert_der: certified.cert.der().as_ref().to_vec(),
key_der: certified.signing_key.serialize_der(),
};
(
id,
StateDirGuard(std::env::temp_dir().join(format!("ffsync_tls_state_{name}"))),
)
}
type SyncResult =
Result<ffsync::protocol::sync::SyncOutcome, ffsync::protocol::sync::SyncError>;
fn tls_sync_case(
name: &str,
client_id: ffsync::transport::Identity,
server_id: ffsync::transport::Identity,
client_pin: Option<Vec<u8>>,
server_pin: Option<Vec<u8>>,
) -> (SyncResult, SyncResult, Vec<u8>, Vec<u8>) {
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let server_handle = std::thread::spawn(move || {
let (tcp, _peer) = listener.accept().unwrap();
let policy = match server_pin {
Some(fp) => ffsync::transport::PinPolicy::Pinned(vec![fp]),
None => ffsync::transport::PinPolicy::FirstUse,
};
ffsync::transport::server_session(tcp, &server_id, &policy)
});
let tcp = std::net::TcpStream::connect(addr).unwrap();
let client_policy = match client_pin {
Some(fp) => ffsync::transport::PinPolicy::Pinned(vec![fp]),
None => ffsync::transport::PinPolicy::FirstUse,
};
let server_name =
rustls::pki_types::ServerName::try_from("ffsync-peer".to_string()).unwrap();
let (client_tls, client_saw) =
match ffsync::transport::client_session(tcp, &client_id, &client_policy, server_name) {
Ok((s, fp)) => (Some(s), fp),
Err(_) => (None, Vec::new()), };
let (server_tls, server_saw) = match server_handle.join() {
Ok(Ok((s, fp))) => (Some(s), fp),
Ok(Err(_)) | Err(_) => (None, Vec::new()),
};
let (s_out, r_out) = match (client_tls, server_tls) {
(Some(mut c), Some(mut s)) => {
let dir = std::env::temp_dir().join(format!("ffsync_tls_files_{name}"));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("data.bin");
std::fs::write(&path, vec![1u8; 256 * 1024]).unwrap();
let send_req = ffsync::protocol::sync::SendRequest {
path: path.clone(),
display_path: "data.bin".into(),
mtime: None,
force_checksum: true,
tree: false,
};
let recv_req = ffsync::protocol::sync::ReceiveRequest {
path: path.clone(),
local_source: path.clone(),
force_checksum: true,
tree: false,
apply_mode: None,
apply_mtime: None,
};
let pipeline = ffsync::chunking::PipelineConfig::default();
let pipeline_recv = ffsync::chunking::PipelineConfig::default();
let c = std::thread::spawn(move || {
let mut p = |_e: ffsync::protocol::sync::Progress| {};
ffsync::protocol::sync::send(&mut c, &send_req, &pipeline, &mut p)
});
let mut p = |_e: ffsync::protocol::sync::Progress| {};
let r = ffsync::protocol::sync::receive(&mut s, &recv_req, &pipeline_recv, &mut p);
let s_out = c.join().unwrap();
let _ = std::fs::remove_dir_all(&dir);
(s_out, r)
}
_ => (
Err(ffsync::protocol::sync::SyncError::Eof),
Err(ffsync::protocol::sync::SyncError::Eof),
),
};
(s_out, r_out, client_saw, server_saw)
}
#[test]
fn mutual_first_use_handshake_and_sync_succeed() {
let (client, _g1) = generate_identity_in("firstuse_c");
let (server, _g2) = generate_identity_in("firstuse_s");
let (s, r, client_saw, server_saw) =
tls_sync_case("firstuse", client.clone(), server.clone(), None, None);
assert!(s.is_ok(), "sender: {s:?}");
assert!(r.is_ok(), "receiver: {r:?}");
assert_eq!(client_saw.len(), 32);
assert_eq!(server_saw.len(), 32);
assert_ne!(client_saw, ffsync::transport::sha256(&client.cert_der));
assert_eq!(
client_saw,
ffsync::transport::sha256(&server.cert_der),
"client must see the server's cert"
);
}
#[test]
fn pinned_peers_sync_and_mitm_is_rejected() {
let (c, _g1) = generate_identity_in("pinmatch_c");
let (s, _g2) = generate_identity_in("pinmatch_s");
let c_fp = ffsync::transport::sha256(&c.cert_der);
let s_fp = ffsync::transport::sha256(&s.cert_der);
let (s_out, r_out, client_saw, server_saw) = tls_sync_case(
"pinmatch",
c.clone(),
s.clone(),
Some(s_fp.clone()),
Some(c_fp.clone()),
);
assert!(s_out.is_ok(), "sender: {s_out:?}");
assert!(r_out.is_ok(), "receiver: {r_out:?}");
assert_eq!(client_saw, s_fp, "client must see the server's cert");
assert_eq!(server_saw, c_fp, "server must see the client's cert");
let attacker_fp = ffsync::transport::sha256(&_g5_unused().cert_der);
let (s_out, _r_out, _cs, _ss) = tls_sync_case("mitm", c, s, Some(attacker_fp), None);
assert!(s_out.is_err(), "client must reject the wrong cert");
}
#[test]
fn unpinned_client_is_rejected_by_pinned_receiver() {
let (legit, _g1) = generate_identity_in("unpinned_legit");
let (server, _g2) = generate_identity_in("unpinned_server");
let (attacker, _g3) = generate_identity_in("unpinned_attacker");
let legit_fp = ffsync::transport::sha256(&legit.cert_der);
let attacker_fp = ffsync::transport::sha256(&attacker.cert_der);
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let server_handle = std::thread::spawn(move || {
let (tcp, _peer) = listener.accept().unwrap();
ffsync::transport::server_session(
tcp,
&server,
&ffsync::transport::PinPolicy::Pinned(vec![legit_fp]),
)
});
let tcp = std::net::TcpStream::connect(addr).unwrap();
let name = rustls::pki_types::ServerName::try_from("ffsync-peer".to_string()).unwrap();
let client_res = ffsync::transport::client_session(
tcp,
&attacker,
&ffsync::transport::PinPolicy::FirstUse,
name,
);
let server_res = server_handle.join().unwrap();
let server_err = server_res.expect_err("receiver must refuse the attacker");
let msg = server_err.to_string();
assert!(
msg.contains("not pinned"),
"receiver error must name the pin failure, got: {msg}"
);
assert!(
msg.contains(&ffsync::transport::format_fingerprint(&attacker_fp)),
"receiver error must show the attacker's fingerprint, got: {msg}"
);
let _ = client_res;
}
fn _g5_unused() -> ffsync::transport::Identity {
generate_identity_in("mitm_attacker").0
}
#[test]
fn identity_round_trip_and_fingerprint_format() {
let (id, _g) = generate_identity_in("codec");
let fp = id.fingerprint();
assert_eq!(fp.len(), 95, "format: {fp}");
assert!(fp.chars().all(|c| c.is_ascii_hexdigit() || c == ':'));
let raw = ffsync::transport::parse_fingerprint(&fp).expect("parses own format");
assert_eq!(raw, ffsync::transport::sha256(&id.cert_der));
assert!(ffsync::transport::parse_fingerprint("AA:BB").is_none());
}
#[test]
fn pins_file_add_remove_round_trip() {
let dir = std::env::temp_dir().join(format!("ffsync_tls_pins_{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let a = vec![1u8; 32];
let b = vec![2u8; 32];
ffsync::transport::add_pin_in(&dir, &a).unwrap();
ffsync::transport::add_pin_in(&dir, &b).unwrap();
ffsync::transport::add_pin_in(&dir, &a).unwrap();
let pins = ffsync::transport::load_pins_from(&dir).unwrap();
assert_eq!(pins.len(), 2);
assert!(pins.iter().any(|p| p == &a));
assert!(pins.iter().any(|p| p == &b));
ffsync::transport::remove_pin_in(&dir, &a).unwrap();
let pins = ffsync::transport::load_pins_from(&dir).unwrap();
assert_eq!(pins.len(), 1);
assert_eq!(pins[0], b);
ffsync::transport::remove_pin_in(&dir, &a).unwrap();
assert_eq!(ffsync::transport::load_pins_from(&dir).unwrap().len(), 1);
let _ = std::fs::remove_dir_all(&dir);
}
#[allow(dead_code)]
fn _uses() {
let _ = Command::new("true");
}
}