Pre-1.0 Notice: This library is under active development. The API may change between minor versions until 1.0.
Kotlin implementation of the AlgoChat protocol for encrypted messaging on Algorand.
kotlin dependencies { implementation("com.corvidlabs:algochat:0.2.0") }
groovy dependencies { implementation 'com.corvidlabs:algochat:0.2.0' }
kotlin import com.corvidlabs.algochat.*
// Configure for LocalNet (development) val config = AlgoChatConfig.localnet()
// Create client from account seed val seed = "YOUR_32_BYTE_SEED_HERE".encodeToByteArray() val address = "YOUR_ALGORAND_ADDRESS"
val algod = AlgodClient("http://localhost:4001", "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") val indexer = IndexerClient("http://localhost:8980") val keyStorage = InMemoryKeyStorage() val messageCache = InMemoryMessageCache()
val client = AlgoChatClient.fromSeed( seed = seed, address = address, config = config, algod = algod, indexer = indexer, keyStorage = keyStorage, messageCache = messageCache )
// Access your encryption public key (for sharing with contacts) val myPublicKey = client.encryptionPublicKey
kotlin // LocalNet (development) val config = AlgoChatConfig.localnet()
// TestNet (testing with real testnet ALGO) val config = AlgoChatConfig.testnet()
// MainNet (production) val config = AlgoChatConfig.mainnet()
kotlin import com.corvidlabs.algochat.*
// Derive keys from a 32-byte seed (e.g., from Algorand account) val senderKeys = Keys.deriveKeysFromSeed(seed) val recipientKeys = Keys.deriveKeysFromSeed(recipientSeed)
// Encrypt a message val envelope = Crypto.encryptMessage( "Hello, World!", senderKeys.privateKey, senderKeys.publicKey, recipientKeys.publicKey )
// Encode for transmission val encoded = envelope.encode()
kotlin // Decode received message val decoded = ChatEnvelope.decode(encoded)
// Decrypt as recipient val result = Crypto.decryptMessage(decoded, recipientKeys.privateKey, recipientKeys.publicKey) result?.let { println(it.text) }
Discover a recipient's encryption public key from the blockchain:
kotlin // Discover key for an address (checks cache first, then indexer) val discoveredKey = client.discoverKey("RECIPIENT_ADDRESS")
if (discoveredKey != null) {
println("Found public key:
// Now you can encrypt messages to this address
val encrypted = client.encrypt("Hello!", discoveredKey.publicKey)
} else { println("No encryption key found for this address") }
Reply to a specific message with thread context:
kotlin import com.corvidlabs.algochat.*
val senderKeys = Keys.deriveKeysFromSeed(seed) val recipientKeys = Keys.deriveKeysFromSeed(recipientSeed)
// Encrypt a reply to a specific transaction val replyEnvelope = Crypto.encryptReply( text = "Yes, I agree with that point!", replyToTxid = "ORIGINAL_TX_ID_HERE", replyToPreview = "Original message preview...", senderPrivateKey = senderKeys.privateKey, senderPublicKey = senderKeys.publicKey, recipientPublicKey = recipientKeys.publicKey )
val encoded = replyEnvelope.encode()
kotlin import com.corvidlabs.algochat.*
suspend fun chatExample() { // Initialize client val config = AlgoChatConfig.localnet() val client = AlgoChatClient.fromSeed(seed, address, config, algod, indexer)
// Discover recipient's key
val recipientAddress = "RECIPIENT_ALGORAND_ADDRESS"
val discoveredKey = client.discoverKey(recipientAddress)
?: throw IllegalStateException("Key not found")
// Encrypt and send a message
val encryptedMessage = client.encrypt("Hello!", discoveredKey.publicKey)
// Send encryptedMessage via Algorand transaction...
// Decrypt a received message
val receivedEnvelope: ByteArray = // ... received from network
val decryptedText = client.decrypt(receivedEnvelope, discoveredKey.publicKey)
println("Decrypted: $decryptedText")
// Decrypt with full reply context
val fullContent = client.decryptFull(receivedEnvelope)
println("Text: ${fullContent.text}")
fullContent.replyToId?.let { txid ->
println("This is a reply to transaction: $txid")
}
// Sync messages from blockchain
val newMessages = client.sync()
for (message in newMessages) {
println("${message.direction}: ${message.content} at ${message.timestamp}")
}
// Access conversation history
val conversation = client.conversation(recipientAddress)
val messages = conversation.messages()
}
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) protocol extends AlgoChat with an additional symmetric key layer:
- Two-level ratchet - Session and position keys derived from an initial PSK via HKDF
- Hybrid encryption - Combines X25519 ECDH with PSK for dual-layer security
- Forward secrecy - Each message uses a unique derived key from the ratchet counter
- Replay protection - Sliding counter window prevents message replay attacks
kotlin import com.corvidlabs.algochat.*
// Create a shared PSK (exchanged out-of-band) val psk = ByteArray(32) // 32 random bytes shared between peers
// Derive the ratcheted PSK for a specific counter val counter = 0u val currentPSK = PSKRatchet.derivePSKAtCounter(psk, counter)
// Encrypt with PSK val envelope = PSKCrypto.encryptMessage( "Hello with PSK!", senderKeys.privateKey, senderKeys.publicKey, recipientKeys.publicKey, currentPSK, counter )
// Encode for transmission val encoded = PSKEnvelopeCodec.encode(envelope)
// Decode and decrypt val decoded = PSKEnvelopeCodec.decode(encoded) val result = PSKCrypto.decryptMessage(decoded, recipientKeys.privateKey, recipientKeys.publicKey, currentPSK) result?.let { println(it.text) }
// Exchange PSKs via URI val uri = PSKExchangeURI(address = "ALGO_ADDRESS", psk = psk, label = "My Chat") val uriString = uri.encode() // algochat-psk://v1?addr=...&psk=...&label=... val parsed = PSKExchangeURI.decode(uriString)
PSK envelope wire format (130-byte header):
[0] version (0x01) [1] protocolId (0x02) [2..5] ratchetCounter (4 bytes, big-endian) [6..37] senderPublicKey (32 bytes) [38..69] ephemeralPublicKey (32 bytes) [70..81] nonce (12 bytes) [82..129] encryptedSenderKey (48 bytes) [130..] ciphertext + 16-byte tag
| Class | Description |
|---|---|
AlgoChatClient |
High-level client for encrypted messaging |
AlgoChatConfig |
Configuration (network, caching, key discovery) |
Keys |
Key derivation and X25519 operations |
Crypto |
Message encryption/decryption |
PSKCrypto |
PSK-encrypted messaging |
ChatEnvelope |
Message envelope structure |
DecryptedContent |
Decryption result with optional reply context |
| Type | Description |
|---|---|
KeyPair |
X25519 key pair (private + public) |
DiscoveredKey |
Discovered public key with verification status |
Message |
Chat message with metadata |
Conversation |
Conversation thread with a participant |
ReplyContext |
Reply-to message reference |
| Exception | When thrown |
|---|---|
AlgoChatException.EncryptionFailed |
Message too large or encryption error |
AlgoChatException.DecryptionFailed |
Invalid envelope or decryption error |
AlgoChatException.KeyDerivationFailed |
Invalid seed length |
AlgoChatException.PublicKeyNotFound |
Recipient key not found on chain |
AlgoChatException.InvalidEnvelope |
Bytes are not a valid AlgoChat message |
This implementation is fully compatible with:
- swift-algochat (Swift)
- ts-algochat (TypeScript)
- py-algochat (Python)
- rs-algochat (Rust)
MIT