G11: Secure TOTP - shalan/CSCE4301-WiKi GitHub Wiki
Project Title: Secure TOTP
| Name | GitHub |
|---|---|
| Ali Elkhouly | khoulykd |
| Peter Aziz | peter-aziz |
| Moaz Hafez | justagoat21 |
1. The Proposal
Abstract / Elevator Pitch:
Modern two-factor authentication (2FA) is one of the most effective defenses against unauthorized account access, yet most people interact with it only through their smartphones. This project brings that mechanism down to the hardware level by building a standalone TOTP (Time-Based One-Time Password) token from scratch — a physical embedded device that generates the same 6-digit codes used by Google Authenticator and similar apps, with no internet connection required.
The core problem is that traditional passwords are static and easily compromised. TOTP solves this by generating a new password every 60 seconds using a shared secret key and the current Unix timestamp, meaning the code is useless after it expires. Our system implements this on a microcontroller paired with a real-time clock (RTC) module, running the HMAC-SHA1 cryptographic algorithm in embedded C. A companion Python script running on a PC independently computes the same codes — if both match, the system works.
The engineering challenge lies in the synchronization and drift management between the hardware clock and real-world time, implementing a non-trivial cryptographic function in a resource-constrained environment, and demonstrating a complete hardware-software co-design with a meaningful security application.
Proposed Solution
A standalone embedded TOTP token built on the STM32L432KCU microcontroller, paired with a DS3231 RTC module for timekeeping. The device computes HMAC-SHA1 over the current time and a shared secret key, applies RFC 4226 dynamic truncation to produce a 6-digit OTP, and displays it on a PmodCLP LCD. A companion Python script running on a PC independently computes the same codes from the same secret — if both match, the system is validated.
Time synchronization is handled over UART: pressing a button on the STM32 sends a SYNC_REQ to the PC, which responds with the current time. The MCU parses the response and programs the DS3231 over I2C, eliminating clock drift.
Project Objectives & Scope:
Minimum Viable Product:
- Microcontroller reads current time from an RTC module (DS3231 via I2C)
- HMAC-SHA1 implemented or ported to embedded C
- TOTP code computed inspired by RFC 6238 and displayed on OLED/LCD every 60 seconds
- PC-side Python script generates identical codes from the same shared secret
- Codes match consistently across both devices
Functional Requirements
| # | Requirement | Priority |
|---|---|---|
| FR-1 | MCU reads current time from DS3231 RTC via I2C | Must Have |
| FR-2 | HMAC-SHA1 computed on-device using STM32 X-CUBE-CRYPTOLIB | Must Have |
| FR-3 | OTP displayed on 16×2 LCD, refreshed every second | Must Have |
| FR-4 | PC Python script generates identical codes from the same shared secret | Must Have |
| FR-5 | Codes match consistently between device and PC | Must Have |
| FR-6 | Button-triggered UART time sync between device and PC | Must Have |
| FR-7 | OTP refreshes when the minute changes (time window boundary) | Must Have |
| FR-8 | Sync timeout detected and reported over UART | Should Have |
| FR-9 | Visual countdown or time display alongside OTP | Should Have |
2. System Architecture
2.1 High-Level Block Diagram:

