Metadata-Version: 2.4
Name: soliton-py
Version: 0.1.3
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
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: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Rust
Classifier: Topic :: Security :: Cryptography
Classifier: Typing :: Typed
Requires-Dist: pytest>=8.0 ; extra == 'test'
Provides-Extra: test
Summary: Python bindings for libsoliton — post-quantum cryptographic library
Keywords: post-quantum,cryptography,x-wing,double-ratchet,e2ee
Home-Page: https://git.lo.sh/lo/libsoliton/wiki
Author: LO Contributors
License: AGPL-3.0-only
Requires-Python: >=3.10
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Documentation, https://docs.rs/libsoliton
Project-URL: Repository, https://git.lo.sh/lo/libsoliton

# soliton

Python bindings for [libsoliton](https://git.lo.sh/lo/libsoliton) — a pure-Rust post-quantum cryptographic library providing composite identity keys (X-Wing + ML-DSA-65), hybrid signatures, KEM-based authentication, asynchronous key exchange, double-ratchet message encryption, streaming AEAD, and encrypted storage.

## Install

```bash
pip install soliton-py
```

Builds from source via [maturin](https://www.maturin.rs/) — requires a Rust toolchain.

## Quick Start

### Identity Keys

```python
import soliton

# Generate a post-quantum identity keypair.
with soliton.Identity.generate() as alice:
    # Sign a message (Ed25519 + ML-DSA-65 hybrid).
    sig = alice.sign(b"hello")
    alice.verify(b"hello", sig)

    # Fingerprint (SHA3-256 of public key).
    print(alice.fingerprint_hex())

    # Persist keys.
    pk = alice.public_key()   # 3200 bytes
    sk = alice.secret_key()   # 2496 bytes
# Secret key is zeroized on context exit.
```

### Key Exchange (KEX)

```python
# Bob generates a signed pre-key.
spk_pub, spk_sk = soliton.xwing_keygen()
spk_sig = soliton.kex_sign_prekey(bob_sk, spk_pub)

# Alice initiates a session.
initiated = soliton.kex_initiate(
    alice_pk, alice_sk, bob_pk,
    spk_pub, spk_id=1, spk_sig=spk_sig,
    crypto_version="lo-crypto-v1",
)

# Bob receives the session.
received = soliton.kex_receive(
    bob_pk, bob_sk, alice_pk,
    initiated.session_init_encoded(),
    initiated.sender_sig(), spk_sk,
)
```

### Ratchet (Ongoing Messaging)

```python
# First message (pre-ratchet).
aad = soliton.kex_build_first_message_aad(
    initiated.sender_fingerprint(),
    initiated.recipient_fingerprint(),
    initiated.session_init_encoded(),
)
ct, rik_a = soliton.Ratchet.encrypt_first_message(
    initiated.take_initial_chain_key(), b"hello bob", aad,
)
pt, rik_b = soliton.Ratchet.decrypt_first_message(
    received.take_initial_chain_key(), ct, aad,
)

# Initialize ratchets.
with soliton.Ratchet.init_alice(
    initiated.take_root_key(), rik_a,
    alice_fp, bob_fp, initiated.ek_pk(), initiated.ek_sk(),
) as alice_r:
    header, ciphertext = alice_r.encrypt(b"message 1")

    # Serialize for persistence.
    blob, epoch = alice_r.to_bytes()  # consumes the ratchet
```

### Encrypted Storage

```python
with soliton.StorageKeyRing(version=1, key=key_bytes) as ring:
    blob = ring.encrypt_blob("channel-1", "segment-0", plaintext)
    data = ring.decrypt_blob("channel-1", "segment-0", blob)

    # Key rotation.
    ring.add_key(version=2, key=new_key, make_active=True)
```

### Streaming AEAD (File Encryption)

```python
with soliton.StreamEncryptor(key) as enc:
    header = enc.header()  # 26 bytes — send first
    ct1 = enc.encrypt_chunk(chunk1)            # non-final: must be 1 MiB
    ct2 = enc.encrypt_chunk(chunk2, is_last=True)  # final: any size

with soliton.StreamDecryptor(key, header) as dec:
    pt1, is_last = dec.decrypt_chunk(ct1)
    pt2, is_last = dec.decrypt_chunk(ct2)
```

### Authentication (Zero-Knowledge)

```python
# Server generates challenge.
ct, token = soliton.auth_challenge(client_pk)

# Client responds.
proof = soliton.auth_respond(client_sk, ct)

# Server verifies (constant-time).
assert soliton.auth_verify(token, proof)
```

### Primitives

```python
digest = soliton.sha3_256(b"data")                      # 32 bytes
tag = soliton.hmac_sha3_256(key, data)                   # 32 bytes
okm = soliton.hkdf_sha3_256(salt, ikm, info, length=64)  # variable
ok = soliton.hmac_sha3_256_verify(tag1, tag2)             # constant-time
phrase = soliton.verification_phrase(pk_a, pk_b)           # 6 EFF words
```

## Error Handling

All errors are subclasses of `soliton.SolitonError`:

| Exception | Meaning |
|-----------|---------|
| `AeadError` | AEAD decryption failed (wrong key, tampered ciphertext) |
| `VerificationError` | Signature verification failed |
| `BundleVerificationError` | Pre-key bundle invalid |
| `DuplicateMessageError` | Replayed message counter |
| `ChainExhaustedError` | Counter-space exhausted — re-establish session |
| `InvalidLengthError` | Wrong-size parameter |
| `InvalidDataError` | Malformed input |

## Context Managers

All types holding secret material support `with` statements for automatic zeroization:

```python
with soliton.Identity.generate() as id:
    ...  # secret key zeroized on exit

with soliton.Ratchet.init_alice(...) as r:
    ...  # ratchet state reset on exit

with soliton.StorageKeyRing(1, key) as ring:
    ...  # key material zeroized on exit
```

## Documentation

- [Specification.md](https://git.lo.sh/lo/libsoliton/src/branch/master/Specification.md) — full cryptographic specification
- [CHEATSHEET.md](https://git.lo.sh/lo/libsoliton/src/branch/master/CHEATSHEET.md) — API quick reference
- [Abstract.md](https://git.lo.sh/lo/libsoliton/src/branch/master/Abstract.md) — formal security model

## License

[AGPL-3.0-only](LICENSE.md)

