decrypt link data

This commit is contained in:
Evgeny @ SimpleX Chat
2026-03-27 17:46:17 +00:00
parent 85e0517594
commit 8c1cfca208
3 changed files with 59 additions and 3 deletions
@@ -66,7 +66,7 @@ Bottom-up, function-by-function. Each TypeScript function tested against its Has
**Project location**: `simplexmq-2/smp-web/`
**Tests**: `simplexmq-2/tests/SMPWebTests.hs` — reuses `callNode`/`jsOut`/`jsUint8` from XFTPWebTests (generalized, not copied)
**xftp-web**: npm dependency (encoding, crypto, padding imported directly)
**xftp-web**: npm dependency via `file:../xftp-web` (encoding, crypto, padding imported directly). Note: libsodium-wrappers-sumo is xftp-web's dependency; tests must init the same sodium instance that xftp-web's secretbox uses. If xftp-web is ever published to npm, libsodium should become a peerDependency.
**File structure**: mirrors Haskell module hierarchy (see RFC section 2)
**Pattern for each function**:
+29 -1
View File
@@ -1,7 +1,9 @@
// Short link key derivation.
// Short link key derivation and decryption.
// Mirrors: Simplex.Messaging.Crypto.ShortLink
import {hkdf} from "../crypto.js"
import {cbDecrypt} from "@simplex-chat/xftp-web/dist/crypto/secretbox.js"
import {Decoder, decodeBytes} from "@simplex-chat/xftp-web/dist/protocol/encoding.js"
const emptySalt = new Uint8Array(0)
@@ -20,3 +22,29 @@ export function contactShortLinkKdf(linkKey: Uint8Array): {linkId: Uint8Array; s
export function invShortLinkKdf(linkKey: Uint8Array): Uint8Array {
return hkdf(emptySalt, linkKey, "SimpleXInvLink", 32)
}
// decryptLinkData (Crypto/ShortLink.hs:100-125)
// Decrypts both EncDataBytes blobs, strips signature prefix, returns raw data.
// Signature verification is skipped for spike.
export function decryptLinkData(
sbKey: Uint8Array,
encFixedData: Uint8Array,
encUserData: Uint8Array
): {fixedData: Uint8Array; userData: Uint8Array} {
return {
fixedData: decryptSigned(sbKey, encFixedData),
userData: decryptSigned(sbKey, encUserData),
}
}
// EncDataBytes format: [nonce 24 bytes][ciphertext with prepended Poly1305 tag]
// After decrypt+unpad: [sig ByteString (1-byte len + 64 bytes)][data]
function decryptSigned(sbKey: Uint8Array, encData: Uint8Array): Uint8Array {
const nonce = encData.subarray(0, 24)
const ct = encData.subarray(24)
const plaintext = cbDecrypt(sbKey, nonce, ct)
// Skip signature: decodeBytes reads 1-byte length + that many bytes
const d = new Decoder(plaintext)
decodeBytes(d) // signature, discarded
return d.takeAll()
}
+29 -1
View File
@@ -54,7 +54,11 @@ impAgentProto :: String
impAgentProto = "import { connShortLinkStrP } from './dist/agent/protocol.js';"
impCryptoShortLink :: String
impCryptoShortLink = "import { contactShortLinkKdf, invShortLinkKdf } from './dist/crypto/shortLink.js';"
impCryptoShortLink = "import { contactShortLinkKdf, invShortLinkKdf, decryptLinkData } from './dist/crypto/shortLink.js';"
-- Init sodium from xftp-web's copy (same instance secretbox.ts uses)
impSodium :: String
impSodium = "import sodium from '@simplex-chat/xftp-web/node_modules/libsodium-wrappers-sumo/dist/modules-sumo/libsodium-wrappers.js'; await sodium.ready;"
jsStr :: B.ByteString -> String
jsStr bs = "'" <> BC.unpack bs <> "'"
@@ -176,6 +180,30 @@ smpWebTests = describe "SMP Web Client" $ do
<> jsOut ("invShortLinkKdf(" <> jsUint8 (B.pack [50..81]) <> ")")
tsResult `shouldBe` hsKey
describe "decryptLinkData" $ do
it "TypeScript decrypts Haskell-encrypted data" $ do
let sbKey = C.unsafeSbKey $ B.pack [1..32]
nonce = C.cbNonce $ B.pack [1..24]
-- Simulate encodeSign: smpEncode signature <> plaintext
fakeSig = B.pack [1..64] -- 64-byte "signature"
fixedPlain = "fixed-data-here"
userPlain = "user-data-here"
signedFixed = smpEncode fakeSig <> fixedPlain
signedUser = smpEncode fakeSig <> userPlain
case (,) <$> C.sbEncrypt sbKey nonce signedFixed 2008
<*> C.sbEncrypt sbKey nonce signedUser 13784 of
Left e -> expectationFailure $ "encrypt failed: " <> show e
Right (ctFixed, ctUser) -> do
let encFixed = C.unCbNonce nonce <> ctFixed
encUser = C.unCbNonce nonce <> ctUser
tsResult <- callNode $ impSodium <> impCryptoShortLink
<> "const r = decryptLinkData("
<> jsUint8 (C.unSbKey sbKey) <> ","
<> jsUint8 encFixed <> ","
<> jsUint8 encUser <> ");"
<> jsOut ("new Uint8Array([...r.fixedData, 0, ...r.userData])")
tsResult `shouldBe` (fixedPlain <> B.singleton 0 <> userPlain)
describe "agent/protocol" $ do
describe "ConnShortLink" $ do
it "parses simplex: contact link" $ do