Skip to content

datumbox/VernamVeil

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

VernamVeil Logo

VernamVeil: A Function-Based Stream Cypher

CI Docs License


⚠️ DISCLAIMER: This is an educational encryption prototype and not meant for real-world use. It has not been audited or reviewed by cryptography experts, and should not be used to store, transmit, or protect sensitive data.

πŸš€ Quick Start

Minimal Installation (without vectorisation or C extension support):

pip install .

Minimal Example:

import hashlib
from vernamveil import FX, VernamVeil


# Step 1: Define a custom key stream function; remember to store this securely
def keystream_fn(i: int, seed: bytes) -> bytes:
    # Simple cryptographically safe fx; see below for more examples
    hasher = hashlib.blake2b(seed)
    hasher.update(i.to_bytes(8, "big"))
    return hasher.digest()

fx = FX(keystream_fn, block_size=64, vectorise=False)


# Step 2: Generate a random initial seed for encryption
initial_seed = VernamVeil.get_initial_seed()  # remember to store this securely

# Step 3: Encrypt and decrypt a single message
cypher = VernamVeil(fx)
encrypted, _ = cypher.encode(b"Hello!", initial_seed)
decrypted, _ = cypher.decode(encrypted, initial_seed)

πŸ”Ž Overview

VernamVeil is an experimental cypher inspired by the One-Time Pad (OTP) developed in Python. The name honours Gilbert Vernam, who is credited with the theoretical foundation of the OTP.

Instead of using a static key, VernamVeil allows the key to be represented by a function fx(i: int | np.ndarray, seed: bytes | bytearray) -> bytes | np.ndarray:

  • i: the index of the block of bytes in the message; a scalar integer or a 1D uint64 NumPy array with a continuous enumeration for vectorised operations.
  • seed: a byte string that provides context and state; should be kept secret.
  • Output: bytes or a 2D uint8 NumPy array representing the key stream values.

Note: numpy is an optional but highly recommended dependency, used to accelerate vectorised operations when available.

This approach enables novel forms of key generation, especially for those who enjoy playing with maths and code. While this is not a secure implementation by cryptographic standards, it offers a fun and flexible way to experiment with function-based encryption. If you're curious about how encryption works, or just want to mess with maths and code in a cool way, this project is a fun starting point. For more information, read the accompanying blog post.


πŸ’‘ Why VernamVeil?

  • Function-based keystreams for enhanced flexibility: Unlike traditional cyphers that rely on fixed-size keys, VernamVeil allows the keystream to be generated by a user-defined function (fx). This enables the creation of arbitrarily long, non-repeating keystreams, provided that the function and seed combination allows it.
  • Modular design for cryptographic experimentation: VernamVeil's architecture makes it easy to swap in different cryptographic primitives and keystream generation strategies. This modularity supports experimentation with various secure pseudorandom functions, hash-based constructions, or even true one-time pad keystreams, while encouraging the use of well-established cryptographic techniques.
  • Inspired by the One-Time Pad (OTP): VernamVeil supports an OTP mode when provided with a truly random, externally sourced keystream. The One-Time Pad offers strong cryptographic guarantees if, and only if, it is implemented correctly: the cypher is bug-free, the keystream is truly random, at least as long as the message, used only once, and kept completely secret.

