Pre-1.0 Notice: This library is under active development. The API may change between minor versions until 1.0.
TypeScript implementation of the AlgoChat protocol for encrypted messaging on Algorand.
pqsig by default. Import without a scheme still recovers Ed25519| Property | Status |
|---|---|
| Message content confidentiality | Protected (E2EE) |
| Message integrity | Protected (authenticated encryption) |
| Forward secrecy | Protected (ephemeral keys per message) |
| Replay attacks | Protected (blockchain uniqueness + PSK counter) |
| Quantum resistance (key exchange) | Optional (PSK mode provides defense-in-depth) |
| Quantum resistance (account identity) | Optional (Falcon-1024 pqsig on new accounts) |
| PSK session forward secrecy | Optional (100-message session boundaries in PSK mode) |
| Metadata privacy | Not protected (addresses, timing visible) |
| Traffic analysis | Not protected |
# npm
npm install @corvidlabs/ts-algochat
# bun
bun add @corvidlabs/ts-algochat
# pnpm
pnpm add @corvidlabs/ts-algochat
import {
AlgorandService,
createChatAccountFromMnemonic,
createRandomChatAccount,
} from '@corvidlabs/ts-algochat';
// Initialize service
const service = new AlgorandService({
algodToken: '',
algodServer: 'https://testnet-api.algonode.cloud',
indexerToken: '',
indexerServer: 'https://testnet-idx.algonode.cloud',
});
// Create a new Falcon-1024 account (default)
const { account, mnemonic } = createRandomChatAccount();
// Import a classical 25-word phrase (Ed25519 address, same as Pera / v1.1)
const imported = createChatAccountFromMnemonic('your 25 word mnemonic...');
// Recover a Falcon account generated from the same words
const falcon = createChatAccountFromMnemonic(mnemonic, { scheme: 'falcon-1024' });
// Discover recipient's encryption key
const recipientKey = await service.discoverPublicKey('RECIPIENT_ADDRESS');
// Send encrypted message
const result = await service.sendMessage(
account,
'RECIPIENT_ADDRESS',
recipientKey,
'Hello from AlgoChat!'
);
console.log('Transaction ID:', result.txid);
// Fetch messages
const messages = await service.fetchMessages(account, 'RECIPIENT_ADDRESS');
// Create from mnemonic (Ed25519 unless scheme is passed)
const account = createChatAccountFromMnemonic('word1 word2 ...');
// Generate a new Falcon-1024 account
const generated = createRandomChatAccount();
console.log('Address:', generated.account.address);
console.log('Scheme:', generated.account.scheme);
console.log('Mnemonic:', generated.mnemonic);
// Validate mnemonic
if (validateMnemonic('word1 word2 ...')) {
// Valid 25-word mnemonic
}
// Validate address
if (validateAddress('ALGO...')) {
// Valid Algorand address
}
// Simple message
await service.sendMessage(account, recipient, recipientKey, 'Hello!');
// Reply to a message
await service.sendReply(account, recipient, recipientKey, 'Reply text', {
txid: 'original-tx-id',
preview: 'Original message preview...',
});
// Get all messages with an address
const messages = await service.fetchMessages(account, 'ADDRESS');
// Get all conversations
const conversations = await service.fetchConversations(account);
// Discover public key
const pubKey = await service.discoverPublicKey('ADDRESS');
import {
deriveEncryptionKeys,
encryptMessage,
decryptMessage,
encodeEnvelope,
decodeEnvelope,
} from '@corvidlabs/ts-algochat';
// Derive keys from seed
const keys = deriveEncryptionKeys(seed);
// Encrypt message
const envelope = encryptMessage(
'Hello!',
senderPublicKey,
recipientPublicKey
);
// Encode for transmission
const bytes = encodeEnvelope(envelope);
// Decode received envelope
const decoded = decodeEnvelope(bytes);
// Decrypt message
const content = decryptMessage(decoded, myPrivateKey, myPublicKey);
interface ChatAccount {
address: string;
scheme: 'ed25519' | 'falcon-1024';
account?: algosdk.Account; // Ed25519 only
encryptionKeys: X25519KeyPair;
ed25519PublicKey: Uint8Array;
txnSigner: algosdk.TransactionSigner;
}
interface Message {
id: string;
sender: string;
recipient: string;
content: string;
timestamp: Date;
confirmedRound: number;
direction: 'sent' | 'received';
replyContext?: ReplyContext;
}
interface Conversation {
participant: string;
participantPublicKey?: Uint8Array;
messages: Message[];
lastMessage?: Message;
}
This library implements the AlgoChat Protocol v1.2 (account agility) and the PSK v1.1 extension. Envelope bytes for 0x01 / 0x02 are unchanged. Falcon identity does not make X25519 key exchange quantum-safe.
[version: 1][protocol: 1][sender_pubkey: 32][ephemeral_pubkey: 32][nonce: 12][encrypted_sender_key: 48][ciphertext: variable]
[version: 1][protocol: 2][ratchet_counter: 4][sender_pubkey: 32][ephemeral_pubkey: 32][nonce: 12][encrypted_sender_key: 48][ciphertext: variable]
| Function | Algorithm |
|---|---|
| Key Agreement | X25519 ECDH |
| Encryption | ChaCha20-Poly1305 |
| Key Derivation | HKDF-SHA256 |
The PSK (Pre-Shared Key) v1.1 protocol adds an additional layer of authentication and security on top of standard ECDH encryption by incorporating a pre-shared key into the key derivation process.
PSK mode provides defense against future quantum attacks through hybrid key derivation:
symmetricKey = HKDF(
ikm = ephemeralECDH || currentPSK,
salt = ephemeralPublicKey,
info = "algochat-psk-v1" || senderPubKey || recipientPubKey
)
The encryption key is derived from both the ephemeral ECDH shared secret and the ratcheted PSK, concatenated before HKDF. This means an attacker must break both layers:
This hybrid approach ensures that even if quantum computers eventually break X25519 ECDH, messages encrypted with PSK mode remain secure as long as the pre-shared key was exchanged securely.
PSK mode derives per-message keys using a two-level ratchet:
sessionPSK = HKDF(initialPSK, sessionIndex) where sessionIndex = counter / 100currentPSK = HKDF(sessionPSK, position) where position = counter % 100This creates 100-message session boundaries. Compromising a session PSK exposes at most 100 messages.
PSK exchange URIs are designed for QR code sharing:
algochat-psk://v1?addr=<algorand_address>&psk=<base64url>&label=<optional>
Use any QR library (e.g., qrcode) to encode the URI for easy scanning between devices.
import {
derivePSKAtCounter,
encryptPSKMessage,
decryptPSKMessage,
encodePSKEnvelope,
decodePSKEnvelope,
isPSKMessage,
createPSKState,
advanceSendCounter,
validateCounter,
recordReceive,
createPSKExchangeURI,
parsePSKExchangeURI,
} from '@corvidlabs/ts-algochat';
// Derive PSK for a specific counter
const psk = derivePSKAtCounter(sharedSecret, counter);
// Encrypt a PSK message
const envelope = encryptPSKMessage(
'Hello with PSK!',
senderPublicKey,
recipientPublicKey,
psk,
counter,
);
// Encode for transmission
const bytes = encodePSKEnvelope(envelope);
// Check if received data is a PSK message
if (isPSKMessage(bytes)) {
const decoded = decodePSKEnvelope(bytes);
const content = decryptPSKMessage(decoded, myPrivateKey, myPublicKey, psk);
}
// Counter state management
let state = createPSKState();
const { counter: sendCounter, state: newState } = advanceSendCounter(state);
state = newState;
// Exchange URI for out-of-band key sharing
const uri = createPSKExchangeURI('ALGO_ADDRESS', pskBytes, 'My Chat');
const parsed = parsePSKExchangeURI(uri);
bun test
This implementation is fully compatible with:
MIT License - See LICENSE for details.