Metadata-Version: 2.4
Name: keyprism
Version: 0.1.0
Summary: Type-safe, API-first plausibly deniable multi-secret encryption containers
Author: Keyprism contributors
License-Expression: MIT
Keywords: cryptography,deniable-encryption,privacy,security
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security :: Cryptography
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: argon2-cffi>=23.1.0
Requires-Dist: cryptography>=42.0.0
Provides-Extra: test
Requires-Dist: pytest>=8.0.0; extra == "test"
Requires-Dist: pytest-cov>=5.0.0; extra == "test"
Provides-Extra: dev
Requires-Dist: build>=1.2.2; extra == "dev"
Requires-Dist: mypy>=1.8.0; extra == "dev"
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
Requires-Dist: ruff>=0.5.0; extra == "dev"
Requires-Dist: twine>=6.2.0; extra == "dev"
Provides-Extra: docs
Requires-Dist: sphinx>=7.2.0; extra == "docs"
Dynamic: license-file

# 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](#what-keyprism-provides)
- [Installation](#installation)
- [Quick start](#quick-start)
- [Container lifecycle](#container-lifecycle)
- [Public API](#public-api)
- [Binary format](#binary-format)
- [Cryptographic profile](#cryptographic-profile)
- [Deniability and information bounds](#deniability-and-information-bounds)
- [Error handling](#error-handling)
- [Integration example](#integration-example)
- [Security boundaries](#security-boundaries)
- [Project structure](#project-structure)
- [Development and verification](#development-and-verification)
- [Building and publishing](#building-and-publishing)
- [Release checklist](#release-checklist)

## 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:

~~~console
python -m pip install keyprism
~~~

From a source checkout:

~~~console
python -m pip install .
~~~

For development, testing, packaging, and documentation:

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

Keyprism requires Python 3.11 or newer.

## Quick start

~~~python
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

~~~text
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

~~~python
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

~~~python
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

~~~python
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

~~~python
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:

~~~text
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:

~~~text
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](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.

~~~python
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](examples/secure_notes_demo.py), a small
third-party-style application that imports only the public API.

~~~console
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](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](https://youtu.be/Kz6IlDCyOUY):

~~~text
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:

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

Run the same gates as CI:

~~~console
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

~~~console
python -m build
~~~

Expected artifacts:

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

### 2. Validate metadata and README rendering

~~~console
python -m twine check dist/*
~~~

### 3. Test the release on TestPyPI

~~~console
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

~~~console
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:

- [x] Runtime package uses the **src/keyprism** layout.
- [x] The public API is fully typed and includes **py.typed**.
- [x] Ruff, strict mypy, tests, coverage, docs, build, and Twine checks pass.
- [x] 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:

- [ ] Obtain independent review of [THREAT_MODEL.md](THREAT_MODEL.md) and
  [src/keyprism/crypto.py](src/keyprism/crypto.py).
- [ ] Address every security-review finding.
- [ ] Document format stability and compatibility commitments.

## Migration and compatibility

The original Tkinter prototype and its ciphertext/decoy format are not
compatible with Keyprism. See [MIGRATION.md](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](LICENSE).