✨ Cryptographic Characteristics

  1. Function-Based, Symmetric Cypher with OTP Support: VernamVeil uses a user-defined function fx and an initial_seed (both secret) to dynamically generate the key stream. This approach is symmetric: identical secrets and encryption configuration are required for both encryption and decryption, making the process fully reversible. VernamVeil also offers the OTPFX callable wrapper that allows it to operate in one-time pad (OTP) mode when provided with a truly random, externally sourced keystream.
  2. Synthetic IV Seed Initialisation, Stateful Seed Evolution & Avalanche Effects: Instead of a traditional nonce, the first internal seed is derived using a Synthetic IV computed as a keyed hash of the user-provided initial seed, the full plaintext, and the current timestamp (inspired by RFC 5297). For each chunk, the seed is further evolved by key-hashing the previous seed with the chunk's plaintext, maintaining state between operations. This hash-based seed refreshing ensures each keystream is unique, prevents keystream reuse, provides resilience against seed reuse and deterministic output, and produces an avalanche effect: small changes in input result in large, unpredictable changes in output. Including the current timestamp in the SIV ensures each encryption produces a unique keystream, making output non-deterministic and providing resilience against accidental seed reuse, even for identical messages and seeds. The scheme does not allow backward derivation of seeds, if a current seed is leaked, past messages remain secure (backward secrecy is preserved).
  3. Message Obfuscation, Zero Metadata & Authenticated Encryption: The cypher injects decoy chunks, pads real chunks with dummy bytes, and shuffles output to obscure chunk boundaries, complicating cryptanalysis methods such as traffic analysis or block boundary detection. Chunk delimiters are randomly generated, encrypted, and not exposed; block delimiters are randomly generated and refreshed for every block. The cyphertext contains no embedded metadata, minimising the risk of attackers identifying recurring patterns or structural information. All encryption details are deterministically recovered from the fx, except for the configuration parameters (e.g., chunk and delimiter sizes) which must be provided and matched exactly during decryption, or the MAC check will fail. During encryption, Message Authentication is enforced using a standard encrypt-then-MAC (EtM) construction. During decryption verification-before-decryption is used to detect tampering and prevent padding oracle-style issues.
  4. Modular & Configurable Keystream Design: The fx function can be swapped to explore different styles of pseudorandom generation, including custom PRNGs, cryptographic hashes or OTP. The implementation also allows full adjustment of configuration, offering flexibility to tailor encryption to specific needs.
  5. Vectorisation & Optional C-backed Fast Hashing: All operations are vectorised using numpy when vectorise=True, with a slower pure Python fallback available. For even faster vectorised fx functions, an optional C module (nphash) can be compiled (with cffi and system dependencies), enabling high-performance BLAKE2b, BLAKE3 and SHA-256 hashing for NumPy-based key stream generation. BLAKE3, in particular, is only available via this extension and benefits from hardware acceleration, including SIMD and assembly optimisations where supported. This is supported both in user-defined fx methods and automatically by helpers like generate_default_fx. See nphash/README.md for details.

⚠️ Caveats & Best Practices

  • Not Secure for Real Use: This is an educational tool and experimental toy, not production-ready cryptography.
  • Use Strong fx Functions: The entire system's unpredictability hinges on the entropy and behaviour of your fx. Avoid anything guessable or biased and the use of periodic mathematical functions which can lead to predictable or repeating outputs.
  • Use Secure Seeds & Avoid Reuse: Generate initial seeds using the provided VernamVeil.get_initial_seed() method which is cryptographically safe. Treat each initial_seed as a one-time-use context and use a fresh initial seed for every encode/decode session. During the same session, the API returns the next seed you should use for the following call.
  • True One-Time Pad Support: If you use the OTPFX callable wrapper with a truly random keystream (generated from a physical entropy source), VernamVeil can be configured to operate as an One-Time Pad cypher. The keystream must be at least as long as the message, used only once, and never reused for any other message. Generating such keystreams is challenging in practice; pseudo-random generators (including cryptographically secure ones) do not provide the same guarantees as a true one-time pad.
  • Message Ordering & Replay: VernamVeil is designed to be nonce-free by evolving the seed with each message or chunk, ensuring keystream uniqueness as long as each session uses a distinct initial_seed. The Synthetic IV mechanism, by incorporating the current timestamp, ensures each cyphertext is unique even for identical messages and seeds, and specifically provides resilience against accidental seed reuse for the first message. However, the cypher itself does not guarantee full replay protection or enforce message ordering; these must be handled by the application. For strict anti-replay or ordering requirements, implement explicit mechanisms (such as sequence numbers or nonces) at a higher layer.

πŸ“ Examples

βœ‰οΈ Encrypting and Decrypting Multiple Messages

from vernamveil import VernamVeil, generate_default_fx


# Step 1: Generate a custom fx using the helper
fx = generate_default_fx()  # remember to store fx.source_code securely

# Step 2: Generate a random initial seed for encryption
initial_seed = VernamVeil.get_initial_seed()  # remember to store this securely

# Step 3: Initialise VernamVeil with the custom fx and parameters
cypher = VernamVeil(fx, chunk_size=64, decoy_ratio=0.2)

# Step 4: Encrypt multiple messages in one session
messages = [
    "This is a secret message!",
    "another one",
    "and another one"
]
encrypted = []
seed = initial_seed
for msg in messages:
    # Each message evolves the seed for the next one
    enc, seed = cypher.encode(msg.encode(), seed)
    encrypted.append(enc)

