9.4 KiB
9.4 KiB
Kecalek Android — Architecture Reference
Project
- Package:
com.kecalek.chat - Root:
E:\encrypted_chat_vyvoj\android\ - Server:
chat.ai-tech.news:9999(TCP, newline-delimited JSON) - iOS ref:
E:\encrypted_chat_vyvoj\ios\Kecalek\Kecalek\ - Python ref:
E:\encrypted_chat_vyvoj\python\encrypted_chat\
Tech Stack
- Kotlin, Jetpack Compose + Material 3, Catppuccin Mocha dark theme
- Crypto: Tink + Bouncy Castle (Ed25519, X25519, AES-256-GCM, HKDF, RSA-PSS)
- DB: Room + SQLCipher (
kecalek_chat.db), DI: Hilt, Network: Raw TCP - Coroutines + Flow throughout
Critical Constants
// Constants.kt
SELF_DEVICE_ID = "00000000-0000-0000-0000-000000000000" // self-copy sentinel
DEFAULT_HOST = "chat.ai-tech.news"
DEFAULT_PORT = 9999
Protocol Field Names (MUST match server exactly)
| Field | Correct | Wrong |
|---|---|---|
| X3DH identity key | "ik" |
"ik_pub" |
| X3DH ephemeral key | "ek" |
"ek_pub" |
| Push sender field | "sender_id" |
"sender_user_id" |
| Ratchet header | {dh_pub: hex, n: int, pn: int} |
— |
| Self-copy ratchet | {"self": true} |
— |
| Multi-device push | "device_entries" array |
— |
Encryption Protocol
X3DH (new session):
4 DH ops, HKDF salt=0x00*32, info="EncryptedChat_X3DH"
Header sent: {"ik": b64(idPub), "ek": b64(ephPub), "opk_id": id?}
Double Ratchet (per-message):
Chain KDF: msg_key=HMAC(ck, 0x01), next_ck=HMAC(ck, 0x02)
encrypt() -> RatchetMessage(header, ciphertext, nonce)
header.toMap() -> {"dh_pub": hex, "n": int, "pn": int}
AES-256-GCM: 12-byte nonce, 16-byte tag
MessagePadding: pad/unpad with 64B..64KB buckets
Self-copy: HKDF(identityPrivate, "self_encryption") -> static AES key
SELF_DEVICE_ID = "00000000-0000-0000-0000-000000000000"
Plaintext payload JSON:
{"sender": "username", "text": "hello", "reply_to": null, "timestamp": "2026-...Z"}
Control message: {"_sender_key": {"conv_id": "...", "key": b64}} → import silently
Key Files — ALL DONE
| File | Purpose |
|---|---|
crypto/AesGcm.kt |
AES-256-GCM encrypt/decrypt |
crypto/Hkdf.kt |
HKDF key derivation |
crypto/ECP1.kt / KeyEncryption.kt |
Password-based key encryption (magic+salt+nonce+ct) |
crypto/Ed25519Crypto.kt |
Ed25519 sign/verify, serialize/deserialize |
crypto/X25519Crypto.kt |
X25519 DH, Ed25519→X25519 conversion |
crypto/RSACrypto.kt |
RSA-PSS for server login signature |
crypto/MessagePadding.kt |
Bucketed padding 64B–64KB |
crypto/X3DH.kt |
X3DH initiate (client) + respond (server) |
crypto/DoubleRatchet.kt |
Double Ratchet encrypt/decrypt/import/export |
crypto/SenderKey.kt |
Group sender key: AAD = chain_id(32B)+n(4B big-endian) |
crypto/ContactVerification.kt |
Safety numbers: SHA-512 × 5200, 60 digits (12×5) |
network/ConnectionManager.kt |
Raw TCP connection, newline-delimited JSON |
network/ProtocolHandler.kt |
Request/response + push notification dispatch |
network/ServerApi.kt |
50 API endpoints |
core/SessionManager.kt |
Login/register, session persistence |
core/KeyStorage.kt |
Encrypted key persistence (SharedPreferences + ECP1) |
core/ChatClient.kt |
Main messaging engine |
core/NotificationRouter.kt |
Routes 18 push notification types |
data/repository/MessageRepository.kt |
Message CRUD + search + reactions |
data/repository/ConversationRepository.kt |
Conversation CRUD |
ui/chat/ChatViewModel.kt |
Chat screen ViewModel |
ui/conversations/ConversationListViewModel.kt |
Conversation list ViewModel |
ui/auth/AuthViewModel.kt |
Auth ViewModel |
di/AppModule.kt |
AppDatabase singleton (SQLCipher) |
di/DatabaseModule.kt |
DAO providers |
di/NetworkModule.kt |
Placeholder (net classes auto-wired) |
di/CryptoModule.kt |
Placeholder (crypto = Kotlin objects) |
DI Architecture
- All
@Singleton @Inject constructorclasses: auto-wired by Hilt (no @Provides needed)ChatClient,ServerApi,ConnectionManager,SessionManager,KeyStorage,NotificationRouter,MessageRepository,ConversationRepository
AppModule: providesAppDatabase(SQLCipher with passphrase from KeyStorage)DatabaseModule: provides DAOs (MessageDao,ConversationDao,UserCacheDao)
ChatClient — Core Flow
initialize(password):
ECP1.decrypt(encryptedIdentityKey, password) -> identityPriv/Pub
Ed25519→X25519 conversion for DH
selfEncryptionKey = HKDF(identityPriv bytes, "self_encryption")
ensurePrekeys() → upload signed prekey + one-time prekeys if low
setupNotificationHandlers() → register all push handlers
sendDm(conversationId, plaintext, memberUserIds):
padded = MessagePadding.pad(plaintext)
for each memberId != self:
bundles = getDeviceBundles(memberId) // api.getKeyBundle()
for each bundle:
loadOrCreateSessionWithHeader(memberId, bundle)
→ if no session: X3DH.initiate() + build x3dhHeader {ik, ek, opk_id?}
→ if existing: load ratchet from keyStorage
ratchetMsg = ratchet.encrypt(padded)
save session
build recipient entry {user_id, device_id, encrypted_content, nonce,
ratchet_header, x3dh_header?}
self-copy: encryptSelf(padded) + SELF_DEVICE_ID + {ratchet_header: {self: true}}
api.sendMessage(conversationId, topLevelHeader, recipients=[...])
handleNewMessage(data): // triggered by new_message push
pick entry from device_entries matching myDeviceId or SELF_DEVICE_ID
if ratchet_header.self == true: decryptSelf()
else: decryptDm(senderId, senderDeviceId, ct, nonce, ratchetHeader, x3dhHeader?)
parse JSON payload
if _sender_key: importSenderKey(), return
emit DecryptedMessage on newMessageFlow
ChatViewModel — Core Flow
init:
loadConversationInfo() → api.listConversations() → find conv → set members
loadMessages() → api.getMessages() → for each: decryptServerMessage() → Room insert
collect chatClient.newMessageFlow → insert to Room → UI updates via Room Flow
collect messageRepository.getMessagesFlow() → _uiState.messages
sendMessage(text):
Build JSON {sender, text, reply_to, timestamp}
chatClient.sendDm(conversationId, payload, memberUserIds)
messageRepository.insertMessage(sent msg) → immediate local display
decryptServerMessage(msgObj):
Handle device_entries OR flat fields (same as handleNewMessage logic)
if self-copy: decryptSelf(); else: decryptDm()
Parse JSON payload → Message domain object
Data Classes
Session(userId, username, email, deviceId, serverVersion)
DecryptedMessage(messageId, conversationId, senderId, senderUsername, text?, replyTo?, timestamp)
Message(id, conversationId, senderId, senderUsername, createdAt, text?, replyTo?,
imageFileId?, file?, image?, isDeleted, readBy, reactions, forwardedFrom?, pinnedAt?, pinnedBy?)
MessageReaction(userId, reaction, createdAt)
ConversationMember(userId, username, email)
MessageRepository Methods
getMessagesFlow(conversationId): Flow<List<Message>> // reactive Room
getMessages(conversationId): List<Message> // suspend
getMessage(messageId): Message? // suspend
getPinnedMessages(conversationId): List<Message> // suspend
searchMessages(conversationId, query): List<Message> // suspend, full-text
insertMessage(message) // suspend
insertMessages(messages) // suspend
markDeleted(messageId) // suspend, soft-delete
updateReactions(messageId, reactions: List<MessageReaction>)
updatePinStatus(messageId, pinnedAt: Date?, pinnedBy: String?)
updateReadBy(messageId, readBy: Set<String>)
deleteByConversation(conversationId)
ServerApi Key Endpoints
// Messaging
sendMessage(conversationId, ratchetHeader, recipients, x3dhHeader?, imageFileId?)
getMessages(conversationId, limit=50, offset=0, afterTs?)
markRead(conversationId, messageIds)
markConversationRead(conversationId)
deleteMessage(messageId)
reactMessage(messageId, reaction, action="add"/"remove")
pinMessage(messageId, conversationId, action="pin"/"unpin")
getPinnedMessages(conversationId)
// Keys
getKeyBundle(userId) // returns device bundles for user
uploadPrekeys(signedPrekey, oneTimePrekeys?)
getPrekeyCount()
// Conversations
listConversations()
createConversation(members, name?)
findConversation(email)
deleteConversation(conversationId)
addMember(conversationId, email)
removeMember(conversationId, userId)
leaveGroup(conversationId)
// Invitations
listInvitations()
acceptInvitation(conversationId)
declineInvitation(conversationId)
// Files
uploadImageStart(conversationId, fileId, fileSize, fileType)
uploadImageChunk(fileId, dataBase64)
uploadImageEnd(fileId)
downloadImage(fileId, offset=0)
// Session
sessionReset(peerUserId, peerDeviceId?)
NotificationRouter — 18 Push Types
NEW_MESSAGE, MESSAGES_READ, MESSAGE_DELETED, MESSAGE_DELIVERED,
CONVERSATION_CREATED, MEMBER_ADDED, MEMBER_REMOVED, GROUP_INVITATION,
CONVERSATION_RENAMED, SESSION_RESET, MESSAGE_REACTED, MESSAGE_PINNED,
MESSAGE_UNPINNED, USER_ONLINE, USER_OFFLINE, ONLINE_USERS,
USERNAME_CHANGED, PROTOCOL_ERROR
ECP1 Key Format
magic(4B "ECP1") + salt(16B) + nonce(12B) + ciphertext+tag
PBKDF2: 600k iterations, SHA-256, AAD = magic bytes
Ed25519 → X25519 Conversion
X25519 private: SHA-512(ed25519_seed)[0:32] clamped (RFC 7748)
X25519 public: Montgomery u = (1+y)/(1-y) mod p