Subsystem Breakdown:
The system is organized into five interacting modules: secure key provisioning, time synchronization, OTP generation, user display, and PC-side validation. Before operation, the same shared secret is loaded onto both the MCU and the Python script, and it is never transmitted during runtime. At startup (or during manual resync), the PC sends a Unix timestamp to the MCU over UART, and the MCU programs the DS3231 RTC over I2C. During normal operation, the MCU reads RTC time every 60 seconds, applies HMAC-SHA1 with the shared secret, and generates the 6-digit TOTP value inspired by RFC 6238. The code is shown on the OLED over SPI/I2C, while the PC script independently computes its own TOTP from system time and the same secret; matching outputs confirm correct synchronization and implementation.
2.2 Software Architecture
The firmware runs on FreeRTOS (CMSIS-OS v2) with four concurrent tasks:
| Task | Stack | Responsibility |
|---|---|---|
RTC_Task |
512 B | Reads DS2321 every second, updates shared time buffers |
OTP_Task |
512 B | Computes HMAC-SHA1 every second, updates current_otp |
Uart_LCD_Task |
512 B | Prints time+OTP over UART and LCD; handles button-triggered SYNC |
Shared data is protected by two FreeRTOS mutexes:
Time_Mutex— guardshourbuffer,minbuffer,secbufferOTP_Mutex— guardscurrent_otpA volatile flagsync_donesignalsRTC_Taskto pause buffer writes during an active UART sync to prevent race conditions.
3. Hardware Design
3.1 Components & Bill of Materials
| Component | Part | Qty | Est. Cost |
|---|---|---|---|
| Microcontroller | STM32L432KCU (Nucleo-32) | 1 | ~$15 |
| RTC Module | DS3231 breakout (I2C) | 1 | ~$2 |
| Display | 16×2 Digilent PmodCLP LCD | 1 | ~$3 |
| Switch | generic Switch | 1 | ~$0.50 |
| Button | Tactile pushbutton | 1 | ~$0.10 |
| Breadboard + wires | — | 1 | ~$5 |
| Battery + Battery pack | 3.7V | 1 | ~$5 |
| Total | ~$31 |
3.2 Wiring / Pinout
| Signal | STM32 Pin | Connected To |
|---|---|---|
| I2C1_SCL | PA9 | DS3231 SCL |
| I2C1_SDA | PA10 | DS3231 SDA |
| LCD RS | PB0 | HD44780 pin 4 |
| LCD E | PB1 | HD44780 pin 6 |
| LCD DB4 | PA3 | HD44780 pin 11 |
| LCD DB5 | PA4 | HD44780 pin 12 |
| LCD DB6 | PA5 | HD44780 pin 13 |
| LCD DB7 | PA6 | HD44780 pin 14 |
| UART TX | PA2 | USB-UART (ST-Link) |
| UART RX | PA15 | USB-UART (ST-Link) |
| Button | PA8 | Tactile switch → GND |
4. Implementation
4.1 Major Implementation Details
HMAC-SHA1 and OTP Generation
The OTP is computed using ST's X-CUBE-CRYPTOLIB (cmox_mac_compute with CMOX_HMAC_SHA1_ALGO). The message input is a 2-byte seed composed of the current hour and minute in BCD. After HMAC-SHA1 produces a 20-byte MAC, RFC 4226 dynamic truncation is applied:
uint8_t offset = mac_out[19] & 0x0F;
uint32_t binary = ((mac_out[offset] & 0x7F) << 24) |
((mac_out[offset + 1] & 0xFF) << 16) |
((mac_out[offset + 2] & 0xFF) << 8) |
( mac_out[offset + 3] & 0xFF);
current_otp = binary % 1000000; /* 6-digit OTP */
The OTP changes whenever the minute changes, giving a ~60-second validity window.
RTC Communication (DS3231 over I2C)
The DS3231 stores time in BCD format. Each field (seconds, minutes, hours) is read by first writing the register address then reading one byte back:
HAL_I2C_Master_Transmit(&hi2c1, 0xD0, ®, 1, 10);
HAL_I2C_Master_Receive (&hi2c1, 0xD1, &val, 1, 10);
Hours are masked with 0x3F to strip the 12/24-hour mode bit.
UART Time Synchronization
When the button on PA8 is pressed (active low), Uart_Task sends SYNC_REQ\r\n over UART and blocks for up to 8 seconds waiting for a SYNC:HHMMSS\r\n response (13 bytes). The PC Python script detects SYNC_REQ and immediately replies with the current system time. The MCU parses the ASCII digits into BCD and writes them to the DS3231:
uint8_t h = ((sync_buf[5]-'0') << 4) | (sync_buf[6]-'0');
uint8_t m = ((sync_buf[7]-'0') << 4) | (sync_buf[8]-'0');
uint8_t s = ((sync_buf[9]-'0') << 4) | (sync_buf[10]-'0');
4.2 Key Design Choices
FreeRTOS over bare-metal superloop — Four independent concerns (RTC reading, OTP computation, UART I/O, LCD rendering) map cleanly to separate tasks. Mutexes prevent data races on shared time and OTP values without disabling interrupts.
BCD time format throughout — The DS3231 natively stores BCD. Keeping values in BCD until the final snprintf call eliminates conversion overhead and round-trip errors.
HMAC-SHA1 seeded with hour+minute only — Using only H:M (not seconds) as the HMAC input gives a stable OTP for a full minute, matching standard TOTP window behavior and reducing unnecessary recomputation.
4.3 Technical Challenges and Solutions
| Challenge | Root Cause | Solution |
|---|---|---|
| LCD blank after splash screen | HAL_Delay() broken inside FreeRTOS tasks (SysTick hijacked) |
Replaced all LCD timing with NOP busy-wait loops |
| LCD never initialized | LCD_Init() called after osKernelStart(), which never returns |
Moved LCD_Init() and splash screen before osKernelStart() |
| SYNC TIMEOUT on STM32 | HAL_UART_Receive window expired before Python responded |
Added ser.flush() on PC side; reduced Python processing latency |
Wrong serial package |
pip install serial installs an unrelated package |
Must use pip install pyserial; both expose import serial |
| COM port not found on Linux | Windows-style COM10 port name used on Linux host |
Changed to /dev/ttyACM0 (ST-Link CDC ACM device) |
| High power consumption during standby | RTC must remain continuously powered to preserve accurate time, but keeping all peripherals active drains the battery | Added a hardware power switch to disconnect all non-essential components while keeping the RTC powered independently |
5. Testing and Validation
5.1 Unit Testing
RTC Driver — Verified DS3231 I2C communication by writing a known time (21:47:00) at startup and reading it back over UART. Confirmed BCD encoding/decoding is consistent.
HMAC-SHA1 / OTP — Cross-validated OTP output against the PC Python script using the same secret key and same H:M input. Both devices produced identical 9-digit values.
LCD Driver — Isolated LCD hardware with a static "HELLO LCD / DEBUG OK" test in StartLCDTask before connecting shared data, confirming the display and wiring were correct independently of the RTOS.
UART Sync — Manually triggered sync via button press and confirmed SYNC OK response over serial monitor. Verified DS3231 time updated to match PC time immediately after sync.
5.2 Integration Testing
With all four tasks running simultaneously:
- UART output confirmed correct time and OTP printing every second
- LCD confirmed displaying matching time and OTP values
- OTP value on LCD matched OTP value on PC Python script
- Button press triggered sync and OTP updated to reflect new time window within 1 second
5.3 Known Limitations
- OTP window is ~60 seconds (minute-granular), not the standard 30-second TOTP window, because the seed uses H:M only
- DS3231 has no automatic drift compensation — long-term accuracy depends on periodic UART sync
- No persistent secret key storage — key is hardcoded in firmware and Python script
- LCD NOP timing is calibrated for 32 MHz system clock; recalibration needed if clock frequency changes
6. Division of Labor
| Team Member | Primary Contributions |
|---|---|
| Ali & Moaz | STM32 firmware architecture, FreeRTOS task design, RTC I2C driver |
| Peter & Ali | HMAC-SHA1 integration (X-CUBE-CRYPTOLIB), OTP truncation logic |
| Moaz | LCD driver (HD44780, 4-bit mode), display task, timing debug |
| Peter | PC Python sync script, UART protocol, integration testing |
7. Media
Demo Video
https://github.com/user-attachments/assets/375b2cb7-8835-4a21-a7f3-76e297f111de
Photos
UART Output Sample
STM32: Time: 01:12:34 | OTP: 778974
STM32: Time: 01:12:35 | OTP: 778974
STM32: SYNC_REQ
Sent: SYNC:011236
STM32: SYNC OK
STM32: Time: 01:12:36 | OTP: 778974
STM32: Time: 01:13:00 | OTP: 711066 ← OTP updates on minute boundary
8. Appendices & References
8.1 Source Code Repository:
https://github.com/Peter-aziz/TOTP