# Step 5: Decrypt multiple messages in one session
seed = initial_seed
for original, enc in zip(messages, encrypted):
    # Each message evolves the seed for the next one
    dec, seed = cypher.decode(enc, seed)
    assert dec.decode() == original

πŸ“‚ Encrypting and Decrypting Files

from vernamveil import VernamVeil, generate_default_fx


# Step 1: Generate a custom fx using the helper
fx = generate_default_fx()  # remember to store fx.source_code securely

# Step 2: Generate a random initial seed for encryption
initial_seed = VernamVeil.get_initial_seed()  # remember to store this securely

# Step 3: Initialise VernamVeil with the custom fx and parameters
cypher = VernamVeil(fx, chunk_size=64, decoy_ratio=0.2)

# Step 4: Encrypt a file
cypher.process_file("encode", "plain.txt", "encrypted.dat", initial_seed)

# Step 5: Decrypt the file
cypher.process_file("decode", "encrypted.dat", "decrypted.txt", initial_seed)

Note: The process_file method uses background threads and queues to perform asynchronous I/O for both reading and writing, enabling efficient processing of large files without blocking the main thread.


πŸ§ͺ How to Design a Custom fx

⚠️ Warning: Designing cryptographic functions is difficult and risky

Creating your own cryptographic methods is a major undertaking, and even small mistakes can introduce severe vulnerabilities. The greatest weakness of this cypher is that it allows users to supply their own fx functions: a non-expert can easily "shoot themselves in the foot" by designing a function that is predictable, biased, or otherwise insecure, potentially making the encryption trivial to break. This project is strictly educational and not intended for real-world security. The following section provides some basic principles for designing fx functions, but it is not a comprehensive guide to cryptographic engineering.

When creating your own key stream function (fx), it is essential to follow best practices to ensure the unpredictability and security of your cypher. Poorly designed functions can introduce vulnerabilities, bias, or even make the encryption reversible by attackers. Use the following guidelines:

  • Uniform & Non-Constant Output: Your fx should produce diverse, unpredictable outputs for different input indices. Avoid constant, biased, low-entropy, or periodic mathematical functions. The distribution of outputs should be as uniform as possible. Use a standard cryptographic pseudorandom function (PRF) before returning the output.
  • Seed Sensitivity: The output of fx must depend on the secret seed. Changing the seed should result in completely different outputs.
  • Type Correctness: The function must return a bytes or a NumPy uint8 array in vectorised mode.
  • Determinism: fx must be deterministic for the same inputs. Do not use external state or randomness inside your function.
  • Avoid Data-Dependent Branching or Timing: Do not introduce data-dependent branching or timing in your fx, as this can lead to side-channel attacks.
  • Performance: Complex or slow fx functions will slow down encryption and decryption. Test performance if speed is important for your use case.

Recommended approach: Apply a unique transformation to the input index using a function that incorporates constant but randomly sampled parameters to make each fx instance unpredictable. Then, combine the result with the secret seed using a cryptographically secure keyed hash method or HMAC. This ensures your keystream is both unpredictable and securely bound to your secret.

Always test your custom fx with the provided check_fx_sanity utility before using it for encryption. Note that this method only performs very basic checks and cannot guarantee cryptographic security; it may catch common mistakes, but passing all checks does not mean your function is secure.

Below we provide some example fx methods to illustrate these principles in practice:

🧠 A more robust, Keyed Hash-based scalar fx (not cryptographically standard)

import hashlib
from vernamveil import FX


def keystream_fn(i: int, seed: bytes) -> bytes:
    # Implements a customisable fx function based on a 10-degree polynomial transformation of the index,
    # followed by a cryptographically secure keyed hash (BLAKE2b) output.
    # Note: The security of `fx` relies entirely on the secrecy of the seed and the strength of the keyed hash.
    # The polynomial transformation adds uniqueness to each fx instance but does not contribute additional entropy.
    weights = [24242, 68652, 77629, 55585, 32284, 78741, 70249, 39611, 54080, 73198, 12426]

    # Transform index i using a polynomial function to introduce uniqueness on fx
    current_pow = 1
    result = 0
    for weight in weights:
        result = (result + weight * current_pow) & 0xFFFFFFFFFFFFFFFF
        current_pow = (current_pow * i) & 0xFFFFFFFFFFFFFFFF

    # Hash using BLAKE2b
    return hashlib.blake2b(i.to_bytes(8, "big"), key=seed).digest()


