Skip to main content

Type-safe, API-first plausibly deniable multi-secret encryption containers

Project description

Keyprism

One container. Different truths for different keys.

Keyprism is a typed, API-first Python library for fixed-size, plausibly deniable multi-secret containers. A single container can hold several complete plaintexts. Each plaintext is independently protected by its own passphrase, while every unwritten slot is replaced with cryptographically secure random bytes before the container is saved.

Security status: Keyprism 0.1.0 is pre-audit software. It is suitable for experimentation and review, but it should not protect high-risk production data until the design and implementation receive independent cryptographic review.

Contents

What Keyprism provides

Capability Behavior
Multiple secrets Each passphrase decrypts one complete, independently encrypted plaintext
Fixed-size slots Every physical slot has exactly the configured byte capacity
Decoy slots Unwritten slots are filled with Python's CSPRNG
Length hiding Plaintexts are padded to the fixed encrypted payload boundary
Misuse-resistant API Callers cannot provide nonces, salts, padding, offsets, or raw slot indexes
Authenticated encryption ChaCha20-Poly1305 rejects altered or incorrectly keyed slots
Password hardening A unique 16-byte salt and Argon2id are used for every real slot
Uniform opening work Every physical slot is attempted; opening never returns early after a match
Static typing Full annotations and a PEP 561 marker support strict type checking
Minimal runtime surface Only argon2-cffi and cryptography are runtime dependencies

The precise security claim is:

Given the container alone, an adversary cannot distinguish a real encrypted slot from a randomly-filled empty slot with better than negligible advantage over guessing.

The number of physical slots is observable from the file size. Keyprism hides which physical slots are real and therefore how many real secrets exist; it does not hide the physical upper bound.

Installation

After the package is published on PyPI:

python -m pip install keyprism

From a source checkout:

python -m pip install .

For development, testing, packaging, and documentation:

python -m pip install -e ".[dev,docs]"

Keyprism requires Python 3.11 or newer.

Quick start

from pathlib import Path

from keyprism import Container

path = Path("private-notes.kpr")

# slot_capacity is the total serialized size of each physical slot.
container = Container.create(num_slots=4, slot_capacity=4096)

# Slot placement is selected internally and is not exposed to the caller.
container.write_slot("ordinary-passphrase", b"Buy milk")
container.write_slot("private-passphrase", b"Recovery instructions")

# save() automatically random-fills the two remaining slots.
container.save(path)

loaded = Container.load(path)

assert loaded.open("ordinary-passphrase") == b"Buy milk"
assert loaded.open("private-passphrase") == b"Recovery instructions"
assert loaded.open("unknown-passphrase") is None

Secrets are accepted and returned as bytes. Keyprism does not guess text encodings, serialize Python objects, log plaintext, or manage user accounts. Those responsibilities belong to the application using the library.

Container lifecycle

Container.create()
       |
       v
writable in-memory container
       |
       +---- write_slot(passphrase, plaintext)  [repeat as needed]
       |
       +---- open(passphrase)                    [optional in-memory check]
       |
       v
finalize() or save()
       |
       v
all unwritten slots receive CSPRNG bytes
       |
       v
read-only finalized container
       |
       +---- save(path)
       +---- open(passphrase)
       |
       v
Container.load(path) -> read-only container

A loaded container cannot accept new secrets. The on-disk format deliberately contains no marker saying which slots are unused, so safely selecting an overwrite target after loading is impossible. Create a new container when the set of secrets must change.

Calling save() automatically calls finalize(). Calling finalize() more than once is safe and has no additional effect.

Public API

Creating a container

container = Container.create(num_slots=4, slot_capacity=4096)
Parameter Meaning Constraint
num_slots Number of physical slots Integer greater than zero
slot_capacity Total serialized bytes per slot At least 48 bytes

Useful read-only properties:

Property Result
container.num_slots Observable physical slot count
container.slot_capacity Serialized bytes per physical slot
container.max_plaintext_size Maximum plaintext bytes per slot
container.serialized_size Exact saved file size
container.finalized Whether every slot has serialized bytes

Writing a secret

container.write_slot(passphrase: str, plaintext: bytes) -> None

Keyprism validates the inputs, selects a random unwritten slot, creates a fresh salt and nonce, pads the plaintext, derives the key, and encrypts the fixed payload. The public API exposes no unsafe cryptographic controls.

Finalizing and saving

container.finalize() -> None
container.save(path: str | Path) -> None

Finalization fills every unwritten slot with output from secrets.token_bytes(). Saving writes only the format header and fixed-size slot bytes. On POSIX systems, newly created files request mode 0600.

Loading and opening

container = Container.load(path)
plaintext = container.open(passphrase)

open() returns matching plaintext bytes or None. A wrong passphrase and the absence of a matching slot deliberately share the same result. The method attempts authentication against every physical slot and does not return early.

Binary format

Keyprism format version 1 starts with an eight-byte framing header.

