Implementing SimpleCipherText: Practical Examples and CodeSimpleCipherText is a minimal, easy-to-understand approach to symmetric encryption intended for learning, small projects, and scenarios where simplicity and clarity matter more than resistance to highly-resourced attackers. This article walks through the design decisions, basic cryptographic building blocks, multiple practical examples (command-line, web, and embedded), and code samples in Python and JavaScript so you can implement and adapt SimpleCipherText safely.
Goals and constraints
SimpleCipherText is designed with these goals:
- Simplicity: clear primitives and small code size for educational use.
- Usability: APIs that are easy to call correctly.
- Portability: implementations across common languages and environments.
- Moderate security: reasonable confidentiality and integrity for low-threat scenarios (local files, small utilities, demos).
Important constraints and caveats:
- SimpleCipherText is not intended as a replacement for well-vetted, modern protocols (e.g., TLS, libsodium, age). For any high-value data, use established cryptographic libraries and follow best practices.
- Security depends on using secure primitives (authenticated encryption, secure key derivation, secure random), protecting keys, and using correct nonces/IVs.
Design overview
At a high level, SimpleCipherText uses:
- A secure authenticated encryption algorithm (AEAD) where available (AES-GCM or ChaCha20-Poly1305).
- A key-derivation function (HKDF or PBKDF2) to derive symmetric keys from passwords or master secrets.
- A random nonce/IV for each encryption operation.
- Associated data (optional) to bind metadata (e.g., header, version, filename).
- A compact, human-readable binary format: magic/version || salt || nonce || ciphertext || tag.
Format example (binary sequence):
- 4 bytes: ASCII magic “SCT1” (versioned)
- 16 bytes: salt (if password-derived; else omitted or zeroed)
- 12 bytes: nonce (for AES-GCM or ChaCha20-Poly1305)
- variable: ciphertext || tag (tag length depends on AEAD)
SimpleCipherText favors AEAD to provide confidentiality + integrity simultaneously; it avoids designing separate MACs.
Key derivation and parameters
If a password is used, derive a strong symmetric key using PBKDF2-HMAC-SHA256 or HKDF with a random salt:
- Salt: 16 bytes random
- PBKDF2 iterations: at least 100,000 (adjust for target platform)
- Derived key length: 32 bytes (256-bit)
If an existing key is supplied (binary), use HKDF to derive per-use keys and nonces:
- HKDF(salt, info=“SimpleCipherText v1”) -> 32-byte key and 12-byte nonce base (nonce still randomized per message or counter-based)
Nonce/IV rules:
- AES-GCM: 12-byte random nonce per message (unique per key)
- ChaCha20-Poly1305: 12-byte nonce per message (can use counters, but random is fine for single-writer scenarios)
Associated data (optional): include a header and metadata (file name, timestamp) as AAD so it is integrity-protected but not encrypted.
Secure random and constant-time
- Use the language’s cryptographically secure RNG (e.g., os.urandom / crypto.getRandomValues).
- Use constant-time comparison for any manual tag checks (avoid timing leaks).
Example 1 — Python: encrypt/decrypt files (AES-GCM)
Dependencies: Python 3.8+, cryptography library (cryptography.io).
Install:
pip install cryptography
Code (file encrypt/decrypt using password, PBKDF2, AES-GCM):
# simple_ciphertext_py.py import os from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.ciphers.aead import AESGCM from base64 import urlsafe_b64encode, urlsafe_b64decode MAGIC = b"SCT1" SALT_LEN = 16 NONCE_LEN = 12 KDF_ITERS = 200_000 KEY_LEN = 32 def derive_key(password: bytes, salt: bytes) -> bytes: kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=KEY_LEN, salt=salt, iterations=KDF_ITERS, ) return kdf.derive(password) def encrypt_file(password: str, in_path: str, out_path: str, associated_data: bytes = b""): password_b = password.encode("utf-8") salt = os.urandom(SALT_LEN) key = derive_key(password_b, salt) aesgcm = AESGCM(key) nonce = os.urandom(NONCE_LEN) with open(in_path, "rb") as f: plaintext = f.read() ct = aesgcm.encrypt(nonce, plaintext, associated_data) with open(out_path, "wb") as f: f.write(MAGIC + salt + nonce + ct) def decrypt_file(password: str, in_path: str, out_path: str, associated_data: bytes = b""): with open(in_path, "rb") as f: data = f.read() if not data.startswith(MAGIC): raise ValueError("Invalid format") salt = data[4:4+SALT_LEN] nonce = data[4+SALT_LEN:4+SALT_LEN+NONCE_LEN] ct = data[4+SALT_LEN+NONCE_LEN:] key = derive_key(password.encode("utf-8"), salt) aesgcm = AESGCM(key) plaintext = aesgcm.decrypt(nonce, ct, associated_data) with open(out_path, "wb") as f: f.write(plaintext)
Notes:
- Associated data can be used to bind filename or version.
- Increase KDF iterations for stronger protection on desktop/server hardware; reduce on constrained devices.
Example 2 — JavaScript (Node.js + Web): ChaCha20-Poly1305
Use Node.js v16+ (or the Web Crypto API in browsers). We’ll show a Node.js example using the built-in crypto module which supports ChaCha20-Poly1305 as of newer Node versions; fallback to AES-GCM if unavailable.
Install: no extra packages required for modern Node.
Code:
// simple_ciphertext_node.js const crypto = require('crypto'); const fs = require('fs'); const MAGIC = Buffer.from('SCT1'); const SALT_LEN = 16; const NONCE_LEN = 12; const KEY_LEN = 32; const PBKDF2_ITERS = 200000; const DIGEST = 'sha256'; function deriveKey(password, salt) { return crypto.pbkdf2Sync(Buffer.from(password, 'utf8'), salt, PBKDF2_ITERS, KEY_LEN, DIGEST); } function encryptFile(password, inPath, outPath, associatedData = Buffer.alloc(0)) { const salt = crypto.randomBytes(SALT_LEN); const key = deriveKey(password, salt); const nonce = crypto.randomBytes(NONCE_LEN); // Use ChaCha20-Poly1305 if available, else AES-256-GCM const algo = crypto.getCiphers().includes('chacha20-poly1305') ? 'chacha20-poly1305' : 'aes-256-gcm'; const cipher = crypto.createCipheriv(algo, key, nonce, { authTagLength: 16 }); cipher.setAAD(associatedData); const plaintext = fs.readFileSync(inPath); const ct = Buffer.concat([cipher.update(plaintext), cipher.final()]); const tag = cipher.getAuthTag(); fs.writeFileSync(outPath, Buffer.concat([MAGIC, salt, nonce, ct, tag])); } function decryptFile(password, inPath, outPath, associatedData = Buffer.alloc(0)) { const data = fs.readFileSync(inPath); if (!data.slice(0,4).equals(MAGIC)) throw new Error('Invalid format'); const salt = data.slice(4, 4 + SALT_LEN); const nonce = data.slice(4 + SALT_LEN, 4 + SALT_LEN + NONCE_LEN); const rest = data.slice(4 + SALT_LEN + NONCE_LEN); const tag = rest.slice(rest.length - 16); const ct = rest.slice(0, rest.length - 16); const key = deriveKey(password, salt); const algo = crypto.getCiphers().includes('chacha20-poly1305') ? 'chacha20-poly1305' : 'aes-256-gcm'; const decipher = crypto.createDecipheriv(algo, key, nonce, { authTagLength: 16 }); decipher.setAAD(associatedData); decipher.setAuthTag(tag); const pt = Buffer.concat([decipher.update(ct), decipher.final()]); fs.writeFileSync(outPath, pt); }
Notes:
- Browser Web Crypto API supports AES-GCM and (in some browsers) ChaCha20 variants; adapt similarly with subtle differences (Promises, ArrayBuffers).
Example 3 — Web app: encrypting messages in the browser
High-level steps:
- Use Web Crypto API for key derivation (PBKDF2 or HKDF) and AES-GCM.
- Keep keys in memory (never send password or derived keys to server).
- Export ciphertext as Base64/URL-safe for transport.
Example (simplified, async):
// browser_simple_ciphertext.js (illustrative) async function deriveKey(password, salt) { const pwUtf8 = new TextEncoder().encode(password); const pwKey = await crypto.subtle.importKey('raw', pwUtf8, 'PBKDF2', false, ['deriveKey']); return crypto.subtle.deriveKey( { name: 'PBKDF2', salt, iterations: 200000, hash: 'SHA-256' }, pwKey, { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'] ); } async function encryptMessage(password, message, associatedData = new Uint8Array()) { const salt = crypto.getRandomValues(new Uint8Array(16)); const key = await deriveKey(password, salt); const iv = crypto.getRandomValues(new Uint8Array(12)); const ct = await crypto.subtle.encrypt( { name: 'AES-GCM', iv, additionalData: associatedData }, key, new TextEncoder().encode(message) ); // Concatenate: MAGIC + salt + iv + ct const magic = new TextEncoder().encode('SCT1'); const out = new Uint8Array(magic.length + salt.length + iv.length + ct.byteLength); out.set(magic, 0); out.set(salt, magic.length); out.set(iv, magic.length + salt.length); out.set(new Uint8Array(ct), magic.length + salt.length + iv.length); return btoa(String.fromCharCode(...out)); }
Security tips for web:
- Use secure context (HTTPS).
- Consider WebAuthn or platform crypto (Credential Management) when possible instead of password-derived keys.
- Do not store plaintext passwords; prefer ephemeral usage or strong user consented storage.
Example 4 — Embedded / constrained devices
Constraints: limited CPU, memory, no hardware AES, low-quality RNG.
Recommendations:
- Use ChaCha20-Poly1305 (software-friendly) or a hardware AES if available.
- Lower PBKDF2 iterations (e.g., 20k) on microcontrollers; compensate by using longer random passwords or hardware-backed secrets.
- Truncate or stream large files (process in chunks) but preserve AEAD semantics: use a streaming AEAD (e.g., RFC 9180 MLS-like) or rekey per chunk and include chunk sequence numbers as AAD.
Pseudo-code for chunked encryption (rekey per chunk):
- master_key <- KDF(password, salt)
- for each chunk i:
- key_i = HMAC(master_key, b”chunk”+i)
- nonce_i = random
- encrypt chunk with AEAD using key_i and nonce_i, AAD includes chunk index
Interoperability and versioning
- Always include a version/magic header (e.g., “SCT1”) so future format changes are manageable.
- Use AAD to include a human-readable JSON header with algorithm identifiers, key-derivation params, timestamp, and filename (store JSON unencrypted when you want quick inspection, but include it in AAD if it must be integrity-protected).
- When changing algorithms, increment the version and keep older code able to read older versions where feasible.
Example file header (JSON as AAD, human-readable)
You can store a small JSON header (not encrypted) and put it in AAD to bind it to ciphertext: { “version”: “SCT1”, “kdf”: “PBKDF2-HMAC-SHA256”, “kdf_iters”: 200000, “cipher”: “AES-256-GCM”, “salt_len”: 16, “nonce_len”: 12, “created”: “2025-08-31T12:00:00Z” }
Include this JSON in the file before binary sections or alongside; when using as AAD, the binary must include the same bytes during decryption.
Security checklist before deploying
- Use authenticated encryption (AEAD).
- Use a secure RNG for salts and nonces.
- Use KDF for password-derived keys with adequate iterations.
- Ensure nonces are never reused with the same key.
- Maintain versioning and algorithm identifiers.
- Use constant-time comparisons for manual tag verification.
- Prefer well-tested libraries and avoid writing your own crypto primitives.
- Consider key storage: hardware-backed keystores, OS keychains, or HSMs for production secrets.
Troubleshooting and common pitfalls
- Reused nonces: will break AEAD security; use random nonces or counters tied to key usage.
- Wrong AAD: decrypt will fail if AAD differs — ensure same bytes and encoding.
- Incompatible field ordering: specify exact header byte layout and document it.
- Low KDF iterations on modern hardware: increases vulnerability to offline guessing.
Compact reference implementation notes
- Keep the reference code small (≈100–200 lines) and well-documented.
- Provide tests: encrypt/decrypt roundtrip, tamper detection (flip ciphertext/tag bytes), wrong-password behavior.
- Provide CLI wrappers for ease of use.
Example CLI usage (Python script)
Encrypt:
python simple_ciphertext_py.py encrypt "my-password" input.txt output.sct
Decrypt:
python simple_ciphertext_py.py decrypt "my-password" output.sct decrypted.txt
(Implement argument parsing in the script using argparse; reuse the functions shown earlier.)
Conclusion
SimpleCipherText is an approachable pattern for symmetric authenticated encryption that emphasizes clarity, portability, and practical usage. It pairs AEAD ciphers with secure KDFs, includes versioning and associated data, and produces a compact file format suitable for learning and low-risk applications. For high-security needs, integrate vetted libraries and consider additional protections such as hardware key storage, secure protocols, and threat modeling.
Leave a Reply