|
1 | 1 | # CodexKotlin |
2 | 2 |
|
3 | | -CodexKotlin is a Kotlin implementation of Operator's Codex, a text-to-text format-transforming-encryption library. |
| 3 | +CodexKotlin is a Kotlin implementation of Operator's Codex, a text-to-text format-transforming-encryption library. CodexKotlin enables encoding arbitrary data into various constrained text formats for transmission through restrictive channels. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +CodexKotlin is part of the Operator Foundation's Codex project, which provides format-transforming encryption capabilities. This library encodes binary data into structured text formats that can pass through systems with strict formatting requirements, such as: |
| 8 | + |
| 9 | +- WSPR (Weak Signal Propagation Reporter) messages for amateur radio |
| 10 | +- SMS-compatible text formats |
| 11 | +- Email-safe encodings |
| 12 | +- Other constrained communication channels |
| 13 | + |
| 14 | +## Installation |
| 15 | + |
| 16 | +Add to your `build.gradle`: |
| 17 | + |
| 18 | +```gradle |
| 19 | +dependencies { |
| 20 | + implementation 'org.operatorfoundation:codex:1.0.0' |
| 21 | +} |
| 22 | +``` |
| 23 | + |
| 24 | +## Core Components |
| 25 | + |
| 26 | +### Symbol System |
| 27 | + |
| 28 | +CodexKotlin uses a flexible symbol system that defines encoding constraints for different formats. Symbols can represent: |
| 29 | +- Fixed characters (Required) |
| 30 | +- Letter/number combinations (CallLetterNumber) |
| 31 | +- Grid coordinates (GridLetter) |
| 32 | +- Numeric values (Number) |
| 33 | +- Power levels (Power) |
| 34 | + |
| 35 | +### Encoder/Decoder |
| 36 | + |
| 37 | +The core `Encoder` and `Decoder` classes transform between binary data and symbol-constrained formats: |
| 38 | + |
| 39 | +```kotlin |
| 40 | +val symbols = listOf(/* symbol definitions */) |
| 41 | +val encoder = Encoder(symbols) |
| 42 | +val decoder = Decoder(symbols) |
| 43 | + |
| 44 | +// Encode data to symbols |
| 45 | +val data = BigInteger("12345") |
| 46 | +val encoded = encoder.encode(data) |
| 47 | + |
| 48 | +// Decode back |
| 49 | +val decoded = decoder.decode(encoded) |
| 50 | +``` |
| 51 | + |
| 52 | +## WSPR Implementation |
| 53 | + |
| 54 | +The library includes a complete WSPR message codec as a reference implementation: |
| 55 | + |
| 56 | +### WSPRCodex |
| 57 | + |
| 58 | +Encodes arbitrary binary data into WSPR message format. |
| 59 | + |
| 60 | +```kotlin |
| 61 | +import org.operatorfoundation.codex.WSPRCodex |
| 62 | + |
| 63 | +val codex = WSPRCodex() |
| 64 | + |
| 65 | +// Encode any binary data |
| 66 | +val data = "Hello World!".toByteArray() |
| 67 | +val messages = codex.encode(data) |
| 68 | + |
| 69 | +// Messages are in WSPR format |
| 70 | +messages.forEach { msg -> |
| 71 | + println("Callsign: ${msg.callsign}") |
| 72 | + println("Grid: ${msg.gridSquare}") |
| 73 | + println("Power: ${msg.powerDbm} dBm") |
| 74 | +} |
| 75 | + |
| 76 | +// Decode back to original |
| 77 | +val decoded = codex.decode(messages) |
| 78 | +``` |
| 79 | + |
| 80 | +### Multi-Message Support |
| 81 | + |
| 82 | +For data larger than a single format frame, CodexKotlin automatically chunks and reassembles: |
| 83 | + |
| 84 | +```kotlin |
| 85 | +val largeData = ByteArray(100) { it.toByte() } |
| 86 | +val messages = codex.encode(largeData) // Automatically chunks |
| 87 | + |
| 88 | +// Handles out-of-order reception |
| 89 | +val shuffled = messages.shuffled() |
| 90 | +val decoded = codex.decode(shuffled) // Reassembles correctly |
| 91 | +``` |
| 92 | + |
| 93 | +### Capacity |
| 94 | + |
| 95 | +WSPR mode supports: |
| 96 | +- **Basic Mode**: Up to 64 bytes (16 messages × 4 bytes/message) |
| 97 | +- **Extended Mode**: Up to 768 bytes (256 messages × 3 bytes/message) |
| 98 | + |
| 99 | +## Custom Format Support |
| 100 | + |
| 101 | +You can define your own symbol constraints for other formats: |
| 102 | + |
| 103 | +```kotlin |
| 104 | +// Define symbols for your format |
| 105 | +val customSymbols = listOf( |
| 106 | + Required('!'.code.toByte()), // Fixed prefix |
| 107 | + Letter(), // A-Z only |
| 108 | + Number(), // 0-9 only |
| 109 | + AlphaNumeric() // A-Z, 0-9 |
| 110 | +) |
| 111 | + |
| 112 | +val encoder = Encoder(customSymbols) |
| 113 | +val decoder = Decoder(customSymbols) |
| 114 | +``` |
| 115 | + |
| 116 | +## Architecture |
| 117 | + |
| 118 | +``` |
| 119 | +CodexKotlin |
| 120 | +├── Core |
| 121 | +│ ├── Encoder/Decoder - Core transformation engine |
| 122 | +│ ├── Symbol System - Format constraint definitions |
| 123 | +│ └── BigInteger Math - Numeric representation |
| 124 | +│ |
| 125 | +└── Implementations |
| 126 | + ├── WSPRCodex - WSPR radio messages (public API) |
| 127 | + ├── WSPRMultiMessageCodex - Chunking/reassembly (internal class) |
| 128 | + └── [Extensible for other formats] |
| 129 | +``` |
| 130 | + |
| 131 | +## Use Cases |
| 132 | + |
| 133 | +- **Amateur Radio**: Send data via WSPR, FT8, or other weak-signal modes |
| 134 | +- **Emergency Communications**: Encode messages for transmission through limited channels |
| 135 | +- **Steganography**: Hide data within format-compliant text |
| 136 | +- **IoT Telemetry**: Send sensor data through SMS or other text-only channels |
| 137 | +- **Censorship Circumvention**: Transform data to pass through restrictive filters |
| 138 | + |
| 139 | +## API Reference |
| 140 | + |
| 141 | +### WSPRCodex Methods |
| 142 | + |
| 143 | +#### `encode(data: ByteArray, messageId: Byte? = null): List<WSPRDataMessage>` |
| 144 | + |
| 145 | +Encodes binary data into WSPR messages. |
| 146 | + |
| 147 | +**Parameters:** |
| 148 | +- `data`: Binary data to encode |
| 149 | +- `messageId`: Optional ID for message grouping (auto-generated if null) |
| 150 | + |
| 151 | +**Returns:** List of WSPR messages |
| 152 | + |
| 153 | +#### `decode(messages: List<WSPRDataMessage>): ByteArray` |
| 154 | + |
| 155 | +Decodes WSPR messages back to binary data. |
| 156 | + |
| 157 | +**Parameters:** |
| 158 | +- `messages`: List of WSPR messages (order doesn't matter) |
| 159 | + |
| 160 | +**Returns:** Original binary data |
| 161 | + |
| 162 | +### WSPRDataMessage Properties |
| 163 | + |
| 164 | +```kotlin |
| 165 | +data class WSPRDataMessage( |
| 166 | + val callsign: String, // 6 characters max |
| 167 | + val gridSquare: String, // 4 characters (Maidenhead) |
| 168 | + val powerDbm: Int // 0-60 dBm |
| 169 | +) |
| 170 | +``` |
| 171 | + |
| 172 | +## Transmission Time |
| 173 | + |
| 174 | +Each WSPR message requires 2 minutes to transmit: |
| 175 | + |
| 176 | +| Data Size | Messages | Transmission Time | |
| 177 | +|-----------|----------|------------------| |
| 178 | +| 1-4 bytes | 1 | 2 minutes | |
| 179 | +| 5-8 bytes | 2 | 4 minutes | |
| 180 | +| 20 bytes | 5 | 10 minutes | |
| 181 | +| 64 bytes | 16 | 32 minutes | |
| 182 | +| 768 bytes | 256 | 8.5 hours | |
| 183 | + |
| 184 | +## Error Handling |
| 185 | + |
| 186 | +```kotlin |
| 187 | +try { |
| 188 | + val encoded = codex.encode(data) |
| 189 | +} catch (e: WSPRCodexException) { |
| 190 | + // Handle encoding errors |
| 191 | +} |
| 192 | + |
| 193 | +try { |
| 194 | + val decoded = codex.decode(messages) |
| 195 | +} catch (e: WSPRMultiMessageException) { |
| 196 | + // Handle incomplete or corrupted messages |
| 197 | +} |
| 198 | +``` |
| 199 | + |
| 200 | +## Example: Encrypted Communication |
| 201 | + |
| 202 | +```kotlin |
| 203 | +import org.operatorfoundation.codex.WSPRCodex |
| 204 | +import javax.crypto.Cipher |
| 205 | +import javax.crypto.spec.SecretKeySpec |
| 206 | + |
| 207 | +fun encryptAndTransmit(plaintext: String, key: ByteArray) { |
| 208 | + // Encrypt |
| 209 | + val cipher = Cipher.getInstance("AES") |
| 210 | + cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES")) |
| 211 | + val encrypted = cipher.doFinal(plaintext.toByteArray()) |
| 212 | + |
| 213 | + // Encode to WSPR |
| 214 | + val codex = WSPRCodex() |
| 215 | + val messages = codex.encode(encrypted) |
| 216 | + |
| 217 | + // Transmit each message |
| 218 | + messages.forEach { msg -> |
| 219 | + transmitWSPR(msg.callsign, msg.gridSquare, msg.powerDbm) |
| 220 | + } |
| 221 | +} |
| 222 | + |
| 223 | +fun receiveAndDecrypt(messages: List<WSPRDataMessage>, key: ByteArray): String { |
| 224 | + // Decode from WSPR |
| 225 | + val codex = WSPRCodex() |
| 226 | + val encrypted = codex.decode(messages) |
| 227 | + |
| 228 | + // Decrypt |
| 229 | + val cipher = Cipher.getInstance("AES") |
| 230 | + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES")) |
| 231 | + val decrypted = cipher.doFinal(encrypted) |
| 232 | + |
| 233 | + return String(decrypted) |
| 234 | +} |
| 235 | +``` |
| 236 | + |
| 237 | +## Limitations |
| 238 | + |
| 239 | +- Some formats may lose trailing zeros in data |
| 240 | +- Each format has maximum capacity constraints |
| 241 | +- All chunks must be received for successful multi-message decoding |
| 242 | +- WSPR format limited to ~50 bits per message |
| 243 | + |
| 244 | +## Contributing |
| 245 | + |
| 246 | +CodexKotlin is part of the Operator Foundation's suite of tools for unrestricted internet access. Contributions are welcome. |
| 247 | + |
| 248 | +## Related Projects |
| 249 | + |
| 250 | +- **Codex** (Swift): Original implementation |
| 251 | +- **CodexPython**: Python implementation |
| 252 | +- **AudioCoder**: Audio signal generation for encoded messages |
| 253 | +- **TransmissionAndroid**: Android USB serial communication |
| 254 | + |
| 255 | +## License |
| 256 | + |
| 257 | +MIT License |
0 commit comments