Pre-1.0 Notice: This library is under active development. The API may change between minor versions until 1.0.
Python implementation of the AlgoChat protocol for encrypted messaging on Algorand.
pip install py-algochatfrom algochat import derive_keys_from_seed, encrypt_message, decrypt_message
from algochat import encode_envelope, decode_envelope
# Derive keys from a 32-byte seed (e.g., from Algorand account)
sender_private, sender_public = derive_keys_from_seed(seed_bytes)
recipient_private, recipient_public = derive_keys_from_seed(recipient_seed)
# Encrypt a message
envelope = encrypt_message(
"Hello, World!",
sender_private,
sender_public,
recipient_public,
)
# Encode for transmission
encoded = encode_envelope(envelope)
# Decode received message
decoded = decode_envelope(encoded)
# Decrypt as recipient
result = decrypt_message(decoded, recipient_private, recipient_public)
print(result.text) # "Hello, World!"AlgoChat uses:
- X25519 for key agreement
- ChaCha20-Poly1305 for authenticated encryption
- HKDF-SHA256 for key derivation
The protocol supports bidirectional decryption, allowing senders to decrypt their own messages.
The PSK (Pre-Shared Key) v1.1 protocol adds a shared secret layer on top of the standard X25519 key agreement, providing forward secrecy through a two-level ratchet system.
from algochat import (
derive_keys_from_seed,
derive_psk_at_counter,
encrypt_psk_message,
decrypt_psk_message,
encode_psk_envelope,
decode_psk_envelope,
is_psk_message,
PSKState,
advance_send_counter,
validate_counter,
record_receive,
create_psk_exchange_uri,
parse_psk_exchange_uri,
)
# Derive keys
sender_private, sender_public = derive_keys_from_seed(sender_seed)
recipient_private, recipient_public = derive_keys_from_seed(recipient_seed)
# Shared PSK (exchanged out-of-band)
initial_psk = os.urandom(32)
# Manage counter state
state = PSKState()
counter, state = advance_send_counter(state)
# Derive ratcheted PSK for this counter
current_psk = derive_psk_at_counter(initial_psk, counter)
# Encrypt with PSK
envelope = encrypt_psk_message(
"Hello with PSK!",
sender_private,
sender_public,
recipient_public,
current_psk,
counter,
)
# Encode for transmission
encoded = encode_psk_envelope(envelope)
assert is_psk_message(encoded)
# Decode and decrypt
decoded = decode_psk_envelope(encoded)
psk_for_counter = derive_psk_at_counter(initial_psk, decoded.ratchet_counter)
text = decrypt_psk_message(decoded, recipient_private, recipient_public, psk_for_counter)
# Exchange PSK via URI
uri = create_psk_exchange_uri(address, initial_psk, label="My Chat")
parsed = parse_psk_exchange_uri(uri)The ratchet derives unique keys per message using a two-level hierarchy:
- Session PSK: Derived from initial PSK + session index (every 100 messages)
- Position PSK: Derived from session PSK + position within session
Counter state tracks send/receive counters with replay protection:
- Rejects duplicate counters (replay attacks)
- Maintains a sliding window (200 messages) for out-of-order delivery
- Prunes old counters automatically
This implementation is fully compatible with:
- swift-algochat (Swift)
- ts-algochat (TypeScript)
- rs-algochat (Rust)
- kt-algochat (Kotlin)
MIT