Deniable encryption — AES-256 with plausible deniability via control files
Project description
deny-sh — Python SDK
Deniable encryption for Python. Same algorithm as the TypeScript reference implementation. Ciphertext is byte-for-byte compatible across SDKs.
Install
pip install deny-sh
Quick Start
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"
How It Works
- Dual-password key derivation — SHA-256(pw1) || SHA-256(pw2) → scrypt → AES-256 key
- XOR deniability layer — plaintext is XOR'd with control data before encryption
- AES-256-CTR encryption — standard authenticated encryption
- 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 ifNone
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: scrypt(N=16384, r=8, p=1, dklen=32).
A file encrypted with the Python SDK can be decrypted with the TypeScript SDK, and vice versa.
Dependencies
- pycryptodome — AES-256-CTR encryption
- hashlib (stdlib) — SHA-256 and scrypt KDF
- os (stdlib) —
urandomfor secure random bytes
License
AGPL-3.0. Commercial licenses available.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file deny_sh-1.0.0.tar.gz.
File metadata
- Download URL: deny_sh-1.0.0.tar.gz
- Upload date:
- Size: 20.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2818c3b2c822fba66f97ee4b08f6b9dc418dc10e9f9b3b36a83eeedc890f9a73
|
|
| MD5 |
4203af87f42641b1b98630ed41c2a6d5
|
|
| BLAKE2b-256 |
6b3c8cb98a1967c63f560571f1e96699e9919eb5986676381973d1433da943ad
|
Provenance
The following attestation bundles were made for deny_sh-1.0.0.tar.gz:
Publisher:
publish.yml on deny-sh-crypto/deny-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
deny_sh-1.0.0.tar.gz -
Subject digest:
2818c3b2c822fba66f97ee4b08f6b9dc418dc10e9f9b3b36a83eeedc890f9a73 - Sigstore transparency entry: 1389076315
- Sigstore integration time:
-
Permalink:
deny-sh-crypto/deny-python@8bdc4fe02daf7962b15bec06e94d0efa67bbb073 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/deny-sh-crypto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8bdc4fe02daf7962b15bec06e94d0efa67bbb073 -
Trigger Event:
push
-
Statement type:
File details
Details for the file deny_sh-1.0.0-py3-none-any.whl.
File metadata
- Download URL: deny_sh-1.0.0-py3-none-any.whl
- Upload date:
- Size: 17.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
61f18fa273a1d64894747aba2475737204d43254795d5aa30150962c6ce01905
|
|
| MD5 |
2762f8c4ac20ad36ad2adf7931e6310b
|
|
| BLAKE2b-256 |
4413b0e25ed51a59d48c390676997abb3a03f2e23f57dc6984b43fe133e7e052
|
Provenance
The following attestation bundles were made for deny_sh-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on deny-sh-crypto/deny-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
deny_sh-1.0.0-py3-none-any.whl -
Subject digest:
61f18fa273a1d64894747aba2475737204d43254795d5aa30150962c6ce01905 - Sigstore transparency entry: 1389076422
- Sigstore integration time:
-
Permalink:
deny-sh-crypto/deny-python@8bdc4fe02daf7962b15bec06e94d0efa67bbb073 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/deny-sh-crypto
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8bdc4fe02daf7962b15bec06e94d0efa67bbb073 -
Trigger Event:
push
-
Statement type: