Start preview
This commit is contained in:
231
ARCHITECTURE.md
Normal file
231
ARCHITECTURE.md
Normal 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 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 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
|
||||||
|
```
|
||||||
164
TODO.md
Normal file
164
TODO.md
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
# Kecalek Android — TODO & Implementation Status
|
||||||
|
|
||||||
|
## ✅ DONE — Infrastructure (Phase 0-3)
|
||||||
|
|
||||||
|
### Phase 0 — Gradle + Base
|
||||||
|
- [x] Gradle setup (Tink, Bouncy Castle, Room/SQLCipher, Hilt, Compose, OkHttp)
|
||||||
|
- [x] Catppuccin Mocha dark theme + Material 3
|
||||||
|
- [x] Navigation (NavHost, routes)
|
||||||
|
- [x] Domain models (Message, Conversation, User, MessageReaction, etc.)
|
||||||
|
- [x] Room entities + DAOs (MessageDao, ConversationDao, UserCacheDao)
|
||||||
|
- [x] AppDatabase with SQLCipher
|
||||||
|
|
||||||
|
### Phase 1 — Crypto (11 files)
|
||||||
|
- [x] AesGcm.kt — AES-256-GCM
|
||||||
|
- [x] Hkdf.kt — HKDF
|
||||||
|
- [x] ECP1.kt / KeyEncryption.kt — password-based key encryption
|
||||||
|
- [x] Ed25519Crypto.kt — Ed25519 sign/verify + Ed→X25519 conversion
|
||||||
|
- [x] X25519Crypto.kt — X25519 DH
|
||||||
|
- [x] RSACrypto.kt — RSA-PSS for login
|
||||||
|
- [x] MessagePadding.kt — bucketed padding 64B–64KB
|
||||||
|
- [x] X3DH.kt — X3DH initiate/respond
|
||||||
|
- [x] DoubleRatchet.kt — Double Ratchet encrypt/decrypt/import/export
|
||||||
|
- [x] SenderKey.kt — group sender key protocol
|
||||||
|
- [x] ContactVerification.kt — safety numbers (SHA-512 × 5200, 60 digits)
|
||||||
|
|
||||||
|
### Phase 2 — Network (3 files)
|
||||||
|
- [x] ConnectionManager.kt — raw TCP, newline-delimited JSON
|
||||||
|
- [x] ProtocolHandler.kt — request/response + push dispatch
|
||||||
|
- [x] ServerApi.kt — 50 endpoints
|
||||||
|
|
||||||
|
### Phase 3 — Core + Repos + DI
|
||||||
|
- [x] SessionManager.kt — login/register/session persistence
|
||||||
|
- [x] KeyStorage.kt — encrypted key persistence
|
||||||
|
- [x] ChatClient.kt — messaging engine (sendDm, decrypt, new_message handler)
|
||||||
|
- [x] NotificationRouter.kt — routes 18 push types
|
||||||
|
- [x] MessageRepository.kt — message CRUD + search + reactions
|
||||||
|
- [x] ConversationRepository.kt — conversation CRUD
|
||||||
|
- [x] AppModule.kt — AppDatabase singleton
|
||||||
|
- [x] DatabaseModule.kt — DAO providers
|
||||||
|
- [x] NetworkModule.kt — placeholder (net classes auto-wired)
|
||||||
|
- [x] CryptoModule.kt — placeholder (crypto = Kotlin objects)
|
||||||
|
|
||||||
|
### Phase 4 — Service + Feature UI
|
||||||
|
- [x] ChatPushService.kt (FCM/push forwarding to NotificationRouter)
|
||||||
|
- [x] AuthViewModel.kt
|
||||||
|
- [x] ConversationListViewModel.kt
|
||||||
|
- [x] ChatViewModel.kt — core (loadMessages, sendMessage, incoming flow)
|
||||||
|
- [x] ChatScreen.kt
|
||||||
|
- [x] ConversationListScreen.kt
|
||||||
|
- [x] Auth screens (login, register)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 READY TO BUILD & TEST
|
||||||
|
|
||||||
|
### Build
|
||||||
|
- [ ] Build in Android Studio (Ctrl+F9 / Build > Make Project)
|
||||||
|
- Note: Cannot build from CLI (JAVA_HOME not set on Windows)
|
||||||
|
- Expected: clean build with no compilation errors
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- [ ] Send DM: type message on Android → verify arrives on Python/iOS client
|
||||||
|
- [ ] Receive DM: send from Python/iOS → verify appears on Android
|
||||||
|
- [ ] Self-copy: message I send appears in my own chat view immediately
|
||||||
|
- [ ] X3DH new session: first message to new user triggers X3DH + works correctly
|
||||||
|
- [ ] Group message: send in group conversation → all members receive
|
||||||
|
- [ ] Reaction: add emoji reaction → reflects on all clients
|
||||||
|
- [ ] Pin: pin a message → shows in pinned list
|
||||||
|
- [ ] Delete: delete message → removed from all clients
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 TODO — ChatViewModel Secondary Features
|
||||||
|
|
||||||
|
### Simple API calls (ready to implement)
|
||||||
|
- [x] deleteMessage(messageId) — `api.deleteMessage()` + `messageRepository.markDeleted()`
|
||||||
|
- [x] reactToMessage(messageId, reaction) — `api.reactMessage()` + `messageRepository.updateReactions()`
|
||||||
|
- [x] pinMessage(messageId) — `api.pinMessage()` + `messageRepository.updatePinStatus()`
|
||||||
|
- [x] markAsRead() — `api.markConversationRead(conversationId)`
|
||||||
|
- [x] search(query) — `messageRepository.searchMessages()` + update searchResults in state
|
||||||
|
- [x] nextSearchResult() / prevSearchResult() — navigate currentSearchIndex
|
||||||
|
- [x] forwardMessage(messageId, targetConversationId) — re-encrypt plaintext for target conv
|
||||||
|
|
||||||
|
### Complex (need encryption pipeline)
|
||||||
|
- [ ] sendImage(uri) — AES encrypt image → chunked upload via uploadImageStart/Chunk/End
|
||||||
|
- ImageInfo {fileId, aesKey, iv, thumbnail, filename, size} stored in message
|
||||||
|
- Payload: {"sender", "text", "image": {"file_id": ..., "key": b64, "iv": b64}, "timestamp"}
|
||||||
|
- [ ] sendFile(uri) — similar to sendImage but with FileInfo
|
||||||
|
- [ ] downloadFile(fileId) — downloadImage() chunks → reassemble → AES decrypt
|
||||||
|
- Use offset pagination: loop downloadImage(fileId, offset) until complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟡 TODO — ChatClient Group Messaging
|
||||||
|
|
||||||
|
- [ ] sendGroupMessage() — sender key protocol
|
||||||
|
- Check if each member has received sender key
|
||||||
|
- If not: send `_sender_key` control message first (DM encrypted per device)
|
||||||
|
- Then encrypt group message with SenderKey.encrypt()
|
||||||
|
- Track which members have the key in keyStorage or DB
|
||||||
|
|
||||||
|
- [ ] Group member add: resend sender key to new member via DM
|
||||||
|
- [ ] Group member remove: rotate sender key, resend to remaining members
|
||||||
|
- [ ] Handle `member_added` / `member_removed` push notifications in ChatClient
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🟠 KNOWN ISSUES
|
||||||
|
|
||||||
|
### ✅ FIXED: SQLCipher passphrase (AppModule.kt)
|
||||||
|
- **Was**: hardcoded `"TODO_REPLACE_WITH_DERIVED_KEY"` passphrase
|
||||||
|
- **Now**: random 32-byte key generated once, stored in EncryptedSharedPreferences (Android Keystore)
|
||||||
|
- **Migration**: if old DB exists without stored passphrase → old DB deleted, new one created
|
||||||
|
- Note: Could use HKDF-derived key from identity private (like Python), but Android Keystore approach is simpler and equally secure
|
||||||
|
|
||||||
|
### ✅ FIXED: AuthUiState.useTls = true (registration bug)
|
||||||
|
- **Was**: `AuthUiState.useTls = true` → registration always tried TLS → SSL error if server is plain TCP
|
||||||
|
- **Now**: `AuthUiState.useTls = false` — consistent with LoginScreen local default
|
||||||
|
- LoginScreen always calls `updateServerConfig(useTls=false)` before login anyway
|
||||||
|
- If server requires TLS, user can toggle it in Server Configuration section on LoginScreen
|
||||||
|
|
||||||
|
### Push notification handler registration
|
||||||
|
- ChatClient.setupNotificationHandlers() is called from initialize()
|
||||||
|
- If ChatClient is not initialized (user not logged in), push notifications are silently dropped
|
||||||
|
- Fix: Check session state before trying to decrypt; store encrypted push and decrypt on next init
|
||||||
|
|
||||||
|
### Session persistence
|
||||||
|
- Sessions (DoubleRatchet state) are saved to KeyStorage per device
|
||||||
|
- If app is reinstalled, all sessions are lost → first message after reinstall needs X3DH
|
||||||
|
- This is expected behavior but worth testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 PROTOCOL COMPATIBILITY CHECKLIST
|
||||||
|
|
||||||
|
### sendDm()
|
||||||
|
- [x] Recipients per-device (not per-user)
|
||||||
|
- [x] Self-copy: device_id = SELF_DEVICE_ID, ratchet_header = {"self": true}
|
||||||
|
- [x] X3DH header fields: "ik", "ek", "opk_id" (NOT "ik_pub", "ek_pub")
|
||||||
|
- [x] Binary encoding: base64 via encodeBinary()/decodeBinary()
|
||||||
|
- [x] Ratchet header: dh_pub (hex), n, pn via header.toMap()
|
||||||
|
- [x] Plaintext payload: JSON {"sender", "text", "reply_to", "timestamp"}
|
||||||
|
- [x] Message padding: MessagePadding.pad() before encrypt, unpad() after decrypt
|
||||||
|
|
||||||
|
### handleNewMessage() / decryptServerMessage()
|
||||||
|
- [x] Push field: "sender_id" (NOT "sender_user_id")
|
||||||
|
- [x] device_entries array: pick by myDeviceId or SELF_DEVICE_ID
|
||||||
|
- [x] Self-copy detection: ratchet_header.self == true
|
||||||
|
- [x] Control messages (_sender_key): import silently, don't display
|
||||||
|
- [x] Flat fallback fields for backward compat
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔮 FUTURE — Nice to Have
|
||||||
|
|
||||||
|
- [ ] Voice messages — record, encrypt with AES, upload as file
|
||||||
|
- [ ] Disappearing messages — server-side TTL
|
||||||
|
- [ ] Message editing — re-encrypt + server update
|
||||||
|
- [ ] Key rotation — rotate RSA/Ed25519 identity keys
|
||||||
|
- [ ] Device pairing — link second device (pairingStart/pairingSend)
|
||||||
|
- [ ] Contact verification — UI to display safety numbers
|
||||||
|
- [ ] Backup / restore — export encrypted sessions
|
||||||
|
- [ ] Push notification channels — Android notification priorities
|
||||||
|
- [ ] App lock (biometric / PIN)
|
||||||
Reference in New Issue
Block a user