The Ouroboros Engine

July 9, 2026 · View on GitHub

image

FREE Reverse Engineering Self-Study Course HERE


The Ouroboros Engine

A bare-metal cryptographic authentication framework written in pure AVR assembly for the ATmega328P.


FieldValue
AuthorKevin Thomas (ket189@pitt.edu)
Version1.0.0
Date2026-06-26
TargetATmega328P
Clock8 MHz internal RC
Toolchainavr-as, avr-ld, avrdude
LicenseCC BY 4.0

Table of Contents


Overview

The Ouroboros Engine is a single-file, dependency-free embedded system that implements a hardware-bound cryptographic challenge-response path. Running entirely in hand-crafted AVR assembly on an ATmega328P clocked at 8 MHz with no external crystal, it accepts a secret passphrase over a serial terminal, stretches it through a deliberately expensive Davies-Meyer hash construction (24,576 iterations of Speck-128/256), derives a CTR-mode keystream, decrypts an encrypted bytecode payload from flash, checks a 32-byte fixed MAC region with a constant-time branchless routine, and — on success — executes a minimal bytecode interpreter that drives WS2812B LEDs and transmits a response string over UART.

The name "Ouroboros" refers to the self-referential nature of the design: the hash output feeds the CTR nonce, which decrypts the program that controls the system, which was originally encrypted by the same algorithm running in Python — a circular, self-contained trust chain.


System Architecture

$ ┌─────────────────────────────────────┐ │ \text{ATmega328P} @ 8 \text{MHz} │ │ │ \text{UART} (9600 8\text{N1}) ─────►│ \text{handle\_uart}() │ │ │ │ │ ▼ │ │ \text{input\_buf}[32] (\text{SRAM}) │ │ │ │ │ ▼ │ │ \text{speck\_256\_key\_schedule}() │ │ → \text{round\_keys}[272] (34 \times 8 \text{bytes}) │ │ │ │ │ ▼ │ │ \text{davies\_meyer\_hash\_loop}() │ │ → \text{hash\_buf}[16] (24{,}576 \text{iters}) │ │ │ │ │ ▼ │ │ \text{blind\_xor\_decryption}() │ │ (\text{constant}-\text{time} \text{over} \text{all} \text{entries}) │ │ │ │ │ ▼ │ │ \text{branchless\_mac\_verification}() │ │ → \text{result\_buf}[48] \text{masked} │ │ │ │ │ ▼ │ │ \text{dispatch\_program}() │ │ (\text{bytecode} \text{VM}) │ │ │ │ │ │ ▼ ▼ │ \text{UART} \text{TX} ◄────────────│ \text{tx\_str}() \text{ws2812\_fill}() │ │ │ │ \text{PD6} ─────────────────│ 4 \times \text{WS2812B} \text{LEDs} │ \text{PD3} ─────────────────│ \text{handle\_button}() │ └─────────────────────────────────────┘ $


Cryptographic Design

Speck-128/256

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:

ParameterValue
Block size128 bits (2 × 64-bit words)
Key size256 bits (4 × 64-bit words)
Rounds34
Word size64 bits
Rotation constantsα=8\alpha = 8, β=3\beta = 3

Round Function:

Each encryption round transforms the 128-bit state (x,y)(x, y) with round key kik_i:

x=(x8+y)mod264kix' = \left( x \ggg 8 + y \right) \bmod 2^{64} \oplus k_i

y=(y3)xy' = (y \lll 3) \oplus x'

where \ggg denotes right rotation and \lll denotes left rotation.

Key Schedule:

Given the 256-bit key split as (k0,l0,l1,l2)(k_0, l_0, l_1, l_2) (each 64-bit word, little-endian):

li+3=(ki+(limod38))mod264il_{i+3} = \left( k_i + (l_{i \bmod 3} \ggg 8) \right) \bmod 2^{64} \oplus i

ki+1=(ki3)li+3k_{i+1} = (k_i \lll 3) \oplus l_{i+3}

for i=0,1,,32i = 0, 1, \ldots, 32, producing 34 round keys k0,k1,,k33k_0, k_1, \ldots, 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: Implements x8x \ggg 8 via a byte-shift rotation of registers R0–R7, followed by a multi-precision 64-bit addition with R8–R15 (the yy word and key).
  • speck_round_half2: Implements y3y \lll 3 via three sequential LSL/ROL chains 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 ll values. Access uses modulo-3 addressing computed by repeated subtraction to avoid the DIV instruction (which AVR lacks):