fx = FX(keystream_fn, block_size=64, vectorise=False)

🏎️ A fast version of the above fx that uses NumPy vectorisation and the nphash C module

import numpy as np
from vernamveil import FX, hash_numpy


def keystream_fn(i: np.ndarray, seed: bytes) -> np.ndarray:
    # Implements a customisable fx function based on a 10-degree polynomial transformation of the index,
    # followed by a cryptographically secure keyed hash (BLAKE2b) output.
    # Note: The security of `fx` relies entirely on the secrecy of the seed and the strength of the keyed hash.
    # The polynomial transformation adds uniqueness to each fx instance but does not contribute additional entropy.
    weights = np.array([24242, 68652, 77629, 55585, 32284, 78741, 70249, 39611, 54080, 73198, 12426], dtype=np.uint64)

    # Transform index i using a polynomial function to introduce uniqueness on fx
    # Compute all powers: shape (len(i), degree + 1)
    powers = np.power.outer(i, np.arange(11, dtype=np.uint64))

    # Weighted sum (polynomial evaluation)
    result = np.dot(powers, weights)

    # Hash using BLAKE2b
    return hash_numpy(result, seed, "blake2b")  # uses C module if available, else NumPy fallback


fx = FX(keystream_fn, block_size=64, vectorise=True)

πŸ›‘οΈ A cryptographically strong Keyed Hash BLAKE2b fx (vectorised & C-accelerated)

import numpy as np
from vernamveil import FX, hash_numpy


def keystream_fn(i: np.ndarray, seed: bytes) -> np.ndarray:
    # The secure default `fx` of the VernamVeil cypher.
    # Implements a standard keyed hash-based pseudorandom function (PRF) using BLAKE2b.
    # The output is deterministically derived from the input index `i` and the secret `seed`.
    # Security relies entirely on the secrecy of the seed and the cryptographic strength of the keyed hash.

    # Hash using BLAKE2b
    return hash_numpy(i, seed, "blake2b")  # uses C module if available, else NumPy fallback


fx = FX(keystream_fn, block_size=64, vectorise=True)

🏁️ A cryptographically strong and Fast Hash BLAKE3 fx (only available with C-acceleration)

import numpy as np
from vernamveil import FX, hash_numpy


def keystream_fn(i: np.ndarray, seed: bytes) -> np.ndarray:
    # Implements a standard keyed hash-based pseudorandom function (PRF) using BLAKE3.
    # BLAKE3 is only available when the C extension is installed, and benefits from hardware acceleration
    # (SIMD and assembly) for maximum performance.
    # The output is deterministically derived from the input index `i` and the secret `seed`.
    # Security relies entirely on the secrecy of the seed and the cryptographic strength of the keyed hash.

    # Hash using BLAKE3
    return hash_numpy(i, seed, "blake3", hash_size=512)  # requires the C module


fx = FX(keystream_fn, block_size=512, vectorise=True)

🎲 One-Time Pad (OTP) Mode

VernamVeil can operate as a one-time pad (OTP) cypher when provided with a truly random keystream. By supplying a keystream of random bytes (generated from a physical entropy source) through the fx interface, VernamVeil achieves the properties of an OTP: the keystream must be at least as long as the message, used only once, and never reused for any other message. Note that pseudo-random generators, including cryptographically secure ones, do not provide the same guarantees as a true one-time pad.

To facilitate OTP mode, VernamVeil provides the OTPFX utility. OTPFX is a callable wrapper that allows you to use an externally generated, truly random keystream as a drop-in replacement for a function-based fx. You provide a list of random byte blocks (each of a fixed size), and OTPFX ensures these are used sequentially as the keystream during encryption and decryption.

Example:

from vernamveil import OTPFX, VernamVeil


def get_true_random_bytes(n: int) -> bytes:
    # Replace with a function that returns n bytes from a true random source.
    # For real OTP, use a true random source (e.g., hardware RNG, quantum RNG, etc.)
    # Using `secrets` or `os.urandom` is not truly random and does not provide the same guarantees.
    raise NotImplementedError()

# Prepare a keystream of random blocks
block_size = 64
keystream = [get_true_random_bytes(block_size) for _ in range(100)]

# Create a cypher with the OTPFX instance
fx = OTPFX(keystream, block_size=block_size, vectorise=False)
cypher = VernamVeil(fx)

# Encrypt a message as per usual
initial_seed = VernamVeil.get_initial_seed()  # remember to store this securely
encrypted_message = cypher.encrypt(b"some message", initial_seed)