Header

Offset Size Field
0 4 bytes Magic and format version: KPR\x01
4 4 bytes Big-endian slot capacity
8 Remaining bytes One or more fixed-size slots

Slot count is not stored as a field. It is derived as:

physical_slot_count = (file_size - 8) / slot_capacity

Physical slot

Every slot is exactly slot_capacity bytes:

Slot offset Size Real slot Empty slot
0 16 bytes Random Argon2id salt CSPRNG bytes
16 12 bytes Random AEAD nonce CSPRNG bytes
28 slot_capacity - 44 bytes Encrypted fixed payload CSPRNG bytes
slot_capacity - 16 16 bytes Poly1305 tag CSPRNG bytes

Before encryption, the fixed payload contains:

Payload field Size
Plaintext length 4 bytes
Plaintext Variable
Random length-hiding padding Remaining payload bytes

Therefore:

max_plaintext_size = slot_capacity - 48
serialized_file_size = 8 + (num_slots * slot_capacity)

For the default 4096-byte slot:

Measurement Bytes
Total slot capacity 4096
Salt + nonce + tag + encrypted length field 48
Maximum plaintext 4048

Cryptographic profile

Format version 1 fixes the following profile:

Component Selection
KDF Argon2id version 1.3
KDF time cost 3 iterations
KDF memory cost 131,072 KiB (128 MiB)
KDF parallelism 1 lane
Derived key 32 bytes
Salt 16 random bytes per real slot
AEAD ChaCha20-Poly1305
Nonce 12 random bytes per real slot
Authentication tag Full 16-byte Poly1305 tag
Randomness Python secrets module

The version and slot capacity are bound into AEAD associated data. KDF values are format-version parameters rather than per-container metadata; changing them silently would make existing version-1 containers unreadable.

The default KDF measured approximately 0.45 seconds per derivation on the development machine used for the 0.1.0 verification. Performance varies by hardware.

Deniability and information bounds

A real slot consists of independently random salt and nonce bytes followed by ChaCha20-Poly1305 output. An empty slot consists entirely of CSPRNG output. Both occupy exactly the same number of bytes.

Automated regression tests compare real and empty slot byte distributions using chi-square statistics, Shannon entropy, and total-variation distance. These tests can detect implementation regressions; they are not a substitute for a cryptographic proof or independent review.

An observer can still learn:

  • that a Keyprism-format file probably exists when the header is visible;
  • the slot capacity;
  • the number of physical slots;
  • the total file size;
  • filesystem, backup, transport, and application metadata outside the format.

Read THREAT_MODEL.md for the complete claim and exclusions.

Error handling

Typed exceptions are available from keyprism.errors:

Exception Meaning
KeyPrismError Base class for package-specific errors
ConfigurationError Invalid container dimensions
ContainerFormatError Malformed or unsupported serialized framing
ContainerStateError Operation is invalid for the current lifecycle state
InvalidPassphraseError Passphrase is empty or unusable
NoAvailableSlotError Every writable slot has already been assigned
PaddingError Authenticated internal payload framing is invalid
PlaintextTooLargeError Plaintext exceeds container.max_plaintext_size

Exception messages are static and never interpolate passphrases, derived keys, plaintext, or raw slot bytes.

from keyprism import Container, PlaintextTooLargeError

container = Container.create(slot_capacity=128)

try:
    container.write_slot("passphrase", b"x" * 100)
except PlaintextTooLargeError:
    # Handle the condition without logging the plaintext or passphrase.
    pass

Integration example

The repository includes examples/secure_notes_demo.py, a small third-party-style application that imports only the public API.

python examples/secure_notes_demo.py create notes.kpr
python examples/secure_notes_demo.py open notes.kpr

Applications should:

  1. collect passphrases without echoing them;
  2. keep plaintext in memory only as long as necessary;
  3. avoid logs, telemetry, crash reports, and shell history containing secrets;
  4. choose a slot capacity that hides expected plaintext-length differences;
  5. store or transmit the finalized container as an opaque byte file.

Security boundaries

Keyprism is designed for an adversary who possesses the container and possibly one disclosed passphrase. It does not protect against:

  • live memory forensics on an unlocked process;
  • a compromised host OS, Python runtime, dependency, or random-number source;
  • keylogging, screen capture, swap, or hibernation capture;
  • an adversary with independent evidence of exact real-secret count;
  • coercion that obtains every passphrase;
  • weak, reused, or externally disclosed passphrases;
  • denial of service, file deletion, corruption, or rollback;
  • metadata created outside the container.

Python cannot promise cycle-level constant-time execution. Keyprism provides constant relative work across slot positions: every physical slot performs the same KDF and authentication attempt. See SECURITY.md for reporting and operational guidance.

Project structure

The repository follows the standard source-layout packaging structure used in the referenced PyPI publishing walkthrough:

