Metadata-Version: 2.4
Name: deny-sh
Version: 2.0.6
Summary: Deniable encryption — AES-256 with plausible deniability via control files
Author-email: "deny.sh" <hello@deny.sh>
License-Expression: Apache-2.0
Project-URL: Homepage, https://deny.sh
Project-URL: Documentation, https://deny.sh/docs
Project-URL: Repository, https://github.com/deny-sh-crypto/deny-python
Project-URL: Issues, https://github.com/deny-sh-crypto/deny-python/issues
Keywords: encryption,deniable,aes,cryptography,plausible-deniability,argon2
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
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.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pycryptodome>=3.15
Requires-Dist: argon2-cffi>=23.1.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Dynamic: license-file

# deny-sh: Python SDK

Deniable encryption for Python. Part of the **Encrypt pillar** of [deny.sh](https://deny.sh), the deniability infrastructure. Apache 2.0, zero copyleft, free for any use.

Same algorithm as the [TypeScript reference implementation](https://www.npmjs.com/package/deny-sh), the [Rust SDK](https://crates.io/crates/deny-sh), and the [Go SDK](https://pkg.go.dev/github.com/deny-sh-crypto/deny-go/v2). Ciphertext is byte-for-byte compatible across all four languages.

## Install

```bash
pip install deny-sh
```

## Quick Start

```python
from deny_sh import encrypt, decrypt, generate_deniable_control

# Encrypt a secret message
ciphertext, control = encrypt(b"seed phrase: abandon ability ...", "pw1", "pw2")

# Decrypt with the real control data
message = decrypt(ciphertext, "pw1", "pw2", control)
# b"seed phrase: abandon ability ..."

# Generate deniable control data; same ciphertext decrypts to a decoy
fake_control = generate_deniable_control(ciphertext, "pw1", "pw2", b"nothing here")
decoy = decrypt(ciphertext, "pw1", "pw2", fake_control)
# b"nothing here"
```

### Multiple decoys from one ciphertext

There is no per-ciphertext cap on the number of decoys. Derive a fresh control file for each cover story:

```python
stories = [
    b"meeting moved to wednesday",
    b"taxi receipts october 2026",
    b"vegetable risotto recipe",
]

for story in stories:
    control = generate_deniable_control(ciphertext, "pw1", "pw2", story)
    recovered = decrypt(ciphertext, "pw1", "pw2", control)
    assert recovered == story
```

The practical upper bound on plaintext length per decoy is the inner-payload envelope (ciphertext length minus the 48-byte header, minus the 4-byte length prefix). Each decoy can be shorter, never longer.

## How It Works

1. **Dual-password key derivation**: SHA-256(pw1) || SHA-256(pw2) → Argon2id → AES-256 key
2. **XOR deniability layer**: plaintext is XOR'd with control data before encryption
3. **AES-256-CTR encryption**: intentionally unauthenticated (an AEAD tag would defeat the deniability property)
4. **Length prefix inside encrypted zone**: no metadata leaks the real message size

The control data is what makes it deniable. Different control data + same ciphertext + same passwords = different plaintext. Both the real and fake control data look like random bytes; there's no way to tell which is "real".

## API

### `encrypt(plaintext, password1, password2, control_data=None)`

Encrypt plaintext with dual passwords and optional control data.

- **plaintext** (`bytes`): data to encrypt
- **password1** (`str`): first password
- **password2** (`str`): second password
- **control_data** (`bytes | None`): control file data; auto-generated if `None`

Returns `(ciphertext: bytes, control_data: bytes)`.

### `decrypt(ciphertext, password1, password2, control_data)`

Decrypt ciphertext with dual passwords and control data.

Returns `bytes`: the decrypted plaintext.

### `generate_deniable_control(ciphertext, password1, password2, desired_plaintext)`

Generate new control data that makes existing ciphertext decrypt to a different message.

Returns `bytes`: new control data.

### `generate_control_data(size)`

Generate cryptographically secure random control data.

Returns `bytes`.

### `derive_key(password1, password2, salt)`

Derive the AES-256 key from two passwords and a salt. Exposed for cross-implementation testing.

Returns `bytes` (32 bytes).

## Algorithm Compatibility

The ciphertext format is identical across all deny-sh SDKs:

```
salt (32 bytes) | iv (16 bytes) | AES-256-CTR(key, payload XOR control_data)
```

Where `payload = length_prefix (4 bytes LE) + plaintext`.

KDF parameters: Argon2id v0x13 (t=3, m=65536 KiB, p=1, hash_len=32).

A file encrypted with the Python SDK can be decrypted with the TypeScript, Go, or Rust SDK, and vice versa. Full wire format and KAT vectors: [deny.sh/sdks](https://deny.sh/sdks).

## Threat model

deny.sh defends against **passive ciphertext leak**: an adversary gets the encrypted artefact (lost laptop, cloud breach, prompt-injected agent) and tries to read it. The construction guarantees that whatever the adversary decrypts is indistinguishable from any other decryption.

It is **not** designed to resist an adaptive adversary who can compel you to perform multiple decryptions, demand additional passwords iteratively, or run forensic side-channel analysis on the host hardware. Full threat model: [deny.sh/threat-model](https://deny.sh/threat-model). Cryptographic argument: [deny.sh/whitepaper](https://deny.sh/whitepaper) §5.

The primitive is intentionally unauthenticated. Wrong passwords return garbage, not an error. If you need decryption to fail loudly on wrong inputs, add a caller-side integrity check (magic bytes + SHA-256 fingerprint) on the plaintext.

## Dependencies

- **pycryptodome**: AES-256-CTR encryption
- **argon2-cffi**: Argon2id KDF
- **hashlib** (stdlib): SHA-256 password hashing before KDF input
- **os** (stdlib): `urandom` for secure random bytes

## License

Apache License 2.0. See [LICENSE](LICENSE). Free for commercial and proprietary use. See [deny.sh/licensing](https://deny.sh/licensing).

## Reporting vulnerabilities

Found a bug in the crypto or the SDK? Email security@deny.sh (PGP fingerprint and disclosure policy at [deny.sh/disclosure](https://deny.sh/disclosure)). Please give us a reasonable window before public disclosure.
