{-# 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