on master help sign in
Simplify code and improve type safety: factor deriveKey and truncate helpers, type Sent as MessageStatus, use match\! in CmdLoadState
commit3f992e7c76ca611744630935339cabed5d593ef9 authoriko <mail@iko.soy> dateFri 10 Apr 2026, 13:22 (6 months ago) parent70b03ed
3 files changed+46−41
client/App.fs+12−13 client/Crypto.fs+7−6 server.knot+27−22
client/App.fs+12−13
@@ -66,7 +66,7 @@ module App =
6666 | DoDisconnect
6767 | SetCompose of string
6868 | DoSend
69− | Sent of Result<ApiClient.SendResult, string>
69+ | Sent of Result<ApiClient.MessageStatus, string>
7070 | SetNewPubkey of string
7171 | SetNewNickname of string
7272 | DoSaveContact
@@ -89,8 +89,10 @@ module App =
8989
9090 // ── Helpers ──
9191
92− let private truncKey (hex: string) =
93− if hex.Length > 16 then hex.[..15] + "..." else hex
92+ let private truncate maxLen (s: string) =
93+ if s.Length > maxLen then s.[..maxLen - 1] + "..." else s
94+
95+ let private truncKey (hex: string) = truncate 16 hex
9496
9597 let private contactName (contacts: Contact list) (pk: string) =
9698 contacts
@@ -201,10 +203,10 @@ module App =
201203 model', CmdSend(session, pk, compose, id) :: saveCmd model'
202204 | _ -> model, []
203205
204− | Sent(Ok { Status = ApiClient.Delivered | ApiClient.Federated | ApiClient.Queued }) -> model, []
205− | Sent(Ok { Status = ApiClient.Rejected }) ->
206+ | Sent(Ok(ApiClient.Delivered | ApiClient.Federated | ApiClient.Queued)) -> model, []
207+ | Sent(Ok ApiClient.Rejected) ->
206208 { model with Error = Some "Message rejected by server" }, []
207− | Sent(Ok { Status = ApiClient.Unauthorized }) ->
209+ | Sent(Ok ApiClient.Unauthorized) ->
208210 { model with Error = Some "Not authorized to deliver message" }, []
209211 | Sent(Error err) -> { model with Error = Some $"Send failed: {err}" }, []
210212
@@ -325,7 +327,7 @@ module App =
325327 let blob = Crypto.encrypt session.Identity.PrivKey recipPub payload
326328 let blobHex = Crypto.toHex blob
327329 let! result = ApiClient.sendMessage session.Url session.Token recipientHex blobHex ts
328− return Sent(Ok result)
330+ return Sent(Ok result.Status)
329331 with ex ->
330332 return Sent(Error ex.Message)
331333 })
@@ -370,11 +372,8 @@ module App =
370372 | CmdLoadState ->
371373 Cmd.ofEffect (fun dispatch ->
372374 async {
373− let! identity = Store.loadIdentity ()
374− match identity with
375− | Some id ->
376− let data = Store.loadData ()
377− dispatch (StateLoaded(id, data))
375+ match! Store.loadIdentity () with
376+ | Some id -> dispatch (StateLoaded(id, Store.loadData ()))
378377 | None -> ()
379378 }
380379 |> Async.Start)
@@ -493,7 +492,7 @@ module App =
493492 model.Messages
494493 |> Map.tryFind c.Pubkey
495494 |> Option.bind List.tryLast
496− |> Option.map (fun m -> if m.Body.Length > 40 then m.Body.[..39] + "..." else m.Body)
495+ |> Option.map (fun m -> truncate 40 m.Body)
497496 |> Option.defaultValue ""
498497
499498 Button($"{c.Nickname}\n{preview}", Nav(Chat(c.Pubkey, "")))
client/Crypto.fs+7−6
@@ -23,15 +23,18 @@ module Crypto =
2323 let message = Encoding.UTF8.GetBytes(challenge)
2424 PublicKeyAuth.SignDetached(message, privKey) |> toHex
2525
26+ let private deriveKey ephPub recipientX25519Pub rawSecret =
27+ let salt = Array.append ephPub recipientX25519Pub
28+ let info = Encoding.UTF8.GetBytes("skrepka-v1")
29+ HKDF.DeriveKey(HashAlgorithmName.SHA256, rawSecret, 32, salt, info)
30+
2631 let encrypt (senderPrivKey: byte[]) (recipientEd25519Pub: byte[]) (plaintext: string) : byte[] =
2732 let senderPubKey = senderPrivKey.[32..63]
2833 let ephKp = PublicKeyBox.GenerateKeyPair()
2934 let recipientX25519Pub =
3035 PublicKeyAuth.ConvertEd25519PublicKeyToCurve25519PublicKey(recipientEd25519Pub)
3136 let rawSecret = ScalarMult.Mult(ephKp.PrivateKey, recipientX25519Pub)
32− let salt = Array.append ephKp.PublicKey recipientX25519Pub
33− let info = Encoding.UTF8.GetBytes("skrepka-v1")
34− let key = HKDF.DeriveKey(HashAlgorithmName.SHA256, rawSecret, 32, salt, info)
37+ let key = deriveKey ephKp.PublicKey recipientX25519Pub rawSecret
3538 let nonce = SodiumCore.GetRandomBytes(24)
3639 let ptBytes = Encoding.UTF8.GetBytes(plaintext)
3740 let ciphertext = SecretAeadXChaCha20Poly1305.Encrypt(ptBytes, nonce, key)
@@ -58,9 +61,7 @@ module Crypto =
5861 let recipientEd25519Pub = recipientPrivKey.[32..63]
5962 let recipientX25519Pub =
6063 PublicKeyAuth.ConvertEd25519PublicKeyToCurve25519PublicKey(recipientEd25519Pub)
61− let salt = Array.append ephPub recipientX25519Pub
62− let info = Encoding.UTF8.GetBytes("skrepka-v1")
63− let key = HKDF.DeriveKey(HashAlgorithmName.SHA256, rawSecret, 32, salt, info)
64+ let key = deriveKey ephPub recipientX25519Pub rawSecret
6465 let plaintext = SecretAeadXChaCha20Poly1305.Decrypt(ciphertext, nonce, key)
6566 Some(Encoding.UTF8.GetString(plaintext), toHex senderPub)
6667 with _ ->
server.knot+27−22
@@ -516,7 +516,7 @@ validGossipEvents = \events t -> do
516516 where expiresAt > t
517517 yield {eventType: e.eventType, pubkey: normalizeHex e.pubkey, server: server, expiresAt: expiresAt, timestamp: e.timestamp, ttl: ttl}
518518
519−sendToEach : [Text] -> (Text -> (a {})) -> a {}
519+sendToEach : [ServerName] -> (ServerName -> (a {})) -> a {}
520520sendToEach = \servers action ->
521521 case headOf servers of
522522 Nothing {} -> yield {}
@@ -994,7 +994,7 @@ serve = \req -> case req of
994994 let updatesOnlyMerged = mergeByPubkeyServer presence updatesOnly in
995995 let updatesOnlyAfterOffline = removeOffline updatesOnlyMerged offlineOnlyKeys trustedOffline t in
996996 let reclaimSlots = maxPresenceEntries - count updatesOnlyAfterOffline in
997− let newEntries = do e <- trustedDeduped; where none presence (\p -> samePubkeyServer p e); where none updatesOnlyAfterOffline (\p -> samePubkeyServer p e); yield e in
997+ let newEntries = do e <- trustedDeduped; where none presence (\p -> samePubkeyServer p e) && none updatesOnlyAfterOffline (\p -> samePubkeyServer p e); yield e in
998998 let reclaimedEntries = takeN newEntries reclaimSlots in
999999 {finalPresence: union updatesOnlyAfterOffline reclaimedEntries,
10001000 appliedOnline: union
@@ -1029,12 +1029,14 @@ serve = \req -> case req of
10291029 t <- now
10301030 let authorized = verifyFederationAuth authorization
10311031 let validOrigin = isFederatedOrigin originServer
1032− let clampedTs = clampTimestamp timestamp t
1033− result <- atomic do
1034− pr <- maybePrune t
1035− if not (authorized && validOrigin)
1036− then yield {pr: pr, ok: False {}}
1037− else do
1032+ if not (authorized && validOrigin)
1033+ then do
1034+ maybePruneAndBroadcast t
1035+ yield (respond {ok: False {}})
1036+ else do
1037+ let clampedTs = clampTimestamp timestamp t
1038+ result <- atomic do
1039+ pr <- maybePrune t
10381040 messages <- *messages
10391041 ackedIds <- *ackedIds
10401042 if any messages (\m -> m.messageId == messageId && m.toKey == toKey)
@@ -1050,27 +1052,30 @@ serve = \req -> case req of
10501052 [{messageId: messageId, toKey: toKey, fromKey: fromKey,
10511053 encryptedBlob: encryptedBlob, timestamp: clampedTs, originServer: originServer, receivedAt: seqTs}]
10521054 yield {pr: pr, ok: True {}}
1053− forkBroadcastOffline t result.pr
1054− yield (respond {ok: result.ok})
1055+ forkBroadcastOffline t result.pr
1056+ yield (respond {ok: result.ok})
10551057
10561058 FederationAck {messageIds, originServer, fromServer, authorization, respond} -> do
10571059 let fromServer = normalizeServerName fromServer
10581060 let originServer = normalizeServerName originServer
10591061 t <- now
10601062 let authorized = verifyFederationAuth authorization
1061− let validAck = authorized && originServer == serverHostN && not (fromServer == serverHostN)
1062− pruneResult <- atomic do
1063− pr <- maybePrune t
1064− when validAck (do
1063+ if not authorized || not (originServer == serverHostN) || fromServer == serverHostN
1064+ then do
1065+ maybePruneAndBroadcast t
1066+ yield (respond {ok: False {}})
1067+ else do
10651068 let dedupedIds = dedupMessageIds messageIds
1066− forwards <- *forwards
1067− set *forwards = do
1068− f <- forwards
1069− where not (f.toServer == fromServer && any dedupedIds (\mid -> f.messageId == mid))
1070− yield f)
1071− yield pr
1072− forkBroadcastOffline t pruneResult
1073− yield (respond {ok: validAck})
1069+ pruneResult <- atomic do
1070+ pr <- maybePrune t
1071+ forwards <- *forwards
1072+ set *forwards = do
1073+ f <- forwards
1074+ where not (f.toServer == fromServer && any dedupedIds (\mid -> f.messageId == mid))
1075+ yield f
1076+ yield pr
1077+ forkBroadcastOffline t pruneResult
1078+ yield (respond {ok: True {}})
10741079
10751080 Prune {authorization, respond} -> do
10761081 t <- now