core: don't reveal another profile when delivering a store purchase; bound store checks and restrict Play ids

This commit is contained in:
spaced4ndy committed 2026-09-25 19:13:04 +04:00
1 parent a329f51620
commit aa8e8271ae
8 files changed
+70 -17

No files matched your search

+16 -1
View File
@@ -10,6 +10,7 @@
module BadgeTests (badgeTests) where
import BadgeService.Service (badgeErrorRetryAfter, shownServiceRequest)
import BadgeService.StoreReceipts (StoreReceipt (..), StoreRefusal (..), StoreVerifier (..), storeReceipt)
import Control.Concurrent.STM (atomically)
import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Base64.URL as B64U
@@ -34,7 +35,7 @@ import Simplex.Chat (defaultChatConfig)
import Simplex.Chat.Controller (ChatError (..), ChatErrorType (..), badgeRetryInterval, chatErrorAgent)
import Simplex.Chat.Library.Commands (badgeErrorRetry, badgeFailureTransient, badgeIssueFailure, badgeRetryAfter, badgeServiceErrorText, badgeStalledInterval, storeTransactionRef)
import Simplex.Chat.PaymentService (ServicePayment (..))
import Simplex.Chat.PaymentService.Types (InvoiceId (..))
import Simplex.Chat.PaymentService.Types (InvoiceId (..), PaymentProvider (..))
import Simplex.Chat.Store.Badges (StoreTransactionRef (..))
import Simplex.Messaging.Agent.Protocol (AgentErrorType (..), AgentServiceError (..), SMPAgentError (..))
import Simplex.Messaging.Agent.RetryInterval (RetryInterval (..), nextRetryDelay)
@@ -104,6 +105,7 @@ badgeTests = do
describe "store purchases" $ do
it "keys a purchase by the store's transaction id, not by the evidence signed over it" testStoreTransactionRef
it "shows a service request in the terminal as its type alone" testShownServiceRequest
it "refuses a Play product id or token that could name another purchase, before any verifier" testGooglePathStrings
proofOf :: BadgeProof -> BBSProof
proofOf (BadgeProof _ _ p _) = p
@@ -856,6 +858,19 @@ testShownServiceRequest = do
shown BSCPurchaseBadge {masterKey = mk, payment = SPApple {jws = "a.b.c"}, upgrade = Nothing} `shouldBe` typeOnly "purchaseBadge"
shown BSCRedeemBadgeCode {masterKey = mk, code = "SB-00000-00000-00000-00001"} `shouldBe` typeOnly "redeemBadgeCode"
testGooglePathStrings :: IO ()
testGooglePathStrings = do
let calledVerifier = StoreVerifier {verifyApple = Nothing, verifyGoogle = Just $ \_ _ -> error "verifier called", verifyTimeout = 500000}
refused productId token = case storeReceipt calledVerifier SPGoogle {productId, token} of
Just (Left SRInvalid {}) -> True
_ -> False
validToken = "fake-play-token.AO-J1Oz9x2kqE7wYt3"
mapM_ (\p -> refused p validToken `shouldBe` True) ["badge_legend_01/tokens/other?", "badge_legend_01?x", "badge_legend_01#x", "..", "../badge_legend_01", "Badge_legend_01", ""]
mapM_ (\t -> refused "badge_supporter_01" t `shouldBe` True) ["a/b", "../x", "t?x", "t#x", "t x", ""]
case storeReceipt calledVerifier SPGoogle {productId = "badge_supporter_01", token = validToken} of
Just (Right StoreReceipt {provider}) -> provider `shouldBe` PPGoogle
_ -> expectationFailure "a valid product id and token were refused"
testCredentialResponseJSON :: IO ()
testCredentialResponseJSON = do
Right (_, sk) <- bbsKeyGen
+23 -4
View File
@@ -133,6 +133,7 @@ badgeServiceTests = do
it "should refuse a store purchase while a badge is held, before anything is sent" testPurchaseWhileBadgeHeld
it "should answer a receipt presented under a second profile as the profile that bought it" testPurchaseSameReceiptOtherProfile
it "should deliver a purchase first presented under another profile to that profile" testPurchaseStrandedUnderOtherProfile
it "should deliver a purchase to a hidden profile without naming it" testPurchaseDeliveredToHiddenProfile
badgeProfile :: Profile
badgeProfile = Profile {displayName = "SimpleX Badges", fullName = "", shortDescr = Nothing, description = Nothing, image = Nothing, contactLink = Nothing, peerType = Just CPTBot, preferences = Nothing, badge = Nothing, contactDomain = Nothing}
@@ -1660,7 +1661,7 @@ testPurchaseSameReceiptOtherProfile ps =
showActiveUser alice "alisa"
-- the store transaction is the device's, so it stays with the profile it was bought under
alice ##> ("/_badge purchase 2 " <> paymentArg supporterPlay)
alice <## "[user: alice] badge already redeemed"
alice <## "badge purchase delivered to another profile"
rowCount cc "sx_badge_service_badge_purchases" `shouldReturn` 1
alice ##> "/p"
showActiveUser alice "alisa"
@@ -1677,10 +1678,28 @@ testPurchaseStrandedUnderOtherProfile ps =
settlePending store
-- presented again under whichever profile is active, the purchase reaches the keys alice stashed
alice ##> unsettled 2
alice <## "[user: alice] badge redeemed"
alice <## "supporter badge - active"
alice <##. "expires "
alice <## "badge purchase delivered to another profile"
rowCount (chatController alice) "badge_store_receipts" `shouldReturn` 1
rowCount cc "sx_badge_service_badge_purchases" `shouldReturn` 1
alice ##> "/user alice"
showActiveUser alice "alice (Alice, * supporter)"
testPurchaseDeliveredToHiddenProfile :: HasCallStack => TestParams -> IO ()
testPurchaseDeliveredToHiddenProfile ps =
withBadgeServiceEnv ps $ \BadgeServiceEnv {bsClientCfg, bsStore = store} ->
withNewTestChatCfg ps bsClientCfg "alice" aliceProfile $ \alice -> do
let unsettled userId = "/_badge purchase " <> show (userId :: Int) <> " " <> paymentArg (googlePayment "badge_supporter_01" googlePendingToken)
alice ##> unsettled 1
alice <## "cannot redeem badge code: badge service error: payment_pending"
alice ##> "/create user alisa"
showActiveUser alice "alisa"
alice ##> "/_hide user 1 \"password\""
alice <## "user alice:"
alice <## "messages are hidden (use /tail to view)"
alice <## "profile is hidden"
settlePending store
-- the answer names only the presenting profile, and nothing printed names the hidden one
alice ##> unsettled 2
alice <## "badge purchase delivered to another profile"
alice ##> "/user alice password"
showActiveUser alice "alice (Alice, * supporter)"
+1 -1
View File
@@ -73,7 +73,7 @@ newFakeStore = do
appleThrowingJWS,
pendingSettled,
googleDown,
fakeVerifier = StoreVerifier {verifyApple = Just verifyApple, verifyGoogle = Just verifyGoogle}
fakeVerifier = StoreVerifier {verifyApple = Just verifyApple, verifyGoogle = Just verifyGoogle, verifyTimeout = 500000}
}
where
fixtureJWS name = unsignedJWS <$> B.readFile (fixtureDir </> name)