diff --git a/rfcs/2026-03-20-smp-agent-web/2026-03-20-smp-agent-web-spike.md b/rfcs/2026-03-20-smp-agent-web/2026-03-20-smp-agent-web-spike.md index 09fc76a3c..278d4f9f3 100644 --- a/rfcs/2026-03-20-smp-agent-web/2026-03-20-smp-agent-web-spike.md +++ b/rfcs/2026-03-20-smp-agent-web/2026-03-20-smp-agent-web-spike.md @@ -218,7 +218,9 @@ Each step produces working, tested code. Steps 1-11 work without block encryptio **Done**. Function: `connShortLinkStrP` in `agent/protocol.ts`. Uses `base64urlDecode` from xftp-web `description.ts`. -**Future**: add long link parsing (`ConnectionRequestUri`) and an either-parser that handles both short and long links. +**Future**: +- Add long link parsing (`ConnectionRequestUri`) and an either-parser that handles both short and long links. +- Add `restoreShortLink`: preset servers are shortened to host-only (`SMPServerOnlyHost` - no port, no keyHash). After parsing, `restoreShortLink` looks up the full server by hostname from a preset servers list. Without this, connections to preset servers will fail. See `Agent/Protocol.hs:1692`. ### Step 7: HKDF Key Derivation diff --git a/smp-web/src/crypto/shortLink.ts b/smp-web/src/crypto/shortLink.ts new file mode 100644 index 000000000..63193e74c --- /dev/null +++ b/smp-web/src/crypto/shortLink.ts @@ -0,0 +1,21 @@ +// Short link key derivation. +// Mirrors: Simplex.Messaging.Crypto.ShortLink + +import {hkdf} from "@noble/hashes/hkdf" +import {sha512} from "@noble/hashes/sha512" + +// contactShortLinkKdf (Agent/Protocol.hs:47-50) +// hkdf("", linkKey, "SimpleXContactLink", 56) -> (linkId[24], sbKey[32]) +export function contactShortLinkKdf(linkKey: Uint8Array): {linkId: Uint8Array; sbKey: Uint8Array} { + const derived = hkdf(sha512, linkKey, new Uint8Array(0), "SimpleXContactLink", 56) + return { + linkId: derived.slice(0, 24), + sbKey: derived.slice(24, 56), + } +} + +// invShortLinkKdf (Agent/Protocol.hs:52-53) +// hkdf("", linkKey, "SimpleXInvLink", 32) -> sbKey[32] +export function invShortLinkKdf(linkKey: Uint8Array): Uint8Array { + return hkdf(sha512, linkKey, new Uint8Array(0), "SimpleXInvLink", 32) +} diff --git a/tests/SMPWebTests.hs b/tests/SMPWebTests.hs index e16655a0f..aab92e04e 100644 --- a/tests/SMPWebTests.hs +++ b/tests/SMPWebTests.hs @@ -18,9 +18,10 @@ import Data.List.NonEmpty (NonEmpty (..)) import Data.Word (Word16) import qualified Simplex.Messaging.Agent.Protocol as AP import qualified Simplex.Messaging.Crypto as C +import Simplex.Messaging.Crypto.ShortLink (contactShortLinkKdf, invShortLinkKdf) import Simplex.Messaging.Encoding import Simplex.Messaging.Encoding.String (strEncode) -import Simplex.Messaging.Protocol (SMPServer, pattern SMPServer) +import Simplex.Messaging.Protocol (EntityId (..), SMPServer, pattern SMPServer) import Simplex.Messaging.Server.Env.STM (AStoreType (..)) import Simplex.Messaging.Server.MsgStore.Types (SMSType (..), SQSType (..)) import Simplex.Messaging.Server.Web (attachStaticAndWS) @@ -52,6 +53,9 @@ impWS = "import { connectSMP, sendBlock, receiveBlock } from './dist/transport/w impAgentProto :: String impAgentProto = "import { connShortLinkStrP } from './dist/agent/protocol.js';" +impCryptoShortLink :: String +impCryptoShortLink = "import { contactShortLinkKdf, invShortLinkKdf } from './dist/crypto/shortLink.js';" + jsStr :: B.ByteString -> String jsStr bs = "'" <> BC.unpack bs <> "'" @@ -154,6 +158,24 @@ smpWebTests = describe "SMP Web Client" $ do <> "})") tsEncoded `shouldBe` hsEncoded + describe "crypto/shortLink" $ do + describe "contactShortLinkKdf" $ do + it "TypeScript produces same linkId and sbKey as Haskell" $ do + let linkKey = AP.LinkKey $ B.pack [1..32] + (EntityId hsLinkId, C.SbKey hsKey) = contactShortLinkKdf linkKey + tsResult <- callNode $ impCryptoShortLink + <> "const r = contactShortLinkKdf(" <> jsUint8 (B.pack [1..32]) <> ");" + <> jsOut ("new Uint8Array([...r.linkId, ...r.sbKey])") + tsResult `shouldBe` (hsLinkId <> hsKey) + + describe "invShortLinkKdf" $ do + it "TypeScript produces same sbKey as Haskell" $ do + let linkKey = AP.LinkKey $ B.pack [50..81] + C.SbKey hsKey = invShortLinkKdf linkKey + tsResult <- callNode $ impCryptoShortLink + <> jsOut ("invShortLinkKdf(" <> jsUint8 (B.pack [50..81]) <> ")") + tsResult `shouldBe` hsKey + describe "agent/protocol" $ do describe "ConnShortLink" $ do it "parses simplex: contact link" $ do