diff --git a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/AppleAppStoreManager.java b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/AppleAppStoreManager.java index fafc1ef7f..628a7a426 100644 --- a/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/AppleAppStoreManager.java +++ b/service/src/main/java/org/whispersystems/textsecuregcm/subscriptions/AppleAppStoreManager.java @@ -9,14 +9,18 @@ import com.apple.itunes.storekit.model.AutoRenewStatus; import com.apple.itunes.storekit.model.Status; import com.apple.itunes.storekit.model.StatusResponse; import com.apple.itunes.storekit.model.SubscriptionGroupIdentifierItem; +import io.micrometer.core.instrument.Tags; import java.io.IOException; import java.io.UncheckedIOException; import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.time.Instant; +import java.util.Base64; import java.util.List; import java.util.Locale; import java.util.Map; -import io.micrometer.core.instrument.Tags; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException; @@ -165,23 +169,20 @@ public class AppleAppStoreManager implements SubscriptionPaymentProcessor { final List txs = item.getLastTransactions().stream() .map(txItem -> appleAppStoreClient.verify(statuses.getEnvironment(), txItem)) + .filter(tx -> tx.signedTransaction().getOriginalTransactionId().equals(originalTransactionId)) .filter(decoded -> productIdToLevel.containsKey(decoded.transaction().getProductId())) .toList(); if (txs.isEmpty()) { - throw new SubscriptionInvalidArgumentsException("transactionId did not include a paid subscription", null); - } - - if (txs.size() > 1) { - logger.warn("Multiple matching product transactions found for transactionId {}, only considering first", - originalTransactionId); - } - - if (!originalTransactionId.equals(txs.getFirst().signedTransaction().getOriginalTransactionId())) { // Get All Subscriptions only requires that the transaction be some transaction associated with the // subscription. This is too flexible, since we'd like to key on the originalTransactionId in the // SubscriptionManager. - throw new SubscriptionInvalidArgumentsException("transactionId was not the transaction's originalTransactionId", null); + throw new SubscriptionInvalidArgumentsException("transactionId did not include a paid subscription or the provided transactionId was not an originalTransactionId", null); + } + + if (txs.size() > 1) { + logger.warn("Multiple matching product transactions found with a sha256(originalTransactionId)={}, only considering first", + sha256(originalTransactionId)); } return txs.getFirst(); } @@ -210,4 +211,14 @@ public class AppleAppStoreManager implements SubscriptionPaymentProcessor { || tx.signedTransaction().getStatus() == Status.BILLING_GRACE_PERIOD; } + private static String sha256(final String input) { + final MessageDigest sha256; + try { + sha256 = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError("Every implementation of the Java platform is required to support SHA-256", e); + } + return Base64.getEncoder().encodeToString(sha256.digest(input.getBytes(StandardCharsets.UTF_8))); + } + } diff --git a/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/AppleAppStoreManagerTest.java b/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/AppleAppStoreManagerTest.java index 285dc484a..3acbc2561 100644 --- a/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/AppleAppStoreManagerTest.java +++ b/service/src/test/java/org/whispersystems/textsecuregcm/subscriptions/AppleAppStoreManagerTest.java @@ -147,6 +147,28 @@ class AppleAppStoreManagerTest { } + @Test + public void multipleLastTransactionsItems() + throws VerificationException, APIException, IOException, SubscriptionPaymentRequiredException, SubscriptionInvalidArgumentsException, SubscriptionNotFoundException, RateLimitExceededException { + when(apiClient.getAllSubscriptionStatuses(ORIGINAL_TX_ID, new Status[]{})) + .thenReturn(new StatusResponse() + .data(List.of(new SubscriptionGroupIdentifierItem() + .subscriptionGroupIdentifier(SUBSCRIPTION_GROUP_ID) + .addLastTransactionsItem(new LastTransactionsItem() + .originalTransactionId(ORIGINAL_TX_ID + "-different") + .status(Status.ACTIVE) + .signedRenewalInfo(SIGNED_RENEWAL_INFO) + .signedTransactionInfo(SIGNED_TX_INFO)) + .addLastTransactionsItem(new LastTransactionsItem() + .originalTransactionId(ORIGINAL_TX_ID) + .status(Status.ACTIVE) + .signedRenewalInfo(SIGNED_RENEWAL_INFO) + .signedTransactionInfo(SIGNED_TX_INFO)))) + .environment(Environment.PRODUCTION)); + mockDecode(AutoRenewStatus.ON); + assertThat(appleAppStoreManager.validateTransaction(ORIGINAL_TX_ID)).isEqualTo(LEVEL); + } + @Test public void cancelRenewalDisabled() throws APIException, VerificationException, IOException { mockSubscription(Status.ACTIVE, AutoRenewStatus.OFF);