Qeltrix V6
Seekable · Encrypted · Streamable
Pack any file into independently-encrypted blocks. Seek any byte without decrypting the rest. Serve live over HTTP Range Requests.
Proof of concept only. Not for production or security-critical use without an independent cryptographic audit. The PBKDF2 salt is hardcoded — see the Security Notes page for what this means and what must change for production.
What It Does
.qltx container. Decrypt it back. Parallel block processing across all CPU cores.Install
Option A — PyPI (recommended)
pip install qeltrix-v6
After installing, import the package in Python as qeltrix_v6:
from qeltrix_v6 import pack, unpack, seek_extract, GatewayServer, SeekServer from qeltrix_v6.crypto import random_master_key
Option B — From Source
git clone https://github.com/Qeltrix/Qeltrix-v6.git cd Qeltrix-v6 # Linux / Mac — build C library cd c_lib && make && cd .. # Windows — build C library with MinGW cd c_lib && mingw32-make && cd .. # Install Python dependencies + package pip install cryptography pip install -e .
Quick Test
# Pack a file python -m qeltrix_v6 pack myfile.txt myfile.qltx --passphrase secret # Inspect the container (no passphrase needed) python -m qeltrix_v6 info myfile.qltx # Unpack it back python -m qeltrix_v6 unpack myfile.qltx recovered.txt --passphrase secret
Architecture at a Glance
How Qeltrix V6 Works
An interactive visual tour of V6's architecture, capacity, and capabilities — from block layout to gateway topologies and security model.
This is a proof of concept. All diagrams and benchmarks below illustrate architectural design and theoretical capacity. Not validated for production use. Key management layer has known PoC limitations — see Security Notes.
The Problem V6 Solves
Traditional encryption forces a full decrypt before you can read a single byte. V6's block model breaks that constraint entirely.
Block Anatomy — What's Inside Each Block
Each block in a .qltx container is self-contained and independently verifiable. The layout below directly reflects what _clib.py writes via make_block_prefix(), make_v6c(), and encrypt_block() in crypto.py. Click a segment to see exactly what each field contains and where it comes from in code.
_clib.py: make_block_prefix)
V6-C Metadata (_clib.py: make_v6c + crypto.py: encrypt_v6c_metadata)
Ciphertext (crypto.py: encrypt_block via CDK)
AEAD Tag (appended inside ciphertext by AESGCM / ChaCha20Poly1305)
Key Derivation Hierarchy
V6 uses a dual-layer key model: a single Master Key (MK) at the root, with a unique Content Derived Key (CDK) derived per block. Compromising one CDK reveals nothing about any other block.
How HTTP Range Requests Work with V6
The SeekServer maps any byte-range request to the minimum set of blocks needed, decrypts only those, and returns exact bytes. This is what enables video seeking without downloading the full file.
Gateway Topologies
The GatewayServer supports three deployment modes. Select a mode below to see its data flow.
Encrypt only
Cipher Performance: AES-256-GCM vs ChaCha20-Poly1305
V6 offers two AEAD ciphers. The right choice depends on your hardware. Both provide authenticated encryption — the performance profile differs by CPU capability.
Concurrent Architecture & Scalability
Both the packer and the GatewayServer use ThreadPoolExecutor. Encryption throughput scales roughly linearly with CPU core count up to the pool limit.
What V6 Can & Cannot Do (PoC Status)
This matrix shows V6's capabilities and clearly marks what is production-ready vs. what is a known PoC limitation.
| Capability | Component | Status | Notes |
|---|---|---|---|
| Block encryption (AEAD) | crypto.py |
✓ Production-quality | AES-256-GCM or ChaCha20-Poly1305 |
| Per-block CDK derivation | crypto.py |
✓ Production-quality | HKDF-SHA256 with block index + content hash |
| Tamper detection (AEAD tag) | All blocks | ✓ Production-quality | Any modification fails authentication |
| HTTP Range Request seek | SeekServer |
✓ Production-quality | VFS index maps byte ranges to blocks |
| Parallel block packing | pack() |
✓ Production-quality | ThreadPoolExecutor, scales with cores |
| TCP encryption gateway | GatewayServer |
✓ Production-quality | All 3 topologies: reflect, router, chained |
| MK never written to disk | All | ✓ Production-quality | In-memory only; header stores encrypted copy |
| PBKDF2 salt (passphrase→MK) | cli.py |
⚠ PoC — hardcoded | Fixed b"QeltrixV6Salt"; enables rainbow tables |
| MK distribution between nodes | None | ⚠ Not in V6 scope | V6 is a file/stream encryption engine, not a key-exchange protocol. MK distribution is intentionally out of scope — build it around V6 using ECDH (X25519), RSA-OAEP, or a KMS. The master_key bytes are the only contract. You can also swap symmetric MK for an asymmetric scheme by editing crypto.py: random_master_key() and the wrap/unwrap logic. |
| Key rotation / re-encryption | None | ⚠ Not in V6 scope | Re-pack with a new MK — V6 blocks are independently encrypted so you can re-encrypt selectively. A rotation wrapper can be built around pack() / unpack() without modifying V6 itself. To use asymmetric MK, modify derive_cdk() in crypto.py. |
| TLS on SeekServer | SeekServer |
⚠ HTTP only | Use nginx/Caddy reverse proxy for HTTPS |
| Client authentication | SeekServer |
⚠ None | Add bearer token or mTLS at reverse proxy |
| Write-only vs read-only key roles | None | ⚠ Not implemented | Symmetric MK: any key holder can encrypt AND decrypt |
Primary Use Cases
V6's architecture makes it a natural fit for several distinct application domains. The chart below reflects the breadth of scenarios the system is designed to address.
Full End-to-End Pipeline
In a complete deployment: a GatewayServer encrypts data at the source, stores V6 containers in untrusted storage, and a SeekServer decrypts on demand for consumers — all without the storage layer ever seeing plaintext.
Encrypt
Decrypt + Serve Range
Core Concepts
Understand the block model, key hierarchy, and what the MK is before diving into usage.
Block Architecture
Every file is split into fixed-size blocks (default 1 MB). Each block is encrypted independently with its own derived key. This is what enables seeking — you only need to decrypt the blocks that contain your requested byte range.
Key Hierarchy
What is the V6-C Metadata?
Each block contains an encrypted metadata struct called V6-C. It holds the IV, salt, SHA-256 hash of the original data, and the wrapped CDK. The V6-C itself is encrypted with the Master Key using AES-256-GCM. This means even the internal structure of the container is hidden.
The Master Key (MK)
What it is
- 32-byte symmetric secret
- Lives in process memory only
- Never written to disk in plaintext
- Container stores it encrypted (wrapped)
- Required for every operation
What V6 does NOT do
- Transmit MK over the wire
- Negotiate MK between nodes
- Distribute MK to clients
- Manage key rotation
The MK must be shared externally via ECDH, RSA encapsulation, or a KMS. V6 is an encryption engine — key distribution is the operator's responsibility. For local testing, passing the same --passphrase on both sides is sufficient.
Cipher Options
| Flag | Cipher | Best for |
|---|---|---|
--cipher aes | AES-256-GCM | Default. Hardware-accelerated (AES-NI) on most CPUs since 2010. |
--cipher chacha | ChaCha20-Poly1305 | Mobile / IoT — fast in software where AES-NI is absent. |
Pack & Unpack
Encrypt a file into a .qltx container, or decrypt a container back to the original file.
Pack a File
python -m qeltrix_v6 pack input.txt output.qltx --passphrase mysecretpassword # With options: python -m qeltrix_v6 pack input.bin output.qltx \ --passphrase mysecretpassword \ --cipher chacha \ --block-size 512 \ --workers 8
| Flag | Default | Description |
|---|---|---|
--passphrase | required | Encryption passphrase. Derives the 32-byte Master Key via PBKDF2. |
--cipher | aes | aes = AES-256-GCM chacha = ChaCha20-Poly1305 |
--block-size | 1024 | Block size in KB. Larger = fewer blocks, smaller seek granularity. |
--workers | 4 | Parallel threads for block encryption. |
Expected Output
Qeltrix V6 Pack Input: myfile.c (5,189 bytes) Output: myfile.qltx Cipher: AES-256-GCM Block size: 1,048,576 bytes Workers: 4 [INFO] Packing 5,189 bytes -> 1 blocks [████████████████████] 100% (1/1 blocks) ✓ Done in 0.047s Blocks: 1 Original: 5,189 bytes Container: 2,047 bytes (0.39x overhead) Speed: 0.1 MB/s
Small files compress well (zlib deflate is applied before encryption), which is why the container can be smaller than the original for text/code files. Binary or already-compressed files will be larger due to encryption overhead.
Unpack a Container
python -m qeltrix_v6 unpack output.qltx recovered.txt --passphrase mysecretpassword # Skip integrity checks for speed (not recommended): python -m qeltrix_v6 unpack output.qltx recovered.txt \ --passphrase mysecretpassword \ --no-verify
Expected Output
Qeltrix V6 Unpack Container: myfile.qltx (2,047 bytes) Output: recovered.txt Verify: Yes Workers: 4 [INFO] Unpacking 1 blocks -> 5,189 bytes [████████████████████] 100% (1/1 blocks) ✓ Done in 0.000s Blocks: 1 Original: 5,189 bytes Speed: 0.0 MB/s Integrity: ✓ OK
Integrity: OK means every block's SHA-256 hash and AEAD authentication tag verified successfully. Any tampering with the container bytes causes this to fail with an error before any data is written.
Seek Extract
Extract any byte range from an encrypted container. Only the minimum necessary blocks are decrypted — everything else stays encrypted and untouched.
Basic Seek
# Extract bytes 0–99 (first 100 bytes) python -m qeltrix_v6 seek myfile.qltx \ --offset 0 --length 100 \ --passphrase mysecretpassword \ --output chunk.bin # Extract from the middle — bytes 5000–5499 python -m qeltrix_v6 seek myfile.qltx \ --offset 5000 --length 500 \ --passphrase mysecretpassword \ --output middle.bin # Print hex if no --output specified python -m qeltrix_v6 seek myfile.qltx \ --offset 0 --length 64 \ --passphrase mysecretpassword
How Seek Works Internally
For a 10 GB video file with 1 MB blocks (~10,000 blocks), seeking to minute 45 of the video decrypts only 1–2 blocks out of 10,000. This is the core performance feature of V6.
Flags
| Flag | Required | Description |
|---|---|---|
--offset | yes | Start byte position in the original (pre-encryption) file. |
--length | yes | Number of bytes to extract. |
--passphrase | yes | Must match the passphrase used during pack. |
--output | no | File to save extracted bytes. If omitted, prints hex of first 64 bytes. |
--no-verify | no | Skip SHA-256 integrity check per block (faster). |
Seek Server
An HTTP server that wraps a .qltx container and serves its decrypted content to any HTTP client. Supports HTTP Range Requests for partial content. Acts as an efficient encrypted file server — it decrypts on demand and sends plaintext to clients.
The SeekServer sends decrypted (plaintext) data to clients. It decrypts blocks server-side and serves the original bytes over HTTP. Anyone who can reach the server's port gets the plaintext. Secure the port with SSL/TLS (reverse proxy like nginx + HTTPS) or restrict it to localhost/VPN for production use.
Start the Server
python -m qeltrix_v6 serve myfile.qltx --port 7621 --passphrase mysecretpassword
# All options:
python -m qeltrix_v6 serve myfile.qltx \
--host 0.0.0.0 \
--port 7621 \
--passphrase mysecretpassword \
--workers 4
Client: Full File Download
# Download the complete decrypted file Invoke-WebRequest -Uri http://localhost:7621/ -OutFile received_file.txt # Verify it matches original Get-Content received_file.txt
Client: HTTP Range Requests (Partial Seek)
# Get first 100 bytes only Invoke-WebRequest -Uri http://localhost:7621/ ` -Headers @{ Range = "bytes=0-99" } ` -OutFile first100.bin # Get bytes 500 to 999 Invoke-WebRequest -Uri http://localhost:7621/ ` -Headers @{ Range = "bytes=500-999" } ` -OutFile middle.bin # Get last 200 bytes Invoke-WebRequest -Uri http://localhost:7621/ ` -Headers @{ Range = "bytes=-200" } ` -OutFile last200.bin
# Full download curl http://localhost:7621/ -o received.txt # Range request curl http://localhost:7621/ -H "Range: bytes=0-99" -o chunk.bin
What Happens on Each Request
Making It Secure: Reverse Proxy with HTTPS
Because the SeekServer sends decrypted data, putting it behind an nginx reverse proxy with TLS turns it into a proper secure file server.
server {
listen 443 ssl;
server_name files.yourdomain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:7621;
proxy_set_header Range $http_range;
}
}
Now clients connect over HTTPS, get TLS encryption in transit, and the SeekServer handles the V6 decryption on the backend. The .qltx file on disk stays encrypted at rest.
Gateway Server
A TCP encryption router. Any raw byte stream sent to it is encrypted into V6 blocks in real time and either reflected back or forwarded to a destination. No application changes required.
Mode 1: Reflect (Testing)
Gateway encrypts incoming data and sends the V6 stream back to the same sender. Good for testing the round-trip.
python -m qeltrix_v6 gateway --port 7620 --passphrase mysecretpassword
$bytes = [System.IO.File]::ReadAllBytes("$PWD\secret.txt")
$client = New-Object System.Net.Sockets.TcpClient("127.0.0.1", 7620)
$stream = $client.GetStream()
$stream.Write($bytes, 0, $bytes.Length)
$stream.Flush()
# Signal end-of-send so gateway flushes and responds
$client.Client.Shutdown([System.Net.Sockets.SocketShutdown]::Send)
# Read back the encrypted V6 stream
$ms = New-Object System.IO.MemoryStream
$buf = New-Object byte[] 4096
do { $r = $stream.Read($buf,0,$buf.Length); if($r -gt 0){$ms.Write($buf,0,$r)} } while ($r -gt 0)
$client.Close()
[System.IO.File]::WriteAllBytes("$PWD\encrypted.qltx", $ms.ToArray())
python -m qeltrix_v6 unpack encrypted.qltx recovered.txt --passphrase mysecretpassword
Mode 2: Router (Forward to Destination)
Gateway encrypts incoming plaintext and forwards the V6 stream to a separate receiver. This is the real gateway use-case — plaintext never reaches the network.
Start the receiver (Terminal 1)
Listens on port 7622, saves whatever arrives as a .qltx file.
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Any, 7622)
$listener.Start()
Write-Host "Receiver ready on :7622"
$conn = $listener.AcceptTcpClient()
$s = $conn.GetStream()
$ms = New-Object System.IO.MemoryStream
$buf = New-Object byte[] 4096
do { $r = $s.Read($buf,0,$buf.Length); if($r -gt 0){$ms.Write($buf,0,$r)} } while ($r -gt 0)
$conn.Close(); $listener.Stop()
[System.IO.File]::WriteAllBytes("$PWD\received.qltx", $ms.ToArray())
Write-Host "Saved $($ms.Length) bytes"
Start the gateway in router mode (Terminal 2)
python -m qeltrix_v6 gateway --port 7620 --passphrase mysecretpassword ^
--dest-host 127.0.0.1 --dest-port 7622
Send plaintext through the gateway (Terminal 3)
$bytes = [System.IO.File]::ReadAllBytes("$PWD\secret.txt")
$client = New-Object System.Net.Sockets.TcpClient("127.0.0.1", 7620)
$s = $client.GetStream()
$s.Write($bytes, 0, $bytes.Length)
$s.Flush()
$client.Client.Shutdown([System.Net.Sockets.SocketShutdown]::Send)
Start-Sleep -Milliseconds 500
$client.Close()
Write-Host "Sent $($bytes.Length) bytes"
Decrypt at the receiver (Terminal 1)
python -m qeltrix_v6 info received.qltx python -m qeltrix_v6 unpack received.qltx final.txt --passphrase mysecretpassword Get-Content final.txt
What the Network Sees
Gateway Stats
The gateway prints stats every 5 seconds while running:
Stats: 1 connections, 118 bytes in, 669 bytes out
bytes in = raw plaintext received. bytes out = encrypted V6 stream sent. The ratio (~5.7x here) reflects encryption overhead: block headers, V6-C metadata, AEAD tags, and the container footer/index.
Info Command
Inspect a .qltx container's metadata without a passphrase. Shows structure, block count, cipher, sizes, and overhead.
Usage
python -m qeltrix_v6 info myfile.qltx
Example Output
Qeltrix V6 Container Info File: myfile.qltx File size: 669 bytes Version: 6 Flags: 0x02 Cipher: AES256-GCM Block size: 1,048,576 bytes (1024 KB) Total blocks: 1 Original size: 118 bytes Footer offset: 493 Footer size: 176 bytes Overhead: 466.9%
| Field | Meaning |
|---|---|
Flags: 0x02 | FLAG_STREAMING set — container was created by the gateway (stream mode) rather than pack. |
Footer offset | Byte position of the VFS index in the container. Used by SeekServer to find blocks fast. |
Footer size | Size of the index. Grows with block count: 1 block = 176 bytes, 1000 blocks ≈ 128 KB. |
Overhead | For tiny files, overhead is huge because each block carries fixed-size headers. For large files (100 MB+) overhead drops below 1%. |
pack() & unpack()
Use Qeltrix V6 directly from Python code. Import the package and call pack / unpack with a master key.
Import
from qeltrix_v6 import ( pack, unpack, CIPHER_AES256_GCM, CIPHER_CHACHA20_POLY1305 ) from qeltrix_v6.crypto import random_master_key
pack()
# Generate a random 32-byte master key mk = random_master_key() # Pack a file result = pack( "input.txt", # source file path "output.qltx", # destination container path mk, # 32-byte master key block_size=1 << 20, # 1 MB blocks (default) cipher_id=CIPHER_AES256_GCM, workers=4, progress_cb=lambda done, total: print(f"{done}/{total}") ) print(result.total_blocks) # int print(result.original_size) # int bytes print(result.elapsed_s) # float seconds
unpack()
result = unpack( "output.qltx", # container path "recovered.txt", # output path mk, # same 32-byte master key no_verify=False, # True skips integrity checks workers=4 ) print(result.integrity_ok) # True = all blocks verified print(result.original_size) # int bytes print(result.elapsed_s)
Pack from Bytes (no file)
# pack() also accepts raw bytes or a file-like object as source data = b"Hello, encrypted world!" result = pack(data, "output.qltx", mk) # Or a BytesIO stream import io stream = io.BytesIO(b"stream data") result = pack(stream, "output.qltx", mk)
seek_extract()
Programmatically extract any byte range from a V6 container. Only the required blocks are read and decrypted.
Usage
from qeltrix_v6 import seek_extract # Extract 500 bytes starting at offset 1000 data = seek_extract( "bigfile.qltx", seek_offset=1000, seek_len=500, master_key=mk, no_verify=False, workers=4 ) print(len(data)) # 500 print(data[:32]) # first 32 bytes of the extracted slice
Video Streaming Example
# Serve a specific second of a video stored as .qltx # Video: 30 fps, 1920x1080, ~4 MB/s bitrate BYTES_PER_SECOND = 4_000_000 timestamp_seconds = 120 # seek to 2 minutes chunk = seek_extract( "movie.qltx", seek_offset=timestamp_seconds * BYTES_PER_SECOND, seek_len=BYTES_PER_SECOND, # 1 second of video master_key=mk ) # chunk contains exactly 1 second of decrypted video data # only the 1-4 blocks covering that range were decrypted
Returns
Always returns bytes. The length will be exactly seek_len unless the range exceeds the end of the file, in which case it returns what's available.
GatewayServer
Embed a V6 encryption gateway directly in your Python application. Accepts raw TCP connections and produces encrypted V6 streams.
Basic Usage
from qeltrix_v6 import GatewayServer, CIPHER_AES256_GCM from qeltrix_v6.crypto import random_master_key import time mk = random_master_key() gw = GatewayServer( mk, listen_host="0.0.0.0", listen_port=7620, cipher_id=CIPHER_AES256_GCM, workers=4, dest_host=None, # None = reflect mode dest_port=None, ) gw.start(blocking=False) # runs in background thread # Your application runs here... try: while True: time.sleep(5) s = gw.stats print(s['connections'], s['bytes_in'], s['bytes_out']) except KeyboardInterrupt: gw.stop()
Router Mode (Forward to Destination)
gw = GatewayServer( mk, listen_port=7620, dest_host="storage-server.internal", # forward here dest_port=7622, ) gw.start(blocking=True) # blocks until Ctrl+C
StreamPacker: Lower-Level API
from qeltrix_v6 import StreamPacker import io # Encrypt a stream to a buffer (no file I/O) packer = StreamPacker(mk, block_size=1 << 20, cipher_id=CIPHER_AES256_GCM) source = io.BytesIO(b"my data stream") output = io.BytesIO() index = packer.pack_stream( source, lambda chunk: output.write(chunk), total_size=None ) encrypted_bytes = output.getvalue() print(len(encrypted_bytes), "bytes encrypted")
HttpStreamGateway: Encrypt a URL Download
from qeltrix_v6 import HttpStreamGateway # Download a URL and save it as an encrypted V6 container gw = HttpStreamGateway( mk, url="https://example.com/data.bin", block_size=1 << 20, cipher_id=CIPHER_AES256_GCM, ) bytes_written = gw.stream_to_file("encrypted_download.qltx") print(bytes_written, "bytes in container")
SeekServer
Embed an HTTP seek server in your application. Exposes a .qltx container over HTTP with full Range Request support. Decrypts and serves data on demand.
Basic Usage
from qeltrix_v6 import SeekServer from qeltrix_v6.crypto import random_master_key import time mk = random_master_key() srv = SeekServer( "bigfile.qltx", # path to .qltx container mk, # master key host="0.0.0.0", port=7621, workers=4 ) srv.start(blocking=False) # background thread # Server runs; any HTTP client can now GET / with Range headers try: while True: time.sleep(1) except KeyboardInterrupt: srv.stop()
Programmatic Seek (without HTTP)
# Use seek_extract directly — no HTTP server needed from qeltrix_v6 import seek_extract # Read bytes 0-1023 of the original file chunk = seek_extract("bigfile.qltx", 0, 1024, mk) # Read a "frame" at position 4096 frame = seek_extract("bigfile.qltx", 4096, 512, mk)
Full Workflow: Pack then Serve
from qeltrix_v6 import pack, SeekServer from qeltrix_v6.crypto import random_master_key import time mk = random_master_key() # 1. Encrypt the file pack("video.mp4", "video.qltx", mk, block_size=1 << 20) # 2. Serve it — any media player with the URL can now seek srv = SeekServer("video.qltx", mk, port=7621) srv.start(blocking=False) print("Serving at http://localhost:7621/") # Media player hits: GET / Range: bytes=X-Y # SeekServer decrypts only those blocks and returns plaintext while True: time.sleep(1)
Crypto API
Low-level cryptographic functions. Use these to integrate V6's key derivation and encryption into your own code.
Key Generation
from qeltrix_v6.crypto import ( random_master_key, random_key, random_iv, random_salt, random_bytes ) mk = random_master_key() # 32 bytes — root key key = random_key() # 32 bytes — AES/ChaCha key iv = random_iv() # 12 bytes — GCM/ChaCha nonce salt = random_salt() # 32 bytes — HKDF salt
Content Derived Key (CDK)
from qeltrix_v6.crypto import derive_cdk, sha256, CIPHER_AES256_GCM block_data = b"raw block content" block_hash = sha256(block_data) salt = random_salt() # CDK is bound to: master key + block position + content hash + salt cdk = derive_cdk( master_key=mk, block_index=0, block_data_hash=block_hash, salt=salt, cipher_id=CIPHER_AES256_GCM ) # cdk is 32 bytes, unique to this block's content AND position
Encrypt / Decrypt a Block
from qeltrix_v6.crypto import encrypt_block, decrypt_block, CIPHER_AES256_GCM plaintext = b"secret data" iv = random_iv() ciphertext = encrypt_block(plaintext, cdk, iv, CIPHER_AES256_GCM, aad=b"context") # Decrypt recovered = decrypt_block(ciphertext, cdk, iv, CIPHER_AES256_GCM, aad=b"context") assert recovered == plaintext
Master Key Wrapping
from qeltrix_v6.crypto import wrap_master_key, unwrap_master_key # Wrap MK with a passphrase for storage wrapped_mk, iv, salt = wrap_master_key(mk, passphrase=b"mypassphrase") # Recover MK recovered_mk = unwrap_master_key(wrapped_mk, b"mypassphrase", iv, salt) assert recovered_mk == mk # Note: CLI uses a HARDCODED salt for PBKDF2 (PoC limitation) # For production: generate random salt per container
Container Internals
Everything inside qeltrix_v6.container — the dataclasses returned by pack() and unpack(), module-level constants, and the private worker functions that run in the thread pool.
Module Constants
from qeltrix_v6.container import DEFAULT_BLOCK_SIZE, MAX_WORKERS DEFAULT_BLOCK_SIZE = 1 << 20 # 1,048,576 bytes (1 MB) — default block size for all operations MAX_WORKERS = min(os.cpu_count() or 4, 16) # thread pool cap, auto-detected at import
Dataclasses
These are returned by pack(), unpack(), and stored in the VFS footer index. All fields are read-only after construction.
BlockIndexEntry
from qeltrix_v6.container import BlockIndexEntry # One entry per block in the QLTXFOOT VFS index. # Used by seek_extract() to find exactly which blocks cover a byte range. @dataclass class BlockIndexEntry: block_index: int # 0-based block number original_offset: int # byte offset of this block in the ORIGINAL file original_size: int # plaintext size of this block (varies for last block) container_offset: int # byte offset of this block in the .qltx CONTAINER container_size: int # total bytes this block occupies in the container # (prefix 28 + v6c_iv 12 + enc_v6c N + payload) block_hash: bytes # SHA-256 of original (pre-compression) block data, 32 bytes iv: bytes # 12-byte nonce used to encrypt the block payload salt: bytes # 32-byte random HKDF salt used for CDK derivation cipher_id: int # 0 = AES-256-GCM, 1 = ChaCha20-Poly1305
V6PackResult
from qeltrix_v6.container import V6PackResult # also: from qeltrix_v6 import V6PackResult # Returned by pack() @dataclass class V6PackResult: container_path: str # output .qltx file path total_blocks: int # number of blocks written original_size: int # original file size in bytes block_size: int # block size used (bytes) cipher_id: int # cipher used (0 or 1) elapsed_s: float # wall-clock seconds for entire pack operation # Usage: r = pack("file.txt", "file.qltx", mk) print(r.total_blocks, r.original_size, r.elapsed_s)
V6UnpackResult
from qeltrix_v6.container import V6UnpackResult # also: from qeltrix_v6 import V6UnpackResult # Returned by unpack() @dataclass class V6UnpackResult: output_path: str # path of the recovered file total_blocks: int # number of blocks decrypted original_size: int # recovered file size in bytes elapsed_s: float # wall-clock seconds integrity_ok: bool # True if all block SHA-256 hashes verified correctly # False signals container tampering or wrong passphrase # Usage: r = unpack("file.qltx", "recovered.txt", mk) if not r.integrity_ok: raise RuntimeError("Container tampered!")
Private Worker Functions
These run inside ThreadPoolExecutor and are the actual encryption/decryption engines. They are intentionally importable so the thread pool can call them cross-process safely on Windows (no lambda or closure capture issues).
_pack_one_block()
from qeltrix_v6.container import _pack_one_block # Signature: def _pack_one_block(block_index, raw_data, master_key, cipher_id) -> dict: # Steps performed: # 1. SHA-256(raw_data) → block_hash # 2. zlib.compress(raw_data, 6) → compressed # 3. random_salt() + random_iv() → per-block entropy # 4. derive_cdk(mk, idx, hash, salt) → CDK # 5. permute_block(compressed, hash) → permuted [C lib] # 6. AEAD encrypt(permuted, CDK, iv, aad=pack(">Q", block_index)) → payload # 7. wrap_cdk(CDK, mk) → (wrapped_cdk 48b, cdk_iv 12b) # 8. make_v6c(...) → v6c_raw [C lib] # 9. encrypt_v6c_metadata(v6c_raw, mk) → (enc_v6c, v6c_iv) # Returns dict with keys: # "block_index", "v6c_iv", "enc_v6c", "payload", # "block_hash", "iv", "salt", "cipher_id", "orig_sz", "comp_sz"
_unpack_one_block()
from qeltrix_v6.container import _unpack_one_block # Signature: def _unpack_one_block(block_index, enc_v6c, v6c_iv, payload, master_key, no_verify) -> tuple[int, bytes]: # Steps performed (exact reverse of _pack_one_block): # 1. decrypt_v6c_metadata(enc_v6c, mk, v6c_iv) → v6c_raw # 2. qltx_read_v6c(v6c_raw) → block_hash, iv, salt, cdk_blob, cipher_id # 3. unwrap_cdk(cdk_blob[12:60], mk, cdk_blob[:12]) → CDK # 4. AEAD decrypt(payload, CDK, iv, aad=pack(">Q", block_index)) → permuted # 5. unpermute_block(permuted, block_hash) → compressed # 6. zlib.decompress(compressed) → raw_data # 7. if not no_verify: assert SHA-256(raw_data) == block_hash # Returns: (block_index: int, raw_data: bytes) # Raises ValueError on bad V6-C magic or integrity failure
Footer Serialization
from qeltrix_v6.container import _serialize_footer, _parse_footer, _read_container_index # Serialize list[BlockIndexEntry] → bytes (QLTXFOOT magic + count + hash + entries) # Layout: b"QLTXFOOT" (8) + entry_count uint64 (8) + SHA-256 of entries (32) + entries # Each entry: 128 bytes from C struct qltx_index_entry via qltx_write_index_entry() footer_bytes = _serialize_footer(entries) # list[BlockIndexEntry] → bytes # Parse QLTXFOOT bytes back into list[BlockIndexEntry] entries = _parse_footer(footer_bytes) # bytes → list[BlockIndexEntry] # Raises ValueError if magic != b"QLTXFOOT" or data is truncated # High-level: read header dict + full index from a .qltx file in one call # Returns (header_info: dict, entries: list[BlockIndexEntry]) hdr, entries = _read_container_index("myfile.qltx") # hdr dict keys: # "version", "flags", "cipher_default", "block_size", # "total_blocks", "original_size", "footer_offset", "footer_size" # Example: check original size without decrypting anything hdr, _ = _read_container_index("secret.qltx") print("Original size:", hdr["original_size"], "bytes")
Crypto Module — All Functions
Every function in qeltrix_v6.crypto with signatures and return types taken directly from the source.
V6-C Metadata Encryption
Every block's metadata struct (IV, salt, hash, wrapped CDK) is itself encrypted with the MK before being written to the container. These two functions handle that layer.
from qeltrix_v6.crypto import encrypt_v6c_metadata, decrypt_v6c_metadata # Encrypt a V6-C metadata bytes blob with the MK # Returns (encrypted_metadata: bytes, iv: bytes) enc_meta, meta_iv = encrypt_v6c_metadata(v6c_raw_bytes, mk) # Uses AES-256-GCM with aad=b"V6C-METADATA" internally # Decrypt it back v6c_raw = decrypt_v6c_metadata(enc_meta, mk, meta_iv)
CDK Wrapping / Unwrapping
The CDK itself is wrapped (AES-GCM encrypted) with the MK before being stored in the V6-C metadata. This is the dual-layer security: both the CDK and the metadata that contains it are protected by the MK.
from qeltrix_v6.crypto import wrap_cdk, unwrap_cdk # Wrap CDK with MK — returns (wrapped_cdk: bytes, iv: bytes) # wrapped_cdk is 48 bytes (32-byte key + 16-byte GCM tag) wrapped_cdk, cdk_iv = wrap_cdk(cdk, mk) # aad=b"V6C-CDK-WRAP" used internally # Recover CDK cdk = unwrap_cdk(wrapped_cdk, mk, cdk_iv)
Hashing
from qeltrix_v6.crypto import sha256, sha256_file, hash_footer_entries # SHA-256 of bytes → 32 bytes h = sha256(b"data") # Streaming SHA-256 of a file (avoids loading whole file into RAM) h = sha256_file("bigfile.bin", chunk_size=1 << 20) # SHA-256 of all footer index entries concatenated # Used internally to verify footer integrity footer_hash = hash_footer_entries([entry1_bytes, entry2_bytes])
Constants
from qeltrix_v6.crypto import ( CIPHER_AES256_GCM, # = 0 CIPHER_CHACHA20_POLY1305, # = 1 CIPHER_NAMES, # = {0: "AES256-GCM", 1: "ChaCha20-Poly1305"} AES_KEY_SIZE, # = 32 AES_IV_SIZE, # = 12 CHACHA_KEY_SIZE, # = 32 CHACHA_IV_SIZE, # = 12 SALT_SIZE, # = 32 MASTER_KEY_SIZE, # = 32 )
Full Function Reference
| Function | Returns | Description |
|---|---|---|
random_bytes(n) | bytes | Cryptographically secure random bytes via os.urandom |
random_key(cipher_id) | bytes 32 | Random AES or ChaCha key |
random_iv(cipher_id) | bytes 12 | Random 96-bit nonce |
random_salt() | bytes 32 | Random HKDF salt |
random_master_key() | bytes 32 | Random MK |
sha256(data) | bytes 32 | SHA-256 digest |
sha256_file(path, chunk_size) | bytes 32 | Streaming SHA-256 of file |
derive_cdk(mk, block_index, block_hash, salt, cipher_id) | bytes 32 | HKDF-SHA256 CDK derivation |
encrypt_block(pt, key, iv, cipher_id, aad) | bytes | AEAD encrypt; tag appended |
decrypt_block(ct, key, iv, cipher_id, aad) | bytes | AEAD decrypt; raises InvalidTag on tamper |
encrypt_v6c_metadata(meta, mk) | (bytes, bytes) | Encrypt V6-C struct → (enc, iv) |
decrypt_v6c_metadata(enc, mk, iv) | bytes | Decrypt V6-C struct |
wrap_cdk(cdk, mk) | (bytes 48, bytes 12) | Wrap CDK with MK → (wrapped, iv) |
unwrap_cdk(wrapped, mk, iv) | bytes 32 | Recover CDK |
wrap_master_key(mk, passphrase) | (bytes, bytes, bytes) | Wrap MK → (wrapped, iv, salt) |
unwrap_master_key(wrapped, passphrase, iv, salt) | bytes 32 | Recover MK |
hash_footer_entries(entries) | bytes 32 | SHA-256 of all index entries |
Gateway Module — All Classes
Complete reference for GatewayServer, StreamPacker, HttpStreamGateway, SeekServer, _SocketStream, and the flag constants from qeltrix_v6.gateway.
Flag Constants
from qeltrix_v6 import FLAG_NO_VERIFY, FLAG_STREAMING FLAG_NO_VERIFY = 0x01 # Skip per-block hash checks on unpack FLAG_STREAMING = 0x02 # Set in container header when created by gateway/StreamPacker # Gateway always sets FLAG_STREAMING. info command shows Flags: 0x02.
GatewayServer — Full Reference
from qeltrix_v6 import GatewayServer, CIPHER_AES256_GCM gw = GatewayServer( master_key=mk, # 32-byte MK — required listen_host="0.0.0.0", # bind address listen_port=7620, # listen port block_size=1 << 20, # 1 MB blocks (default) cipher_id=CIPHER_AES256_GCM, # or CIPHER_CHACHA20_POLY1305 workers=4, # ThreadPoolExecutor workers per connection dest_host=None, # None = reflect; str = forward to this host dest_port=None, # forward port (required if dest_host set) ) gw.start(blocking=False) # blocking=True blocks thread until stop() gw.stop() # sets _running=False; does a brief self-connect to unblock accept() # Live stats dict — updated per connection s = gw.stats # s["connections"] int — total connections accepted since start # s["bytes_in"] int — total raw plaintext bytes received # s["bytes_out"] int — total encrypted V6 bytes sent # s["blocks"] int — total V6 blocks produced
StreamPacker — Full Reference
The core of the Gateway. Reads a source stream block-by-block, encrypts each block in a thread pool, and writes a valid V6 container to a dest_write callback in real time. Used by GatewayServer internally; also usable standalone.
from qeltrix_v6 import StreamPacker import io packer = StreamPacker( master_key=mk, block_size=1 << 20, cipher_id=0, # CIPHER_AES256_GCM workers=4, no_verify=False, # if True, sets FLAG_NO_VERIFY in header ) # Stream source bytes → dest_write callback → valid .qltx bytes emitted buf = io.BytesIO() index_entries = packer.pack_stream( source=io.BytesIO(b"data to encrypt"), dest_write=buf.write, # any callable(bytes) — socket.sendall, file.write, etc. total_size=None, # int or None; used for progress_cb denominator progress_cb=None # optional callable(blocks_done, total_blocks) ) # Returns list[BlockIndexEntry] — the VFS index # After pack_stream, patch the header with correct footer_offset / total_blocks # (required when buf is seekable; skip for pure streaming sockets) packer.patch_header(buf, total_size=15) # Rewrites first N bytes of buf with correct header in place # Inspect state after pack_stream: packer._block_index # int: total blocks written packer._footer_offset # int: byte offset of QLTXFOOT in container packer._footer_size # int: size of footer packer._container_offset # int: current write position packer._index # list[BlockIndexEntry]: built index
Streaming Direct to a Socket
import socket, io dest = socket.create_connection(("storage-server", 7622)) packer = StreamPacker(mk) source = io.BytesIO(b"payload") packer.pack_stream(source, dest.sendall) # Note: patch_header not possible on a socket (not seekable) # For socket destinations, header footer_offset will be 0 in PoC. dest.close()
HttpStreamGateway — Full Reference
Downloads a URL and saves the content as a live V6 encrypted container. Streams the HTTP response directly into the encryption pipeline — no temporary files needed for the plaintext.
from qeltrix_v6 import HttpStreamGateway gw = HttpStreamGateway( master_key=mk, url="https://example.com/dataset.bin", block_size=1 << 20, cipher_id=0, # CIPHER_AES256_GCM workers=4, ) # Download and save as .qltx (returns bytes written) n = gw.stream_to_file( "output.qltx", progress_cb=lambda done, total: print(f"{done}/{total}") ) print(n, "bytes written") # If Content-Length header is present in HTTP response, total_size is # passed to StreamPacker for progress reporting. Otherwise total=0.
SeekServer — Full Reference
HTTP server that wraps a .qltx file. Loads the VFS index once at startup. Each request decrypts only the blocks needed for the requested byte range and returns plaintext over HTTP.
from qeltrix_v6 import SeekServer srv = SeekServer( container_path="bigfile.qltx", # .qltx file path master_key=mk, # 32-byte MK host="0.0.0.0", port=7621, workers=4 ) # __init__ immediately calls _read_container_index() and caches: # srv._header — dict with version, flags, block_size, original_size, etc. # srv._index — list[BlockIndexEntry] # srv._entry_map — dict[block_index -> BlockIndexEntry] srv.start(blocking=False) # background thread srv.stop() # sets _running=False; accept() exits on next 1s timeout # HTTP request handling: # GET / (no Range) → unpack() full container to tmp file → serve 200 OK # GET / Range: bytes=X-Y → seek_extract(X, Y-X+1) → serve 206 Partial Content # GET / Range: bytes=-N → serve last N bytes (suffix range)
What the Server Returns
| Request | Response | Body |
|---|---|---|
GET / no Range header | 200 OK | Full decrypted original file. Uses unpack() to a temp file. |
GET / Range: bytes=0-999 | 206 Partial Content | Bytes 0–999 of decrypted original. Uses seek_extract(0, 1000). |
GET / Range: bytes=-500 | 206 Partial Content | Last 500 bytes. Resolved to original_size - 500. |
_SocketStream — Adapter Class
Wraps a raw socket (or fd) as a Python io.RawIOBase stream so it can be passed to StreamPacker.pack_stream(). Two backends: Python socket (default, recommended) and C qltx_recv_exact (for known-length streams only).
from qeltrix_v6.gateway import _SocketStream # Python backend (default) — use for all normal cases stream = _SocketStream(sock) # sock is a socket.socket stream = _SocketStream(fd_int) # int fd — wrapped via socket.fromfd() # C backend — only for known-length streams, maximum throughput # blocks until exactly n bytes arrive; no partial-read or EOF handling lib = _clib.get_lib() stream = _SocketStream(raw_fd, lib=lib, use_c=True) # Use as a regular readable stream packer.pack_stream(stream, dest_write)
Private Handler Methods
These are instance methods called in background threads. They are not part of the public API but are documented here for completeness and for anyone extending the gateway.
GatewayServer._handle_client()
# Called in a daemon thread per accepted TCP connection. # Signature: _handle_client(self, client_sock, ip: str, port: int) # # Flow: # 1. Read all incoming raw bytes from client_sock (recv loop, 30s timeout) # 2. Accumulate chunks → raw_data bytes; update stats["bytes_in"] # 3. Build BytesIO buffer, create StreamPacker # 4. StreamPacker.pack_stream(BytesIO(raw_data), buf.write, total_size) # 5. StreamPacker.patch_header(buf, total_size) — fix footer_offset in header # 6a. If dest_host set: socket.create_connection(dest_host, dest_port) # sendall(container); update stats["bytes_out"] # 6b. If no dest_host: client_sock.sendall(container) — reflect mode # 7. Update stats["blocks"] += packer._block_index # 8. Close client_sock in finally block
SeekServer._handle_http()
# Called in a daemon thread per accepted HTTP connection. # Signature: _handle_http(self, conn: socket.socket, addr) # # Flow: # 1. Recv until b"\r\n\r\n" found — accumulates full HTTP headers # 2. Parse first line (e.g. "GET / HTTP/1.1") and header dict # 3a. If Range header present: # → qltx_parse_range_header(range_val) → (seek_start, seek_end) # → Handle suffix range: bytes=-N means last N bytes # → seek_extract(container, seek_start, seek_len, mk) # → Send HTTP/1.1 206 Partial Content + Content-Range header + data # 3b. If no Range header: # → unpack(container, tmp_file, mk) # → Read tmp_file, send HTTP/1.1 200 OK + full content # → Delete tmp_file (tempfile.NamedTemporaryFile) # 4. Close conn in finally block # Response headers sent: # 206: Content-Type, Content-Length, Content-Range, Accept-Ranges, Connection # 200: Content-Type, Content-Length, Accept-Ranges, Connection
StreamPacker._emit_block()
# Called by pack_stream() to write one completed block to dest_write. # Signature: _emit_block(self, result: dict, dest_write: callable) # # Steps: # 1. Unpack result dict: block_index, v6c_iv, enc_v6c, payload, hash, iv, salt, cipher_id, orig_sz # 2. Compute v6c_size_field = len(v6c_iv) + len(enc_v6c) = 12 + N # 3. make_block_prefix(bidx, v6c_size_field, len(payload)) → 28-byte prefix # 4. Concatenate: prefix + v6c_iv + enc_v6c + payload → data bytes # 5. dest_write(data) — socket.sendall, file.write, BytesIO.write, etc. # 6. Append BlockIndexEntry to self._index # 7. Advance self._container_offset by len(data) # # Called in-order by _flush_pending() inside the ThreadPoolExecutor context.
_clib Module — C Library Bindings
Python wrappers around every exported C function in libqeltrix_v6.so / qeltrix_v6.dll. These are the low-level building blocks used by container.py and gateway.py.
Library Loading
from qeltrix_v6._clib import get_lib, c_version # Load and return the ctypes CDLL (singleton — loaded once) lib = get_lib() # Version string from the C library print(c_version()) # e.g. "6.0.0" # Library search order (auto-detected by _find_lib()): # Windows: qeltrix_v6.dll next to package dir, then ../c_lib/ # Linux: libqeltrix_v6.so next to package dir, then ../c_lib/ # macOS: libqeltrix_v6.dylib next to package dir, then ../c_lib/
Block Math
from qeltrix_v6._clib import block_count, block_range, seek_blocks # How many blocks does a file of file_size bytes need? n = block_count(file_size=10_000_000, block_size=1 << 20) # → 10 # Byte range of block N in the original file # Returns (offset: int, size: int) off, sz = block_range(block_index=2, file_size=10_000_000, block_size=1 << 20) # off=2097152, sz=1048576 # Which block indices cover a byte range seek? # Returns list[int] of block indices needed blocks = seek_blocks( seek_offset=5_000_000, seek_len=500_000, file_size=10_000_000, block_size=1 << 20 ) # → [4] — only block 4 needed
Header / Footer / Prefix Construction
from qeltrix_v6._clib import ( make_header, make_block_prefix, parse_block_prefix, header_size, v6c_size, index_entry_size, block_prefix_size ) # Write a QLTX file header (returns bytes of size header_size()) hdr = make_header( block_size=1 << 20, total_blocks=10, original_file_size=10_000_000, footer_offset=10_500_000, footer_size=2048, flags=0, cipher_default=0, encrypted_mk=None, # bytes or None (zeroed if None) mk_iv=None, mk_salt=None ) # Write a 28-byte block prefix (magic + block_index + v6c_size + payload_size) prefix = make_block_prefix(block_index=0, v6c_size=200, payload_size=50000) # Parse a 28-byte block prefix back # Returns (block_index: int, v6c_size: int, payload_size: int) bidx, v6c_sz, payload_sz = parse_block_prefix(prefix_bytes) # Size constants (from C struct definitions) header_size() # int — QLTX header bytes v6c_size() # int — V6-C metadata struct bytes index_entry_size() # int — one footer index entry bytes (128 bytes) block_prefix_size() # int — block prefix bytes (28)
Permutation
from qeltrix_v6._clib import permute_block, unpermute_block # Deterministic byte-level permutation keyed to seed (block_hash) # Applied to compressed data before AES-GCM encryption permuted = permute_block(data, seed) # seed = SHA-256 of original block original = unpermute_block(permuted, seed) # Both implemented in C for speed. Returns bytes of same length as input.
Full C Function Reference
| Python wrapper | C function | Description |
|---|---|---|
block_count(file_size, block_size) | qltx_block_count | Number of blocks needed |
block_range(idx, file_sz, blk_sz) | qltx_block_range | (offset, size) of block N in original |
seek_blocks(off, len, fsz, bsz) | qltx_seek_blocks | Block indices covering a seek range |
make_header(...) | qltx_write_header | Serialize QLTX file header |
make_block_prefix(idx, v6c_sz, pay_sz) | qltx_write_block_prefix | 28-byte block framing prefix |
parse_block_prefix(data) | qltx_read_block_prefix | Parse 28-byte prefix back |
permute_block(data, seed) | qltx_permute_block | Deterministic permutation (C) |
unpermute_block(data, seed) | qltx_unpermute_block | Reverse permutation (C) |
header_size() | qltx_header_size | Header struct size in bytes |
v6c_size() | qltx_v6c_size | V6-C metadata struct size |
index_entry_size() | qltx_index_entry_size | Footer index entry size |
block_prefix_size() | qltx_block_prefix_size | Block prefix size (28) |
c_version() | qltx_version | C library version string |
via get_lib() | qltx_listen_tcp | Create + bind TCP server socket |
via get_lib() | qltx_accept_tcp | Accept connection, return fd |
via get_lib() | qltx_connect_tcp | Connect to host:port, return fd |
via get_lib() | qltx_send_all | Send all bytes on socket fd |
via get_lib() | qltx_recv_exact | Receive exact N bytes (blocking) |
via get_lib() | qltx_recv_line | Receive one line (HTTP header parsing) |
via get_lib() | qltx_parse_range_header | Parse HTTP Range: header → start/end int64 |
via get_lib() | qltx_format_206_header | Build HTTP 206 response header string |
via get_lib() | qltx_send_http_get | Send HTTP GET request with optional Range |
via get_lib() | qltx_time_us | Monotonic microseconds (uint64) |
via get_lib() | qltx_close_socket | Close a socket fd |
via get_lib() | qltx_write_v6c | Serialize V6-C metadata struct to bytes |
via get_lib() | qltx_read_v6c | Parse V6-C metadata bytes → fields (block_index, sizes, hash, iv, salt, cdk_blob, cipher_id) |
via get_lib() | qltx_write_index_entry | Serialize one footer index entry (block_index, offsets, sizes, hash, iv, salt, cipher_id) to 128 bytes |
via get_lib() | qltx_read_header | Parse raw QLTX header bytes into struct |
via get_lib() | qltx_footer_base_size | Size of the QLTXFOOT base section (magic + count + hash = 48 bytes) |
via get_lib() | qltx_permute_table_gen | Generate the permutation lookup table from a seed — called internally by qltx_permute_block |
make_v6c() — V6-C Struct Builder
The only _clib function not exposed through __init__.py but used directly by container.py block workers. Builds the binary V6-C metadata struct that gets encrypted and stored with each block.
from qeltrix_v6._clib import make_v6c # Build a V6-C metadata struct (encrypted per-block metadata) # Returns bytes of size v6c_size() v6c_bytes = make_v6c( block_index=0, original_size=1048576, # plaintext block size compressed_size=800000, # after zlib.compress() data_hash=block_hash, # SHA-256 of original (32 bytes) iv=iv, # 12-byte nonce (stored in 16-byte field) salt=salt, # 32-byte HKDF salt encrypted_cdk=cdk_blob, # 64-byte blob: [cdk_iv 12][wrapped_cdk 48][padding 4] cipher_id=0 # 0=AES-256-GCM, 1=ChaCha20-Poly1305 ) # This bytes object is then passed to encrypt_v6c_metadata(v6c_bytes, mk) # before being written to the container. # Internal layout of the cdk_blob (64 bytes total): # [0:12] cdk_iv — IV used to AES-GCM wrap the CDK # [12:60] wrapped_cdk — AES-GCM(CDK, MK, cdk_iv) = 32 + 16 tag bytes # [60:64] padding — zeroed
_bytes_ptr() — Low-Level Utility
from qeltrix_v6._clib import _bytes_ptr # Cast a Python bytes object to a ctypes uint8* pointer # Required to pass bytes to C functions that expect uint8_t* ptr = _bytes_ptr(b"raw bytes") # ctypes.POINTER(ctypes.c_uint8) # Used internally in every _clib wrapper: lib.qltx_permute_block(_bytes_ptr(data), len(data), out, _bytes_ptr(seed))
CLI Internals
The helper functions inside qeltrix_v6.cli that glue the command-line interface to the Python API. Useful if you want to replicate the CLI's passphrase-to-MK derivation or progress bar in your own code.
_mk_from_passphrase()
Converts a human-readable passphrase string into the 32-byte Master Key used for all encryption operations. This is the exact function the CLI calls for every command.
from qeltrix_v6.cli import _mk_from_passphrase # Derive a 32-byte master key from a passphrase mk = _mk_from_passphrase("mysecretpassword") # Equivalent to: import hashlib mk = hashlib.pbkdf2_hmac( "sha256", "mysecretpassword".encode(), b"QeltrixV6Salt", # ← hardcoded PoC salt — see Security Notes 200_000 # ← 200,000 PBKDF2 iterations )
PoC limitation: The salt b"QeltrixV6Salt" is hardcoded. Two users with the same passphrase get the same MK, making precomputed rainbow tables possible. For production, generate a random 32-byte salt per container and store it in the header.
_progress()
The block-level progress callback used by pack(), unpack(), and seek() in the CLI. Prints an in-place progress bar to stdout.
from qeltrix_v6.cli import _progress # Signature: _progress(done: int, total: int) -> None # Prints: [████████████████████] 100% (10/10 blocks) # Use it as progress_cb= in pack() / unpack(): pack("file.txt", "file.qltx", mk, progress_cb=_progress) # Or write your own: def my_progress(done, total): print(f"Block {done}/{total} done") pack("file.txt", "file.qltx", mk, progress_cb=my_progress)
CLI Command Functions
Each subcommand is a standalone function. They can be called programmatically by constructing an argparse.Namespace directly, which is useful for embedding CLI behaviour in scripts or tests without subprocess.
from qeltrix_v6.cli import cmd_pack, cmd_unpack, cmd_seek, cmd_info import argparse # Call cmd_pack programmatically ns = argparse.Namespace( input="file.txt", output="file.qltx", passphrase="secret", cipher="aes", block_size=1024, # KB workers=4 ) cmd_pack(ns) # Call cmd_info programmatically cmd_info(argparse.Namespace(input="file.qltx"))
| Function | Args namespace fields | Description |
|---|---|---|
cmd_pack(args) | input, output, passphrase, cipher, block_size, workers | Runs full pack with progress output |
cmd_unpack(args) | input, output, passphrase, no_verify, workers | Runs full unpack with integrity report |
cmd_seek(args) | input, offset, length, passphrase, output, no_verify, workers | Seek-extract; prints hex if no output file |
cmd_gateway(args) | host, port, passphrase, cipher, dest_host, dest_port, workers | Starts gateway, blocks printing stats every 5s |
cmd_serve(args) | input, host, port, passphrase, workers | Starts SeekServer, blocks until Ctrl+C |
cmd_info(args) | input | Prints container header info (no passphrase needed) |
Entry Point
# __main__.py — invoked by `python -m qeltrix_v6` from .cli import main main() # main() builds the argparse parser with all subcommands # and dispatches to cmd_pack / cmd_unpack / etc. # Can also be called directly: from qeltrix_v6.cli import main main() # reads sys.argv
Security Notes
What is strong, what are the PoC limitations, and what must change before production use.
This is a proof of concept. The core encryption is sound. The key management layer has hardcoded values that are unsafe for production. Read below before deploying in any real context.
What Is Strong
Cryptographically sound
- AES-256-GCM (AEAD)
- ChaCha20-Poly1305 (AEAD)
- HKDF-SHA256 for CDK derivation
- Per-block independent CDKs
- V6-C metadata encrypted with MK
- MK never written to disk plaintext
- AEAD auth tag detects any tampering
- SHA-256 block integrity hashes
PoC weaknesses
- Hardcoded PBKDF2 salt
- No MK distribution mechanism
- No key rotation
- Symmetric MK only (no write/read role split)
- SeekServer sends plaintext over HTTP
- No authentication of clients
The Hardcoded PBKDF2 Salt
In cli.py, the passphrase-to-MK derivation uses a hardcoded salt b"QeltrixV6Salt". This means two people using the same passphrase get identical MKs, and an attacker could precompute a rainbow table of passphrase -> MK mappings valid for all V6 containers ever created.
# CURRENT (PoC) — hardcoded salt, same passphrase = same MK always return hashlib.pbkdf2_hmac("sha256", passphrase.encode(), b"QeltrixV6Salt", 200_000) # PRODUCTION FIX — random salt per container, stored in header salt = os.urandom(32) return hashlib.pbkdf2_hmac("sha256", passphrase.encode(), salt, 200_000), salt
MK Distribution: Use ECDH or KMS
V6 does not handle MK distribution. For two nodes to share the same MK securely:
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey from cryptography.hazmat.primitives.kdf.hkdf import HKDF from cryptography.hazmat.primitives import hashes # Node A generates keypair, sends public key to Node B priv_a = X25519PrivateKey.generate() pub_a = priv_a.public_key() # Node B generates keypair, sends public key to Node A priv_b = X25519PrivateKey.generate() pub_b = priv_b.public_key() # Both independently derive the same shared secret shared_a = priv_a.exchange(pub_b) shared_b = priv_b.exchange(pub_a) # Derive identical MK on both sides — MK never on the wire mk_a = HKDF(hashes.SHA256(), 32, None, b"QeltrixV6-MK").derive(shared_a) mk_b = HKDF(hashes.SHA256(), 32, None, b"QeltrixV6-MK").derive(shared_b) # mk_a == mk_b (identical, without ever being transmitted)
SeekServer: Plaintext Over HTTP
The SeekServer decrypts server-side and sends plaintext. To secure in transit:
| Option | How | Effort |
|---|---|---|
| nginx + Let's Encrypt | Reverse proxy SeekServer behind nginx with free TLS cert | Low |
| Caddy | Caddy auto-provisions TLS — one config line | Very low |
| Localhost only | Bind to 127.0.0.1 instead of 0.0.0.0, access via SSH tunnel | Medium |
| VPN | Run on private network, clients connect via VPN | Medium |
PoC to Production Checklist
| Item | Status | Fix |
|---|---|---|
| Block encryption (AES-256-GCM) | Production-ready | — |
| CDK key derivation (HKDF) | Production-ready | — |
| AEAD integrity verification | Production-ready | — |
| PBKDF2 salt | Hardcoded | Random per-container salt stored in header |
| MK distribution | Not implemented | ECDH handshake or KMS integration |
| Key rotation | Not implemented | Re-key API; schedule re-encryption |
| SeekServer TLS | HTTP only | nginx / Caddy reverse proxy with HTTPS |
| Client authentication | None | Add bearer token / mTLS at reverse proxy |