From 7b29b9725331f7c6bdce2f55a92ba453502005b3 Mon Sep 17 00:00:00 2001 From: Vladimir Pankrashkin Date: Tue, 29 May 2018 20:12:32 +0300 Subject: [PATCH] GP-2: GooglePay Implementation --- .gitignore | 119 ++++++++++++ pom.xml | 183 ++++++++++++++++++ .../provider/googlepay/Application.java | 17 ++ .../provider/googlepay/config/AppConfig.java | 42 ++++ .../provider/googlepay/domain/Auth.java | 8 + .../provider/googlepay/domain/Auth3DS.java | 49 +++++ .../provider/googlepay/domain/AuthType.java | 11 ++ .../provider/googlepay/domain/Card.java | 59 ++++++ .../provider/googlepay/domain/CardInfo.java | 73 +++++++ .../googlepay/domain/DecryptedMessage.java | 100 ++++++++++ .../googlepay/domain/PaymentCredential.java | 8 + .../googlepay/domain/PaymentData.java | 49 +++++ .../googlepay/domain/PaymentMethod.java | 13 ++ .../googlepay/domain/PaymentToken.java | 47 +++++ .../googlepay/domain/TokenizationType.java | 13 ++ .../googlepay/domain/TokenizedCard.java | 86 ++++++++ .../iface/decrypt/ProviderHandler.java | 144 ++++++++++++++ .../iface/decrypt/ProviderServlet.java | 27 +++ .../googlepay/service/CryptoException.java | 25 +++ .../service/DecryptedMessageDeserializer.java | 59 ++++++ .../googlepay/service/DecryptionService.java | 24 +++ .../googlepay/service/GPKeyStore.java | 22 +++ .../service/TokenizedCardDeserializer.java | 48 +++++ .../service/ValidationException.java | 25 +++ .../googlepay/service/ValidationService.java | 20 ++ src/main/resources/application.properties | 11 ++ .../rbkmoney/provider/googlepay/DataTest.java | 82 ++++++++ .../provider/googlepay/IntegrationTest.java | 86 ++++++++ src/test/resources/keys.txt | 1 + 29 files changed, 1451 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/Application.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/config/AppConfig.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/domain/Auth.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/domain/Auth3DS.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/domain/AuthType.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/domain/Card.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/domain/CardInfo.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/domain/DecryptedMessage.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentCredential.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentData.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentMethod.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentToken.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/domain/TokenizationType.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/domain/TokenizedCard.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/iface/decrypt/ProviderHandler.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/iface/decrypt/ProviderServlet.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/service/CryptoException.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/service/DecryptedMessageDeserializer.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/service/DecryptionService.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/service/GPKeyStore.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/service/TokenizedCardDeserializer.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/service/ValidationException.java create mode 100644 src/main/java/com/rbkmoney/provider/googlepay/service/ValidationService.java create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/com/rbkmoney/provider/googlepay/DataTest.java create mode 100644 src/test/java/com/rbkmoney/provider/googlepay/IntegrationTest.java create mode 100644 src/test/resources/keys.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..320872a --- /dev/null +++ b/.gitignore @@ -0,0 +1,119 @@ +# Created by .ignore support plugin (hsz.mobi) +### Maven template +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +# Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) +!/.mvn/wrapper/maven-wrapper.jar +### Java template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/dictionaries +.idea/**/shelf + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# CMake +cmake-build-debug/ +cmake-build-release/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws +*.ipr +*.iml +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7e180a5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,183 @@ + + + 4.0.0 + + + com.rbkmoney + spring-boot-starter-parent + 2.0.1.RELEASE + + + com.rbkmoney.provider + googlepay + 1.0.0-SNAPSHOT + + + Vladimir Pankrashkin <v.pankrashkin@rbkmoney.com> + UTF-8 + 22c57470c4fc47161894f036b7cf9d70f42b75f5 + 1.236-1510cd7 + 0.3.2 + 8022 + ${server.port} + + + + + + com.rbkmoney.woody + woody-thrift + 1.1.13 + + + com.rbkmoney.geck + common + 0.6.9 + + + com.rbkmoney.logback + nop-rolling + 1.0.1 + + + com.rbkmoney + damsel + ${damsel.version} + + + com.rbkmoney + shared-resources + ${shared.resources.version} + + + + com.google.crypto.tink + apps-paymentmethodtoken + 1.1.0 + + + com.google.guava + guava + 24.1-jre + + + net.logstash.logback + logstash-logback-encoder + 5.0 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + org.slf4j + slf4j-api + + + javax.servlet + javax.servlet-api + provided + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + org.hibernate + hibernate-validator + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.hamcrest + hamcrest-library + + + + + junit + junit + test + + + commons-io + commons-io + 2.6 + test + + + + + + ${project.build.directory}/maven-shared-archive-resources + ${project.build.directory} + + Dockerfile + + true + + + ${project.build.directory}/maven-shared-archive-resources + true + + Dockerfile + + + + src/main/resources + true + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-dependency-plugin + + + org.apache.maven.plugins + maven-remote-resources-plugin + 1.5 + + + org.apache.maven.shared + maven-filtering + 1.3 + + + + + com.rbkmoney:shared-resources:${shared.resources.version} + + false + false + + + + + process + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/rbkmoney/provider/googlepay/Application.java b/src/main/java/com/rbkmoney/provider/googlepay/Application.java new file mode 100644 index 0000000..f3ac817 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/Application.java @@ -0,0 +1,17 @@ +package com.rbkmoney.provider.googlepay; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.servlet.ServletComponentScan; + +/** + * Created by vpankrashkin on 29.05.18. + */ +@SpringBootApplication(scanBasePackages = "com.rbkmoney.provider.googlepay") +@ServletComponentScan +public class Application { + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/config/AppConfig.java b/src/main/java/com/rbkmoney/provider/googlepay/config/AppConfig.java new file mode 100644 index 0000000..b98e4c5 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/config/AppConfig.java @@ -0,0 +1,42 @@ +package com.rbkmoney.provider.googlepay.config; + +import com.google.crypto.tink.apps.paymentmethodtoken.GooglePaymentsPublicKeysManager; +import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenRecipient; +import com.rbkmoney.damsel.payment_tool_provider.PaymentToolProviderSrv; +import com.rbkmoney.provider.googlepay.iface.decrypt.ProviderHandler; +import com.rbkmoney.provider.googlepay.service.DecryptionService; +import com.rbkmoney.provider.googlepay.service.GPKeyStore; +import com.rbkmoney.provider.googlepay.service.ValidationService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; + +import java.io.IOException; +import java.security.GeneralSecurityException; + +/** + * Created by vpankrashkin on 29.05.18. + */ +@Configuration +public class AppConfig { + + @Bean + public DecryptionService decryptionService(@Value("${google.test}") boolean testMode, @Value("${google.gateway_id}") String gatewayId, @Value("${google.keys_path}")Resource keys) throws GeneralSecurityException, IOException { + + PaymentMethodTokenRecipient.Builder builder = new PaymentMethodTokenRecipient.Builder(); + builder.fetchSenderVerifyingKeysWith( + testMode ? GooglePaymentsPublicKeysManager.INSTANCE_TEST : GooglePaymentsPublicKeysManager.INSTANCE_PRODUCTION) + .recipientId("gateway:"+gatewayId); + for (String key: new GPKeyStore(keys.getURI()).getKeys()) { + builder.addRecipientPrivateKey(key); + } + + return new DecryptionService(builder.build()); + } + + @Bean + public PaymentToolProviderSrv.Iface decryptHandler(DecryptionService decryptionService, @Value("${google.use_validation}") boolean enableValidation) { + return new ProviderHandler(new ValidationService(), decryptionService, enableValidation); + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/domain/Auth.java b/src/main/java/com/rbkmoney/provider/googlepay/domain/Auth.java new file mode 100644 index 0000000..39d02d3 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/domain/Auth.java @@ -0,0 +1,8 @@ +package com.rbkmoney.provider.googlepay.domain; + +/** + * Created by vpankrashkin on 28.05.18. + */ + +public class Auth { +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/domain/Auth3DS.java b/src/main/java/com/rbkmoney/provider/googlepay/domain/Auth3DS.java new file mode 100644 index 0000000..84a0b1a --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/domain/Auth3DS.java @@ -0,0 +1,49 @@ +package com.rbkmoney.provider.googlepay.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Created by vpankrashkin on 28.05.18. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class Auth3DS extends Auth { + private String cryptogram; + private String eci; + + @JsonCreator + public Auth3DS( + @JsonProperty(value = "3dsCryptogram", required = true) String cryptogram, + @JsonProperty(value = "3dsEciIndicator") String eci) { + this.cryptogram = cryptogram; + this.eci = eci; + } + + public Auth3DS() { + } + + public String getCryptogram() { + return cryptogram; + } + + public void setCryptogram(String cryptogram) { + this.cryptogram = cryptogram; + } + + public String getEci() { + return eci; + } + + public void setEci(String eci) { + this.eci = eci; + } + + @Override + public String toString() { + return "Auth3DS{" + + "cryptogram='" + (cryptogram == null ? null : "***") + '\'' + + ", eci='" + (eci == null ? null : "***") + '\'' + + "}"; + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/domain/AuthType.java b/src/main/java/com/rbkmoney/provider/googlepay/domain/AuthType.java new file mode 100644 index 0000000..73c2c0c --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/domain/AuthType.java @@ -0,0 +1,11 @@ +package com.rbkmoney.provider.googlepay.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Created by vpankrashkin on 28.05.18. + */ +public enum AuthType { + @JsonProperty("3DS") + AUTH_3DS +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/domain/Card.java b/src/main/java/com/rbkmoney/provider/googlepay/domain/Card.java new file mode 100644 index 0000000..f8ce835 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/domain/Card.java @@ -0,0 +1,59 @@ +package com.rbkmoney.provider.googlepay.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Created by vpankrashkin on 28.05.18. + */ +public class Card extends PaymentCredential { + private String pan; + private int expirationMonth; + private int expirationYear; + + @JsonCreator + public Card( + @JsonProperty(value = "pan", required = true) String pan, + @JsonProperty(value = "expirationMonth", required = true) int expirationMonth, + @JsonProperty(value = "expirationYear", required = true) int expirationYear) { + this.pan = pan; + this.expirationMonth = expirationMonth; + this.expirationYear = expirationYear; + } + + public Card() { + } + + public String getPan() { + return pan; + } + + public void setPan(String pan) { + this.pan = pan; + } + + public int getExpirationMonth() { + return expirationMonth; + } + + public void setExpirationMonth(int expirationMonth) { + this.expirationMonth = expirationMonth; + } + + public int getExpirationYear() { + return expirationYear; + } + + public void setExpirationYear(int expirationYear) { + this.expirationYear = expirationYear; + } + + @Override + public String toString() { + return "Card{" + + "pan='" + (pan == null ? null: "***") + '\'' + + ", expirationMonth=" + "**" + + ", expirationYear=" + "****" + + '}'; + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/domain/CardInfo.java b/src/main/java/com/rbkmoney/provider/googlepay/domain/CardInfo.java new file mode 100644 index 0000000..4675201 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/domain/CardInfo.java @@ -0,0 +1,73 @@ +package com.rbkmoney.provider.googlepay.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Created by vpankrashkin on 28.05.18. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class CardInfo { + private String cardDescription; + private String cardClass; + private String last4digits; + private String cardNetwork; + + @JsonCreator + public CardInfo( + @JsonProperty(value = "cardDescription", required = true) String cardDescription, + @JsonProperty(value = "cardClass", required = true) String cardClass, + @JsonProperty(value = "cardDetails", required = true) String last4digits, + @JsonProperty(value = "cardNetwork", required = true) String cardNetwork) { + this.cardDescription = cardDescription; + this.cardClass = cardClass; + this.last4digits = last4digits; + this.cardNetwork = cardNetwork; + } + + public CardInfo() { + } + + public String getCardDescription() { + return cardDescription; + } + + public void setCardDescription(String cardDescription) { + this.cardDescription = cardDescription; + } + + public String getCardNetwork() { + return cardNetwork; + } + + public void setCardNetwork(String cardNetwork) { + this.cardNetwork = cardNetwork; + } + + public String getCardClass() { + return cardClass; + } + + public void setCardClass(String cardClass) { + this.cardClass = cardClass; + } + + public String getLast4digits() { + return last4digits; + } + + public void setLast4digits(String last4digits) { + this.last4digits = last4digits; + } + + @Override + public String toString() { + return "CardInfo{" + + "cardDescription='" + cardDescription + '\'' + + ", cardClass='" + cardClass + '\'' + + ", last4digits='" + last4digits + '\'' + + ", cardNetwork='" + cardNetwork + '\'' + + '}'; + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/domain/DecryptedMessage.java b/src/main/java/com/rbkmoney/provider/googlepay/domain/DecryptedMessage.java new file mode 100644 index 0000000..8ef4d53 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/domain/DecryptedMessage.java @@ -0,0 +1,100 @@ +package com.rbkmoney.provider.googlepay.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.rbkmoney.provider.googlepay.service.DecryptedMessageDeserializer; + +import java.time.Instant; +import java.util.Map; + +/** + * Created by vpankrashkin on 28.05.18. + */ +@JsonDeserialize(using = DecryptedMessageDeserializer.class) +public class DecryptedMessage { + private Instant expiration; + private String messageId; + private String gatewayMerchantId; + private PaymentMethod paymentMethod; + private PaymentCredential paymentCredential; + private Map paymentCredentialMap; + + + public DecryptedMessage() { + } + + @JsonCreator + public DecryptedMessage( + @JsonProperty(value = "messageExpiration", required = true) Instant expiration, + @JsonProperty(value = "messageId", required = true) String messageId, + @JsonProperty(value = "gatewayMerchantId", required = true) String gatewayMerchantId, + @JsonProperty(value = "paymentMethod", required = true) PaymentMethod paymentMethod, + @JsonProperty(value = "paymentMethodDetails", required = true) Map paymentCredentialMap) { + this.expiration = expiration; + this.messageId = messageId; + this.gatewayMerchantId = gatewayMerchantId; + this.paymentMethod = paymentMethod; + this.paymentCredentialMap = paymentCredentialMap; + } + + public Instant getExpiration() { + return expiration; + } + + public void setExpiration(Instant expiration) { + this.expiration = expiration; + } + + public String getMessageId() { + return messageId; + } + + public void setMessageId(String messageId) { + this.messageId = messageId; + } + + public String getGatewayMerchantId() { + return gatewayMerchantId; + } + + public void setGatewayMerchantId(String gatewayMerchantId) { + this.gatewayMerchantId = gatewayMerchantId; + } + + public PaymentMethod getPaymentMethod() { + return paymentMethod; + } + + public void setPaymentMethod(PaymentMethod paymentMethod) { + this.paymentMethod = paymentMethod; + } + + public PaymentCredential getPaymentCredential() { + return paymentCredential; + } + + public void setPaymentCredential(PaymentCredential paymentCredential) { + this.paymentCredential = paymentCredential; + } + + public Map getPaymentCredentialMap() { + return paymentCredentialMap; + } + + public void setPaymentCredentialMap(Map paymentCredentialMap) { + this.paymentCredentialMap = paymentCredentialMap; + } + + @Override + public String toString() { + return "DecryptedMessage{" + + "expiration=" + expiration + + ", messageId='" + messageId + '\'' + + ", gatewayMerchantId='" + gatewayMerchantId + '\'' + + ", paymentMethod=" + paymentMethod + + ", paymentCredential=" + paymentCredential + + ", paymentCredentialMap=" + paymentCredentialMap.size() + + '}'; + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentCredential.java b/src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentCredential.java new file mode 100644 index 0000000..cf32a69 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentCredential.java @@ -0,0 +1,8 @@ +package com.rbkmoney.provider.googlepay.domain; + +/** + * Created by vpankrashkin on 28.05.18. + */ + +public class PaymentCredential { +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentData.java b/src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentData.java new file mode 100644 index 0000000..8d14a29 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentData.java @@ -0,0 +1,49 @@ +package com.rbkmoney.provider.googlepay.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Created by vpankrashkin on 28.05.18. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class PaymentData { + private CardInfo cardInfo; + private PaymentToken paymentMethodToken; + + @JsonCreator + public PaymentData( + @JsonProperty(value = "paymentMethodToken", required = true) PaymentToken paymentMethodToken, + @JsonProperty(value = "cardInfo", required = true) CardInfo cardInfo) { + this.paymentMethodToken = paymentMethodToken; + this.cardInfo = cardInfo; + } + + public PaymentData() { + } + + public PaymentToken getPaymentMethodToken() { + return paymentMethodToken; + } + + public void setPaymentMethodToken(PaymentToken paymentMethodToken) { + this.paymentMethodToken = paymentMethodToken; + } + + public CardInfo getCardInfo() { + return cardInfo; + } + + public void setCardInfo(CardInfo cardInfo) { + this.cardInfo = cardInfo; + } + + @Override + public String toString() { + return "PaymentData{" + + "cardInfo=" + cardInfo + + ", paymentMethodToken=" + paymentMethodToken + + '}'; + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentMethod.java b/src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentMethod.java new file mode 100644 index 0000000..e7fba4e --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentMethod.java @@ -0,0 +1,13 @@ +package com.rbkmoney.provider.googlepay.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Created by vpankrashkin on 28.05.18. + */ +public enum PaymentMethod { + @JsonProperty("TOKENIZED_CARD") + TOKENIZED, + @JsonProperty("CARD") + CARD +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentToken.java b/src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentToken.java new file mode 100644 index 0000000..c8106b6 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/domain/PaymentToken.java @@ -0,0 +1,47 @@ +package com.rbkmoney.provider.googlepay.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Created by vpankrashkin on 28.05.18. + */ +public class PaymentToken { + private String token; + private TokenizationType tokenizationType; + + @JsonCreator + public PaymentToken( + @JsonProperty(value = "tokenizationType", required = true) TokenizationType tokenizationType, + @JsonProperty(value = "token", required = true) String token) { + this.tokenizationType = tokenizationType; + this.token = token; + } + + public PaymentToken() { + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public TokenizationType getTokenizationType() { + return tokenizationType; + } + + public void setTokenizationType(TokenizationType tokenizationType) { + this.tokenizationType = tokenizationType; + } + + @Override + public String toString() { + return "PaymentToken{" + + "token='" + token + '\'' + + ", tokenizationType=" + tokenizationType + + '}'; + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/domain/TokenizationType.java b/src/main/java/com/rbkmoney/provider/googlepay/domain/TokenizationType.java new file mode 100644 index 0000000..7227ac7 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/domain/TokenizationType.java @@ -0,0 +1,13 @@ +package com.rbkmoney.provider.googlepay.domain; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Created by vpankrashkin on 28.05.18. + */ +public enum TokenizationType { + @JsonProperty("PAYMENT_GATEWAY") + GATEWAY, + //@JsonProperty("DIRECT") - not supported + //DIRECT +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/domain/TokenizedCard.java b/src/main/java/com/rbkmoney/provider/googlepay/domain/TokenizedCard.java new file mode 100644 index 0000000..df0fb8f --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/domain/TokenizedCard.java @@ -0,0 +1,86 @@ +package com.rbkmoney.provider.googlepay.domain; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.rbkmoney.provider.googlepay.service.TokenizedCardDeserializer; + +/** + * Created by vpankrashkin on 28.05.18. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonDeserialize(using = TokenizedCardDeserializer.class) +public class TokenizedCard extends PaymentCredential { + private String dpan; + private int expirationMonth; + private int expirationYear; + private AuthType authType; + private Auth auth; + + @JsonCreator + public TokenizedCard( + @JsonProperty(value = "dpan", required = true) String dpan, + @JsonProperty(value = "expirationMonth", required = true) int expirationMonth, + @JsonProperty(value = "expirationYear", required = true) int expirationYear, + @JsonProperty(value = "authMethod", required = true) AuthType authType) { + this.dpan = dpan; + this.expirationMonth = expirationMonth; + this.expirationYear = expirationYear; + this.authType = authType; + } + + public TokenizedCard() { + } + + public String getDpan() { + return dpan; + } + + public void setDpan(String dpan) { + this.dpan = dpan; + } + + public int getExpirationMonth() { + return expirationMonth; + } + + public void setExpirationMonth(int expirationMonth) { + this.expirationMonth = expirationMonth; + } + + public int getExpirationYear() { + return expirationYear; + } + + public void setExpirationYear(int expirationYear) { + this.expirationYear = expirationYear; + } + + public AuthType getAuthType() { + return authType; + } + + public void setAuthType(AuthType authType) { + this.authType = authType; + } + + public Auth getAuth() { + return auth; + } + + public void setAuth(Auth auth) { + this.auth = auth; + } + + @Override + public String toString() { + return "TokenizedCard{" + + "dpan='" + (dpan == null ? null : "***") + '\'' + + ", expirationMonth=" + "**" + + ", expirationYear=" + "****" + + ", authType=" + authType + + ", auth=" + auth + + "} "; + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/iface/decrypt/ProviderHandler.java b/src/main/java/com/rbkmoney/provider/googlepay/iface/decrypt/ProviderHandler.java new file mode 100644 index 0000000..686f5ed --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/iface/decrypt/ProviderHandler.java @@ -0,0 +1,144 @@ +package com.rbkmoney.provider.googlepay.iface.decrypt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectReader; +import com.rbkmoney.damsel.base.Content; +import com.rbkmoney.damsel.base.InvalidRequest; +import com.rbkmoney.damsel.domain.BankCardPaymentSystem; +import com.rbkmoney.damsel.payment_tool_provider.*; +import com.rbkmoney.geck.common.util.TypeUtil; +import com.rbkmoney.provider.googlepay.domain.DecryptedMessage; +import com.rbkmoney.provider.googlepay.domain.PaymentData; +import com.rbkmoney.provider.googlepay.domain.PaymentToken; +import com.rbkmoney.provider.googlepay.service.CryptoException; +import com.rbkmoney.provider.googlepay.service.DecryptionService; +import com.rbkmoney.provider.googlepay.service.ValidationException; +import com.rbkmoney.provider.googlepay.service.ValidationService; +import org.apache.thrift.TException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Optional; + +/** + * Created by vpankrashkin on 03.04.18. + */ +public class ProviderHandler implements PaymentToolProviderSrv.Iface { + private final Logger log = LoggerFactory.getLogger(this.getClass()); + + private final ValidationService validator; + private final DecryptionService decryptor; + private final ObjectReader srcReader = new ObjectMapper().readerFor(PaymentData.class); + private final ObjectReader resReader = new ObjectMapper().readerFor(DecryptedMessage.class); + private final boolean enableValidation; + + public ProviderHandler(ValidationService validator, DecryptionService decryptor, boolean enableValidation) { + this.validator = validator; + this.decryptor = decryptor; + this.enableValidation = enableValidation; + } + + @Override + public UnwrappedPaymentTool unwrap(WrappedPaymentTool payment_tool) throws InvalidRequest, TException { + log.info("New unwrap request: {}", payment_tool); + Content content = payment_tool.getRequest().getGoogle().getPaymentToken(); + if (!content.getType().equalsIgnoreCase("application/json")) { + throw new InvalidRequest(Arrays.asList("Wrong content type")); + } + + try { + PaymentData paymentData = srcReader.readValue(content.getData()); + PaymentToken paymentToken = paymentData.getPaymentMethodToken(); + + String tokenData = decryptor.decryptToken(paymentToken.getToken()); + log.info("Payment data decrypted"); + DecryptedMessage decryptedMessage = resReader.readValue(tokenData); + log.info("Decryption result: {}", decryptedMessage); + + if (enableValidation) { + validator.validate(payment_tool.getRequest().getGoogle().getGatewayMerchantId(), decryptedMessage); + log.info("Request successfully validated"); + } else { + log.info("Request validation skipped"); + } + UnwrappedPaymentTool result = new UnwrappedPaymentTool(); + result.setCardInfo(extractCardInfo(paymentData.getCardInfo())); + result.setPaymentData(extractPaymentData(decryptedMessage)); + result.setDetails(extractPaymentDetails(decryptedMessage)); + + UnwrappedPaymentTool logResult = new UnwrappedPaymentTool(result); + if (logResult.getPaymentData().isSetCard()) { + logResult.getPaymentData().setCard(new Card()); + } else { + logResult.getPaymentData().setTokenizedCard(new TokenizedCard()); + } + log.info("Unwrap partial result: {}", logResult); + return result; + } catch (IOException e) { + log.error("Failed to read json data: {}", filterPan(e.getMessage())); + throw new InvalidRequest(Arrays.asList("Failed to read json data")); + } catch (ValidationException e) { + log.error("Failed to validate request", e); + throw new InvalidRequest(Arrays.asList(e.getMessage())); + } catch (CryptoException e) { + log.error("Decryption error", e); + throw new RuntimeException(e); + } + } + + private String filterPan(String src) { + return src.replaceAll("([^\\d])\\d{8,19}([^\\d])", "$1***$2"); + } + + private CardInfo extractCardInfo(com.rbkmoney.provider.googlepay.domain.CardInfo srcCardInfo) { + CardInfo cardInfo = new CardInfo(); + cardInfo.setDisplayName(srcCardInfo.getCardDescription()); + cardInfo.setLast4Digits(srcCardInfo.getLast4digits()); + cardInfo.setCardClass(TypeUtil.toEnumField( + Optional.of(srcCardInfo.getCardClass()).map(s -> s.toLowerCase()).orElse(null), + CardClass.class, CardClass.unknown)); + cardInfo.setPaymentSystem(TypeUtil.toEnumField( + Optional.ofNullable(srcCardInfo.getCardNetwork()).map(s -> s.toLowerCase()).orElse(null), + BankCardPaymentSystem.class, null)); + return cardInfo; + } + + private CardPaymentData extractPaymentData(DecryptedMessage decryptedMessage) { + CardPaymentData cardPaymentData = new CardPaymentData(); + switch (decryptedMessage.getPaymentMethod()) { + case CARD: + com.rbkmoney.provider.googlepay.domain.Card srcCard = (com.rbkmoney.provider.googlepay.domain.Card) decryptedMessage.getPaymentCredential(); + Card card = new Card(srcCard.getPan(), new ExpDate((byte) srcCard.getExpirationMonth(), (short) srcCard.getExpirationYear())); + cardPaymentData.setCard(card); + break; + case TOKENIZED: + com.rbkmoney.provider.googlepay.domain.TokenizedCard srcToken = (com.rbkmoney.provider.googlepay.domain.TokenizedCard) decryptedMessage.getPaymentCredential(); + AuthData authData; + switch (srcToken.getAuthType()) { + case AUTH_3DS: + com.rbkmoney.provider.googlepay.domain.Auth3DS srcAuth3DS = (com.rbkmoney.provider.googlepay.domain.Auth3DS) srcToken.getAuth(); + Auth3DS auth3DS = new Auth3DS(srcAuth3DS.getCryptogram()); + auth3DS.setEci(srcAuth3DS.getEci()); + authData = AuthData.auth_3ds(auth3DS); + break; + default: + throw new ValidationException("Unknown type:" + srcToken.getAuthType()); + } + TokenizedCard tokenizedCard = new TokenizedCard( + srcToken.getDpan(), + new ExpDate((byte) srcToken.getExpirationMonth(), (short) srcToken.getExpirationYear()), + authData); + cardPaymentData.setTokenizedCard(tokenizedCard); + break; + default: + throw new ValidationException("Unknown type:" + decryptedMessage.getPaymentMethod()); + } + return cardPaymentData; + } + + private PaymentDetails extractPaymentDetails(DecryptedMessage decryptedMessage) { + return PaymentDetails.google(new GooglePayDetails(decryptedMessage.getMessageId(), TypeUtil.temporalToString(decryptedMessage.getExpiration()))); + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/iface/decrypt/ProviderServlet.java b/src/main/java/com/rbkmoney/provider/googlepay/iface/decrypt/ProviderServlet.java new file mode 100644 index 0000000..b4c912e --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/iface/decrypt/ProviderServlet.java @@ -0,0 +1,27 @@ +package com.rbkmoney.provider.googlepay.iface.decrypt; + +import com.rbkmoney.damsel.payment_tool_provider.PaymentToolProviderSrv; +import com.rbkmoney.woody.thrift.impl.http.THServiceBuilder; + +import javax.servlet.*; +import javax.servlet.annotation.WebServlet; +import java.io.IOException; + +/** + * Created by vpankrashkin on 29.05.18. + */ +@WebServlet("/provider/google") +public class ProviderServlet extends GenericServlet { + private final Servlet handlerServlet; + + public ProviderServlet(PaymentToolProviderSrv.Iface handler) { + this.handlerServlet = new THServiceBuilder() + .build(PaymentToolProviderSrv.Iface.class, handler); + } + + @Override + public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { + handlerServlet.service(req, res); + } + +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/service/CryptoException.java b/src/main/java/com/rbkmoney/provider/googlepay/service/CryptoException.java new file mode 100644 index 0000000..e3eb259 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/service/CryptoException.java @@ -0,0 +1,25 @@ +package com.rbkmoney.provider.googlepay.service; + +/** + * Created by vpankrashkin on 12.04.18. + */ +public class CryptoException extends Exception { + public CryptoException() { + } + + public CryptoException(String message) { + super(message); + } + + public CryptoException(String message, Throwable cause) { + super(message, cause); + } + + public CryptoException(Throwable cause) { + super(cause); + } + + public CryptoException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/service/DecryptedMessageDeserializer.java b/src/main/java/com/rbkmoney/provider/googlepay/service/DecryptedMessageDeserializer.java new file mode 100644 index 0000000..46f5017 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/service/DecryptedMessageDeserializer.java @@ -0,0 +1,59 @@ +package com.rbkmoney.provider.googlepay.service; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.rbkmoney.provider.googlepay.domain.Card; +import com.rbkmoney.provider.googlepay.domain.DecryptedMessage; +import com.rbkmoney.provider.googlepay.domain.TokenizedCard; + +import java.io.IOException; + +/** + * Created by vpankrashkin on 28.05.18. + */ +public class DecryptedMessageDeserializer extends StdDeserializer { + private final ObjectMapper mapper; + + + public DecryptedMessageDeserializer() { + super(DecryptedMessage.class); + JacksonAnnotationIntrospector introspector = new JacksonAnnotationIntrospector() { + @Override + public Object findDeserializer(Annotated a) { + if (a.getRawType() == DecryptedMessage.class) + return null; + return super.findDeserializer(a); + } + }; + mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS); + mapper.setAnnotationIntrospector(introspector); + } + + @Override + public DecryptedMessage deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException, JsonProcessingException { + TreeNode root = parser.readValueAsTree(); + + DecryptedMessage message = mapper.treeToValue(root, DecryptedMessage.class); + + switch (message.getPaymentMethod()) { + case CARD: + message.setPaymentCredential(mapper.convertValue(message.getPaymentCredentialMap(), Card.class)); + break; + case TOKENIZED: + message.setPaymentCredential(mapper.convertValue(message.getPaymentCredentialMap(), TokenizedCard.class)); + break; + } + + return message; + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/service/DecryptionService.java b/src/main/java/com/rbkmoney/provider/googlepay/service/DecryptionService.java new file mode 100644 index 0000000..b534d26 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/service/DecryptionService.java @@ -0,0 +1,24 @@ +package com.rbkmoney.provider.googlepay.service; + +import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenRecipient; + +import java.security.GeneralSecurityException; + +/** + * Created by vpankrashkin on 29.05.18. + */ +public class DecryptionService { + private final PaymentMethodTokenRecipient decryptor; + + public DecryptionService(PaymentMethodTokenRecipient decryptor) { + this.decryptor = decryptor; + } + + public String decryptToken(String paymentToken) throws CryptoException { + try { + return decryptor.unseal(paymentToken); + } catch (GeneralSecurityException e) { + throw new CryptoException("Message decryption failed", e); + } + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/service/GPKeyStore.java b/src/main/java/com/rbkmoney/provider/googlepay/service/GPKeyStore.java new file mode 100644 index 0000000..6f26834 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/service/GPKeyStore.java @@ -0,0 +1,22 @@ +package com.rbkmoney.provider.googlepay.service; + +import java.io.IOException; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; + +/** + * Created by vpankrashkin on 29.05.18. + */ +public class GPKeyStore { + private final URI path; + + public GPKeyStore(URI path) { + this.path = path; + } + + public List getKeys() throws IOException { + return Files.readAllLines(Paths.get(path)); + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/service/TokenizedCardDeserializer.java b/src/main/java/com/rbkmoney/provider/googlepay/service/TokenizedCardDeserializer.java new file mode 100644 index 0000000..178d0df --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/service/TokenizedCardDeserializer.java @@ -0,0 +1,48 @@ +package com.rbkmoney.provider.googlepay.service; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.Annotated; +import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector; +import com.rbkmoney.provider.googlepay.domain.Auth3DS; +import com.rbkmoney.provider.googlepay.domain.TokenizedCard; + +import java.io.IOException; + +/** + * Created by vpankrashkin on 28.05.18. + */ +public class TokenizedCardDeserializer extends JsonDeserializer { + private final ObjectMapper mapper; + + public TokenizedCardDeserializer() { + JacksonAnnotationIntrospector introspector = new JacksonAnnotationIntrospector() { + @Override + public Object findDeserializer(Annotated a) { + if (a.getRawType() == TokenizedCard.class) + return null; + return super.findDeserializer(a); + } + }; + mapper = new ObjectMapper(); + mapper.setAnnotationIntrospector(introspector); + } + + @Override + public TokenizedCard deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException, JsonProcessingException { + TreeNode root = parser.readValueAsTree(); + TokenizedCard tokenizedCard = mapper.treeToValue(root, TokenizedCard.class); + + switch (tokenizedCard.getAuthType()) { + case AUTH_3DS: + tokenizedCard.setAuth(mapper.reader().treeToValue(root, Auth3DS.class)); + break; + } + + return tokenizedCard; + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/service/ValidationException.java b/src/main/java/com/rbkmoney/provider/googlepay/service/ValidationException.java new file mode 100644 index 0000000..440bfc2 --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/service/ValidationException.java @@ -0,0 +1,25 @@ +package com.rbkmoney.provider.googlepay.service; + +/** + * Created by vpankrashkin on 28.05.18. + */ +public class ValidationException extends RuntimeException { + public ValidationException() { + } + + public ValidationException(String message) { + super(message); + } + + public ValidationException(String message, Throwable cause) { + super(message, cause); + } + + public ValidationException(Throwable cause) { + super(cause); + } + + public ValidationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/src/main/java/com/rbkmoney/provider/googlepay/service/ValidationService.java b/src/main/java/com/rbkmoney/provider/googlepay/service/ValidationService.java new file mode 100644 index 0000000..ffe1bbe --- /dev/null +++ b/src/main/java/com/rbkmoney/provider/googlepay/service/ValidationService.java @@ -0,0 +1,20 @@ +package com.rbkmoney.provider.googlepay.service; + +import com.rbkmoney.provider.googlepay.domain.DecryptedMessage; + +import java.time.Instant; + +/** + * Created by vpankrashkin on 29.05.18. + */ +public class ValidationService { + public void validate(String gMerchantId, DecryptedMessage message) throws ValidationException { + if (!gMerchantId.equals(message.getGatewayMerchantId())) { + throw new ValidationException(String.format("Merchant ID: '%s' doesn't match decrypted one: '%s'", gMerchantId, message.getGatewayMerchantId())); + } + + if (Instant.now().isAfter(message.getExpiration())) { + throw new ValidationException("Message expired: "+message.getExpiration()); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..8cc2f7a --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,11 @@ +server.port=@server.port@ + +spring.application.name=@project.name@ +info.version=@project.version@ +info.damsel.version=@damsel.version@ + +google.test=true +google.gateway_id=rbkmoney +google.keys_path=classpath:keys.txt +google.use_validation=true + diff --git a/src/test/java/com/rbkmoney/provider/googlepay/DataTest.java b/src/test/java/com/rbkmoney/provider/googlepay/DataTest.java new file mode 100644 index 0000000..0505ade --- /dev/null +++ b/src/test/java/com/rbkmoney/provider/googlepay/DataTest.java @@ -0,0 +1,82 @@ +package com.rbkmoney.provider.googlepay; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.crypto.tink.apps.paymentmethodtoken.GooglePaymentsPublicKeysManager; +import com.google.crypto.tink.apps.paymentmethodtoken.PaymentMethodTokenRecipient; +import com.rbkmoney.provider.googlepay.domain.DecryptedMessage; +import com.rbkmoney.provider.googlepay.domain.PaymentData; +import com.rbkmoney.provider.googlepay.service.GPKeyStore; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; + +/** + * Created by vpankrashkin on 25.05.18.Ø + */ +public class DataTest { + + static { + GooglePaymentsPublicKeysManager.INSTANCE_TEST.refreshInBackground(); + } + + @org.junit.Test + public void test() throws GeneralSecurityException, IOException, URISyntaxException { + String paymentMethodToken = "{\"signature\":\"MEUCIQDiT2/dI+H7LiiP0Yu/p8Z62Laumnk/js9cQrTUpYufIAIgN+e3tDoBDnqk6pt+MEwsVWD3oq/DfbbcLPJgdo3AMfM\\u003d\",\"protocolVersion\":\"ECv1\",\"signedMessage\":\"{\\\"encryptedMessage\\\":\\\"k84ZAjkWfAibZmWxJfBJ8+3lhNiVvPrMYvLsZQwSt5dEmvO1S7davQ5rq04MNLKjwRezz301L6kDxv6abe2M7Z2KIM/tlmgtsAlV2xbOxS2piChvEeB0RyVbAxjLKfL5vnOlxivLgoPbCUrm7/hSwxsPhjVvyTXMCBbo744ZnIINuk8W3aROtFnGBqdzWSEjmlrhUBHoTJ1IEMuowoklUyI2Qd8pCQHA4it8mvITPE/KDQNt4J4HmNAx1GkW/Va9SEHZXP/J5xNjFeijshDZgd9oam1JB2yg3sgIh3e2JmgkVChqHoilaEdjKTLJc95FlJfbZyxytmh4E7jxvJ//vB5ySGV1pyVVRT8IX6K2oIXjsJ+JAZQmr7/MNo7W/cq6guz07nFHMqk8FCJbsWA75krj4Y9Ls8QkDyI7ccayapE2N9/QP5koLglASxWFfUEgWyWPWA\\\\u003d\\\\u003d\\\",\\\"ephemeralPublicKey\\\":\\\"BLhY0C4bLP+Cv6AKH7Hz2YKICO2QqFZYxFDOWZ07S0LfEAwCmiusTIW/gz377AveU3Fw5JNKZPNjHkBG4OkEnKk\\\\u003d\\\",\\\"tag\\\":\\\"JvdgTyndj5fDugbG3rEoN52adgA/x24aGmRAKyoqxrk\\\\u003d\\\"}\"}"; + PaymentMethodTokenRecipient.Builder builder = new PaymentMethodTokenRecipient.Builder(); + builder.fetchSenderVerifyingKeysWith( + GooglePaymentsPublicKeysManager.INSTANCE_TEST) + .recipientId("gateway:rbkmoney"); + for (String key: new GPKeyStore(this.getClass().getClassLoader().getResource("keys.txt").toURI()).getKeys()) { + builder.addRecipientPrivateKey(key); + } + String decryptedMessage = builder.build() + .unseal(paymentMethodToken); + System.out.println(decryptedMessage); + } + + @org.junit.Test + public void testDeserializationEncryptedMsg() throws IOException { + String encCard = "{\n" + + " \"cardInfo\": {\n" + + " \"cardNetwork\": \"VISA\",\n" + + " \"cardDetails\": \"9391\",\n" + + " \"cardImageUri\": \"https://lh6.ggpht.com/NvYf_33MleY1waJfW6O98wb3KU6XeinwiahmvUIyu46LcWeQdTMGm7WYe81uZYWLUbkjvz0E\",\n" + + " \"cardDescription\": \"Visa •••• 9391\",\n" + + " \"cardClass\": \"CREDIT\"\n" + + " },\n" + + " \"paymentMethodToken\": {\n" + + " \"tokenizationType\": \"PAYMENT_GATEWAY\",\n" + + " \"token\": \"{\\\"signature\\\":\\\"MEUCIQCBj5ClFzpJTg3UB4yKdXRP5O12vqpcTCM+x51/99ugYAIgPnCmr8k2G4oKWQqxmAawPgTd42vX1rx91eyqIHgSju4\\\\u003d\\\",\\\"protocolVersion\\\":\\\"ECv1\\\",\\\"signedMessage\\\":\\\"{\\\\\\\"encryptedMessage\\\\\\\":\\\\\\\"tSj1k3qfKoVg5rcZg8uOzMaGrk31qTIUkjYehgjV7BVAC/SGVfPE40q9PHppoZJqFsRwM1xMKKpoZatHa/fct/2YlVwxySxNR5uu3PHR4ZzuWb7vh/Nnb8BWlSiUMMELLaHFA1+FR2zbLpgEG9QdmeQRpyq07SETaH43WF0MLTDbKr9Con3SLZVCnoNS6CZZsPVTYNm0IjLWABAf66O5VlMWAPwke+kMaiJmq5ttPaxn6zGVJq4/JT7T7iKuc/T0rWDh7BzzVEzi/wyvaYDl2SkOljqFcjNYMTEr76GHlsgpAjtzCCPJkPJ9m7wkd3v+mgdQgazJ+ZwFvKTk8Ru7wZ4eywgb1G36/+IBpH/7936PvkayOTuXgtQkjts027mnH9/SyRA3unoJrNXRpUPytxwVO048edVUBKB9Q4ZZYadeGErH4xRTg64fPS9KiHSswBeY\\\\\\\",\\\\\\\"ephemeralPublicKey\\\\\\\":\\\\\\\"BLeiRaJ1+9DkK9x8rPK1377KKgQVv3kSbomJZxoTgqaOugBa8PAR3QDIH3VfqscW8zfMbrNrA736Nm4AahbogaU\\\\\\\\u003d\\\\\\\",\\\\\\\"tag\\\\\\\":\\\\\\\"oBPVR6dw8t4GM0ZC3QLd45hcx6oeag5D98BSdIsV0No\\\\\\\\u003d\\\\\\\"}\\\"}\"\n" + + " },\n" + + " \"email\": \"keinasylum@gmail.com\"\n" + + "}"; + + PaymentData paymentData = new ObjectMapper().readerFor(PaymentData.class).readValue(encCard); + System.out.println(paymentData); + } + + @org.junit.Test + public void testDeserializationDecryptedMsg() throws IOException { + String decCard = "{\"gatewayMerchantId\":\"fadsfasdf\",\"messageExpiration\":\"1527861216388\",\"messageId\":\"AH2EjteSBP3heqxYKyKkh88NzU95u3sIc6Kup-vwgbF9k3_SzwWBylswvYvHSEdjwzYorh-tQ1VGEvTGht4c2CJdb0Pm9eDzfKtiIjstdl0QcbUWcNQM83ujxFhBmKcHG809dgnsWuOB\",\"paymentMethod\":\"CARD\",\"paymentMethodDetails\":{\"expirationYear\":2023,\"expirationMonth\":12,\"pan\":\"4111111111111111\"}}"; + DecryptedMessage decCardMsg = new ObjectMapper().readerFor(DecryptedMessage.class).readValue(decCard); + System.out.println(decCardMsg); + String decToken = "{\n" + + "\"gatewayMerchantId\":\"fadsfasdf\","+ + " \"paymentMethod\": \"TOKENIZED_CARD\",\n" + + " \"paymentMethodDetails\": {\n" + + " \"dpan\": \"4444444444444444\",\n" + + " \"expirationMonth\": 10,\n" + + " \"expirationYear\": 2020,\n" + + " \"authMethod\": \"3DS\",\n" + + " \"3dsCryptogram\": \"AAAAAA...\",\n" + + " \"3dsEciIndicator\": \"eci indicator\"\n" + + " },\n" + + " \n" + + " \"messageId\": \"some-message-id\",\n" + + " \"messageExpiration\": \"1520836260646\"\n" + + "}"; + DecryptedMessage decTokenMsg = new ObjectMapper().readerFor(DecryptedMessage.class).readValue(decToken); + System.out.println(decTokenMsg); + } +} diff --git a/src/test/java/com/rbkmoney/provider/googlepay/IntegrationTest.java b/src/test/java/com/rbkmoney/provider/googlepay/IntegrationTest.java new file mode 100644 index 0000000..4ca9e68 --- /dev/null +++ b/src/test/java/com/rbkmoney/provider/googlepay/IntegrationTest.java @@ -0,0 +1,86 @@ +package com.rbkmoney.provider.googlepay; + +import com.rbkmoney.damsel.base.Content; +import com.rbkmoney.damsel.base.InvalidRequest; +import com.rbkmoney.damsel.domain.BankCardPaymentSystem; +import com.rbkmoney.damsel.payment_tool_provider.*; +import com.rbkmoney.woody.thrift.impl.http.THClientBuilder; +import org.apache.thrift.TException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.time.Instant; + +import static org.junit.Assert.*; + +/** + * Created by vpankrashkin on 29.05.18. + */ +@RunWith(SpringRunner.class) +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT +) +public class IntegrationTest { + @Value("http://127.0.0.1:${server.port}/provider/google") + private String url; + + private PaymentToolProviderSrv.Iface client; + + private String encMsg = "{\n" + + " \"cardInfo\": {\n" + + " \"cardNetwork\": \"VISA\",\n" + + " \"cardDetails\": \"9391\",\n" + + " \"cardImageUri\": \"https://lh6.ggpht.com/NvYf_33MleY1waJfW6O98wb3KU6XeinwiahmvUIyu46LcWeQdTMGm7WYe81uZYWLUbkjvz0E\",\n" + + " \"cardDescription\": \"Visa •••• 9391\",\n" + + " \"cardClass\": \"CREDIT\"\n" + + " },\n" + + " \"paymentMethodToken\": {\n" + + " \"tokenizationType\": \"PAYMENT_GATEWAY\",\n" + + " \"token\": \"{\\\"signature\\\":\\\"MEUCIQCBj5ClFzpJTg3UB4yKdXRP5O12vqpcTCM+x51/99ugYAIgPnCmr8k2G4oKWQqxmAawPgTd42vX1rx91eyqIHgSju4\\\\u003d\\\",\\\"protocolVersion\\\":\\\"ECv1\\\",\\\"signedMessage\\\":\\\"{\\\\\\\"encryptedMessage\\\\\\\":\\\\\\\"tSj1k3qfKoVg5rcZg8uOzMaGrk31qTIUkjYehgjV7BVAC/SGVfPE40q9PHppoZJqFsRwM1xMKKpoZatHa/fct/2YlVwxySxNR5uu3PHR4ZzuWb7vh/Nnb8BWlSiUMMELLaHFA1+FR2zbLpgEG9QdmeQRpyq07SETaH43WF0MLTDbKr9Con3SLZVCnoNS6CZZsPVTYNm0IjLWABAf66O5VlMWAPwke+kMaiJmq5ttPaxn6zGVJq4/JT7T7iKuc/T0rWDh7BzzVEzi/wyvaYDl2SkOljqFcjNYMTEr76GHlsgpAjtzCCPJkPJ9m7wkd3v+mgdQgazJ+ZwFvKTk8Ru7wZ4eywgb1G36/+IBpH/7936PvkayOTuXgtQkjts027mnH9/SyRA3unoJrNXRpUPytxwVO048edVUBKB9Q4ZZYadeGErH4xRTg64fPS9KiHSswBeY\\\\\\\",\\\\\\\"ephemeralPublicKey\\\\\\\":\\\\\\\"BLeiRaJ1+9DkK9x8rPK1377KKgQVv3kSbomJZxoTgqaOugBa8PAR3QDIH3VfqscW8zfMbrNrA736Nm4AahbogaU\\\\\\\\u003d\\\\\\\",\\\\\\\"tag\\\\\\\":\\\\\\\"oBPVR6dw8t4GM0ZC3QLd45hcx6oeag5D98BSdIsV0No\\\\\\\\u003d\\\\\\\"}\\\"}\"\n" + + " },\n" + + " \"email\": \"keinasylum@gmail.com\"\n" + + "}"; + + private String encMechId = "rbkmoney"; + + @Before + public void setUp() throws URISyntaxException { + client = new THClientBuilder().withAddress(new URI(url)).build(PaymentToolProviderSrv.Iface.class); + } + + @Test + public void testCard() throws TException { + UnwrappedPaymentTool res = client.unwrap(new WrappedPaymentTool(PaymentRequest.google(new GooglePayRequest(encMechId, toContent(encMsg))))); + assertEquals("4111111111111111", res.getPaymentData().getCard().getPan()); + assertEquals(new ExpDate((byte) 12, (short) 2023), res.getPaymentData().getCard().getExpDate()); + + assertEquals(CardClass.credit, res.getCardInfo().getCardClass()); + assertEquals(BankCardPaymentSystem.visa, res.getCardInfo().getPaymentSystem()); + assertEquals("Visa •••• 9391", res.getCardInfo().getDisplayName()); + assertEquals("9391", res.getCardInfo().getLast4Digits()); + + assertEquals("2018-06-05T12:36:44.549Z", res.getDetails().getGoogle().getMessageExpiration()); + assertEquals("AH2EjtfIuPojiNSCv4SarecQOmML6hX1x-0yOZImfiPubV_vd6vVc0vScpQV0ExK1eJ2C9_D1KSeg4T45QyurCF6wTCJMoIgJR4zYcak0cAZ6XXOUk_hs72b45NLHbHEvAziG7ZJmLhv", res.getDetails().getGoogle().getMessageId()); + } + + @Test(expected = InvalidRequest.class) + public void testWrongMerchantId() throws TException { + client.unwrap(new WrappedPaymentTool(PaymentRequest.google(new GooglePayRequest(encMechId+"1", toContent(encMsg))))); + } + + private Content toContent(String data) { + return new Content("application/json", ByteBuffer.wrap(data.getBytes())); + } + + + + + +} diff --git a/src/test/resources/keys.txt b/src/test/resources/keys.txt new file mode 100644 index 0000000..71f3765 --- /dev/null +++ b/src/test/resources/keys.txt @@ -0,0 +1 @@ +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgAx+oQpiveM7fhYBq8KTOdfzD3VUn1RfB4t7DCViMtIuhRANCAAQqmrw+gIJIqjpRLurRl7RjQ3LTJUrT8O3ZNG6/xxRPhjue4xcvBPFtRg0BnVB8sxSNjhGD+t2s4soIIp6YRYf/ \ No newline at end of file