FREE Reverse Engineering Self-Study Course HERE
A 100% strict, generic Embedded Rust implementation of the Ouroboros cryptographic engine, maintaining constant-time execution.
Because encryption is a #![no_std] generic library, you will need the Rust toolchain installed on your system. Follow the steps below for your operating system.
If you haven't installed Rust yet, install it via rustup:
macOS / Linux:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh(On Windows, download and run rustup-init.exe from the official site).
Next, if you plan to compile this library for an embedded target like AVR, you will need the nightly toolchain and the rust-src component:
rustup default nightly
rustup component add rust-srcYou can build the generic encryption crate natively. The Cargo.toml is configured for #![no_std].
To build the project:
cargo buildThis project includes an extensive suite of unit tests, verifying the Speck-128/256 cipher, Davies-Meyer hash, CTR-mode decryption, and constant-time MAC verification.
You can run the tests directly on your host machine:
cargo testThe repository includes a host-based mock hardware demo. Rather than just printing "hello world", this demo fully simulates the bare-metal firmware using your computer's terminal! It sets your terminal to raw mode and processes keystrokes through the exact same OuroborosEngine used on the microcontrollers.
To run the demo natively on Linux, Mac (Intel / Silicon):
cargo run --bin demoInteraction:
- You will be greeted with a
>prompt. - Type a password (e.g.,
hello) and press Enter. (The engine echoes your keystrokes back to the terminal). - The engine instantly computes the 24,576 Speck iterations and decrypts the payload. If the password was correct, it will immediately print the decrypted payload (e.g.,
world). If incorrect, it will print nothing. - The demo will then purposefully hang for exactly 10 seconds to enforce a mandatory constant-time hardware security delay.
- It will prompt
>again for the next entry.
The repository also includes a hardware implementation for the RP2350 microcontroller. This implementation utilizes the onboard LED (GPIO 25) and creates a USB CDC ACM (Serial) device for UART communication.
To flash the firmware to a Raspberry Pi Pico 2:
-
Connect the Pico 2 to your computer via USB while holding the BOOTSEL button to mount it as a mass storage device.
cd rp2350 cargo build --release picotool uf2 convert target/thumbv8m.main-none-eabihf/release/rp2350 -t elf rp2350.uf2 --family rp2350-arm-s picotool load -x rp2350.uf2Note: macOS Finder sometimes hangs when copying UF2 files to the Pico's BOOTSEL drive due to Spotlight indexing. We use
picotool load -xto securely flash and execute the UF2 without relying on Finder. -
Once flashed, open a serial terminal to communicate with the USB device:
- On macOS/Linux:
screen /dev/tty.usbmodem* 115200 - On Windows: Use PuTTY or similar to connect to the new COM port.
- On macOS/Linux:
-
Type
helloand press Enter. The serial terminal will instantly outputworldand the onboard LED will light up, followed by a mandatory 10-second constant-time security delay. Typing any other password will result in the exact same 10-second delay with the LED remaining off and no output.
While encryption utilizes mathematically sound cryptography, true security depends on the threat vector.
All security guarantees in this engine assume the user has deployed a high-entropy, non-dictionary password (e.g., a 256-bit random string). If a weak dictionary word (like hello) is used, the password can be trivially brute-forced offline, rendering all other defenses moot.
Assuming a cryptographically secure, high-entropy password is used, our official adversarial posture is as follows:
If an adversary hacks your computer and steals the rp2350.uf2 firmware file, or interacts with the device purely over the USB/UART serial interface (no physical access):
- The Firmware Contains No Keys: The firmware
.uf2file contains the encrypted ciphertext and the algorithmic blueprints, but the master password is never stored on the device. - Brute-Force & Quantum Cryptanalysis: The engine uses Speck-256 (NIST Post-Quantum compliant for symmetric keys) wrapped in a 24,576-iteration Davies-Meyer KDF. This cryptographic "tar pit" makes a high-entropy key space computationally impossible to guess over a serial port, while simultaneously crippling the theoretical quadratic speed-up of a quantum Grover's algorithm.
- Conclusion: From a software perspective, even if the adversary fully extracts the firmware, the payload is mathematically impenetrable. It is 100% secure.
Even if an adversary has physical access to the bare-metal microcontroller (like the RP2040, RP2350, or AVR ATmega) and utilizes a specialized hardware hacking laboratory, the software cryptography mathematically resists silicon-level bypasses:
- Fault Injection (EMFI / Voltage Glitching): An adversary could fire a nanosecond electromagnetic pulse directly at the Cortex-M33 ALU precisely when it evaluates the MAC validation branch to cause the CPU to hallucinate and skip the memory wipe. However, this bypasses nothing of value. Because the key is cryptographically stretched and mixed into the CTR stream, the payload itself will decrypt to randomized garbage. The attacker still lacks the true key.
- Side-Channel Analysis (DPA / CPA): By executing a massive 24,576 Speck iterations, the engine creates a vast, noisy 10-second power trace that makes statistical correlation of the Hamming weight exponentially difficult to extract bit-by-bit from the noise.
- Cold-Boot RAM Extraction: The memory wipe is executed in constant time. Unless the device is physically stolen and frozen with liquid nitrogen precisely during the 10-second execution window before decay, the decrypted payload is never resident in SRAM.
The Ouroboros Engine uses the Speck-128/256 block cipher from the Simon/Speck family (NSA, 2013). Speck is an Add-Rotate-XOR (ARX) cipher optimized for constrained hardware.
Parameters:
| Parameter | Value |
|---|---|
| Block size | 128 bits (2 × 64-bit words) |
| Key size | 256 bits (4 × 64-bit words) |
| Rounds | 34 |
| Word size | 64 bits |
| Rotation constants | alpha = 8, beta = 3 |
Round Function:
Each encryption round transforms the 128-bit state (x, y) with round key k_i:
x' = ((x >>> 8) + y) mod 2^64 XOR k_i
y' = (y <<< 3) XOR x'
where >>> denotes right rotation and <<< denotes left rotation.
Key Schedule:
Given the 256-bit key split as (k_0, l_0, l_1, l_2) (each 64-bit word, little-endian):
l_{i+3} = (k_i + (l_{i mod 3} >>> 8)) mod 2^64 XOR i
k_{i+1} = (k_i <<< 3) XOR l_{i+3}
for i = 0..32, producing 34 round keys k_0..k_33.
AVR Implementation:
The 64-bit words are stored in 8 contiguous byte registers in little-endian order. The assembly uses:
speck_round_half1: Implementsx >>> 8via a byte-shift rotation of registers R0–R7, followed by a multi-precision 64-bit addition with R8–R15 (theyword and key).speck_round_half2: Implementsy <<< 3via three sequentialLSL/ROLchains across registers R8–R15 with carry feed-back, followed by XOR with R0–R7.
All 34 rounds are executed in a loop; each round invokes both half-functions with intermediate key material loaded from round_keys in SRAM.
Key schedule circular buffer (l_buf):
The key schedule uses a 32-byte SRAM circular buffer (l_buf) holding three 8-byte l values. Access uses modulo-3 addressing computed by repeated subtraction to avoid the DIV instruction (which AVR lacks):
offset = (i mod 3) * 8
The user's passphrase (zero-padded to 32 bytes) serves as the Speck-128/256 key in a Davies-Meyer hash construction run for 24,576 iterations over a 16-byte hash buffer initialized to the first 128 bits of the SHA-256 initialization constants:
IV = 0x6A09E667_BB67AE85_3C6EF372_A54FF53A
The key stretching proceeds as:
H_0 = IV
H_j = E(K_user, H_{j-1}) for j = 1..24576
H_final = H_24576 XOR IV
The final feed-forward XOR (the Davies-Meyer step) ensures that H_final is not directly recoverable by inverting the cipher. The resulting 16-byte H_final is stored in hash_buf and serves as the CTR nonce.
Work Factor:
Each encrypt_block call executes 34 Speck rounds. At 8 MHz, a single encrypt_block invocation takes approximately:
T_enc ~= (34 * 103 cycles) / 8,000,000 Hz ~= 437 us
Over 24,576 iterations:
T_hash ~= 24576 * 437 us ~= 10.74 seconds
This 10-second window per attempt makes online brute-force infeasible on the device itself, and creates a significant cost barrier for offline simulation. An attacker must faithfully replicate 24,576 sequential Speck-128/256 encryptions per password candidate.
After key stretching, a CTR-mode keystream is generated to decrypt the flash-resident ciphertext table. The CTR block for block index b in {0, 1, 2} is constructed as:
CTR_b = H_final[0:8] || b || (7 zero bytes)
where H_final[0:8] is the 8-byte nonce from the stretched hash, and b is the 8-bit block counter. The 16-byte CTR block is encrypted with Speck-128/256 using the same user key to produce the keystream:
KS_b = E(K_user, CTR_b)
Decryption of block b of the 48-byte ciphertext entry:
P_b = C_b XOR KS_b
The three decrypted 16-byte blocks concatenate to form the 48-byte result_buf:
- Bytes 0–15: bytecode payload
- Bytes 16–47: MAC padding (
32 * 0xAA)
After decryption, bytes 16–47 of result_buf must equal 0xAA if the password was correct. Verification is performed in constant time with no data-dependent branches, preventing timing side-channels:
Error accumulation (OR-reduce):
Let P[i] denote result_buf[i]. Each byte is checked via SUBI 0xAA; differences accumulate via OR into delta in [0, 255]:
delta = OR_reduce( P[16..47] - 0xAA )
Mask generation (2's-complement trick):
The AVR instructions NEG, SBC, COM implement a branchless transformation:
m = 0xFF if delta == 0 (all bytes match)
m = 0x00 if delta != 0 (any mismatch)
Proof: If delta == 0: NEG(0) = 0, carry = 0; SBC(0,0,0) = 0; COM(0) = 0xFF ✓
If delta != 0: NEG(``delta``) = 256 - delta, carry = 1; SBC(256-delta, 256-delta, 1) = -1 = 0xFF; COM(0xFF) = 0x00` ✓
Branchless result masking:
P[i] = P[i] AND m, for i = 0..47
If the MAC fails, the entire 48-byte buffer is zeroed. The bytecode dispatcher then encounters 0x00 (END opcode) immediately, executing nothing.
The engine contains a flash-resident table of N encrypted 48-byte entries (table_ciphers). All N entries are always tried in full, regardless of early matches, to prevent a timing oracle:
found ← 0xFF (sentinel = "not found")
for i = 0 to N-1:
decrypt entry i → result_buf
verify MAC → mask_i (0xFF or 0x00)
r0 ← i AND mask_i ; 0 if fail, i if pass
found ← (found AND NOT mask_i) OR r0
Implemented in AVR as:
MOV R0, R22 ; R0 = i
AND R0, R17 ; R0 = i if pass, 0 if fail
COM R17 ; invert mask
AND R23, R17 ; R23 = old_found if fail, 0 if pass
OR R23, R0 ; R23 = (pass ? i : old_found)After all N iterations, if found != 0xFF, the winning entry is re-decrypted and dispatched. The total number of decrypt+verify operations is always exactly N, regardless of the password or table contents.
The Ouroboros Engine — where the cipher devours its own tail.
See LICENSE.