offset=(imod3)×8\text{offset} = (i \bmod 3) \times 8


Davies-Meyer Key Stretching

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=6A09E667 BB67AE85 3C6EF372 A54FF53A16IV = \text{6A09E667 BB67AE85 3C6EF372 A54FF53A}_{16}

The key stretching proceeds as:

H0=IVH_0 = IV

Hj=EKuser(Hj1),j=1,2,,24576H_j = E_{K_{\text{user}}}(H_{j-1}), \quad j = 1, 2, \ldots, 24576

H^=H24576IV\hat{H} = H_{24576} \oplus IV

The final feed-forward XOR (the Davies-Meyer step) ensures that H^\hat{H} is not directly recoverable by inverting the cipher. The resulting 16-byte H^\hat{H} 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_{\text{enc}} \approx \frac{34 \times 103 \text{ cycles}}{8 \times $10^{6}$ \text{ Hz}} \approx 437\ \mu s

Over 24,576 iterations:

Thash24576×437 μs10.74 secondsT_{\text{hash}} \approx 24576 \times 437\ \mu s \approx 10.74\ \text{seconds}

This roughly 10-second window per attempt reduces the rate of on-device online guessing and raises the modeled cost of offline simulation. An attacker must still replicate 24,576 sequential Speck-128/256 encryptions per password candidate.


CTR-Mode Decryption

After key stretching, a CTR-mode keystream is generated to decrypt the flash-resident ciphertext table. The CTR block for block index b{0,1,2}b \in \{0, 1, 2\} is constructed as:

CTRb=H^[0:8]b077 zero bytes\text{CTR}_b = \hat{H}[0:8] \| b \| \underbrace{0^7}_{\text{7 zero bytes}}

where H^[0:8]\hat{H}[0:8] is the 8-byte nonce from the stretched hash, and bb 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:

KSb=EKuser(CTRb)\text{KS}_b = E_{K_{\text{user}}}(\text{CTR}_b)

Decryption of block bb of the 48-byte ciphertext entry:

Pb=CbKSbP_b = C_b \oplus \text{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 \times \text{0xAA}$)

Branchless MAC Verification

After decryption, bytes 16–47 of result_buf must equal 0xAA if the password was correct. Verification is performed in a fixed-iteration branchless routine with no data-dependent branches in that check:

Error accumulation (OR-reduce):

Let P[i]P[i] denote result_buf[i]. Each byte is checked via SUBI 0xAA; differences accumulate via OR into δ[0,255]\delta \in [0, 255]:

δ=i=1647(P[i]0xAA)\delta = \bigvee_{i=16}^{47} \bigl(P[i] - \text{0xAA}\bigr)