# Optionally clip the keystream to the used portion
fx.keystream = fx.keystream[:fx.position]  # remember to store this securely

# Reset the pointer for decryption
fx.position = 0

# Decrypt the message
decrypted_message = cypher.decrypt(encrypted_message, initial_seed)

Note: The keystream must be truly random, at least as long as the message, and never reused. Reusing a keystream completely breaks the security of OTP encryption.

Warning: Do not use or test your OTPFX instance (e.g., by calling it or running sanity checks) before actual encryption or decryption. Any use will consume part of the keystream, which cannot be recovered, and will cause decryption to fail. Always use a fresh, unused OTPFX instance for each encryption/decryption operation or reset its fx.position to 0.


🧰 Provided fx Utilities

VernamVeil includes helper tools to make working with key stream functions easier:

  • OTPFX: A callable wrapper for using externally generated, one-time-pad keystreams as a drop-in replacement for function-based fx.
  • check_fx_sanity: Runs basic sanity checks on your custom fx to ensure it produces diverse and seed-sensitive outputs.
  • generate_keyed_hash_fx (same as generate_default_fx): Generates a deterministic fx function that applies a specified hash algorithm (e.g., BLAKE2b, BLAKE3 or SHA-256) directly to the index and seed. The seed is the only secret key but the keyed hash is a cryptographically strong and proven fx. Supports both scalar and vectorised (NumPy) modes. This is the recommended secure default fx for the VernamVeil cypher. The BLAKE3 option is only available with the C extension.
  • generate_polynomial_fx: Generates a random fx function that first transforms the index using a polynomial with random weights, then applies keyed hashing (BLAKE2b) for cryptographic output. Supports both scalar and vectorised (NumPy) modes.
  • load_fx_from_file: Loads a custom fx function from a Python file. This is useful for testing and validating your own implementations. It uses importlib internally to import the fx. Never use this with files from untrusted sources, as it can run arbitrary code on your system.

These utilities help you prototype and validate your own key stream functions before using them in encryption.

Example:

from vernamveil import generate_default_fx, check_fx_sanity

# Generate a vectorised fx function
fx = generate_default_fx(vectorise=True)

# Show the generated function's source code
print("Generated fx source code:\n", fx.source_code)

# Check if the generated fx passes basic sanity checks
seed = b"mysecretseed"
num_samples = 100
passed = check_fx_sanity(fx, seed, num_samples)
print("Sanity check passed:", passed)

πŸ•΅οΈ Plausible Deniability Utilities

Plausible deniability in cryptography enables users to plausibly claim that an encrypted message contains different content from its true meaning. This is especially valuable in situations where an adversary may compel a user to reveal keys or decrypt sensitive data. By constructing alternative cryptographic parameters (such as a fake fx function and seed) the user can make the cyphertext decrypt to an innocuous decoy message, while the genuine message remains secure and undisclosed.

Here is an example of Forging a Decoy Decryption:

from vernamveil import VernamVeil, generate_default_fx, forge_plausible_fx

# Original cypher and encryption
fx = generate_default_fx()
real_cypher = VernamVeil(fx, padding_range=(5, 25), chunk_size=32, decoy_ratio=0.3)
secret_message = b"Top secret!"
seed = real_cypher.get_initial_seed()
cyphertext, _ = real_cypher.encode(secret_message, seed)

# Decoy message to plausibly reveal
decoy = b"This is a harmless message. Nothing to see here. Look away!"

# Forge plausible fx and seed
plausible_fx, fake_seed = forge_plausible_fx(real_cypher, cyphertext, decoy)

# Use the forged fx and seed to decrypt the cyphertext to the decoy
# Note: The SIV and MAC must be turned off for this to work.
decoy_cypher = VernamVeil(plausible_fx, padding_range=(5, 25), chunk_size=32, decoy_ratio=0.3,
                          siv_seed_initialisation=False, auth_encrypt=False)
revealed, _ = decoy_cypher.decode(cyphertext, fake_seed)
print(revealed)  # b'This is a harmless message. Nothing to see here. Look away!'

This approach allows you to demonstrate that a given cyphertext could plausibly contain a harmless message, providing a credible alternative explanation under duress, while the original secret remains protected.


πŸ–₯️ Command-Line Interface (CLI)

