OTP Cipher - One Time Pad Implementation
Introduction
A secure one-time pad (OTP) cipher implementation in C99.
Why One-Time Pads
Nostr and much of the web runs on public key cryptography. Public key cryptography is great, but it is vulnerable. Cryptographers know this, and they know what it takes to attack it, so what they do is just make the keys large enough such that the system is resistant to attack given computers as they are today.
There is one type of cryptography, however, that is invulnerable to any type of attack in our universe, and that is known as a one-time pad.
One-time pads rely directly on the laws of physics and what it means for a number to be truly random.
If you take your secret message and mix it with truly random numbers, and don't use those random numbers again, then that message is unbreakable by any computer, no matter how powerful, quantum or not, forever.
In fact, one-time pads are so powerful that if you have data encrypted by a one-time pad located in a distant galaxy, and that data is not kept anywhere else, then by destroying the pad used for encryption in your galaxy, the data is wiped from the universe and can never be recovered.
Advantages and Limitations
Limitations
- The pad must be shared between the parties wanting to use it.
- The pad must be as long or longer than what you want to encrypt, and it can't be used a second time.
Modern Advantages
While in the past, pad length might have been a problem, readily available USB drives in the terabytes make size less of a problem for many uses.
We are also becoming very accustomed to YubiKey authenticators in the USB ports of our computers. A small USB drive in our devices can now easily contain a key of greater length than all the text messages we would expect to send over a lifetime.
Multi-Device Coordination & Dynamic Partitioning ("Work Stealing" Protocol)
One of the core challenges of using a One-Time Pad across multiple devices or between bilateral peers is ensuring that two devices never encrypt using the same region of the pad. Reusing any section of a pad breaks its "Information Theoretically Secure" guarantee.
We solve this with Dynamic Partition Splitting (Work Stealing):
- Exclusive Ownership Invariant: Every unused byte of the pad has a single exclusive owner (
[offset, end_offset)). Because no two devices ever share an active write range, devices can encrypt simultaneously or completely offline with zero risk of collision. - One
.stateFile Per Pad, No Device Names: Each app/device keeps exactly one<chksum>.statefile. There are no device names or IDs — partitions are identified purely by their byte ranges. The.statefile records the device's own partition (offset/end_offset), queued grants (next_extent), and known peer partitions (peer_extent=start:end). - Start with 1 Device (or
NDevices) and Add Devices On The Fly: You do not need to know in advance how many devices or users will share a pad. A single device starts owning 100% of the pad ([32, pad_size)). Whenever you want to add a new device (or user), any existing device simply splits its remaining unused space in half (aligned to 256-byte chunk boundaries), saves its new lowerend_offsetand apeer_extentrecord to disk first, and emits a Pure-OTPOTP-GRANT-V1message giving the upper half to the new device. - Pure-OTP Rebalancing: Both onboarding a new device and giving more space to a device that is running low are the exact same operation: an existing device splits its unused
[offset, end_offset)in half, shrinks its ownend_offsettomidlocally first, and produces a Pure-OTPOTP-GRANT-V1message containing[mid+256, old_end_offset)(the 256 bytes atmidare consumed by the grant message itself). That OTP message can be passed to the other device over any channel (USB, QR code, file, email, or inside a normal OTP conversation) — indistinguishable from any normal OTP message! - Passive Peer Tracking: Every OTP message carries its
Pad-Offset. When a device decrypts a message whose offset falls inside a knownpeer_extent, it automatically advances that peer's start — so each device passively learns how much pad space its peers have left just by exchanging normal messages.
Visualizing Dynamic Device Addition & Work-Stealing Rebalancing
Suppose you generate a 120 MB pad on Device 1 (Laptop), later add Device 2 (Phone) and Device 3 (Tablet) on the fly, and eventually rebalance when the Phone runs low:
1. INITIAL STATE: Device 1 (Laptop) owns 100% of the pad (120 MB)
+---------------------------------------------------------------------------------------------------------------------+
| Device 1: Laptop (120 MB) |
| [0..120M) |
+---------------------------------------------------------------------------------------------------------------------+
^
Laptop cursor (0M)
2. ON-THE-FLY ONBOARDING (Adding Device 2: Phone)
- Laptop has used 10 MB [0..10M), leaving 110 MB unused [10M..120M).
- You run `./otp split` on Laptop to add your Phone.
- Laptop cuts its 110 MB unused space in half at 65M:
* Laptop shrinks its own end_offset from 120M -> 65M and records
peer_extent=65000256:120M in its single .state file (saved FIRST).
* Laptop prints an encrypted OTP-GRANT-V1 message for [65000256..120M).
* Phone (with a copy of the same .pad file) decrypts that message once
and its own single .state file adopts [65000256..120M).
+---------+-----------------------------------------------+-----------------------------------------------------------+
|#########| Device 1: Laptop [10M..65M) | Device 2: Phone [65M..120M) |
| (10M) | (55 MB remaining) | (55 MB remaining) |
+---------+-----------------------------------------------+-----------------------------------------------------------+
^ ^
Laptop cursor (10M) Phone cursor (65M)
3. ON-THE-FLY ONBOARDING AGAIN (Adding Device 3: Tablet)
- Later, you want to add a Tablet. You split from the Phone (currently at 70M of [65M..120M), so 50 MB unused).
- Phone shrinks its end_offset from 120M -> 95M, records the peer extent,
and emits an OTP-GRANT-V1 message giving [95000256..120M) to the Tablet:
+---------+-----------------------------------------------+-----+-------------------------+---------------------------+
|#########| Device 1: Laptop [10M..65M) |#####| Device 2: Phone | Device 3: Tablet |
| | |(5M) | [70M..95M) (25 MB) | [95M..120M) (25 MB) |
+---------+-----------------------------------------------+-----+-------------------------+---------------------------+
^ ^ ^
Laptop cursor (10M) Phone cursor (70M) Tablet cursor (95M)
4. LATER: Phone is a heavy sender and reaches 94M of 95M (only 1 MB left!).
Laptop barely sent anything and is still at 15M of 65M (50 MB unused).
+---------+----+------------------------------------------+------------------------------+--+-------------------------+
|##############| Laptop [15M..65M = 50M UNUSED] |#############################.|##| Tablet [97M..120M) |
+---------+----+------------------------------------------+------------------------------+--+-------------------------+
^ ^
Laptop cursor (15M) Phone cursor (94M)
5. WORK-STEALING REBALANCE (Pure OTP — indistinguishable from normal messages):
- Laptop runs `split` on its 50 MB unused space [15M..65M), cutting it in half at 40M.
- Laptop updates its local .state end_offset to 40M and records the peer extent FIRST.
- Laptop outputs a Pure-OTP OTP-GRANT-V1 message: "You now own [40000256..65M)!"
- Phone decrypts/imports the grant, queues it as its next_extent, and seamlessly jumps there once 95M is reached:
+--------------+-------------------------+-------------------------+------------------------+-------------------------+
|##############| Laptop [15M..40M) | NOW PHONE'S! |########################| Tablet [97M..120M) |
| | (25 MB remaining) | Phone [40M..65M) | (Phone 65M..95M used) | (23 MB remaining) |
+--------------+-------------------------+-------------------------+------------------------+-------------------------+
^ ^
Laptop (15M) Phone jumps here (40M) and keeps encrypting!
Extended State File Format (.state)
Each device keeps exactly one .state file per pad, tracking its own partition, queued grants, and known peer partitions — all identified namelessly by byte range:
offset=10485760
end_offset=65000000
next_extent=95000000:120000000
peer_extent=40000256:65000000
peer_extent=70002048:95000000
offset: Current write cursor (starts at32, after the 32-byte checksum encryption key).end_offset: Upper bound of this device's owned partition. If omitted (legacy.statefiles), it defaults topad_size(100% single-owner pad, backward-compatible).next_extent: Optionalstart:endrange(s) received from a split/rebalance grant. Whenoffset + chunk_size > end_offset, the encryptor automatically popsnext_extentand setsoffset = next_start, end_offset = next_end.peer_extent: Optionalstart:endrange(s) known to belong to other devices (recorded when this device splits off space). Automatically advanced when decrypting a message whosePad-Offsetfalls inside the range — passive, nameless tracking of how much pad space each peer has consumed.
Additional Benefits
One-time pads can be trivially encrypted and decrypted using pencil and paper, making them accessible even without electronic devices.
Features
- Perfect Security: Implements true one-time pad encryption with information-theoretic security
- Traffic Analysis Resistance: Exponential bucketing with ISO/IEC 9797-1 Method 2 (Padmé) padding hides message lengths
- Text & File Encryption: Supports both inline text and file encryption
- Multiple Output Formats: Binary (.otp) and ASCII armored (.otp.asc) file formats
- Hardware RNG Support: Direct entropy collection from TrueRNG USB devices with automatic detection
- Keyboard Entropy: Optional keyboard entropy collection for enhanced randomness
- Modular Architecture: Clean separation of concerns across multiple source modules
- Short Command Flags: Convenient single-character flags for all operations
- Automatic Versioning: Built-in semantic versioning with automatic patch increment
- Multiple Build Options: Standard and static linking builds
- Cross-Platform: Works on Linux and other UNIX-like systems
Quick Start
Download Pre-Built Binaries
Download Current Raspberry Pi 64
After downloading:
# Rename for convenience, then make executable
mv otp-v0.3.53-linux-x86_64 otp
chmod +x otp
# Run it
./otp
First Steps
-
Generate your first pad:
./otp generate 1GB -
Encrypt a message:
./otp encrypt # Follow the interactive prompts -
Decrypt a message:
./otp decrypt # Paste the encrypted message
Building from Source
Prerequisites
- GCC compiler
- Make
Build Commands
make # Build for current architecture
make static # Static linking (standalone binary)
make clean # Clean build artifacts
make install # Install to /usr/local/bin/otp
make uninstall # Remove from system
Output: build/otp-$(ARCH) (e.g., build/otp-x86_64)
After building, run with:
./build/otp-x86_64
Usage
The OTP Cipher operates in two modes:
Interactive Mode: Run without arguments to access a menu-driven interface. Best for exploring features, managing pads, and performing operations step-by-step with prompts and guidance.
Command Line Mode: Provide arguments to execute specific operations directly. Ideal for scripting, automation, and quick one-off tasks.
Interactive Mode
Launch the menu-driven interface:
./otp
Navigate through menus to generate pads, encrypt/decrypt messages, manage pads, and configure settings.
Command Line Mode
Execute operations directly with arguments:
# Generate a new pad
./otp generate 1GB
# Encrypt text (will prompt for input)
./otp encrypt <pad_hash_or_prefix>
# Decrypt message (will prompt for input)
./otp decrypt <pad_hash_or_prefix>
# List available pads
./otp list
Version System
Centralized Version Management
Version is defined in a single location: src/main.h
#define OTP_VERSION "v0.3.24"
All code references this constant, ensuring consistency across:
- Main menu display
- ASCII armor output
- Help/usage text
Automatic Version Increment
The build.sh script automatically:
- Increments patch version (v0.3.24 → v0.3.25)
- Updates
OTP_VERSIONinsrc/main.h - Creates git commit and tag
- Pushes to remote repository
Increment and Push Script
The increment_and_push.sh script handles versioned commits and releases:
./increment_and_push.sh "commit message" # patch bump, commit, tag, push
./increment_and_push.sh -m "commit message" # minor bump
./increment_and_push.sh -M "commit message" # major bump
./increment_and_push.sh -r "commit message" # release: build x86_64 + arm64,
# create Gitea release, upload
# binaries + source tarball
Manual Version Control
For major/minor releases, create tags manually:
# Feature release (minor bump)
git tag v0.4.0 # Next build: v0.4.1
# Breaking change (major bump)
git tag v1.0.0 # Next build: v1.0.1
Security Features
- Uses
/dev/urandomfor cryptographically secure random number generation - Optional keyboard entropy mixing using simple XOR operations
- Custom 256-bit XOR checksum for pad identification (encrypted with pad data)
- Read-only pad files to prevent accidental modification
- State tracking to prevent pad reuse
- Message Length Hiding: Exponential bucketing (256B, 512B, 1KB, 2KB, 4KB...) prevents traffic analysis
- ISO/IEC 9797-1 Method 2 Padding: Standard-compliant Padmé padding with 0x80 marker
- Zero external crypto dependencies - completely self-contained implementation
Message Padding
All encrypted messages and files are automatically padded using exponential bucketing to resist traffic analysis attacks:
- Minimum size: 256 bytes
- Bucket sizes: 256B → 512B → 1KB → 2KB → 4KB → 8KB → ...
- Padding method: ISO/IEC 9797-1 Method 2 (Padmé padding)
- Appends
0x80byte after message - Fills remaining space with
0x00bytes - Unambiguous padding removal during decryption
- Appends
Example: A 10-byte message is padded to 256 bytes, while a 300-byte message is padded to 512 bytes. This provides strong protection for small messages where length leakage matters most, with logarithmic overhead for larger messages.
Project Structure
otp/
├── run.sh # Build-if-needed launcher (./run.sh, ./run.sh test)
├── build.sh # Build script with automatic versioning
├── increment_and_push.sh # Version increment, commit, tag, push, release
├── Makefile # Traditional make build system
├── README.md # This file
├── .gitignore # Git ignore rules
├── src/
│ ├── main.h # Main header with all prototypes and OTP_VERSION
│ ├── main.c # Application entry point and command line handling
│ ├── ui.c # Interactive user interface and menu system
│ ├── state.c # Global state management (pads directory, preferences)
│ ├── crypto.c # Core cryptographic operations (XOR, base64)
│ ├── padding.c # Message padding (exponential bucketing, Padmé padding)
│ ├── pads.c # Pad management and file operations
│ ├── entropy.c # Entropy collection from various sources
│ ├── trng.c # Hardware RNG device detection and collection
│ ├── util.c # Utility functions and helpers
│ ├── nostr_chacha20.c # ChaCha20 implementation for entropy expansion
│ └── nostr_chacha20.h # ChaCha20 header
├── build/
│ ├── otp-x86_64 # Native x86_64 binary (created by build)
│ └── otp-arm64 # ARM64 binary (created by cross-compilation)
├── plans/ # Design documents and protocol specifications
├── pads/ # OTP pad storage directory (created at runtime)
├── files/ # Encrypted file storage (created at runtime)
└── tests/ # Test scripts and utilities
├── test_padding.sh # Padding implementation tests
└── test_partition.sh # Dynamic partition splitting tests
Architecture
The OTP cipher uses a modular architecture with clean separation of concerns:
- main.c: Application entry point, command line parsing, and mode selection
- ui.c: Interactive user interface, menus, and terminal management
- state.c: Global state management (pads directory, terminal dimensions, preferences)
- crypto.c: Core cryptographic operations (XOR encryption, base64 encoding)
- padding.c: Message padding implementation (exponential bucketing, ISO/IEC 9797-1 Method 2)
- pads.c: Pad file management, checksums, and state tracking
- entropy.c: Entropy collection from keyboard, dice, files, and hardware RNG
- trng.c: Hardware RNG device detection and entropy collection from USB devices
- util.c: Utility functions, file operations, and helper routines
- nostr_chacha20.c: ChaCha20 stream cipher for entropy expansion
All modules share a common header (src/main.h) that defines the public API, data structures, and version constant.
Hardware RNG Device Support
The OTP cipher includes comprehensive support for hardware random number generators (RNGs) to enhance entropy quality for pad generation and entropy addition operations.
Supported Devices
The system automatically detects and supports the following hardware RNG devices:
| Device | VID:PID | Status | Notes |
|---|---|---|---|
| TrueRNG | 04d8:f5fe | ✅ Working | Original TrueRNG device |
| TrueRNG (Alt) | 1fc9:8111 | ✅ Working | Alternative VID/PID combination |
| TrueRNG Pro | 04d8:f5fe | ✅ Working | Professional version |
| TrueRNG Pro V2 | 04d8:f5fe | ✅ Working | Latest professional version |
Device Detection
The system automatically scans /dev/ttyACM* ports and identifies hardware RNG devices by:
- USB VID/PID Detection: Reading vendor and product IDs from sysfs
- Device Type Classification: Identifying specific device variants
- Port Configuration: Applying device-specific serial port settings
- Interactive Selection: Presenting available devices for user selection
Testing Hardware Devices
A comprehensive test script is included to verify hardware RNG functionality:
# Run hardware device tests
./test.sh
Testing Partition Splitting
A test script covering the full multi-device partition workflow is included:
# Run partition splitting tests (pad generation, split, grants,
# extent rollover, boundary enforcement, cross-tool compatibility)
./tests/test_partition.sh # or: ./run.sh test
Splitting a Pad Interactively (Multi-Device Setup)
To divide a pad among multiple devices from the interactive menu:
- Launch
./otpand choose Pads - Select sPlit pad (add device / rebalance)
- Pick the pad to split (by checksum prefix)
The split halves the donor's remaining unused space, records the donated range as a peer_extent in the donor's single .state file, and prints an encrypted OTP-GRANT-V1 message. Deliver that message to the other device (paste into its decrypt, send over Nostr/email/QR, or carry on USB) — the recipient decrypts it once and its own .state file adopts the new partition. Repeat any time a new device joins.
Current Test Results
Based on testing with actual hardware devices:
✅ Working Devices:
-
TrueRNG (Type 1): Full functionality confirmed
-
TrueRNG Pro V2 (Type 3): Full functionality confirmed
- Device is detected and identified correctly
- Serial port configuration may need adjustment for this device variant
Usage in Entropy Collection
When generating pads or adding entropy, the system will:
- Auto-detect all connected hardware RNG devices
- Present a menu of available devices if multiple are found
- Test connectivity before beginning entropy collection
- Estimate completion time based on device speed testing
- Collect entropy with progress indicators and quality metrics
Device Configuration
Each device type uses optimized serial port settings:
- TrueRNG devices: 3Mbps baud rate, 8N1, no flow control
- Automatic timeout protection: Prevents hanging on unresponsive devices
- Error recovery: Graceful handling of device disconnection during operation
Troubleshooting
If hardware RNG devices are not detected:
- Check USB connections: Ensure devices are properly connected
- Verify permissions: User must have access to
/dev/ttyACM*devices - Check device enumeration: Use
lsusbto verify USB device recognition - Review sysfs entries: Ensure VID/PID information is available in
/sys/bus/usb/devices/
File Formats
.otp File Format (Binary)
Binary encrypted files use a structured header format:
Offset | Size | Field | Description
-------|------|-------------------|----------------------------------
0 | 4 | Magic | "OTP\0" - File type identifier
4 | 2 | Version | Format version (currently 1)
6 | 32 | Pad Checksum | Binary pad checksum (32 bytes)
38 | 8 | Pad Offset | Offset in pad file (uint64_t)
46 | 4 | File Mode | Original file permissions (uint32_t)
50 | 8 | File Size | Original file size (uint64_t)
58 | var | Encrypted Data | XOR-encrypted file contents
.otp.asc File Format (ASCII Armored)
ASCII armored files use the same format as encrypted text messages:
-----BEGIN OTP MESSAGE-----
Version: v0.2.15
Pad-ChkSum: <64-character-hex-checksum>
Pad-Offset: <decimal-offset-value>
<base64-encoded-encrypted-data>
-----END OTP MESSAGE-----
Note: ASCII armored files do not preserve original file permissions metadata.
Usage Examples
Short Command Flags
# Quick commands using short flags
./otp -g 1GB # Generate 1GB pad
./otp -l # List available pads
./otp -e 1a2b "Hello world" # Encrypt text inline
./otp -d "-----BEGIN OTP..." # Decrypt message inline
# File operations
./otp -f document.pdf 1a2b # Encrypt file (binary)
./otp -f document.pdf 1a2b -a # Encrypt file (ASCII)
./otp -f document.pdf 1a2b -o secret.otp # Custom output name
Text Encryption
# Interactive text encryption
./otp encrypt 1a2b3c
Enter text to encrypt: This is my secret message
# Outputs ASCII armored message
# Inline text encryption
./otp -e 1a2b3c "This is my secret message"
# Outputs ASCII armored message immediately
File Encryption
# Binary format (preserves metadata)
./otp -f sensitive.doc a1b2c3
# ASCII armored format (text-safe)
./otp -f sensitive.doc a1b2c3 -a
# Custom output filename
./otp -f sensitive.doc a1b2c3 -o encrypted_document.otp
Decryption
# Auto-detect format and pad from message/file
./otp -d encrypted.otp.asc
./otp -d "-----BEGIN OTP MESSAGE-----..."
# Interactive mode
./otp decrypt
# Prompts for encrypted message input
Build and Version Tracking
$ ./build.sh build
[INFO] Incrementing version...
[INFO] Current version: v0.2.14
[INFO] New version: v0.2.15
[SUCCESS] Created new version tag: v0.2.15
[SUCCESS] Build completed successfully
$ ./otp --help
OTP Cipher - One Time Pad Implementation v0.2.15
Built on 2025-08-10 at 14:07:58 from commit ae0afcf on branch master
Advanced Features
# Generate pad with keyboard entropy
./otp generate 5GB
# Follow prompts for keyboard entropy collection
# Check pad usage
./otp -l
Available pads:
No. ChkSum (first 16 chars) Size Used % Used
--- ------------------- ---------- ---------- ------
1 97d9d82b5414a943 1.00GB 156B 0.0%
2 0c8e19fde996e683 1000B 248B 24.8%
# Show detailed pad information
./otp
# Select "S" for show pad info, enter checksum or prefix
Important Notes
Size Units: Decimal (SI) vs Binary (IEC)
This program uses decimal (SI) units for all size specifications, matching the behavior of most system tools like ls -lh, df -h, and file managers:
- 1 KB = 1,000 bytes (not 1,024)
- 1 MB = 1,000,000 bytes (not 1,048,576)
- 1 GB = 1,000,000,000 bytes (not 1,073,741,824)
- 1 TB = 1,000,000,000,000 bytes (not 1,099,511,627,776)
Why decimal units?
- Consistency with system tools (
ls,df, file managers) - Matches storage device marketing (a "1TB" USB drive has ~1,000,000,000,000 bytes)
- Avoids confusion when comparing sizes across different tools
- Industry standard for storage devices and file systems
Example: When you request a 100GB pad, the program creates exactly 100,000,000,000 bytes, which will display as "100GB" in ls -lh and your file manager.
Note: Some technical tools may use binary units (GiB, MiB) where 1 GiB = 1,024³ bytes. This program intentionally uses decimal units for user-friendliness and consistency with common tools.
License
This project includes automatic versioning system based on the Generic Automatic Version Increment System.
State Files
Pad state files (.state) use a human-readable text format:
offset=1234567890
This tracks how many bytes of each pad have been used. The format is:
- Human-readable: Can inspect with
cat checksum.state - Backward compatible: Automatically reads old binary format
- Easy to debug: Can manually edit if needed
Contributing
When contributing:
- The version will automatically increment on builds via
build.sh - Version is centralized in
src/main.hasOTP_VERSION - For major features, manually create minor/major version tags
- Build artifacts in
build/and object files are auto-cleaned