Computational explorations and scientific visualizations
Building a Cryptographic Hash Function from Scratch in Rust (part 2) · codz.uz
rustcryptographysystems-programmingweb3
building a cryptographic hash function from scratch in rust (part 2)
I gave TorqueHash the ability to stream data using Rust's Write trait - turning it from a byte-slice toy into a composable I/O component with a real CLI.
5 min read
162 views
title: "Building a Cryptographic Hash Function from Scratch in Rust (part 2)"
date: "2026-02-15"
description: "I gave TorqueHash the ability to stream data using Rust's Write trait — turning it from a byte-slice toy into a composable I/O component with a real CLI."
tags: ["rust", "cryptography", "systems-programming", "web3"]
author: "Khusniddin Fakhritdinov"
slug: "building-a-cryptographic-hash-function-from-scratch-in-rust-part-2"
Building a Cryptographic Hash Function from Scratch in Rust (part 2)
In part 1, I built TorqueHash — a minimal cryptographic hash function using sponge construction and ARX operations. It worked. You fed it a byte slice, it scrambled the bits through 24 rounds of permutations, and out came a 256-bit digest.
But it had a limitation: the only way to use it was to call update() with a &[u8]. Want to hash a file? You'd have to read the entire thing into memory first. That's not how real tools work. sha256sum doesn't load a 4GB file into RAM - it streams data through the hash function in chunks.
This time, I wanted TorqueHash to plug into Rust's I/O ecosystem - the same way a file, a network socket, or stdout does.
The Write Trait: Turning a Hasher into a Byte Sink
Rust's standard library has a trait called std::io::Write. Anything that implements it can receive bytes - files, TCP streams, buffered writers, even stdout. The trait requires just two methods:
flush() is a no-op — hashing is a one-way absorption, there's nothing to flush. And write() delegates to update(), returning how many bytes were consumed (all of them, always).
This is about 10 lines of code. But those 10 lines unlock something powerful.
io::copy — Streaming for Free
Once TorqueHash implements Write, it works with std::io::copy out of the box:
io::copy reads from the source in chunks and writes them into the hasher. No intermediate Vec<u8>. No loading the entire file into memory. Rust's standard library handles the plumbing — you just connect the pipes.
This is what I find satisfying about Rust's trait system. You implement a small interface, and suddenly your type composes with the entire I/O stack. There's no inheritance hierarchy, no "framework" to buy into. Just a contract: "I can receive bytes."
Building the CLI
With streaming in place, turning TorqueHash into a command-line tool was straightforward:
It works the same way sha256sum does - point it at a file, get a hash. The entire streaming pipeline - file read, chunked transfer, absorption, finalization - happens without ever holding more than a small buffer in memory.
Testing the Streaming Path
I wanted to verify that hashing through the Write trait produces the same result as calling update() directly. They take the same internal path, but a test makes that guarantee explicit:
#[test]fntest_write_trait() {
letmut hasher = TorqueHash::new();
write!(hasher, "The answer is 42!").unwrap();
letdigest = hasher.finalize();
letmut manual = TorqueHash::new();
manual.update(b"The answer is 42!");
assert_eq!(digest, manual.finalize());
}
And the full streaming path with io::copy, using a Cursor as a fake file:
running 7 tests
test tests::test_avalanche_effect ... ok
test tests::test_determinism ... ok
test tests::test_empty_input ... ok
test tests::test_incremental_update ... ok
test tests::test_output_length ... ok
test tests::test_streaming_io_copy ... ok
test tests::test_write_trait ... ok
test result: ok. 7 passed; 0 failed; 0 ignored
Both pass. The streaming path and the manual path produce identical digests.
What I Learned
Traits are interfaces with leverage. Implementing Write is ~10 lines of code, but it connects your type to the entire standard I/O stack - io::copy, BufWriter, the write! macro, anything that expects a writer.
Think in terms of data flow. A hasher isn't an "object that hashes things" - it's a sink. Once I saw it that way, implementing Write felt obvious rather than clever.
Rust rewards composability over frameworks. You don't build a "file hashing framework." You implement a trait and let the standard library do the rest. Coming from Python, where I'd reach for argparse and wrapper classes, this minimalism is refreshing.
What's Next
The next challenge is internal hardening - making TorqueHash more robust without changing its external behavior. That means length-encoded padding, chunk-based absorption instead of byte-by-byte processing, and hiding internal state from debug output. The kind of work that separates a sketch from something structurally sound.