VernamVeil provides a convenient CLI for file encryption and decryption. The CLI supports both encoding (encryption) and decoding (decryption) operations, allowing you to specify custom key stream functions (fx) and seeds, or have them generated automatically.

βš™οΈ Features

  • Encrypt and decrypt files or streams using a user-defined or auto-generated fx function and seed represented in hex.
  • Auto-generate fx.py and seed.hex during encoding if not provided; these files are saved in the current working directory.
  • Custom fx and seed support: Supply your own fx.py and seed.hex for both encoding and decoding.
  • Configurable parameters: Adjust chunk size, delimiter size, padding, decoy ratio, and more. Set --verbosity info to see progress information (off by default).
  • Sanity checks: Optionally verify that your fx function is suitable for cryptographic use. These checks are automatically skipped for OTPFX to avoid consuming the keystream.

πŸ’» Usage

# Encrypt a file with auto-generated fx and seed
vernamveil encode --infile plain.txt --outfile encrypted.dat

# Encrypt a file with a custom fx function and seed
vernamveil encode --infile plain.txt --outfile encrypted.dat --fx-file fx.py --seed-file seed.hex

# Decrypt a file (requires the same fx and seed used for encryption)
vernamveil decode --infile encrypted.dat --outfile decrypted.txt --fx-file fx.py --seed-file seed.hex

# Encrypt and Decrypt from stdin to stdout (using - or omitting the argument)
vernamveil encode --infile - --outfile - --fx-file fx.py --seed-file seed.hex < plain.txt > encrypted.dat
vernamveil decode --infile - --outfile - --fx-file fx.py --seed-file seed.hex < encrypted.dat > decrypted.txt

# Enable sanity check for fx and seed during encryption
vernamveil encode --infile plain.txt --outfile encrypted.dat --fx-file fx.py --seed-file seed.hex --check-sanity

⚠️ Warning: CLI Parameter Consistency

When decoding, you must use the exact same parameters (such as --chunk-size, --delimiter-size, --padding-range, --decoy-ratio, --siv-seed-initialisation, --auth-encrypt and --hash-name) as you did during encoding.

For example, the following will fail with a Authentication failed: MAC tag mismatch. error because the --chunk-size parameter differs between encoding and decoding:

vernamveil encode --infile plain.txt --outfile encrypted.dat --chunk-size 2048
vernamveil decode --infile encrypted.dat --outfile decrypted.txt --chunk-size 1024 --fx-file fx.py --seed-file seed.hex

Always use identical parameters for both encoding and decoding. Any mismatch will result in decryption failure. The only exception is the --buffer-size parameter, which can be different for encoding and decoding.

πŸ—„οΈ File Handling

  • For both --infile and --outfile, passing - or omitting the argument means stdin/stdout will be used. This allows for piping and streaming data directly.
  • When encoding without --fx-file or --seed-file, the CLI generates fx.py and seed.hex in the current working directory. The absolute paths to these files are displayed after generation. Store these files securely; they are required for decryption.
  • When decoding, you must provide both --fx-file and --seed-file pointing to the originals used for encryption.
  • For safety, the CLI will not overwrite existing output files, fx.py, or seed.hex. If these files already exist, you must delete or rename them manually before running the command. Overwrite protection does not apply when outputting to stdout.

⚠️ Warning: Binary Output to Terminals

If you use - or omit --outfile, the output will be written to stdout in binary mode. Writing binary data directly to a terminal may corrupt your session. Only redirect binary output to files or pipes, not to an interactive terminal.

See vernamveil encode --help and vernamveil decode --help for all available options.


πŸ› οΈ Technical Details

  • Compact Implementation: The core cypher implementation (_vernamveil.py) is about 200 lines of code, excluding comments, documentation and empty lines.
  • External Dependencies: Built using only Python's standard library, with NumPy being optional for vectorisation.
  • Optional C/C++ Module for Fast Hashing and Byte Search: An optional C/C++ module (nphash) is provided, built using cffi, which enables fast BLAKE2b, BLAKE3, and SHA-256 keyed hashing for NumPy arrays, and also provides efficient byte search utilities for internal use. BLAKE3 support is exclusive to this extension and benefits from hardware acceleration for optimal performance. Specifically, BLAKE3 uses the official implementation and can utilise SIMD instruction sets such as SSE2, SSE4.1, AVX2, AVX512F, AVX512VL (on x86_64), and NEON (on ARM), as well as hand-written assembly, where supported by your hardware and compiler. These acceleration features are detected and enabled automatically during the build process. The extension includes both C and C++ code (the C++ component is from the BLAKE3 project), so both a C and a C++ compiler (for example, gcc, g++, or MSVC) are required to build it. See the nphash README for further details.
  • Tested with: Python 3.10 and NumPy 2.2.5.