Mask generation (2's-complement trick):

The AVR instructions NEG, SBC, COM implement a branchless transformation:

m={0xFFif δ=0 (all bytes match)0x00if δ0 (any mismatch)m = \begin{cases} \text{0xFF} & \text{if } \delta = 0 \text{ (all bytes match)} \\ \text{0x00} & \text{if } \delta \neq 0 \text{ (any mismatch)} \end{cases}

Proof: If δ=0\delta = 0: NEG(0) = 0, carry =0= 0; SBC(0,0,0) = 0; COM(0) = 0xFF
If δ0\delta \neq 0: NEG(δ\delta) = $256 - \delta,carry, carry = 1;SBC(; `SBC(`256-\delta, \256-\delta, \1$) = -1 = 0xFF; COM(0xFF) = 0x00

Branchless result masking:

P[i]P[i]m,i=0,,47P[i] \leftarrow P[i] \wedge m, \quad i = 0, \ldots, 47

If the MAC fails, the entire 48-byte buffer is zeroed. The bytecode dispatcher then encounters 0x00 (END opcode) immediately, executing nothing.


Blind Cipher Selection

The engine contains a flash-resident table of NN encrypted 48-byte entries (table_ciphers). All NN 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 NN iterations, if found0xFF\text{found} \neq \text{0xFF}, the winning entry is re-decrypted and dispatched. The total number of decrypt+verify operations is always exactly NN, regardless of the password or table contents.


Hardware Subsystems

WS2812B LED Driver

Four WS2812B LEDs are driven on PD6 via cycle-accurate bit-banging. At 8 MHz (125 ns/cycle):

Bit typeTHT_H (HIGH)TLT_L (LOW)Total
0-bit2 cycles = 250 ns8 cycles = 1000 ns1.25 μs
1-bit6 cycles = 750 ns4 cycles = 500 ns1.25 μs

The code uses OUT PORTD, Rn (1 cycle, single-word instruction) rather than SBI/CBI (2 cycles) to maintain deterministic timing. Interrupts are disabled (CLI) for the entire fill operation and re-enabled (SEI) after the last LED's data is shifted out.

The reset condition (>80 μs LOW) occurs naturally between ws2812_fill calls, since the main loop delay vastly exceeds 80 μs. No explicit reset subroutine is required.

Data is sent in GRB order as required by the WS2812B protocol: Green → Red → Blue (24 bits × 4 LEDs = 96 bits total per fill).

Wait — looking at the fill:

MOV R24, R17 ; R24 = Red
RCALL send_byte
MOV R24, R18 ; R24 = Green
RCALL send_byte
MOV R24, R16 ; R24 = Blue
RCALL send_byte

The registers are ordered R (R17), G (R18), B (R16) by the calling convention, sent Red first, then Green, then Blue. The WS2812B wire protocol is GRB, so effectively the LED display order is configured to match the wire format by register assignment in ws2812_fill.

Bit-banging loop (per bit, 10 cycles):

Cycle 1:    OUT HIGH       ; PD6 = 1 (always)
Cycle 2:    SBRS R24, 7    ; test bit 7 (1 or 2 cycles)
Cycle 2/3:  OUT LOW        ; PD6 = 0 (0-bit path only)
Cycle 3/4:  LSL R24        ; shift next bit into position
Cycle 4/5:  NOP            ; timing pad
Cycle 5/6:  NOP            ; timing pad
Cycle 6/7:  OUT LOW        ; PD6 = 0 (1-bit path, end of high)
Cycle 7/8:  DEC R23        ; bit counter
Cycle 8-10: BRNE .bit_loop ; 2 cycles (taken), 1 cycle (not taken)

UART Interface

UART0 is configured for 9600 baud, 8N1 using the ATmega328P's built-in USART peripheral. The baud rate register value is:

UBRR=fCPU16×baud1=8,000,00016×96001=51\text{UBRR} = \left\lfloor \frac{f_{\text{CPU}}}{16 \times \text{baud}} \right\rfloor - 1 = \left\lfloor \frac{8{,}000{,}000}{16 \times 9600} \right\rfloor - 1 = 51

All UART operations are polling-based (no interrupts). The receive loop checks RXC0 (Receive Complete flag in UCSR0A) before reading UDR0. Transmit loops on UDRE0 (Data Register Empty).

After the crypto pipeline completes (success or fail), uart_flush_rx hard-resets the UART receiver by toggling RXEN0. This clears the Data OverRun (DOR) flag accumulated during the ~10-second hash computation window and flushes any stale bytes in the hardware shift register.

The input buffer (input_buf, 32 bytes) is zero-padded to exactly 32 bytes on ENTER, then immediately zeroed after use by clear_input_buf to prevent key material persistence in SRAM.


Button Logic

A normally-open tactile switch is connected to PD3 with the internal pull-up resistor enabled (SBI PORTD, PD3). The pin is active-LOW.

The button cycles the system state machine: ANIM → RED → GREEN → BLUE → ANIM. During STATE_INPUT, button presses are ignored. Debounce is software-only:

T_{\text{debounce}} = 256 \times 256 \times \frac{3\ \text{cycles}}{8 \times $10^{6}$\ \text{Hz}} \approx 24.6\ \text{ms}

The outer loop (R17) runs 256 times, each iteration running the inner loop (R16) 256 times at ~3 cycles/iteration, totaling 196,608\approx 196,608 cycles 24.6\approx 24.6 ms of debounce blanking.


Timer Architecture

Two independent 16-bit software counters are maintained in SRAM, incremented once per main-loop iteration by delay_and_timers:

Animation timer (anim_timer):

T_{\text{iter}} = \frac{4 \times 1349\ \text{cycles}}{8 \times $10^{6}$\ \text{Hz}} \approx 674.5\ \mu s

Thresholdanim=(0x1C8)0xD4=7380\text{Threshold}_{\text{anim}} = (\text{0x1C} \ll 8) \mid \text{0xD4} = 7380

Tanim=7380×674.5 μs4.98 sT_{\text{anim}} = 7380 \times 674.5\ \mu s \approx 4.98\ \text{s}

Colors advance every ~5 seconds in STATE_ANIM.

Input inactivity timeout (input_timer):

Thresholdinput=(0xAC8)0xF8=44280\text{Threshold}_{\text{input}} = (\text{0xAC} \ll 8) \mid \text{0xF8} = 44280

Ttimeout=44280×674.5 μs29.87 sT_{\text{timeout}} = 44280 \times 674.5\ \mu s \approx 29.87\ \text{s}

The input session auto-cancels after ~30 seconds of inactivity. Timer1 (TCCR1B, TCNT1H/L) is available to hardware_jitter_and_exe as an entropy source for side-channel mitigation; TCNT0 is read for jitter.


Bytecode Interpreter

dispatch_program executes a minimal 4-opcode bytecode language from result_buf. Execution halts on unknown opcodes (fail-safe):

OpcodeMnemonicOperandsAction
0x00ENDHalt execution
0x01LED_FILLR, G, BFill all 4 LEDs with solid colour
0x03TX_STRlen, char...Transmit len bytes over UART
0xAAMACHalt (integrity marker, never valid after strip)

The interpreter is a simple fetch-decode-execute loop with linear forward-only execution. No jumps, no stack, no variables — purely sequential. Unknown opcodes call RET immediately (the BRNE .done path), preventing runaway execution.

Example payload (hello → world):

01 FF 00 FF          ; LED_FILL R=255 G=0 B=255  (purple)
03 07                ; TX_STR len=7
77 6F 72 6C 64 0D 0A ; "world\r\n"
00                   ; END
[00 00 ...]          ; zero-padding to 16 bytes
AA AA ... AA         ; 32-byte MAC padding (0xAA × 32)

Side-Channel-Relevant Implementation Notes

Hardware Jitter (hardware_jitter_and_exe)

Before every encrypt_block call during the hash loop and during key schedule, a random timing jitter is injected:

IN   R16, 0x26    ; Read TCNT0 (free-running Timer/Counter 0)
ANDI R16, 0x07    ; Isolate bottom 3 bits → jitter in [0, 7] cycles
.jitter_loop:
DEC  R16
BRPL .jitter_loop ; loop while ≥ 0 (signed)

This introduces 0–7 cycles of delay before each Speck encryption, derived from the low-order bits of the free-running hardware timer. This changes trace alignment characteristics, but this repo does not provide measured DPA/FI success-rate data.

Constant-Time MAC Comparison

branchless_mac_verification never branches on the compared MAC-region bytes. The 32-byte MAC region is checked with an OR-reduce accumulator and a 2's-complement mask in that routine.

Constant-Iteration Cipher Scan

blind_xor_decryption + branchless_mac_verification is always called exactly CIPHER_ENTRIES times, regardless of match position. This removes that specific early-exit timing difference.

Key Material Zeroization

clear_input_buf zeros all 32 bytes of input_buf after the crypto pipeline completes, reducing how long that buffer remains populated.

UART DOR Recovery

uart_flush_rx toggles RXEN0 after each crypto operation to flush UART overflow state accumulated during the ~10-second hash window before the next receive cycle.

Watchdog Neutralization

clear_reset_flags disables the watchdog timer at boot using the two-step unlock sequence required by the ATmega328P silicon:

CLI                         ; Global interrupt disable
WDR                         ; Pet the dog one last time
CLR R16
OUT MCUSR, R16              ; Clear WDRF (watchdog reset flag)
LDI R16, (1<<WDCE)|(1<<WDE)
STS WDTCSR, R16             ; Unlock WDTCSR
CLR R16
STS WDTCSR, R16             ; Disable WDT permanently

Failure to clear WDRF before disabling the WDT causes the ATmega328P to ignore all WDT disable instructions, producing an infinite 15 ms boot loop.


State Machine

                    ┌──────────┐
              ┌────►│  ANIM    │◄────────────────────────┐
              │     │ (R→G→B)  │                         │
              │     └─────┬────┘                         │
              │           │ button                       │ 5s timeout
              │           ▼                              │ (after fail)
              │     ┌──────────┐                         │
              │     │   RED    │                         │
              │     └─────┬────┘                         │
              │           │ button                       │
              │           ▼                              │
              │     ┌──────────┐                   ┌─────┴────┐
              │     │  GREEN   │                   │  PURPLE  │
              │     └─────┬────┘                   │ (fail)   │
              │           │ button                 └──────────┘
              │           ▼
              │     ┌──────────┐
              │     │   BLUE   │
              │     └─────┬────┘
              │           │ button
              └───────────┘

              Any state + UART keystroke ──────► STATE_INPUT
              STATE_INPUT + ENTER (correct) ───► YELLOW (5s) → restore
              STATE_INPUT + ENTER (wrong)  ───► PURPLE (5s) → restore
              STATE_INPUT + 30s timeout   ───► restore prev_state
StateValueLED ColourTrigger
STATE_ANIM0Cycling R/G/BBoot / button wrap
`STATE_RED$1\text{Solid} \text{red}1 \times \text{button}
STATEGREENSTATE_GREEN2\text{Solid} \text{green}2 \times \text{button}
STATEBLUESTATE_BLUE3\text{Solid} \text{blue}3 \times \text{button}
$STATE_INPUT`4White (keystroke)Any UART character
YellowCrypto success
PurpleCrypto failure

Boot Sequence

On power-on or reset, main executes the following initialization sequence before entering the main loop:

  1. Stack pointer init: SPH:SPL ← RAMEND (0x08FF)
  2. SRAM clear: Zero all 2,048 bytes (0x01000x08FF) via X-pointer loop
  3. Watchdog kill: clear_reset_flags (clears WDRF, disables WDTCSR)
  4. UART init: uart_init (9600 baud, 8N1, RX/TX enabled)
  5. Pin config: config_pins (PD6 output/LOW for WS2812; PD3 input/pull-up for button)
  6. State init: Zero sys_state, uart_idx, anim_idx, prev_state, anim_timer
  7. Initial render: render_state → LEDs show red (first animation frame)
  8. Prompt: tx_prompt → sends "> " over UART

Memory Map

Flash (32 KB, .text section)

RegionContent
0x0000Reset vector → RJMP main
0x0002+main, all included .s files
iv_const16-byte SHA-256 IV constant
table_ciphersN×48N \times 48 bytes encrypted bytecode entries

SRAM (2 KB, .bss section, base 0x0100)

SymbolSizePurpose
`round_keys$272 \text{bytes}34 \times 8-\text{byte} \text{Speck} \text{round} \text{keys}
$l_buf`32 bytesKey schedule circular buffer
input_buf32 bytesUART receive buffer (padded to 32)
hash_buf16 bytesDavies-Meyer hash output / CTR nonce
ctr_buf16 bytesCTR counter block
result_buf48 bytesDecrypted plaintext + MAC
sys_state1 byteCurrent system mode
prev_state1 byteMode saved before input
anim_idx1 byteAnimation sub-colour (0=R,1=G,2=B)
uart_idx1 byteUART write index
anim_timer2 bytes16-bit animation counter
input_timer2 bytes16-bit inactivity counter
Total425 bytes(20.8% of 2,048 bytes)

Build & Flash

Prerequisites

# macOS (Homebrew)
brew install avr-binutils avr-gcc avrdude

# Ubuntu/Debian
sudo apt install binutils-avr avr-libc avrdude

Build

make       # Assemble, link, generate .hex, .lss, and ELF info
make flash # Flash .hex via USBtiny ISP
make fuses # Set fuses: 8 MHz internal, no CKDIV8
make lock  # Set lock bits: prevent external flash readback
make clean # Remove all build artifacts

Fuse Settings

FuseValueMeaning
LFUSE0xE2Internal 8 MHz RC, CKDIV8 disabled
HFUSE0xD5SPIEN on, JTAG off
EFUSE0xFDBOD at 2.7 V

Warning: make lock sets lock bits to 0x00 (no external read/write of flash or EEPROM). This is irreversible without a chip-erase (which destroys all flash contents).

Adding Cipher Entries

Use scripts/dec.py to generate new encrypted bytecode entries:

python3 scripts/dec.py

Edit MASTER_KEY and TARGET_FLAG at the top of the script. The script outputs .byte directives ready to paste into data.s. Increment CIPHER_ENTRIES in defines.s accordingly.


File Structure

ouroboros/
├── Makefile        # Build system (avr-as + avr-ld)
├── asm/
│   ├── main.s      # Entry point, stack init, SRAM clear, main loop
│   ├── defines.s   # All .equ constants (registers, state codes, thresholds)
│   ├── variables.s # BSS section (all SRAM buffers)
│   ├── data.s      # Flash data: IV constant, cipher table
│   ├── boot.s      # clear_reset_flags (WDT kill)
│   ├── config.s    # config_pins (PD3/PD6)
│   ├── uart.s      # uart_init, uart_tx_byte, uart_flush_rx, tx_prompt, tx_crlf
│   ├── ws2812.s    # send_byte, ws2812_fill, render_state
│   ├── button.s    # handle_button (debounce, state cycle)
│   ├── input.s     # handle_uart (echo, backspace, crypto pipeline trigger)
│   ├── ouroboros.s # speck_256_key_schedule, speck_round_half1/2,
│   │               # encrypt_block, davies_meyer_hash_loop,
│   │               # keystream_generation, blind_xor_decryption,
│   │               # branchless_mac_verification, hardware_jitter_and_exe
│   ├── dispatch.s  # dispatch_program (bytecode VM)
│   └── delay.s     # delay_and_timers, force_advance, delay_5s
├── scripts/
│   └── dec.py      # Python reference implementation & ciphertext generator
├── docs/
│   ├── ATmega328P-Datasheet.pdf
│   ├── 5050-WS2812B.pdf
│   ├── Atmel-AVR-InstructionSet.pdf
│   └── Atmel-Mixing-C-ASM.pdf
├── pinout/
│   └── Atmega328-Pinout.png
└── LICENSE

Security Analysis

Threat Model

The Ouroboros Engine is analyzed here against an adversary who:

  • Has physical access to the assembled hardware
  • Can observe all UART traffic
  • Can measure power consumption during operation
  • Has a copy of the firmware binary
  • Knows the algorithm (Kerckhoffs's principle)

The secret is exclusively the passphrase stored in input_buf during authentication.

Attack Surface

AttackMitigation
Online brute-force~10-second hash per attempt; physically on-device
UART timing oracleConstant-time MAC; constant-iteration scan
Power analysis (SPA)Hardware jitter (0–7 random cycles per Speck block)
DPAJitter misaligns traces and raises analysis cost; no measured DPA/FI success-rate dataset is published in this repo
Cold-boot attackclear_input_buf zeros key material after use
Flash readbackLock bits 0x00 prevent external ISP reads
Firmware reverse engineeringAlgorithm is public (Speck); security through key entropy
Ciphertext malleabilityBranchless MAC zeroes result on any byte mismatch
Watchdog reset loopWDRF cleared and WDTCSR disabled at boot

Key Space

The 32-byte input_buf represents a maximum of $256^{32} = 2^{256}keys(padded).Thestrictadversarialscenariousedbelowassumesanexternallyenforced32characternondictionaryrandommasterkeyoverbase62symbols( keys (padded). The strict adversarial scenario used below assumes an externally enforced 32-character non-dictionary random master key over base62 symbols (|\Sigma| = 62$). For any human-chosen passphrase, effective entropy can be much lower and depends on passphrase length and character set:

H=log2(Σn)=nlog2ΣH = \log_2(|\Sigma|^n) = n \log_2 |\Sigma|

For printable ASCII (Σ=95|\Sigma| = 95) and n=16n = 16 characters: H105H \approx 105 bits.

Offline Attack Cost

Reference attacker model (same as the paper):

NGPU1012 Speck ops/sN_{\text{GPU}} \approx 10^{12}\ \text{Speck ops/s}

Tattempt=24576101224.6 ns per candidateT_{\text{attempt}} = \frac{24576}{10^{12}} \approx 24.6\ \text{ns per candidate}

Classical exhaustive-search estimate for mandatory random base62 n=32n=32:

Tclassical=6232×24.6 ns5.59×1049 s1.77×1042 yearsT_{\text{classical}} = 62^{32} \times 24.6\ \text{ns} \approx 5.59 \times 10^{49}\ \text{s} \approx 1.77 \times 10^{42}\ \text{years}

Optimistic Grover-style average-time estimate (toy model, favorable to attacker):

Tquantum62322×24.6 ns5.86×1020 s1.86×1013 yearsT_{\text{quantum}} \approx \frac{\sqrt{62^{32}}}{2} \times 24.6\ \text{ns} \approx 5.86 \times 10^{20}\ \text{s} \approx 1.86 \times 10^{13}\ \text{years}

These are model outputs, not measured throughput benchmarks. They do not imply post-quantum security; the accurate claim is quantum-costly under stated assumptions.


The Ouroboros Engine — where the cipher devours its own tail.


See LICENSE.