mirror of
https://github.com/simplex-chat/simplexmq.git
synced 2026-03-30 14:16:00 +00:00
* agent: split creating connection to two steps to prepare connection link in advance * linkEntityId, newOwnerAuth * simplify
261 lines
12 KiB
Haskell
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
|