πŸ”§ Installation

To install the library with all optional dependencies (development tools, NumPy for vectorisation, and cffi for the C module):

pip install .[dev,numpy,cffi]
  • The [dev] extra installs development and testing dependencies.
  • The [numpy] extra enables fast vectorised operations.
  • The [cffi] extra is required for building the nphash C extension for accelerated BLAKE2b, BLAKE3 and SHA-256 in NumPy-based fx functions. You need to compile the C extension afterwards. See below.

⚑ Fast Vectorised fx Functions

If you want to use fast vectorised key stream functions, install with both numpy and cffi enabled. The included nphash C module provides high-performance BLAKE2b, BLAKE3, and SHA-256 keyed hash implementations for NumPy arrays, which are automatically used by generate_default_fx(vectorise=True) when available. BLAKE3 is only available via the C extension and is hardware-accelerated for maximum speed. If the extension is not present, a slower pure NumPy fallback is used (excluding BLAKE3).

To use the C extension you must build it from source. For more details, see nphash/README.md.


🚦 Benchmarks: VernamVeil vs AES-256-CBC

VernamVeil is competitive with OpenSSL's AES-256-CBC in terms of speed. As both cyphers leverage hardware acceleration, the exact benchmarks vary significantly from machine to machine. On an Apple MacBook Pro with an M1 Max chip, VernamVeil is about 15% faster. Note that our benchmarks test VernamVeil with all its default security features enabled (Authenticated Encryption, Synthetic IV, and Obfuscation), whereas AES-256-CBC performs raw, unauthenticated encryption.

πŸ“Š Summary

The following benchmarks compare VernamVeil (NumPy vectorisation, C extension enabled, and blake3 hashing) to OpenSSL's AES-256-CBC on the same machine. The tests were run on a 1GB random file, measuring the time taken for encoding and decoding operations.

Algorithm Encode Time Decode Time Total Time
VernamVeil 1.119 s 1.220 s 2.339 s
AES-256-CBC 1.760 s 0.950 s 2.710 s

β€πŸ’» Benchmark Setup

1. Create a 1GB random file:

dd if=/dev/urandom of=/tmp/original.bin bs=1M count=1024 status=progress

2. Generate a random 256-bit key and IV for AES-256-CBC:

openssl rand -hex 32 > key.hex
openssl rand -hex 16 > iv.hex

πŸ‡ VernamVeil (Vectorised + C extension + Keyed Hash fx using BLAKE3)

Encoding:

vernamveil encode --infile /tmp/original.bin --outfile /tmp/output.enc --fx-file fx.py --seed-file seed.hex --buffer-size 134217728 --chunk-size 1048576 --delimiter-size 64 --padding-range 100 200 --decoy-ratio 0.01 --hash-name blake3 --verbosity info

Time: 1.119s

Decoding:

vernamveil decode --infile /tmp/output.enc --outfile /tmp/output.dec --fx-file fx.py --seed-file seed.hex --buffer-size 136349200 --chunk-size 1048576 --delimiter-size 64 --padding-range 100 200 --decoy-ratio 0.01 --hash-name blake3 --verbosity info

Time: 1.220s

🐒 AES-256-CBC (OpenSSL)

Encoding:

time openssl enc -aes-256-cbc -in /tmp/original.bin -out /tmp/output.enc -K $(cat key.hex) -iv $(cat iv.hex) -pbkdf2

Time: 1.760s

Decoding:

time openssl enc -d -aes-256-cbc -in /tmp/output.enc -out /tmp/output.dec -K $(cat key.hex) -iv $(cat iv.hex) -pbkdf2

Time: 0.950s


πŸ“š Documentation

Full API and usage docs are available at: https://datumbox.github.io/VernamVeil/


🀝 Contributing

Contributions, bug reports, and feature requests are welcome! Please open an issue or pull request on GitHub.


πŸ“„ Copyright & License

Copyright (C) 2025 Vasilis Vryniotis.

The code is licensed under the Apache License, Version 2.0.

About

VernamVeil: A Function-Based Stream Cypher

Topics

Resources

License

Stars

Watchers

Forks

Contributors 2

  •  
  •