diff --git a/core/src/account.rs b/core/src/account.rs index 9b04d9a..38c7d61 100644 --- a/core/src/account.rs +++ b/core/src/account.rs @@ -274,7 +274,9 @@ mod tests { // Both halves must sign: flipping a signature byte breaks it. let mut broken = cert; broken[72] ^= 1; - assert!(!RotationCert::from_bytes(&broken).unwrap().verify(&rc.username)); + assert!(!RotationCert::from_bytes(&broken) + .unwrap() + .verify(&rc.username)); assert!(RotationCert::from_bytes(&cert[..199]).is_err()); } diff --git a/core/src/address.rs b/core/src/address.rs index c48ab0e..0885349 100644 --- a/core/src/address.rs +++ b/core/src/address.rs @@ -172,8 +172,7 @@ impl Address { } fn decode_key(text: &str, key: &str) -> Result<[u8; KEY_LEN], Error> { - let identity = - unb32(key).map_err(|e| Error::Other(format!("{text}: undecodable key: {e}")))?; + let identity = unb32(key).map_err(|e| Error::Other(format!("{text}: undecodable key: {e}")))?; identity.try_into().map_err(|v: Vec| { Error::Other(format!( "{text}: key is {} bytes, expected {KEY_LEN}", diff --git a/core/src/client.rs b/core/src/client.rs index ce65554..5a2a814 100644 --- a/core/src/client.rs +++ b/core/src/client.rs @@ -58,9 +58,7 @@ impl Session { /// the hint rides along this way rather than in the stored row. #[cfg(feature = "store")] fn account_with_dial(store: &Store, dial: Option<&str>) -> Result { - let addr = store - .account()? - .ok_or(Error::NotRegistered)?; + let addr = store.account()?.ok_or(Error::NotRegistered)?; Ok(match dial { Some(dial) => addr.with_dial(dial), None => addr, @@ -140,8 +138,9 @@ pub fn connect( { let _ = (addr, require_pin); Err(Error::Other( - "smol+rns:// addresses need the rns feature; rebuild with --features rns".into(), -)) + "smol+rns:// addresses need the rns feature; rebuild with --features rns" + .into(), + )) } } } @@ -437,7 +436,12 @@ pub fn rotate( /// `connect` + `resolve`, `find_rotation_index`, then the four `set_*` /// calls below. #[cfg(feature = "store")] -pub fn restore(store: &Store, master: &[u8; KEY_LEN], addr: &Address, timeout: u64) -> Result { +pub fn restore( + store: &Store, + master: &[u8; KEY_LEN], + addr: &Address, + timeout: u64, +) -> Result { let mut session = connect(store, addr, false, timeout)?; let (identity, _) = resolve(session.transport(), &addr.user)?; session.close(); @@ -671,7 +675,10 @@ pub fn fetch_with( acknowledged: !opts.keep, cancelled: false, }; - let cancelled = || opts.cancel.is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed)); + let cancelled = || { + opts.cancel + .is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed)) + }; let mut session = connect(store, &addr, true, opts.timeout)?; let transport = session.transport(); authenticate(transport, &addr.user, account, store)?; diff --git a/core/src/crypto.rs b/core/src/crypto.rs index 977d15e..3d36816 100644 --- a/core/src/crypto.rs +++ b/core/src/crypto.rs @@ -106,7 +106,9 @@ pub fn ed25519_to_x25519(identity: &[u8; KEY_LEN]) -> Result<[u8; KEY_LEN], Erro pub fn agree(secret: &StaticSecret, peer: &[u8; KEY_LEN]) -> Result<[u8; KEY_LEN], Error> { let shared = secret.diffie_hellman(&PublicKey::from(*peer)); if shared.as_bytes().iter().all(|&b| b == 0) { - return Err(Error::Other("rejected all-zero key agreement output".into())); + return Err(Error::Other( + "rejected all-zero key agreement output".into(), + )); } Ok(*shared.as_bytes()) } @@ -143,7 +145,12 @@ mod tests { fn hkdf_matches_rfc5869_case_1() { let v = crate::vectors::load(); let hk = &v.hkdf_rfc5869_case1; - let okm = hkdf_sha256(&v.hex(&hk.ikm_hex), &v.hex(&hk.salt_hex), &v.hex(&hk.info_hex), 42); + let okm = hkdf_sha256( + &v.hex(&hk.ikm_hex), + &v.hex(&hk.salt_hex), + &v.hex(&hk.info_hex), + 42, + ); assert_eq!(data_encoding::HEXLOWER.encode(&okm[..32]), hk.okm_hex_32); } @@ -166,7 +173,10 @@ mod tests { let pk0 = *ed25519_dalek::SigningKey::from_bytes(&seed0) .verifying_key() .as_bytes(); - assert_eq!(b32(&ed25519_to_x25519(&pk0).unwrap()), v.ed25519_to_x25519_pk0_b32); + assert_eq!( + b32(&ed25519_to_x25519(&pk0).unwrap()), + v.ed25519_to_x25519_pk0_b32 + ); // 32 zero bytes do decode to a curve point, but a low-order one; // sec 2's rejection happens at agreement time, where the all-zero // output surfaces. diff --git a/core/src/error.rs b/core/src/error.rs index 5ef7904..ee3c596 100644 --- a/core/src/error.rs +++ b/core/src/error.rs @@ -27,7 +27,10 @@ pub enum Error { /// The host has no pinned server key, so the session cannot be /// authenticated (sec 4). A GUI routes this to pinning; an auth failure /// is terminal. The port scopes the pin (address::trust_label). - NotPinned { host: String, port: u16 }, + NotPinned { + host: String, + port: u16, + }, /// A pinned server presented a different key (sec 4): a hard abort, the /// pin is the entire trust model. The port scopes the pin /// (address::trust_label). @@ -47,10 +50,15 @@ pub enum Error { /// The store has no account yet; registering or restoring creates one. NotRegistered, /// The rotation index would exceed the chain limit (sec 2). - ChainLimit { index: u32 }, + ChainLimit { + index: u32, + }, /// The store was written by a schema this build cannot read; a host /// decides when to migrate, not the library. - SchemaVersion { found: i32, expected: i32 }, + SchemaVersion { + found: i32, + expected: i32, + }, /// The server could not be reached at all. Unreachable { host: String, diff --git a/core/src/export.rs b/core/src/export.rs index 9fd7b59..3450924 100644 --- a/core/src/export.rs +++ b/core/src/export.rs @@ -171,7 +171,14 @@ pub fn export(store: &Store, master: &[u8; KEY_LEN]) -> Result { until, }) .collect(); - contacts.insert(address, ContactExport { key: b32(&key), verified, history }); + contacts.insert( + address, + ContactExport { + key: b32(&key), + verified, + history, + }, + ); } // Each folder listing carries its own tier, so rows round-trip into the @@ -180,7 +187,12 @@ pub fn export(store: &Store, master: &[u8; KEY_LEN]) -> Result { .mail("inbox")? .into_iter() .map(|row| inbox_row(row, TIER_MAIN)) - .chain(store.mail("requests")?.into_iter().map(|row| inbox_row(row, TIER_REQUESTS))) + .chain( + store + .mail("requests")? + .into_iter() + .map(|row| inbox_row(row, TIER_REQUESTS)), + ) .collect::>(); let sent = store .mail("sent")? @@ -227,8 +239,8 @@ fn inbox_row(row: crate::store::Stored, tier: u8) -> InboxRow { /// fetch does not re-store it; pins and contacts merge without clobbering; /// one malformed record is skipped and counted, never fatal. pub fn import(store: &Store, master: &[u8; KEY_LEN], text: &str) -> Result { - let container: Container = serde_json::from_str(text) - .map_err(|_| Error::Other("not a gsmol export file".into()))?; + let container: Container = + serde_json::from_str(text).map_err(|_| Error::Other("not a gsmol export file".into()))?; if container.gsmol_export != 2 { return Err(Error::Other("not a gsmol export file".into())); } @@ -238,10 +250,11 @@ pub fn import(store: &Store, master: &[u8; KEY_LEN], text: &str) -> Result Result = - unb32(&contact.key).ok().and_then(|k| k.try_into().ok()); + let key: Option<[u8; KEY_LEN]> = unb32(&contact.key).ok().and_then(|k| k.try_into().ok()); match key { None => summary.malformed += 1, Some(key) => match store.contact(address)? { @@ -347,7 +359,9 @@ mod tests { fn seeded_store() -> (Store, [u8; KEY_LEN]) { let store = Store::open_in_memory().unwrap(); let master = [7u8; KEY_LEN]; - store.pin_server("example.org", crate::address::DEFAULT_PORT, &[1u8; KEY_LEN]).unwrap(); + store + .pin_server("example.org", crate::address::DEFAULT_PORT, &[1u8; KEY_LEN]) + .unwrap(); store .save_contact("alice@example.org", &[2u8; KEY_LEN], true) .unwrap(); @@ -360,7 +374,9 @@ mod tests { store .store_inbox(&[4u8; ID_LEN], b"request", 43, true, false) .unwrap(); - store.store_sent(&[5u8; ID_LEN], "bob@example.org", b"sent", 44).unwrap(); + store + .store_sent(&[5u8; ID_LEN], "bob@example.org", b"sent", 44) + .unwrap(); (store, master) } @@ -376,11 +392,19 @@ mod tests { assert_eq!(summary.contacts_added, 1); assert_eq!(summary.malformed, 0); - assert_eq!(fresh.server_pin("example.org", crate::address::DEFAULT_PORT).unwrap(), Some([1u8; KEY_LEN])); + assert_eq!( + fresh + .server_pin("example.org", crate::address::DEFAULT_PORT) + .unwrap(), + Some([1u8; KEY_LEN]) + ); let (key, verified) = fresh.contact("alice@example.org").unwrap().unwrap(); assert_eq!(key, [2u8; KEY_LEN]); assert!(verified); - assert_eq!(fresh.history("alice@example.org").unwrap(), [([9u8; KEY_LEN], 500)]); + assert_eq!( + fresh.history("alice@example.org").unwrap(), + [([9u8; KEY_LEN], 500)] + ); assert_eq!(fresh.mail("inbox").unwrap().len(), 1); assert_eq!(fresh.mail("requests").unwrap().len(), 1); assert_eq!(fresh.mail("inbox").unwrap()[0].kept, true); @@ -405,13 +429,18 @@ mod tests { let file = export(&store, &master).unwrap(); let mine = Store::open_in_memory().unwrap(); - mine.pin_server("example.org", crate::address::DEFAULT_PORT, &[6u8; KEY_LEN]).unwrap(); + mine.pin_server("example.org", crate::address::DEFAULT_PORT, &[6u8; KEY_LEN]) + .unwrap(); mine.save_contact("alice@example.org", &[7u8; KEY_LEN], false) .unwrap(); let summary = import(&mine, &master, &file).unwrap(); assert_eq!(summary.pins_conflicted, 1); assert_eq!(summary.contacts_conflicted, 1); - assert_eq!(mine.server_pin("example.org", crate::address::DEFAULT_PORT).unwrap(), Some([6u8; KEY_LEN])); + assert_eq!( + mine.server_pin("example.org", crate::address::DEFAULT_PORT) + .unwrap(), + Some([6u8; KEY_LEN]) + ); let (key, _) = mine.contact("alice@example.org").unwrap().unwrap(); assert_eq!(key, [7u8; KEY_LEN]); } @@ -423,12 +452,18 @@ mod tests { // Corrupt one inbox row's id inside the payload by re-sealing a // hand-edited payload with the same key. let container: Container = serde_json::from_str(&file).unwrap(); - let nonce = data_encoding::BASE64.decode(container.nonce.as_bytes()).unwrap(); - let sealed = data_encoding::BASE64.decode(container.ciphertext.as_bytes()).unwrap(); + let nonce = data_encoding::BASE64 + .decode(container.nonce.as_bytes()) + .unwrap(); + let sealed = data_encoding::BASE64 + .decode(container.ciphertext.as_bytes()) + .unwrap(); let plaintext = open(&export_key(&master), &nonce, &sealed).unwrap(); let mut payload: Bundle = serde_json::from_slice(&plaintext).unwrap(); payload.inbox[0].id = "zz".to_string(); - payload.servers.insert("bad".to_string(), "not-base32!".to_string()); + payload + .servers + .insert("bad".to_string(), "not-base32!".to_string()); let (nonce, sealed) = seal(&export_key(&master), &serde_json::to_vec(&payload).unwrap()); let file = serde_json::to_string(&Container { gsmol_export: 2, diff --git a/core/src/lib.rs b/core/src/lib.rs index 0e86989..b47e719 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -24,4 +24,3 @@ pub mod tcp; pub mod transport; #[cfg(test)] mod vectors; - diff --git a/core/src/message.rs b/core/src/message.rs index ce8fb92..ea7923c 100644 --- a/core/src/message.rs +++ b/core/src/message.rs @@ -169,7 +169,9 @@ pub fn unseal(identities: &[Identity], envelope: &[u8], now: i64) -> Result plaintext.len() { - return Err(Error::Other("payload body length exceeds the payload".into())); + return Err(Error::Other( + "payload body length exceeds the payload".into(), + )); } let body = &plaintext[PAYLOAD_HEADER..PAYLOAD_HEADER + body_len]; let signature = &plaintext[PAYLOAD_HEADER + body_len..PAYLOAD_HEADER + body_len + SIG_LEN]; @@ -276,7 +278,6 @@ mod tests { core::array::from_fn(|i| i as u8) } - fn identity(n: u32) -> Identity { Identity::from_seed(identity_seed(&master(), n)) } diff --git a/core/src/rns/ffi.rs b/core/src/rns/ffi.rs index 86335a9..887c6b9 100644 --- a/core/src/rns/ffi.rs +++ b/core/src/rns/ffi.rs @@ -98,9 +98,7 @@ pub fn connect(destination: &[u8; 16], timeout: Duration) -> Result<[u8; 16], Er -2 => Err(Error::Other(format!( "path known but identity not yet learned for {hex}; try again shortly" ))), - -3 => Err(Error::Other(format!( - "could not establish a link to {hex}" - ))), + -3 => Err(Error::Other(format!("could not establish a link to {hex}"))), _ => Err(Error::Other(format!( "RNS connect to {hex} failed (code {rc})" ))), @@ -133,15 +131,18 @@ pub fn request(request: &[u8], timeout: Duration) -> Result, Error> { Ok(out) } -1 => Err(Error::Other("no RNS link is open".into())), - -2 => Err(Error::Other("failed to send the request over the link".into())), + -2 => Err(Error::Other( + "failed to send the request over the link".into(), + )), -3 | -4 => Err(Error::Other( "request failed: no response (closed link, rejected transfer, or timeout \ - -- not a status code, upstream spec 13.5)".into(), -)), + -- not a status code, upstream spec 13.5)" + .into(), + )), -5 => Err(Error::Other("malformed response from server".into())), -6 => Err(Error::Other( "response larger than one application frame; refusing to truncate it".into(), -)), + )), _ => Err(Error::Other(format!("RNS request failed (code {rc})"))), } } diff --git a/core/src/store.rs b/core/src/store.rs index 08ed98b..284e451 100644 --- a/core/src/store.rs +++ b/core/src/store.rs @@ -140,9 +140,7 @@ impl Store { expected: SCHEMA_VERSION, }); } - Ok(Store { - db: Mutex::new(db), - }) + Ok(Store { db: Mutex::new(db) }) } /// One lock acquisition; the guard's lifetime is the operation's. @@ -315,12 +313,7 @@ impl Store { /// Pins a server key, scoped by port (address::trust_label): the default /// port keeps the bare-host row existing stores already hold, and any /// other port earns its own row. - pub fn pin_server( - &self, - host: &str, - port: u16, - key: &[u8; KEY_LEN], - ) -> Result<(), Error> { + pub fn pin_server(&self, host: &str, port: u16, key: &[u8; KEY_LEN]) -> Result<(), Error> { self.db() .execute( "INSERT INTO servers (host, static, pinned_at) VALUES (?1, ?2, ?3) \ @@ -597,10 +590,7 @@ impl Store { _ => "inbox", }; Ok(self - .one::( - &format!("SELECT 1 FROM {table} WHERE id = ?1"), - &[id], - )? + .one::(&format!("SELECT 1 FROM {table} WHERE id = ?1"), &[id])? .is_some()) } @@ -616,8 +606,10 @@ impl Store { }; self.db() .execute(&format!("DELETE FROM {table} WHERE id = ?1"), params![id])?; - self.db() - .execute("INSERT OR IGNORE INTO seen (id, at) VALUES (?1, ?2)", params![id, now()])?; + self.db().execute( + "INSERT OR IGNORE INTO seen (id, at) VALUES (?1, ?2)", + params![id, now()], + )?; Ok(()) } @@ -792,9 +784,13 @@ mod tests { fn inbox_and_sent_store_sealed_and_deduplicate() { let store = temp_store("mail"); let id = [3u8; 32]; - store.store_inbox(&id, b"envelope", 100, false, false).unwrap(); + store + .store_inbox(&id, b"envelope", 100, false, false) + .unwrap(); // A replayed id is not re-stored (sec 10). - store.store_inbox(&id, b"envelope2", 100, true, false).unwrap(); + store + .store_inbox(&id, b"envelope2", 100, true, false) + .unwrap(); assert!(store.seen(&id).unwrap()); let inbox = store.mail("inbox").unwrap(); assert_eq!(inbox.len(), 1); @@ -812,11 +808,24 @@ mod tests { #[test] fn pins_and_contacts_round_trip() { let store = temp_store("pins"); - assert!(store.server_pin("example.org", DEFAULT_PORT).unwrap().is_none()); - store.pin_server("example.org", DEFAULT_PORT, &[5u8; 32]).unwrap(); - assert_eq!(store.server_pin("example.org", DEFAULT_PORT).unwrap(), Some([5u8; 32])); - store.pin_server("example.org", DEFAULT_PORT, &[6u8; 32]).unwrap(); - assert_eq!(store.server_pin("example.org", DEFAULT_PORT).unwrap(), Some([6u8; 32])); + assert!(store + .server_pin("example.org", DEFAULT_PORT) + .unwrap() + .is_none()); + store + .pin_server("example.org", DEFAULT_PORT, &[5u8; 32]) + .unwrap(); + assert_eq!( + store.server_pin("example.org", DEFAULT_PORT).unwrap(), + Some([5u8; 32]) + ); + store + .pin_server("example.org", DEFAULT_PORT, &[6u8; 32]) + .unwrap(); + assert_eq!( + store.server_pin("example.org", DEFAULT_PORT).unwrap(), + Some([6u8; 32]) + ); let key = Account::new(master(), 1).unwrap().me().pk(); assert!(store.contact("bob@example.org").unwrap().is_none()); @@ -848,10 +857,13 @@ mod tests { let path = scratch_path("schema"); drop(Store::open(&path).expect("open store")); let db = Connection::open(&path).unwrap(); - let version: i32 = db.query_row("PRAGMA user_version", [], |row| row.get(0)).unwrap(); + let version: i32 = db + .query_row("PRAGMA user_version", [], |row| row.get(0)) + .unwrap(); assert_eq!(version, SCHEMA_VERSION); // A store from the future is refused, not misread. - db.pragma_update(None, "user_version", SCHEMA_VERSION + 1).unwrap(); + db.pragma_update(None, "user_version", SCHEMA_VERSION + 1) + .unwrap(); drop(db); match Store::open(&path) { Err(Error::SchemaVersion { found, expected }) => { @@ -903,7 +915,9 @@ mod tests { let store = Store::open(&path).expect("v1 store migrates"); let db = Connection::open(&path).unwrap(); - let version: i32 = db.query_row("PRAGMA user_version", [], |row| row.get(0)).unwrap(); + let version: i32 = db + .query_row("PRAGMA user_version", [], |row| row.get(0)) + .unwrap(); drop(db); assert_eq!(version, SCHEMA_VERSION); // Existing rows survive with the column's default. @@ -911,8 +925,13 @@ mod tests { assert_eq!(mail.len(), 1); assert!(!mail[0].kept); // The history table exists and round-trips. - store.save_history("alice@example.org", &[9u8; 32], 42).unwrap(); - assert_eq!(store.history("alice@example.org").unwrap(), [([9u8; 32], 42)]); + store + .save_history("alice@example.org", &[9u8; 32], 42) + .unwrap(); + assert_eq!( + store.history("alice@example.org").unwrap(), + [([9u8; 32], 42)] + ); } #[test] @@ -964,18 +983,24 @@ mod tests { #[test] fn unpin_returns_to_trust_on_first_use() { let store = temp_store("unpin"); - store.pin_server("example.org", DEFAULT_PORT, &[1u8; KEY_LEN]).unwrap(); + store + .pin_server("example.org", DEFAULT_PORT, &[1u8; KEY_LEN]) + .unwrap(); store.unpin_server("example.org", DEFAULT_PORT).unwrap(); assert_eq!(store.server_pin("example.org", DEFAULT_PORT).unwrap(), None); // Unpinning what was never pinned is not an error. - store.unpin_server("never.example.org", DEFAULT_PORT).unwrap(); + store + .unpin_server("never.example.org", DEFAULT_PORT) + .unwrap(); } #[test] fn kept_survives_the_folder_listing() { let store = temp_store("kept"); store.store_inbox(&[1u8; 32], b"a", 1, false, true).unwrap(); - store.store_inbox(&[2u8; 32], b"b", 2, false, false).unwrap(); + store + .store_inbox(&[2u8; 32], b"b", 2, false, false) + .unwrap(); let all = store.mail("all").unwrap(); assert!(all[0].kept && !all[1].kept); } @@ -998,7 +1023,9 @@ mod tests { fn local_delete_marks_seen_so_a_refetch_does_not_restore() { let store = temp_store("delete-local"); let id = [3u8; 32]; - store.store_inbox(&id, b"envelope", 100, false, true).unwrap(); + store + .store_inbox(&id, b"envelope", 100, false, true) + .unwrap(); store.delete_local("inbox", &id).unwrap(); assert!(store.mail("all").unwrap().is_empty()); // Sec 10: the id is seen, so the fetch pipeline will not re-store @@ -1007,7 +1034,9 @@ mod tests { assert!(store.seen(&id).unwrap()); // Sent copies delete locally too, with the same protection. let sent_id = [4u8; 32]; - store.store_sent(&sent_id, "bob@example.org", b"sent", 50).unwrap(); + store + .store_sent(&sent_id, "bob@example.org", b"sent", 50) + .unwrap(); store.delete_local("sent", &sent_id).unwrap(); assert!(store.mail("sent").unwrap().is_empty()); } @@ -1021,7 +1050,9 @@ mod tests { store.account().unwrap().unwrap().short(), "alice@example.org" ); - store.store_inbox(&[1u8; 32], b"envelope", 5, false, false).unwrap(); + store + .store_inbox(&[1u8; 32], b"envelope", 5, false, false) + .unwrap(); assert_eq!(store.mail("inbox").unwrap().len(), 1); // The same handle through the path spelling. let path = std::path::Path::new(":memory:"); diff --git a/core/src/tcp.rs b/core/src/tcp.rs index b462350..f555707 100644 --- a/core/src/tcp.rs +++ b/core/src/tcp.rs @@ -177,7 +177,15 @@ fn handshake( host: &str, port: u16, timeout: u64, -) -> Result<(TcpStream, TransportState, [u8; KEY_LEN], TransportBindValues), Error> { +) -> Result< + ( + TcpStream, + TransportState, + [u8; KEY_LEN], + TransportBindValues, + ), + Error, +> { let mut stream = connect_timeout(host, port, timeout).map_err(|source| Error::Unreachable { host: host.to_string(), port, @@ -219,7 +227,9 @@ impl Transport for TcpTransport { fn request(&mut self, op: u8, body: &[u8]) -> Result { let length = 1 + body.len(); if length > MAX_FRAME { - return Err(Error::Other("request exceeds the maximum frame size".into())); + return Err(Error::Other( + "request exceeds the maximum frame size".into(), + )); } let mut frame = Vec::with_capacity(4 + length); frame.extend_from_slice(&(length as u32).to_be_bytes()); diff --git a/core/src/vectors.rs b/core/src/vectors.rs index 23499a8..b6f97dc 100644 --- a/core/src/vectors.rs +++ b/core/src/vectors.rs @@ -114,6 +114,8 @@ impl Vectors { } pub fn hex(&self, hex: &str) -> Vec { - data_encoding::HEXLOWER.decode(hex.as_bytes()).expect("hex decodes") + data_encoding::HEXLOWER + .decode(hex.as_bytes()) + .expect("hex decodes") } }