keyprism/
├── src/
│   └── keyprism/
│       ├── __init__.py
│       ├── container.py
│       ├── crypto.py
│       ├── errors.py
│       └── py.typed
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_api_misuse.py
│   ├── test_container.py
│   ├── test_crypto.py
│   ├── test_errors.py
│   ├── test_indistinguishability.py
│   └── test_timing.py
├── docs/
│   ├── api.rst
│   ├── conf.py
│   ├── format.rst
│   └── index.rst
├── examples/
│   └── secure_notes_demo.py
├── .github/workflows/ci.yml
├── CHANGELOG.md
├── LICENSE
├── MANIFEST.in
├── MIGRATION.md
├── pyproject.toml
├── README.md
├── SECURITY.md
└── THREAT_MODEL.md

The installable distribution contains the keyprism package under src/. Tests, documentation, examples, and release files are included in the source archive but not in the runtime wheel.

Development and verification

Install all contributor tools:

python -m pip install -e ".[dev,docs]"

Run the same gates as CI:

ruff check .
ruff format --check .
mypy --strict src tests examples
pytest -v --tb=short
pytest --cov=keyprism --cov-report=term-missing
sphinx-build -W -b html docs docs/_build/html
python -m build
python -m twine check dist/*

The current suite covers:

  • Argon2id cross-implementation known-answer behavior;
  • the RFC 8439 ChaCha20-Poly1305 test vector;
  • empty, maximum-size, and boundary plaintexts;
  • wrong passphrases and ciphertext tampering;
  • malformed file framing;
  • public API misuse resistance;
  • real-versus-decoy distribution checks;
  • runtime correlation with matching slot position;
  • save/load and third-party integration behavior.

The required coverage floor is 90% across the complete keyprism package.

Building and publishing

The packaging workflow follows the build/check/upload sequence demonstrated in the referenced video.

1. Build clean distributions

python -m build

Expected artifacts:

dist/keyprism-0.1.0-py3-none-any.whl
dist/keyprism-0.1.0.tar.gz

2. Validate metadata and README rendering

python -m twine check dist/*

3. Test the release on TestPyPI

python -m twine upload --repository testpypi \
  dist/keyprism-0.1.0-py3-none-any.whl \
  dist/keyprism-0.1.0.tar.gz

# In a fresh environment, install runtime dependencies from PyPI first.
python -m pip install "argon2-cffi>=23.1.0" "cryptography>=42.0.0"
python -m pip install --index-url https://test.pypi.org/simple/ \
  --no-deps keyprism==0.1.0
python -c "import keyprism; print(keyprism.__version__)"

4. Publish to PyPI

python -m twine upload \
  dist/keyprism-0.1.0-py3-none-any.whl \
  dist/keyprism-0.1.0.tar.gz

Use a scoped PyPI API token or PyPI Trusted Publishing. Never place tokens in source files, command history, README examples, CI logs, or chat messages.

Release checklist

Before publishing 0.1.0:

  • Runtime package uses the src/keyprism layout.
  • The public API is fully typed and includes py.typed.
  • Ruff, strict mypy, tests, coverage, docs, build, and Twine checks pass.
  • Runtime dependencies are limited to argon2-cffi and cryptography.
  • Rewrite Git history to purge legacy key and log files.
  • Run gitleaks against the full rewritten history.
  • Configure a scoped PyPI token or Trusted Publishing.
  • Upload and smoke-test on TestPyPI.
  • Upload version 0.1.0 to PyPI and create the Git tag.

Before a 1.0 release:

Migration and compatibility

The original Tkinter prototype and its ciphertext/decoy format are not compatible with Keyprism. See MIGRATION.md before publishing the repository.

The earlier unpublished local package name used a different format magic and AEAD domain. Recreate any experimental containers with Keyprism 0.1.0; do not rename old container files and assume compatibility.

License

Keyprism is distributed under the MIT License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

keyprism-0.1.0.tar.gz (29.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

keyprism-0.1.0-py3-none-any.whl (14.8 kB view details)

Uploaded Python 3

File details

Details for the file keyprism-0.1.0.tar.gz.

File metadata

  • Download URL: keyprism-0.1.0.tar.gz
  • Upload date:
  • Size: 29.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for keyprism-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ca47a7178e5b78a7d46ef093f7bdbf20e63aa3da55a132c9feb93ac3075d76cc
MD5 83669a629114bfe6f2ab67e40f1fbd89
BLAKE2b-256 cbb9d18a182d46bb996cdf3314ef542ebfa6ec6e10f23cc9a9a0eb515a2c8447

See more details on using hashes here.

File details

Details for the file keyprism-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: keyprism-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 14.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for keyprism-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a8622aa861b0c7a84d428638447c4a26dc5533fd51f26752197f00981d9f4684
MD5 34a9a21ece0bca86601e27f888cfdafc
BLAKE2b-256 837a09c913758cc16d39ac423c6e05884f141f2a333d8f4568abd344961a7c5b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page