Files
simplexmq/tests/AgentTests/ShortLinkTests.hs
Evgeny d10e05b796 agent: split creating connection to two steps to prepare connection link in advance (#1704)
* agent: split creating connection to two steps to prepare connection link in advance

* linkEntityId, newOwnerAuth

* simplify
2026-01-27 10:54:13 +00:00

261 lines
12 KiB
Haskell

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module AgentTests.ShortLinkTests (shortLinkTests) where
import AgentTests.ConnectionRequestTests (contactConnRequest, invConnRequest)
import AgentTests.EqInstances ()
import Control.Concurrent.STM
import Control.Monad.Except
import Crypto.Random (ChaChaDRG)
import qualified Data.ByteString.Base64.URL as B64
import qualified Data.ByteString.Char8 as B
import Simplex.Messaging.Agent.Protocol
import qualified Simplex.Messaging.Crypto as C
import qualified Simplex.Messaging.Crypto.ShortLink as SL
import Simplex.Messaging.Encoding.String
import Simplex.Messaging.Protocol (EncFixedDataBytes)
import Test.Hspec hiding (fit, it)
import Util
shortLinkTests :: Spec
shortLinkTests = do
describe "invitation short link" $ do
it "should encrypt and decrypt link data" testInvShortLink
it "should fail to decrypt invitation data with bad hash" testInvShortLinkBadDataHash
describe "contact short link" $ do
it "should encrypt and decrypt data" testContactShortLink
it "should encrypt updated user data" testUpdateContactShortLink
it "should fail to decrypt contact data with bad hash" testContactShortLinkBadDataHash
it "should fail to decrypt contact data with bad signature" testContactShortLinkBadSignature
describe "contact link with additional owners" $ do
it "should encrypt and decrypt data with additional owner" testContactShortLinkOwner
it "should encrypt and decrypt data with many additional owners" testContactShortLinkManyOwners
it "should fail to decrypt contact data with invalid or unauthorized owners" testContactShortLinkInvalidOwners
testInvShortLink :: IO ()
testInvShortLink = do
-- encrypt
g <- C.newRandom
sigKeys <- atomically $ C.generateKeyPair @'C.Ed25519 g
let userData = UserLinkData "some user data"
userLinkData = UserInvLinkData userData
(linkKey, linkData) = SL.encodeSignLinkData sigKeys supportedSMPAgentVRange invConnRequest Nothing userLinkData
k = SL.invShortLinkKdf linkKey
Right srvData <- runExceptT $ SL.encryptLinkData g k linkData
-- decrypt
Right (FixedLinkData {linkConnReq = connReq}, connData') <- pure $ SL.decryptLinkData linkKey k srvData
connReq `shouldBe` invConnRequest
linkUserData connData' `shouldBe` userData
testInvShortLinkBadDataHash :: IO ()
testInvShortLinkBadDataHash = do
-- encrypt
g <- C.newRandom
sigKeys <- atomically $ C.generateKeyPair @'C.Ed25519 g
let userData = UserLinkData "some user data"
userLinkData = UserInvLinkData userData
(_linkKey, linkData) = SL.encodeSignLinkData sigKeys supportedSMPAgentVRange invConnRequest Nothing userLinkData
-- different key
linkKey <- LinkKey <$> atomically (C.randomBytes 32 g)
let k = SL.invShortLinkKdf linkKey
Right srvData <- runExceptT $ SL.encryptLinkData g k linkData
-- decryption fails
SL.decryptLinkData @'CMInvitation linkKey k srvData
`shouldBe` Left (AGENT (A_LINK "link data hash"))
relayLink1 :: ConnShortLink 'CMContact
relayLink1 = either error id $ strDecode "https://localhost/a#4AkRDmhf64tdRlN406g8lJRg5OCmhD6ynIhi6glOcCM?p=7001&c=LcJUMfVhwD8yxjAiSaDzzGF3-kLG4Uh0Fl_ZIjrRwjI"
relayLink2 :: ConnShortLink 'CMContact
relayLink2 = either error id $ strDecode "https://localhost/a#4AkRDmhf64tdRlN406g8lJRg5OCmhD6ynIhi6glOcCM"
testContactShortLink :: IO ()
testContactShortLink = do
-- encrypt
g <- C.newRandom
sigKeys <- atomically $ C.generateKeyPair @'C.Ed25519 g
let userData = UserLinkData "some user data"
userCtData = UserContactData {direct = True, owners = [], relays = [], userData}
userLinkData = UserContactLinkData userCtData
(linkKey, linkData) = SL.encodeSignLinkData sigKeys supportedSMPAgentVRange contactConnRequest Nothing userLinkData
(_linkId, k) = SL.contactShortLinkKdf linkKey
Right srvData <- runExceptT $ SL.encryptLinkData g k linkData
-- decrypt
Right (FixedLinkData {linkConnReq = connReq}, ContactLinkData _ userCtData') <- pure $ SL.decryptLinkData @'CMContact linkKey k srvData
connReq `shouldBe` contactConnRequest
userCtData' `shouldBe` userCtData
testUpdateContactShortLink :: IO ()
testUpdateContactShortLink = do
-- encrypt
g <- C.newRandom
sigKeys <- atomically $ C.generateKeyPair @'C.Ed25519 g
let userData = UserLinkData "some user data"
userCtData = UserContactData {direct = True, owners = [], relays = [], userData}
userLinkData = UserContactLinkData userCtData
(linkKey, linkData) = SL.encodeSignLinkData sigKeys supportedSMPAgentVRange contactConnRequest Nothing userLinkData
(_linkId, k) = SL.contactShortLinkKdf linkKey
Right (fd, _ud) <- runExceptT $ SL.encryptLinkData g k linkData
-- encrypt updated user data
let updatedUserData = UserLinkData "updated user data"
userCtData' = UserContactData {direct = False, owners = [], relays = [relayLink1, relayLink2], userData = updatedUserData}
userLinkData' = UserContactLinkData userCtData'
signed = SL.encodeSignUserData SCMContact (snd sigKeys) supportedSMPAgentVRange userLinkData'
Right ud' <- runExceptT $ SL.encryptUserData g k signed
-- decrypt
Right (FixedLinkData {linkConnReq = connReq}, ContactLinkData _ userCtData'') <- pure $ SL.decryptLinkData @'CMContact linkKey k (fd, ud')
connReq `shouldBe` contactConnRequest
userCtData'' `shouldBe` userCtData'
testContactShortLinkBadDataHash :: IO ()
testContactShortLinkBadDataHash = do
-- encrypt
g <- C.newRandom
sigKeys <- atomically $ C.generateKeyPair @'C.Ed25519 g
let userData = UserLinkData "some user data"
userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData}
(_linkKey, linkData) = SL.encodeSignLinkData sigKeys supportedSMPAgentVRange contactConnRequest Nothing userLinkData
-- different key
linkKey <- LinkKey <$> atomically (C.randomBytes 32 g)
let (_linkId, k) = SL.contactShortLinkKdf linkKey
Right srvData <- runExceptT $ SL.encryptLinkData g k linkData
-- decryption fails
SL.decryptLinkData @'CMContact linkKey k srvData
`shouldBe` Left (AGENT (A_LINK "link data hash"))
testContactShortLinkBadSignature :: IO ()
testContactShortLinkBadSignature = do
-- encrypt
g <- C.newRandom
sigKeys <- atomically $ C.generateKeyPair @'C.Ed25519 g
let userData = UserLinkData "some user data"
userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData}
(linkKey, linkData) = SL.encodeSignLinkData sigKeys supportedSMPAgentVRange contactConnRequest Nothing userLinkData
(_linkId, k) = SL.contactShortLinkKdf linkKey
Right (fd, _ud) <- runExceptT $ SL.encryptLinkData g k linkData
-- encrypt updated user data
let updatedUserData = UserLinkData "updated user data"
userLinkData' = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData = updatedUserData}
-- another signature key
(_, pk) <- atomically $ C.generateKeyPair @'C.Ed25519 g
let signed = SL.encodeSignUserData SCMContact pk supportedSMPAgentVRange userLinkData'
Right ud' <- runExceptT $ SL.encryptUserData g k signed
-- decryption fails
SL.decryptLinkData @'CMContact linkKey k (fd, ud')
`shouldBe` Left (AGENT (A_LINK "user data signature"))
testContactShortLinkOwner :: IO ()
testContactShortLinkOwner = do
-- encrypt
g <- C.newRandom
(pk, lnk) <- encryptLink g
-- encrypt updated user data
(ownerPK, owner) <- authNewOwner g pk
let ud = UserContactData {direct = True, owners = [owner], relays = [], userData = UserLinkData "updated user data"}
testEncDec g pk lnk ud
testEncDec g ownerPK lnk ud
(_, wrongKey) <- atomically $ C.generateKeyPair @'C.Ed25519 g
testEncDecFail g wrongKey lnk ud $ A_LINK "user data signature"
encryptLink :: TVar ChaChaDRG -> IO (C.PrivateKeyEd25519, (EncFixedDataBytes, LinkKey, C.SbKey))
encryptLink g = do
sigKeys@(_, pk) <- atomically $ C.generateKeyPair @'C.Ed25519 g
let userData = UserLinkData "some user data"
userLinkData = UserContactLinkData UserContactData {direct = True, owners = [], relays = [], userData}
(linkKey, linkData) = SL.encodeSignLinkData sigKeys supportedSMPAgentVRange contactConnRequest Nothing userLinkData
(_linkId, k) = SL.contactShortLinkKdf linkKey
Right (fd, _ud) <- runExceptT $ SL.encryptLinkData g k linkData
pure (pk, (fd, linkKey, k))
authNewOwner :: TVar ChaChaDRG -> C.PrivateKeyEd25519 -> IO (C.PrivateKeyEd25519, OwnerAuth)
authNewOwner g pk = do
(ownerKey, ownerPK) <- atomically $ C.generateKeyPair @'C.Ed25519 g
ownerId <- atomically $ C.randomBytes 16 g
let authOwnerSig = C.sign' pk $ ownerId <> C.encodePubKey ownerKey
pure (ownerPK, OwnerAuth {ownerId, ownerKey, authOwnerSig})
testEncDec :: TVar ChaChaDRG -> C.PrivateKeyEd25519 -> (EncFixedDataBytes, LinkKey, C.SbKey) -> UserContactData -> IO ()
testEncDec g pk (fd, linkKey, k) ctData = do
let signed = SL.encodeSignUserData SCMContact pk supportedSMPAgentVRange $ UserContactLinkData ctData
Right ud <- runExceptT $ SL.encryptUserData g k signed
Right (FixedLinkData {linkConnReq = connReq'}, ContactLinkData _ ctData') <- pure $ SL.decryptLinkData @'CMContact linkKey k (fd, ud)
connReq' `shouldBe` contactConnRequest
ctData' `shouldBe` ctData
testContactShortLinkManyOwners :: IO ()
testContactShortLinkManyOwners = do
-- encrypt
g <- C.newRandom
(pk, lnk) <- encryptLink g
-- encrypt updated user data
(ownerPK1, owner1) <- authNewOwner g pk
(ownerPK2, owner2) <- authNewOwner g pk
(ownerPK3, owner3) <- authNewOwner g ownerPK1
(ownerPK4, owner4) <- authNewOwner g ownerPK1
(ownerPK5, owner5) <- authNewOwner g ownerPK3
let owners = [owner1, owner2, owner3, owner4, owner5]
ud = UserContactData {direct = True, owners, relays = [], userData = UserLinkData "updated user data"}
testEncDec g pk lnk ud
testEncDec g ownerPK1 lnk ud
testEncDec g ownerPK2 lnk ud
testEncDec g ownerPK3 lnk ud
testEncDec g ownerPK4 lnk ud
testEncDec g ownerPK5 lnk ud
(_, wrongKey) <- atomically $ C.generateKeyPair @'C.Ed25519 g
testEncDecFail g wrongKey lnk ud $ A_LINK "user data signature"
testContactShortLinkInvalidOwners :: IO ()
testContactShortLinkInvalidOwners = do
-- encrypt
g <- C.newRandom
(pk, lnk) <- encryptLink g
-- encrypt updated user data
(ownerPK, owner) <- authNewOwner g pk
let mkCtData owners = UserContactData {direct = True, owners, relays = [], userData = UserLinkData "updated user data"}
-- decryption fails: owner uses root key
let ud = mkCtData [owner {ownerKey = C.publicKey pk}]
err = A_LINK $ "owner key for ID " <> ownerIdStr owner <> " matches root key"
testEncDecFail g pk lnk ud err
testEncDecFail g ownerPK lnk ud err
-- decryption fails: duplicate owner ID or key
(ownerPK1, owner1) <- authNewOwner g pk
let ud1 = mkCtData [owner, owner1 {ownerId = ownerId owner}]
ud1' = mkCtData [owner, owner1 {ownerKey = ownerKey owner}]
err1 o = A_LINK $ "duplicate owner key or ID " <> ownerIdStr o
testEncDecFail g pk lnk ud1 $ err1 owner
testEncDecFail g pk lnk ud1' $ err1 owner1
-- decryption fails: wrong order
(ownerPK2, owner2) <- authNewOwner g ownerPK
let ud2 = mkCtData [owner, owner1, owner2]
ud2' = mkCtData [owner, owner2, owner1]
testEncDec g pk lnk ud2
testEncDec g pk lnk ud2'
testEncDec g ownerPK lnk ud2
testEncDec g ownerPK1 lnk ud2
testEncDec g ownerPK2 lnk ud2
let ud2'' = mkCtData [owner2, owner, owner1]
err2 = A_LINK $ "invalid authorization of owner ID " <> ownerIdStr owner2
testEncDecFail g pk lnk ud2'' err2
-- decryption fails: authorized with wrong key
(_, wrongKey) <- atomically $ C.generateKeyPair @'C.Ed25519 g
(_, owner3) <- authNewOwner g wrongKey
let ud3 = mkCtData [owner3]
ud3' = mkCtData [owner, owner1, owner2, owner3]
err3 = A_LINK $ "invalid authorization of owner ID " <> ownerIdStr owner3
testEncDecFail g pk lnk ud3 err3
testEncDecFail g pk lnk ud3' err3
testEncDecFail :: TVar ChaChaDRG -> C.PrivateKeyEd25519 -> (EncFixedDataBytes, LinkKey, C.SbKey) -> UserContactData -> SMPAgentError -> IO ()
testEncDecFail g pk (fd, linkKey, k) ctData err = do
let signed = SL.encodeSignUserData SCMContact pk supportedSMPAgentVRange $ UserContactLinkData ctData
Right ud <- runExceptT $ SL.encryptUserData g k signed
SL.decryptLinkData @'CMContact linkKey k (fd, ud) `shouldBe` Left (AGENT err)
ownerIdStr :: OwnerAuth -> String
ownerIdStr OwnerAuth {ownerId} = B.unpack $ B64.encodeUnpadded ownerId