Files
Kecalek/ARCHITECTURE.md
2026-03-12 19:30:43 +01:00

9.4 KiB
Raw Blame History

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 64B64KB
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 constructor classes: auto-wired by Hilt (no @Provides needed)
    • ChatClient, ServerApi, ConnectionManager, SessionManager, KeyStorage, NotificationRouter, MessageRepository, ConversationRepository
  • AppModule: provides AppDatabase (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