Rust <> Security

Topics:
- Local First: Distributed Authentication via @MichaelGilbert
- Typical Programming via Elio Grieco
- Using zeroize to Zero Out Secrets from Memory via @DanielPBank
Local First: Distributed Authentication via @MichaelGilbert
Slides: local_first_security.pdf
Repo: https://github.com/Zyell/beelay-iroh-chat
What is Local First?
Concept originally outlined by Ink & Switch:
- No spinners β your work is always at your fingertips
- Your data is not trapped on a single device
- Network is optional
- Enables seamless collaboration with colleagues
- Doesn't rely on reconciliation through a central authority
- Supports "The Long Now" β your data should be yours forever
- Security and privacy by default
- You retain ultimate ownership and control
Why Local First Now?
- Increasingly common: services and devices losing support (e.g., smart homes)
- Security breaches are constant in today's centralized internet
- Centralized server-client architecture is a forced solution, not ideal
Can a Distributed, P2P Solution Be a Better Alternative?
Yes β but authentication and identity are historically centralized and hard to decentralize.
Benefits of an P2P Auth Architecture
- Aligns with Local First principles
- Data and algorithmic sovereignty
- Resiliency and fault tolerance
- End-to-end privacy
- Security by design
Challenges in P2P Systems
- No central gatekeeper to handle:
- Logins
- Identity
- Collation
- Database management
- Strong consistency is hard to guarantee
Causal Order and Eventual Consistency
- Traditional timestamp ordering is unreliable due to clock drift
- Causal ordering: each update hashes what it has seen before
- Updates must be commutative to converge - enter CRDTs
CRDTs: Conflict-free Replicated Data Types
- Allow concurrent updates without coordination
- Automatically reconcile inconsistencies
- Guarantee eventual convergence across replicas
Tools in the Rust Ecosystem
- Iroh: P2P networking over QUIC, high hole-punch success, IPv4 & IPv6 support
- Automerge: Well-known CRDT library, originally from JS world
- Loro: High-performance CRDT engine
- Keyhive / Beelay: Causal encryption, E2EE, encryption-at-rest, tailored for wasm
Keyhive / Beelay Highlights
- Causal encryption layer that supports:
- End-to-end encryption
- Encryption at rest
- Concept of βsedimentβ β data solidification without breaking compression
- PQ (Post-Quantum) encryption exploration is still ongoing
Building an Encrypted Chat App
Code: https://github.com/Zyell/beelay-iroh-chat
- Beelay: Single-threaded actor, runs in browser (via WASM)
- Iroh: Multi-threaded P2P backend, handles network transport
- Tauri: Desktop shell; typesafe Rust backend, TS frontend
- Note: IPC in Tauri is not type-safe
- Learnings around NAT traversal:
- Uses STUN/TURN-style relays
- WIFI-aware systems: aka Neighbor Awareness Network (NAN)
- UDP port exposure enables reaching devices behind shared IPs
Future Directions & Projects
- Psyche by Nous Research: Distributed heterogeneous model training
- Malai: Like Tailscale over Iroh; exposes services over P2P
- Symplasma: Sync and access your data across devices via Iroh
- Ink & Switch Local-First Research: Local-First Software Research
- Weird: Social platform concept over Iroh
Links & References
- Local First Essay: inkandswitch.com
- Iroh: iroh.computer
- CRDT Reference: Wikipedia
- Keyhive/Beelay Talk: Brooklyn Zelenka - ReCAP'25
- Symplasma: symplasma.org
- Symplasma GitHub: https://github.com/symplasma
Typical Programming via Elio Grieco
Slides: https://www.eliogrieco.com/typical-programming
C and C++ have static type systems that define data structure, memory layout, and provide some compile-time type checking. Rust also uses a static type system, but it goes further by enforcing strict type safety and ownership rules at compile time, preventing entire classes of bugs and making code safer and more reliable. Two interesting patterns that we can use are New Types and Typestate.
New Type
When modeling semantics in your code, it's important to consider language-theoretic security (LangSec). This means examining the structure of the data coming into your system. For example, JSON has a specific structure. Some data is well-formed JSON, while other data might be garbage. You must avoid accepting garbage data.
The principle here is: "Parse, don't validate." Instead of just checking if data looks right, you should actually parse it and ensure it fits the expected structure.
Using a newtype is a good way to achieve some of the benefits that LangSec provides. For example:
authenticate(name: String, pass: String);
authenticate(name: UserName, pass: Password);
By using new types like UserName and Password, you can ensure that any new string must go through a constructor that checks its validity. New types provide guard rails for your code.
Typestate
Typestate is an API design pattern that helps you prevent mistakes by making it impossible to call methods on objects that are in the wrong state. It lets you use the type system to enforce valid state transitions, so your code follows the correct logic for a state machine.
With this approach, you write down all your types and their signatures before writing the actual code. This gives your code a clear structure and helps you avoid invalid states.
For example, you might want to make sure that certain operations can only be performed in a specific order. Do you perform a MAC (Message Authentication Code) and then encrypt, or encrypt and then MAC? You can use TypeState to enforce the correct sequence and prevent dangerous combinations. (hint: the correct answer is encrypt then MAC)
Demo: Eunicode CLI Code
Here's an example in the Eunicode crate, which Elio wrote to process Unicode text safely by using TypeState to track whether text has been cleaned or not:
/// TypeState state definitions
pub mod string_states {
use super::StringState;
pub struct RawInput {}
pub struct CleanedText {}
/// RawInput state: may data may contain dangerous characters - allows detect and chars operations to examine content
impl StringState for RawInput {}
/// CleanedText state: dangerous characters have been removed - allows strip, defang, censor, and sluggify operations
impl StringState for CleanedText {}
}
The UnicodeString struct uses a type parameter to track the state:
/// TypeState wrapper for text processing
pub struct UnicodeString<S: StringState> {
text: String,
_marker: PhantomData<S>,
}
When you call the clean method, it transitions the string from the RawInput state to the CleanedText state:
/// Normalize Unicode characters to only safe, ASCII text chars and transition to CleanedText state
pub fn clean(self) -> UnicodeString<string_states::CleanedText> {
UnicodeString::<string_states::CleanedText> {
text: deunicode(&self.text),
_marker: PhantomData,
}
}
By using TypeState, you can ensure that only safe operations are available at each stage, and you can't accidentally use a function on data that's in the wrong state.
Also notice the usage of PhantomData, which is a zero-sized type that acts like a field even though it doesn't consume memory. It is needed because the UnicodeString struct is generic over a state type S (which could be RawInput or CleanedText). The struct only stores a String as actual data, but the type parameter is used to track the state at the type level. So having PhantomData<S> allows Rust's type system to enforce at compile time which methods are available for which state, even though there's no actual value stored. We covered another PhantomData example at the 2021 Halloween Edition of the Desert Rust meetup.
More about the Eunicode Crate
Eunicode is a text processing CLI and library that helps sanitize text by removing the naughty bits to make strings good and safe:
- No unexpected unicode characters to prevent homograph attacks.
- No dangerous characters (zero-width, text direction changes, etc.) used to hide dangerous text in innocent looking strings.
- "De-fanging" links so that sharing links to IoCs and malware analysis repositories can't be accidentally clicked.
- Remove profanity
Using zeroize to Zero Out Secrets from Memory via @DanielPBank
Repo: https://github.com/danielbank/rust-zeroize-memory
When dealing with secret data (e.g. passwords or private keys), one might naively assume that when the value goes out of scope at the end of a function, it is cleared from memory. Actually it is just deallocated, but still exists in memory. Here is a simple example illustrating that (if following along in the repo, run cargo run --example 1_moving_example):
#[derive(Debug)]
struct EncryptionKey([u8; 4]);
fn get_encryption_key() -> EncryptionKey {
EncryptionKey(*b"AKey")
}
fn main() {
let encryption_key = get_encryption_key();
let ptr = encryption_key.0.as_ptr();
println!("Using key to encrypt stuff: {:?}", encryption_key);
println!("About to drop.");
drop(encryption_key);
println!("Dropped.");
// Your output may vary, reading this memory is undefined behavior.
println!("Memory: {:?}", unsafe {
core::slice::from_raw_parts(ptr, 4)
});
}
This prints:
Using key to encrypt stuff: EncryptionKey([65, 75, 101, 121])
About to drop.
Dropped.
Memory: [65, 75, 101, 121]
Even though we dropped the key, it still exists in memory. Using zeroize, we can specify that the value get zeroed out in memory when it is dropped:
use zeroize::Zeroize;
#[derive(Debug)]
struct EncryptionKey([u8; 4]);
impl Drop for EncryptionKey {
fn drop(&mut self) {
self.0.zeroize();
println!("Zeroed. Remaining value: {:?}", self.0);
}
}
fn get_encryption_key() -> EncryptionKey {
EncryptionKey(*b"AKey")
}
fn main() {
let encryption_key = get_encryption_key();
let ptr = encryption_key.0.as_ptr();
println!("Using key to encrypt stuff: {:?}", encryption_key);
println!("About to drop.");
drop(encryption_key);
println!("Dropped.");
println!("Memory: {:?}", unsafe {
core::slice::from_raw_parts(ptr, 4)
});
}
Looking at the output (cargo run --example 2_moving_example), we see that the value is zeroed out as expected, however we still the original data when we print it:
Using key to encrypt stuff: EncryptionKey([65, 75, 101, 121])
About to drop.
Zeroed. Remaining value: [0, 0, 0, 0]
Dropped.
Memory: [65, 75, 101, 121]
This is because there are several operations in Rust can unintentionally leave copies of data in memory. This includes but is not limited to:
- Moves and
Copy - Heap reallocation when using
VecandString - Borrowers of a reference making copies of the data
In the example, key is a stack variable. When we copy it, it is a memory copy and not a reference to the original memory. So we would need to zero out the new memory address as well. We can get around this by Box'ing the key which forces it be allocated on the heap.
This issue is all documented in a lot more detail in benma's blog article, "A pitfall of Rust's move/copy/drop semantics and zeroing data". The blog article is also the source of the examples presented in the talk.
The final fixed version of the code looks like this (cargo run --example 3_moving_example):
use zeroize::Zeroize;
#[derive(Debug)]
struct EncryptionKey(Box<[u8; 4]>);
impl Drop for EncryptionKey {
fn drop(&mut self) {
println!("Pointer when zeroing: {:p}", self.0.as_ptr());
self.0.zeroize();
println!("Zeroed. Remaining value: {:?}", self.0);
}
}
fn get_encryption_key() -> EncryptionKey {
let key = EncryptionKey(Box::new(*b"AKey"));
println!("Pointer at creation: {:p}", key.0.as_ptr());
key
}
fn main() {
let encryption_key = get_encryption_key();
let ptr = encryption_key.0.as_ptr();
println!("Pointer when using: {:p}", encryption_key.0.as_ptr());
println!("Using key to encrypt stuff: {:?}", encryption_key);
println!("About to drop.");
drop(encryption_key);
println!("Dropped.");
println!("Memory: {:?}", unsafe {
core::slice::from_raw_parts(ptr, 4)
});
}
The output is correctly zeroed (though on a MacBookPro, the memory gets reused and is not actually zeroes but instead some other unrelated data):
Pointer at creation: 0x6000029c8030
Pointer when using: 0x6000029c8030
Using key to encrypt stuff: EncryptionKey([65, 75, 101, 121])
About to drop.
Pointer when zeroing: 0x6000029c8030
Zeroed. Remaining value: [0, 0, 0, 0]
Dropped.
Memory: [48, 128, 66, 25]
Crates you should know
- https://crates.io/crates/eunicode: A text processing CLI and library that helps sanitize text by removing the naughty bits to make strings good and safe. (by our very own Elio Grieco)
- https://crates.io/crates/iroh: P2P Quic connections dialed by public key
- https://crates.io/crates/secrecy: Wrapper types and traits for secret management which help ensure they aren't accidentally copied, logged, or otherwise exposed (as much as possible), and also ensure secrets are securely wiped from memory when dropped.
- https://crates.io/crates/zeroize: Securely clear secrets from memory with a simple trait built on stable Rust primitives which guarantee memory is zeroed using an operation will not be 'optimized away' by the compiler.
- https://crates.io/crates/iced: A cross-platform GUI library inspired by Elm
- https://crates.io/crates/ratatui: A library that's all about cooking up terminal user interfaces
- https://crates.io/crates/genco: A whitespace-aware quasiquoter for beautiful code generation.
- https://crates.io/crates/loom: Permutation testing for concurrent code
- https://crates.io/crates/yaml_rust2: A fully YAML 1.2 compliant YAML parser
- https://crates.io/crates/clap: A simple to use, efficient, and full-featured Command Line Argument Parser