Start preview

This commit is contained in:
filip
2026-03-12 19:30:43 +01:00
parent 3204bd6605
commit c73820b9ce
2 changed files with 395 additions and 0 deletions

231
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,231 @@
# 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
```kotlin
// 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
```kotlin
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
```kotlin
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
```kotlin
// 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
```kotlin
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
```