From 1618e00c1b31eac1fe52d274e8ce0a5784250e92 Mon Sep 17 00:00:00 2001 From: irotech Date: Thu, 9 Mar 2023 13:51:34 +0000 Subject: [PATCH] SDK-2262: Add Digital Identity Session Receipt retrieval service --- .../api/client/DigitalIdentityClient.java | 26 +- .../identity/ShareSessionNotification.java | 6 +- .../client/identity/ShareSessionRequest.java | 18 +- .../client/identity/extension/Extension.java | 4 +- .../LocationConstraintExtensionBuilder.java | 6 +- .../extension/ThirdPartyAttributeContent.java | 18 +- .../ThirdPartyAttributeExtensionBuilder.java | 20 +- .../TransactionalFlowExtensionBuilder.java | 6 +- .../client/identity/policy/WantedAnchor.java | 7 +- .../identity/policy/WantedAttribute.java | 5 +- .../spi/remote/AttributeListReader.java | 11 +- .../client/spi/remote/ExtraDataReader.java | 5 +- .../spi/remote/call/SignedRequestBuilder.java | 2 +- .../call/factory/UnsignedPathFactory.java | 20 +- .../call/identity/DigitalIdentityService.java | 119 +++++++--- .../spi/remote/call/identity/Receipt.java | 173 ++++++++++++++ .../remote/call/identity/ReceiptItemKey.java | 79 +++++++ .../remote/call/identity/ReceiptParser.java | 114 +++++++++ .../remote/call/identity/WrappedReceipt.java | 222 ++++++++++++++++++ .../main/java/com/yoti/crypto/Algorithm.java | 17 ++ .../main/java/com/yoti/crypto/CipherType.java | 28 +++ .../src/main/java/com/yoti/crypto/Crypto.java | 68 ++++++ .../java/com/yoti/crypto/CryptoException.java | 17 ++ .../java/com/yoti/json/ResourceMapper.java | 17 +- .../api/client/DigitalIdentityClientTest.java | 9 +- .../com/yoti/json/ResourceMapperTest.java | 6 +- 26 files changed, 904 insertions(+), 119 deletions(-) create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/Receipt.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptItemKey.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptParser.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/WrappedReceipt.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/crypto/Algorithm.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/crypto/CipherType.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/crypto/Crypto.java create mode 100644 yoti-sdk-api/src/main/java/com/yoti/crypto/CryptoException.java diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/DigitalIdentityClient.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/DigitalIdentityClient.java index 3729aaffd..48c39fc9f 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/DigitalIdentityClient.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/DigitalIdentityClient.java @@ -1,8 +1,5 @@ package com.yoti.api.client; -import static com.yoti.validation.Validation.notNull; -import static com.yoti.validation.Validation.notNullOrEmpty; - import java.io.IOException; import java.security.KeyPair; import java.security.Security; @@ -13,6 +10,8 @@ import com.yoti.api.client.spi.remote.KeyStreamVisitor; import com.yoti.api.client.spi.remote.call.identity.DigitalIdentityException; import com.yoti.api.client.spi.remote.call.identity.DigitalIdentityService; +import com.yoti.api.client.spi.remote.call.identity.Receipt; +import com.yoti.validation.Validation; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -27,8 +26,8 @@ public class DigitalIdentityClient { private final DigitalIdentityService identityService; DigitalIdentityClient(String sdkId, KeyPairSource keyPair, DigitalIdentityService identityService) { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); + Validation.notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNull(keyPair, "Application Key Pair"); this.sdkId = sdkId; this.keyPair = loadKeyPair(keyPair); @@ -79,8 +78,17 @@ public ShareSessionQrCode fetchShareQrCode(String qrCodeId) throws DigitalIdenti return identityService.fetchShareQrCode(sdkId, keyPair, qrCodeId); } - public Object fetchShareReceipt(String receiptId) { - return identityService.fetchShareReceipt(receiptId); + /** + * Retrieve the decrypted share receipt. + * + *

A receipt will contain the shared user attributes.

+ * + * @param receiptId ID of the receipt + * @return Shared user attributes + * @throws DigitalIdentityException Thrown if the receipt retrieval is unsuccessful + */ + public Receipt fetchShareReceipt(String receiptId) throws DigitalIdentityException { + return identityService.fetchShareReceipt(sdkId, keyPair, receiptId); } private KeyPair loadKeyPair(KeyPairSource keyPairSource) throws InitialisationException { @@ -103,14 +111,14 @@ public static class Builder { private Builder() { } public Builder withClientSdkId(String sdkId) { - notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNullOrEmpty(sdkId, "SDK ID"); this.sdkId = sdkId; return this; } public Builder withKeyPairSource(KeyPairSource keyPairSource) { - notNull(keyPairSource, "Key Pair Source"); + Validation.notNull(keyPairSource, "Key Pair Source"); this.keyPairSource = keyPairSource; return this; diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionNotification.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionNotification.java index 99aaada1e..3c57702c5 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionNotification.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionNotification.java @@ -1,11 +1,11 @@ package com.yoti.api.client.identity; -import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; - import java.net.URI; import java.util.HashMap; import java.util.Map; +import com.yoti.validation.Validation; + import com.fasterxml.jackson.annotation.JsonProperty; public final class ShareSessionNotification { @@ -85,7 +85,7 @@ public Builder withHeader(String key, String value) { } public ShareSessionNotification build() { - notNullOrEmpty(url, Property.URL); + Validation.notNullOrEmpty(url, Property.URL); return new ShareSessionNotification(this); } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java index ca50dc5a4..7f8db0eb9 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/ShareSessionRequest.java @@ -1,8 +1,5 @@ package com.yoti.api.client.identity; -import static com.yoti.api.client.spi.remote.util.Validation.notNull; -import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; - import java.net.URI; import java.util.ArrayList; import java.util.Collections; @@ -11,6 +8,7 @@ import com.yoti.api.client.identity.extension.Extension; import com.yoti.api.client.identity.policy.Policy; +import com.yoti.validation.Validation; import com.fasterxml.jackson.annotation.JsonProperty; @@ -23,7 +21,7 @@ public class ShareSessionRequest { private final Policy policy; @JsonProperty(Property.EXTENSIONS) - private final List extensions; + private final List> extensions; @JsonProperty(Property.REDIRECT_URI) private final String redirectUri; @@ -47,7 +45,7 @@ public Policy getPolicy() { return policy; } - public List getExtensions() { + public List> getExtensions() { return extensions; } @@ -67,7 +65,7 @@ public static final class Builder { private Map subject; private Policy policy; - private List extensions; + private List> extensions; private String redirectUri; private ShareSessionNotification notification; @@ -85,12 +83,12 @@ public Builder withPolicy(Policy policy) { return this; } - public Builder withExtensions(List extensions) { + public Builder withExtensions(List> extensions) { this.extensions = Collections.unmodifiableList(extensions); return this; } - public Builder withExtension(Extension extension) { + public Builder withExtension(Extension extension) { extensions.add(extension); return this; } @@ -106,8 +104,8 @@ public Builder withNotification(ShareSessionNotification notification) { } public ShareSessionRequest build() { - notNull(policy, Property.POLICY); - notNullOrEmpty(redirectUri, Property.REDIRECT_URI); + Validation.notNull(policy, Property.POLICY); + Validation.notNullOrEmpty(redirectUri, Property.REDIRECT_URI); return new ShareSessionRequest(this); } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/Extension.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/Extension.java index b7ee1b2da..83e8e8f5c 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/Extension.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/Extension.java @@ -1,5 +1,6 @@ package com.yoti.api.client.identity.extension; +import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; public class Extension { @@ -10,7 +11,8 @@ public class Extension { @JsonProperty(Property.CONTENT) private final T content; - Extension(String type, T content) { + @JsonCreator + Extension(@JsonProperty(Property.TYPE) String type, @JsonProperty(Property.CONTENT) T content) { this.type = type; this.content = content; } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintExtensionBuilder.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintExtensionBuilder.java index 4ec604949..81248840f 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintExtensionBuilder.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/LocationConstraintExtensionBuilder.java @@ -1,11 +1,9 @@ package com.yoti.api.client.identity.extension; -import com.yoti.api.client.spi.remote.util.Validation; +import com.yoti.validation.Validation; public class LocationConstraintExtensionBuilder implements ExtensionBuilder { - public static final String TYPE = "LOCATION_CONSTRAINT"; - private double latitude; private double longitude; private double radius = 150d; @@ -42,7 +40,7 @@ public LocationConstraintExtensionBuilder withMaxUncertainty(double maxUncertain @Override public Extension build() { LocationConstraintContent content = new LocationConstraintContent(latitude, longitude, radius, maxUncertainty); - return new Extension<>(TYPE, content); + return new Extension<>("LOCATION_CONSTRAINT", content); } private static final class Property { diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeContent.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeContent.java index 6948afb86..05eb6c7ac 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeContent.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeContent.java @@ -1,33 +1,27 @@ package com.yoti.api.client.identity.extension; -import java.text.SimpleDateFormat; -import java.util.Date; +import java.time.OffsetDateTime; import java.util.List; -import java.util.TimeZone; import com.yoti.api.client.AttributeDefinition; -import com.yoti.api.client.spi.remote.call.YotiConstants; import com.fasterxml.jackson.annotation.JsonProperty; public class ThirdPartyAttributeContent { - private final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(YotiConstants.RFC3339_PATTERN_MILLIS); - - private final Date expiryDate; + private final OffsetDateTime expiry; @JsonProperty(Property.DEFINITIONS) private final List definitions; - ThirdPartyAttributeContent(Date expiryDate, List definitions) { - this.expiryDate = expiryDate; + ThirdPartyAttributeContent(OffsetDateTime expiry, List definitions) { + this.expiry = expiry; this.definitions = definitions; } @JsonProperty(Property.EXPIRY_DATE) - public String getExpiryDate() { - DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC")); - return DATE_FORMAT.format(expiryDate.getTime()); + public OffsetDateTime getExpiryDate() { + return expiry; } public List getDefinitions() { diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeExtensionBuilder.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeExtensionBuilder.java index 614a13ab7..0a53c09bf 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeExtensionBuilder.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/ThirdPartyAttributeExtensionBuilder.java @@ -1,34 +1,30 @@ package com.yoti.api.client.identity.extension; -import static com.yoti.api.client.spi.remote.util.Validation.notNull; -import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; - +import java.time.OffsetDateTime; import java.util.ArrayList; -import java.util.Date; import java.util.List; import com.yoti.api.client.AttributeDefinition; +import com.yoti.validation.Validation; public class ThirdPartyAttributeExtensionBuilder implements ExtensionBuilder { - public static final String TYPE = "THIRD_PARTY_ATTRIBUTE"; - - private Date expiryDate; + private OffsetDateTime expiryDate; private List definitions; public ThirdPartyAttributeExtensionBuilder() { this.definitions = new ArrayList<>(); } - public ThirdPartyAttributeExtensionBuilder withExpiryDate(Date expiryDate) { - notNull(expiryDate, Property.EXPIRY_DATE); + public ThirdPartyAttributeExtensionBuilder withExpiryDate(OffsetDateTime expiryDate) { + Validation.notNull(expiryDate, Property.EXPIRY_DATE); - this.expiryDate = new Date(expiryDate.getTime()); + this.expiryDate = expiryDate; return this; } public ThirdPartyAttributeExtensionBuilder withDefinition(String definition) { - notNullOrEmpty(definition, Property.DEFINITION); + Validation.notNullOrEmpty(definition, Property.DEFINITION); this.definitions.add(new AttributeDefinition(definition)); return this; @@ -45,7 +41,7 @@ public ThirdPartyAttributeExtensionBuilder withDefinitions(List definiti public Extension build() { ThirdPartyAttributeContent thirdPartyAttributeContent = new ThirdPartyAttributeContent(expiryDate, definitions); - return new Extension<>(TYPE, thirdPartyAttributeContent); + return new Extension<>("THIRD_PARTY_ATTRIBUTE", thirdPartyAttributeContent); } private static final class Property { diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/TransactionalFlowExtensionBuilder.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/TransactionalFlowExtensionBuilder.java index 8723c30cd..913630ca2 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/TransactionalFlowExtensionBuilder.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/extension/TransactionalFlowExtensionBuilder.java @@ -1,11 +1,9 @@ package com.yoti.api.client.identity.extension; -import com.yoti.api.client.spi.remote.util.Validation; +import com.yoti.validation.Validation; public class TransactionalFlowExtensionBuilder implements ExtensionBuilder { - public static final String TYPE = "TRANSACTIONAL_FLOW"; - private Object content; public TransactionalFlowExtensionBuilder withContent(Object content) { @@ -17,7 +15,7 @@ public TransactionalFlowExtensionBuilder withContent(Object content) { @Override public Extension build() { - return new Extension<>(TYPE, content); + return new Extension<>("TRANSACTIONAL_FLOW", content); } private static final class Property { diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAnchor.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAnchor.java index d8cc1f5fd..ed6c306f9 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAnchor.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAnchor.java @@ -1,7 +1,6 @@ package com.yoti.api.client.identity.policy; -import static com.yoti.api.client.spi.remote.util.Validation.notNull; -import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; +import com.yoti.validation.Validation; import com.fasterxml.jackson.annotation.JsonProperty; @@ -46,8 +45,8 @@ public Builder withSubType(String subType) { } public WantedAnchor build() { - notNullOrEmpty(value, Property.NAME); - notNull(subType, Property.SUB_TYPE); + Validation.notNullOrEmpty(value, Property.NAME); + Validation.notNull(subType, Property.SUB_TYPE); return new WantedAnchor(this); } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAttribute.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAttribute.java index a2cd32f5b..f59c895ad 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAttribute.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/identity/policy/WantedAttribute.java @@ -1,12 +1,11 @@ package com.yoti.api.client.identity.policy; -import static com.yoti.api.client.spi.remote.util.Validation.notNullOrEmpty; - import java.util.ArrayList; import java.util.Collections; import java.util.List; import com.yoti.api.client.identity.constraint.Constraint; +import com.yoti.validation.Validation; import com.fasterxml.jackson.annotation.JsonProperty; @@ -102,7 +101,7 @@ public Builder withConstraint(Constraint constraint) { } public WantedAttribute build() { - notNullOrEmpty(name, Property.NAME); + Validation.notNullOrEmpty(name, Property.NAME); return new WantedAttribute(this); } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/AttributeListReader.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/AttributeListReader.java index 778048d8c..5960f0cea 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/AttributeListReader.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/AttributeListReader.java @@ -7,25 +7,26 @@ import com.yoti.api.client.Attribute; import com.yoti.api.client.ProfileException; -class AttributeListReader { +public class AttributeListReader { private final EncryptedDataReader encryptedDataReader; private final AttributeListConverter attributeListConverter; - private AttributeListReader(EncryptedDataReader encryptedDataReader, - AttributeListConverter attributeListConverter) { + private AttributeListReader( + EncryptedDataReader encryptedDataReader, + AttributeListConverter attributeListConverter) { this.encryptedDataReader = encryptedDataReader; this.attributeListConverter = attributeListConverter; } - static AttributeListReader newInstance() { + public static AttributeListReader newInstance() { return new AttributeListReader( EncryptedDataReader.newInstance(), AttributeListConverter.newInstance() ); } - List> read(byte[] encryptedProfileBytes, Key secretKey) throws ProfileException { + public List> read(byte[] encryptedProfileBytes, Key secretKey) throws ProfileException { List> attributeList = new ArrayList<>(); if (encryptedProfileBytes != null && encryptedProfileBytes.length > 0) { byte[] profileData = encryptedDataReader.decryptBytes(encryptedProfileBytes, secretKey); diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/ExtraDataReader.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/ExtraDataReader.java index 9bc1a069a..0c8e7137d 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/ExtraDataReader.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/ExtraDataReader.java @@ -18,14 +18,14 @@ private ExtraDataReader( this.extraDataConverter = extraDataConverter; } - static ExtraDataReader newInstance() { + public static ExtraDataReader newInstance() { return new ExtraDataReader( EncryptedDataReader.newInstance(), ExtraDataConverter.newInstance() ); } - ExtraData read(byte[] encryptedBytes, Key secretKey) throws ProfileException, ExtraDataException { + public ExtraData read(byte[] encryptedBytes, Key secretKey) throws ProfileException, ExtraDataException { ExtraData extraData; if (encryptedBytes != null && encryptedBytes.length > 0) { byte[] extraDataBytes = encryptedDataReader.decryptBytes(encryptedBytes, secretKey); @@ -36,4 +36,5 @@ ExtraData read(byte[] encryptedBytes, Key secretKey) throws ProfileException, Ex return extraData; } + } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilder.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilder.java index f073fa218..64da85845 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilder.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/SignedRequestBuilder.java @@ -19,7 +19,7 @@ import com.yoti.api.client.spi.remote.call.factory.HeadersFactory; import com.yoti.api.client.spi.remote.call.factory.PathFactory; import com.yoti.api.client.spi.remote.call.factory.SignedMessageFactory; -import com.yoti.api.client.spi.remote.util.Validation; +import com.yoti.validation.Validation; import org.apache.http.HttpEntity; import org.apache.http.entity.ContentType; diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/factory/UnsignedPathFactory.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/factory/UnsignedPathFactory.java index 07bffa478..a8a57f589 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/factory/UnsignedPathFactory.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/factory/UnsignedPathFactory.java @@ -12,6 +12,8 @@ public class UnsignedPathFactory { private static final String IDENTITY_SESSION_RETRIEVAL = "/v2/sessions/%s"; private static final String IDENTITY_SESSION_QR_CODE_CREATION = "/v2/sessions/%s/qr-codes"; private static final String IDENTITY_SESSION_QR_CODE_RETRIEVAL = "/v2/qr-codes/%s"; + private static final String IDENTITY_SESSION_RECEIPT_RETRIEVAL = "/v2/receipts/%s"; + private static final String IDENTITY_SESSION_RECEIPT_KEY_RETRIEVAL = "/v2/wrapped-item-keys/%s"; // Share V1 private static final String PROFILE = "/profile/%s?appId=%s"; @@ -40,15 +42,27 @@ public String createIdentitySessionPath() { } public String createIdentitySessionRetrievalPath(String sessionId) { - return format(IDENTITY_SESSION_RETRIEVAL, sessionId); + return format(IDENTITY_SESSION_RETRIEVAL, base64ToBase64url(sessionId)); } public String createIdentitySessionQrCodePath(String sessionId) { - return format(IDENTITY_SESSION_QR_CODE_CREATION, sessionId); + return format(IDENTITY_SESSION_QR_CODE_CREATION, base64ToBase64url(sessionId)); } public String createIdentitySessionQrCodeRetrievalPath(String qrCodeId) { - return format(IDENTITY_SESSION_QR_CODE_RETRIEVAL, qrCodeId); + return format(IDENTITY_SESSION_QR_CODE_RETRIEVAL, base64ToBase64url(qrCodeId)); + } + + public String createIdentitySessionReceiptRetrievalPath(String receiptId) { + return format(IDENTITY_SESSION_RECEIPT_RETRIEVAL, base64ToBase64url(receiptId)); + } + + public String createIdentitySessionReceiptKeyRetrievalPath(String wrappedItemKeyId) { + return format(IDENTITY_SESSION_RECEIPT_KEY_RETRIEVAL, base64ToBase64url(wrappedItemKeyId)); + } + + private static String base64ToBase64url(String value) { + return value.replace('+', '-').replace('/', '_'); } public String createProfilePath(String appId, String connectToken) { diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityService.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityService.java index 996617181..f2d9d6421 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityService.java +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/DigitalIdentityService.java @@ -7,8 +7,6 @@ import static com.yoti.api.client.spi.remote.call.YotiConstants.CONTENT_TYPE_JSON; import static com.yoti.api.client.spi.remote.call.YotiConstants.DEFAULT_IDENTITY_URL; import static com.yoti.api.client.spi.remote.call.YotiConstants.PROPERTY_YOTI_API_URL; -import static com.yoti.validation.Validation.notNull; -import static com.yoti.validation.Validation.notNullOrEmpty; import java.io.IOException; import java.io.UnsupportedEncodingException; @@ -27,6 +25,7 @@ import com.yoti.api.client.spi.remote.call.SignedRequestBuilderFactory; import com.yoti.api.client.spi.remote.call.factory.UnsignedPathFactory; import com.yoti.json.ResourceMapper; +import com.yoti.validation.Validation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,25 +38,34 @@ public class DigitalIdentityService { private final UnsignedPathFactory pathFactory; private final SignedRequestBuilderFactory requestBuilderFactory; + private final ReceiptParser receiptParser; private final String apiUrl; - public DigitalIdentityService(UnsignedPathFactory pathFactory, SignedRequestBuilderFactory requestBuilderFactory) { + public DigitalIdentityService( + UnsignedPathFactory pathFactory, + SignedRequestBuilderFactory requestBuilderFactory, + ReceiptParser receiptParser) { this.pathFactory = pathFactory; this.requestBuilderFactory = requestBuilderFactory; + this.receiptParser = receiptParser; this.apiUrl = System.getProperty(PROPERTY_YOTI_API_URL, DEFAULT_IDENTITY_URL); } public static DigitalIdentityService newInstance() { - return new DigitalIdentityService(new UnsignedPathFactory(), new SignedRequestBuilderFactory()); + return new DigitalIdentityService( + new UnsignedPathFactory(), + new SignedRequestBuilderFactory(), + ReceiptParser.newInstance() + ); } public ShareSession createShareSession(String sdkId, KeyPair keyPair, ShareSessionRequest shareSessionRequest) throws DigitalIdentityException { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); - notNull(shareSessionRequest, "Share Session request"); + Validation.notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNull(keyPair, "Application Key Pair"); + Validation.notNull(shareSessionRequest, "Share Session request"); String path = pathFactory.createIdentitySessionPath(); @@ -65,9 +73,7 @@ public ShareSession createShareSession(String sdkId, KeyPair keyPair, ShareSessi try { byte[] payload = ResourceMapper.writeValueAsString(shareSessionRequest); - SignedRequest request = createSignedRequest(sdkId, keyPair, path, HTTP_POST, payload); - - return request.execute(ShareSession.class); + return createSignedRequest(sdkId, keyPair, path, HTTP_POST, payload).execute(ShareSession.class); } catch (IOException ex) { throw new DigitalIdentityException("Error while parsing the share session creation request ", ex); } catch (URISyntaxException ex) { @@ -81,21 +87,19 @@ public ShareSession createShareSession(String sdkId, KeyPair keyPair, ShareSessi public ShareSession fetchShareSession(String sdkId, KeyPair keyPair, String sessionId) throws DigitalIdentityException { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); - notNull(sessionId, "Session ID"); + Validation.notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNull(keyPair, "Application Key Pair"); + Validation.notNull(sessionId, "Session ID"); String path = pathFactory.createIdentitySessionRetrievalPath(sessionId); - LOG.debug("Requesting share session with ID '{}' at '{}'", sessionId, path); + LOG.debug("Requesting share session '{}' at '{}'", sessionId, path); try { - SignedRequest request = createSignedRequest(sdkId, keyPair, path); - - return request.execute(ShareSession.class); + return createSignedRequest(sdkId, keyPair, path).execute(ShareSession.class); } catch (Exception ex) { throw new DigitalIdentityException( - String.format("Error while fetching the share session with ID '{%s}' ", sessionId), + String.format("Error while fetching the share session '{%s}' ", sessionId), ex ); } @@ -103,18 +107,16 @@ public ShareSession fetchShareSession(String sdkId, KeyPair keyPair, String sess public ShareSessionQrCode createShareQrCode(String sdkId, KeyPair keyPair, String sessionId) throws DigitalIdentityException { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); - notNullOrEmpty(sessionId, "Session ID"); + Validation.notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNull(keyPair, "Application Key Pair"); + Validation.notNullOrEmpty(sessionId, "Session ID"); String path = pathFactory.createIdentitySessionQrCodePath(sessionId); LOG.debug("Requesting share session '{}' QR code creation at '{}'", sessionId, path); try { - SignedRequest request = createSignedRequest(sdkId, keyPair, path, HTTP_POST, EMPTY_JSON); - - return request.execute(ShareSessionQrCode.class); + return createSignedRequest(sdkId, keyPair, path, HTTP_POST, EMPTY_JSON).execute(ShareSessionQrCode.class); } catch (GeneralSecurityException ex) { throw new DigitalIdentityException("Error while signing the share QR code creation request ", ex); } catch (IOException | URISyntaxException | ResourceException ex) { @@ -124,28 +126,67 @@ public ShareSessionQrCode createShareQrCode(String sdkId, KeyPair keyPair, Strin public ShareSessionQrCode fetchShareQrCode(String sdkId, KeyPair keyPair, String qrCodeId) throws DigitalIdentityException { - notNullOrEmpty(sdkId, "SDK ID"); - notNull(keyPair, "Application Key Pair"); - notNullOrEmpty(qrCodeId, "QR Code ID"); + Validation.notNullOrEmpty(sdkId, "SDK ID"); + Validation.notNull(keyPair, "Application Key Pair"); + Validation.notNullOrEmpty(qrCodeId, "QR Code ID"); String path = pathFactory.createIdentitySessionQrCodeRetrievalPath(qrCodeId); - LOG.info("Requesting share session QR code with ID '{} at '{}'", qrCodeId, path); + LOG.debug("Requesting share session QR code '{} at '{}'", qrCodeId, path); try { - SignedRequest request = createSignedRequest(sdkId, keyPair, path); + return createSignedRequest(sdkId, keyPair, path).execute(ShareSessionQrCode.class); + } catch (Exception ex) { + throw new DigitalIdentityException( + String.format("Error while fetching the share session QR code '{%s}' ", qrCodeId), + ex + ); + } + } + + public Receipt fetchShareReceipt(String sdkId, KeyPair keyPair, String receiptId) throws DigitalIdentityException { + WrappedReceipt wrappedReceipt = doFetchShareReceipt(sdkId, keyPair, receiptId); + + return Optional.ofNullable(wrappedReceipt.getError()) + .map(ignored -> receiptParser.create(wrappedReceipt)) + .orElseGet(() -> { + ReceiptItemKey receiptKey = fetchShareReceiptKey(sdkId, keyPair, wrappedReceipt); + + return receiptParser.create(wrappedReceipt, receiptKey, keyPair.getPrivate()); + }); + } + + private WrappedReceipt doFetchShareReceipt(String sdkId, KeyPair keyPair, String receiptId) { + String path = pathFactory.createIdentitySessionReceiptRetrievalPath(receiptId); - return request.execute(ShareSessionQrCode.class); + LOG.debug("Requesting share session receipt '{}' at '{}'", receiptId, path); + + try { + return createSignedRequest(sdkId, keyPair, path).execute(WrappedReceipt.class); } catch (Exception ex) { throw new DigitalIdentityException( - String.format("Error while fetching the share session QR code with ID '{%s}' ", qrCodeId), + String.format("Error while fetching the share session QR code '{%s}' ", receiptId), ex ); } } - public Object fetchShareReceipt(String receiptId) { - return null; + private ReceiptItemKey fetchShareReceiptKey(String sdkId, KeyPair keyPair, WrappedReceipt wrappedReceipt) + throws DigitalIdentityException { + String wrappedItemKeyId = wrappedReceipt.getWrappedItemKeyId(); + + String path = pathFactory.createIdentitySessionReceiptKeyRetrievalPath(wrappedItemKeyId); + + LOG.debug("Requesting share session receipt item key '{}' at '{}'", wrappedItemKeyId, path); + + try { + return createSignedRequest(sdkId, keyPair, path).execute(ReceiptItemKey.class); + } catch (Exception ex) { + throw new DigitalIdentityException( + String.format("Error while fetching the share session receipt key '{%s}' ", wrappedItemKeyId), + ex + ); + } } SignedRequest createSignedRequest(String sdkId, KeyPair keyPair, String path) @@ -155,18 +196,18 @@ SignedRequest createSignedRequest(String sdkId, KeyPair keyPair, String path) SignedRequest createSignedRequest(String sdkId, KeyPair keyPair, String path, String method, byte[] payload) throws GeneralSecurityException, UnsupportedEncodingException, URISyntaxException { - SignedRequestBuilder requestBuilder = requestBuilderFactory.create() + SignedRequestBuilder request = requestBuilderFactory.create() .withKeyPair(keyPair) .withBaseUrl(apiUrl) .withEndpoint(path) .withHeader(AUTH_ID_HEADER, sdkId) .withHttpMethod(method); - Optional.ofNullable(payload).map(v -> - requestBuilder.withPayload(v).withHeader(CONTENT_TYPE, CONTENT_TYPE_JSON) - ); - - return requestBuilder.build(); + return Optional.ofNullable(payload) + .map(request::withPayload) + .map(r -> r.withHeader(CONTENT_TYPE, CONTENT_TYPE_JSON)) + .orElse(request) + .build(); } } diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/Receipt.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/Receipt.java new file mode 100644 index 000000000..13709fc6e --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/Receipt.java @@ -0,0 +1,173 @@ +package com.yoti.api.client.spi.remote.call.identity; + +import java.time.OffsetDateTime; +import java.util.Optional; + +import com.yoti.api.client.ApplicationProfile; +import com.yoti.api.client.ExtraData; +import com.yoti.api.client.HumanProfile; + +public final class Receipt { + + private final String id; + private final String sessionId; + private final String rememberMeId; + private final String parentRememberMeId; + private final OffsetDateTime timestamp; + private final ApplicationContent applicationContent; + private final UserContent userContent; + private final String error; + + private Receipt(Builder builder) { + id = builder.id; + sessionId = builder.sessionId; + rememberMeId = builder.rememberMeId; + parentRememberMeId = builder.parentRememberMeId; + timestamp = builder.timestamp; + applicationContent = builder.applicationContent; + userContent = builder.userContent; + error = builder.error; + } + + public String getId() { + return id; + } + + public String getSessionId() { + return sessionId; + } + + public String getRememberMeId() { + return rememberMeId; + } + + public String getParentRememberMeId() { + return parentRememberMeId; + } + + public OffsetDateTime getTimestamp() { + return timestamp; + } + + public Optional getProfile() { + return userContent.getProfile(); + } + + public Optional getExtraData() { + return userContent.getExtraData(); + } + + public ApplicationContent getApplicationContent() { + return applicationContent; + } + + public UserContent getUserContent() { + return userContent; + } + + public Optional getError() { + return Optional.ofNullable(error); + } + + public static Builder forReceipt(String id) { + return new Builder(id); + } + + public static final class Builder { + + private final String id; + + private String sessionId; + private String rememberMeId; + private String parentRememberMeId; + private OffsetDateTime timestamp; + private ApplicationContent applicationContent; + private UserContent userContent; + private String error; + + private Builder(String id) { + this.id = id; + } + + public Builder sessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + public Builder rememberMeId(String rememberMeId) { + this.rememberMeId = rememberMeId; + return this; + } + + public Builder parentRememberMeId(String parentRememberMeId) { + this.parentRememberMeId = parentRememberMeId; + return this; + } + + public Builder timestamp(OffsetDateTime timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder applicationContent(ApplicationProfile profile, ExtraData extraData) { + this.applicationContent = new ApplicationContent(profile, extraData); + return this; + } + + public Builder userContent(HumanProfile profile, ExtraData extraData) { + this.userContent = new UserContent(profile, extraData); + return this; + } + + public Builder error(String error) { + this.error = error; + return this; + } + + public Receipt build() { + return new Receipt(this); + } + + } + + public static class ApplicationContent { + + private final ApplicationProfile profile; + private final ExtraData extraData; + + private ApplicationContent(ApplicationProfile profile, ExtraData extraData) { + this.profile = profile; + this.extraData = extraData; + } + + public ApplicationProfile getProfile() { + return profile; + } + + public Optional getExtraData() { + return Optional.ofNullable(extraData); + } + + } + + public static class UserContent { + + private final HumanProfile profile; + private final ExtraData extraData; + + private UserContent(HumanProfile profile, ExtraData extraData) { + this.profile = profile; + this.extraData = extraData; + } + + public Optional getProfile() { + return Optional.ofNullable(profile); + } + + public Optional getExtraData() { + return Optional.ofNullable(extraData); + } + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptItemKey.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptItemKey.java new file mode 100644 index 000000000..4f6241932 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptItemKey.java @@ -0,0 +1,79 @@ +package com.yoti.api.client.spi.remote.call.identity; + +import java.util.Base64; + +import javax.crypto.spec.IvParameterSpec; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(builder = ReceiptItemKey.Builder.class) +public final class ReceiptItemKey { + + private final String id; + private final IvParameterSpec iv; + private final byte[] value; + + private ReceiptItemKey(Builder builder) { + id = builder.id; + iv = builder.iv; + value = builder.value; + } + + public String getId() { + return id; + } + + public IvParameterSpec getIv() { + return iv; + } + + public byte[] getValue() { + return value.clone(); + } + + public static final class Builder { + + private String id; + private IvParameterSpec iv; + private byte[] value; + + private Builder() { } + + @JsonProperty(Property.ID) + public Builder id(String id) { + this.id = id; + return this; + } + + @JsonProperty(Property.IV) + public Builder iv(String iv) { + this.iv = new IvParameterSpec(decode(iv)); + return this; + } + + @JsonProperty(Property.VALUE) + public Builder value(String value) { + this.value = decode(value); + return this; + } + + public ReceiptItemKey build() { + return new ReceiptItemKey(this); + } + + private static byte[] decode(String value) { + return Base64.getDecoder().decode(value); + } + + } + + private static class Property { + + private static final String ID = "id"; + private static final String IV = "iv"; + private static final String VALUE = "value"; + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptParser.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptParser.java new file mode 100644 index 000000000..9b3a1e99b --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/ReceiptParser.java @@ -0,0 +1,114 @@ +package com.yoti.api.client.spi.remote.call.identity; + +import java.nio.charset.StandardCharsets; +import java.security.Key; +import java.security.PrivateKey; +import java.util.Base64; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import javax.crypto.spec.SecretKeySpec; + +import com.yoti.api.client.ApplicationProfile; +import com.yoti.api.client.Attribute; +import com.yoti.api.client.ExtraData; +import com.yoti.api.client.ExtraDataException; +import com.yoti.api.client.HumanProfile; +import com.yoti.api.client.ProfileException; +import com.yoti.api.client.spi.remote.AttributeListReader; +import com.yoti.api.client.spi.remote.ExtraDataReader; +import com.yoti.crypto.CipherType; +import com.yoti.crypto.Crypto; +import com.yoti.validation.Validation; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ReceiptParser { + + private static final Logger LOG = LoggerFactory.getLogger(ReceiptParser.class); + + private final AttributeListReader attributeListReader; + private final ExtraDataReader extraDataReader; + + private ReceiptParser(AttributeListReader attributeListReader, ExtraDataReader extraDataReader) { + this.attributeListReader = Validation.notNull(attributeListReader, "profileReader"); + this.extraDataReader = Validation.notNull(extraDataReader, "extraDataReader"); + } + + public static ReceiptParser newInstance() { + return new ReceiptParser( + AttributeListReader.newInstance(), + ExtraDataReader.newInstance() + ); + } + + public Receipt create(WrappedReceipt wrappedReceipt, ReceiptItemKey wrappedItemKey, PrivateKey privateKey) + throws DigitalIdentityException { + Key receiptKey = decryptReceiptKey(wrappedReceipt.getWrappedKey(), wrappedItemKey, privateKey); + + Receipt.Builder receipt = Receipt.forReceipt(wrappedReceipt.getId()) + .sessionId(wrappedReceipt.getSessionId()) + .timestamp(wrappedReceipt.getTimestamp()) + .applicationContent( + new ApplicationProfile(parseProfileAttr(wrappedReceipt.getProfile(), receiptKey)), + wrappedReceipt.getExtraData().map(data -> parseExtraData(data, receiptKey)).orElse(null) + ); + + receipt.userContent( + wrappedReceipt.getOtherPartyProfile() + .map(profile -> parseProfileAttr(profile, receiptKey)) + .map(HumanProfile::new) + .orElse(null), + wrappedReceipt.getOtherPartyExtraData() + .map(data -> parseExtraData(data, receiptKey)) + .orElse(null) + ); + + wrappedReceipt.getRememberMeId().ifPresent(id -> receipt.rememberMeId(parseRememberMeId(id))); + wrappedReceipt.getParentRememberMeId().ifPresent(id -> receipt.parentRememberMeId(parseRememberMeId(id))); + + return receipt.build(); + } + + public Receipt create(WrappedReceipt failureReceipt) { + return Receipt.forReceipt(failureReceipt.getId()) + .sessionId(failureReceipt.getSessionId()) + .timestamp(failureReceipt.getTimestamp()) + .error(failureReceipt.getError()) + .build(); + } + + private static Key decryptReceiptKey(byte[] wrappedKey, ReceiptItemKey wrappedItemKey, PrivateKey privateKey) { + byte[] itemKey = Crypto.decrypt(wrappedItemKey.getValue(), privateKey, CipherType.RSA_NONE_PKCS1); + byte[] receiptKey = Crypto.decrypt(wrappedKey, itemKey, wrappedItemKey.getIv(), CipherType.AES_GCM); + return new SecretKeySpec(Objects.requireNonNull(receiptKey), CipherType.AES_CBC.value()); + } + + private List> parseProfileAttr(byte[] profile, Key key) { + try { + return attributeListReader.read(profile, key); + } catch (ProfileException ex) { + throw new DigitalIdentityException(ex); + } + } + + private ExtraData parseExtraData(byte[] extraData, Key key) { + try { + return extraDataReader.read(extraData, key); + } catch (ExtraDataException ex) { + LOG.error("Failed to parse extra data from receipt"); + return new ExtraData(); + } catch (ProfileException ex) { + throw new DigitalIdentityException(ex); + } + } + + private static String parseRememberMeId(byte[] id) { + return Optional.ofNullable(id) + .map(v -> new String(Base64.getEncoder().encode(v), StandardCharsets.UTF_8)) + .orElse(null); + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/WrappedReceipt.java b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/WrappedReceipt.java new file mode 100644 index 000000000..32d8d94eb --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/api/client/spi/remote/call/identity/WrappedReceipt.java @@ -0,0 +1,222 @@ +package com.yoti.api.client.spi.remote.call.identity; + +import java.time.OffsetDateTime; +import java.util.Base64; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonDeserialize(builder = WrappedReceipt.Builder.class) +public final class WrappedReceipt { + + private final String id; + private final String sessionId; + private final OffsetDateTime timestamp; + private final Content content; + private final Content otherPartyContent; + private final String wrappedItemKeyId; + private final byte[] rememberMeId; + private final byte[] parentRememberMeId; + private final byte[] wrappedKey; + private final String error; + + private WrappedReceipt(Builder builder) { + id = builder.id; + sessionId = builder.sessionId; + timestamp = builder.timestamp; + content = builder.content; + otherPartyContent = builder.otherPartyContent; + wrappedItemKeyId = builder.wrappedItemKeyId; + rememberMeId = builder.rememberMeId; + parentRememberMeId = builder.parentRememberMeId; + wrappedKey = builder.wrappedKey; + error = builder.error; + } + + public String getId() { + return id; + } + + public String getSessionId() { + return sessionId; + } + + public OffsetDateTime getTimestamp() { + return timestamp; + } + + public byte[] getProfile() { + return content.profile() + .orElseThrow(() -> new DigitalIdentityException("Application profile should not be missing")); + } + + public Optional getExtraData() { + return content.extraData(); + } + + public Optional getOtherPartyProfile() { + return Optional.ofNullable(otherPartyContent).flatMap(Content::profile); + } + + public Optional getOtherPartyExtraData() { + return Optional.ofNullable(otherPartyContent).flatMap(Content::extraData); + } + + public String getWrappedItemKeyId() { + return wrappedItemKeyId; + } + + public Optional getRememberMeId() { + return Optional.ofNullable(rememberMeId).map(byte[]::clone); + } + + public Optional getParentRememberMeId() { + return Optional.ofNullable(parentRememberMeId).map(byte[]::clone); + } + + public byte[] getWrappedKey() { + return wrappedKey.clone(); + } + + public String getError() { + return error; + } + + public static final class Builder { + + private String id; + private String sessionId; + private OffsetDateTime timestamp; + private Content content; + private Content otherPartyContent; + private String wrappedItemKeyId; + private byte[] rememberMeId; + private byte[] parentRememberMeId; + private byte[] wrappedKey; + private String error; + + private Builder() { } + + @JsonProperty(Property.ID) + public Builder id(String id) { + this.id = id; + return this; + } + + @JsonProperty(Property.SESSION_ID) + public Builder sessionId(String sessionId) { + this.sessionId = sessionId; + return this; + } + + @JsonProperty(Property.TIMESTAMP) + public Builder timestamp(String timestamp) { + this.timestamp = OffsetDateTime.parse(timestamp); + return this; + } + + @JsonProperty(Property.CONTENT) + public Builder content(Content content) { + this.content = content; + return this; + } + + @JsonProperty(Property.OTHER_PARTY_CONTENT) + public Builder otherPartyContent(Content otherPartyContent) { + this.otherPartyContent = otherPartyContent; + return this; + } + + @JsonProperty(Property.WRAPPED_ITEM_KEY_ID) + public Builder wrappedItemKeyId(String wrappedItemKeyId) { + this.wrappedItemKeyId = wrappedItemKeyId; + return this; + } + + @JsonProperty(Property.REMEMBER_ME_ID) + public Builder rememberMeId(String rememberMeId) { + this.rememberMeId = decode(rememberMeId); + return this; + } + + @JsonProperty(Property.PARENT_REMEMBER_ME_ID) + public Builder parentRememberMeId(String parentRememberMeId) { + this.parentRememberMeId = decode(parentRememberMeId); + return this; + } + + @JsonProperty(Property.WRAPPED_KEY) + public Builder wrappedKey(String wrappedKey) { + this.wrappedKey = decode(wrappedKey); + return this; + } + + @JsonProperty(Property.ERROR) + public Builder error(String error) { + this.error = error; + return this; + } + + public WrappedReceipt build() { + return new WrappedReceipt(this); + } + + private static byte[] decode(String value) { + return Base64.getDecoder().decode(value); + } + + } + + private static class Content { + + private byte[] profile; + private byte[] extraData; + + public Optional profile() { + return Optional.ofNullable(profile).map(byte[]::clone); + } + + public Optional extraData() { + return Optional.ofNullable(extraData).map(byte[]::clone); + } + + @JsonProperty(Property.Content.PROFILE) + public void setProfile(String profile) { + this.profile = decode(profile); + } + + @JsonProperty(Property.Content.EXTRA_DATA) + public void setExtraData(String extraData) { + this.extraData = decode(extraData); + } + + private static byte[] decode(String value) { + return Base64.getDecoder().decode(value); + } + + } + + private static class Property { + + private static final String ID = "id"; + private static final String SESSION_ID = "sessionId"; + private static final String TIMESTAMP = "timestamp"; + private static final String REMEMBER_ME_ID = "rememberMeId"; + private static final String PARENT_REMEMBER_ME_ID = "parentRememberMeId"; + private static final String CONTENT = "content"; + private static final String OTHER_PARTY_CONTENT = "otherPartyContent"; + private static final String WRAPPED_ITEM_KEY_ID = "wrappedItemKeyId"; + private static final String WRAPPED_KEY = "wrappedKey"; + private static final String ERROR = "error"; + + private static class Content { + + private static final String PROFILE = "profile"; + private static final String EXTRA_DATA = "extraData"; + + } + + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/crypto/Algorithm.java b/yoti-sdk-api/src/main/java/com/yoti/crypto/Algorithm.java new file mode 100644 index 000000000..28827d2ea --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/crypto/Algorithm.java @@ -0,0 +1,17 @@ +package com.yoti.crypto; + +public enum Algorithm { + + AES("AES"); + + private final String value; + + Algorithm(String value) { + this.value = value; + } + + public String value() { + return value; + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/crypto/CipherType.java b/yoti-sdk-api/src/main/java/com/yoti/crypto/CipherType.java new file mode 100644 index 000000000..21ce85a08 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/crypto/CipherType.java @@ -0,0 +1,28 @@ +package com.yoti.crypto; + +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; + +public enum CipherType { + + AES_GCM("AES/GCM/NoPadding"), + AES_CBC("AES/CBC/PKCS7Padding"), + RSA_NONE_PKCS1("RSA/NONE/PKCS1Padding"); + + private final String value; + + CipherType(String value) { + this.value = value; + } + + public String value() { + return value; + } + + Cipher getInstance() throws NoSuchPaddingException, NoSuchAlgorithmException { + return Cipher.getInstance(value); + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/crypto/Crypto.java b/yoti-sdk-api/src/main/java/com/yoti/crypto/Crypto.java new file mode 100644 index 000000000..acc1399ae --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/crypto/Crypto.java @@ -0,0 +1,68 @@ +package com.yoti.crypto; + +import java.security.Key; +import java.security.PrivateKey; +import java.util.function.Supplier; + +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public final class Crypto { + + private static final String DECRYPT_ERROR_TEMPLATE = "Failed to decrypt message using '%s' transformation"; + + private Crypto() { } + + public static byte[] decrypt(byte[] msg, PrivateKey key, CipherType transformation) { + return wrapOnException( + () -> { + try { + return doCipher(msg, key, transformation); + } catch (Exception ex) { + throw new CryptoException(ex); + } + }, + String.format(DECRYPT_ERROR_TEMPLATE, transformation.value()) + ); + } + + private static byte[] doCipher(byte[] msg, Key key, CipherType transformation) throws Exception { + Cipher cipher = transformation.getInstance(); + cipher.init(Cipher.DECRYPT_MODE, key); + return cipher.doFinal(msg); + } + + public static byte[] decrypt(byte[] msg, byte[] key, IvParameterSpec iv, CipherType transformation) { + return decrypt(msg, new SecretKeySpec(key, Algorithm.AES.value()), iv, transformation); + } + + public static byte[] decrypt(byte[] msg, SecretKeySpec key, IvParameterSpec iv, CipherType transformation) { + return wrapOnException( + () -> { + try { + return doCipher(msg, key, iv, transformation); + } catch (Exception ex) { + throw new CryptoException(ex); + } + }, + String.format(DECRYPT_ERROR_TEMPLATE, transformation.value()) + ); + } + + private static byte[] doCipher(byte[] msg, SecretKeySpec key, IvParameterSpec iv, CipherType transformation) + throws Exception { + Cipher cipher = transformation.getInstance(); + cipher.init(Cipher.DECRYPT_MODE, key, iv); + return cipher.doFinal(msg); + } + + private static T wrapOnException(Supplier op, String msg) { + try { + return op.get(); + } catch (Exception ex) { + throw new CryptoException(msg, ex); + } + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/crypto/CryptoException.java b/yoti-sdk-api/src/main/java/com/yoti/crypto/CryptoException.java new file mode 100644 index 000000000..1e99a37a9 --- /dev/null +++ b/yoti-sdk-api/src/main/java/com/yoti/crypto/CryptoException.java @@ -0,0 +1,17 @@ +package com.yoti.crypto; + +public class CryptoException extends RuntimeException { + + public CryptoException(String message) { + super(message); + } + + public CryptoException(Throwable cause) { + super(cause); + } + + public CryptoException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/yoti-sdk-api/src/main/java/com/yoti/json/ResourceMapper.java b/yoti-sdk-api/src/main/java/com/yoti/json/ResourceMapper.java index d3f8088bd..975485821 100644 --- a/yoti-sdk-api/src/main/java/com/yoti/json/ResourceMapper.java +++ b/yoti-sdk-api/src/main/java/com/yoti/json/ResourceMapper.java @@ -1,6 +1,9 @@ package com.yoti.json; import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.TimeZone; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonSetter; @@ -10,6 +13,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.cfg.MapperConfig; import com.fasterxml.jackson.databind.introspect.VisibilityChecker; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; public final class ResourceMapper { @@ -20,9 +24,20 @@ public final class ResourceMapper { .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) .disable(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS) .setVisibility(configureVisibility(MAPPER.getDeserializationConfig())) - .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SKIP)); + .setDefaultSetterInfo(JsonSetter.Value.forValueNulls(Nulls.SKIP)) + .setDateFormat(utc()) + .registerModule(new JavaTimeModule()); } + private static SimpleDateFormat utc() { + SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.ROOT); + format.setTimeZone(TimeZone.getTimeZone("UTC")); + + return format; + } + + private ResourceMapper() { } + private static VisibilityChecker configureVisibility(MapperConfig config) { return config.getDefaultVisibilityChecker() .withFieldVisibility(JsonAutoDetect.Visibility.NONE) diff --git a/yoti-sdk-api/src/test/java/com/yoti/api/client/DigitalIdentityClientTest.java b/yoti-sdk-api/src/test/java/com/yoti/api/client/DigitalIdentityClientTest.java index 7cb69fdab..eaeb6abe1 100644 --- a/yoti-sdk-api/src/test/java/com/yoti/api/client/DigitalIdentityClientTest.java +++ b/yoti-sdk-api/src/test/java/com/yoti/api/client/DigitalIdentityClientTest.java @@ -203,15 +203,18 @@ public void client_FetchShareQrCodeException_DigitalIdentityException() throws I } @Test - public void client_FetchShareReceiptException_DigitalIdentityException() { + public void client_FetchShareReceiptException_DigitalIdentityException() throws IOException { + when(keyPairSource.getFromStream(any(KeyStreamVisitor.class))).thenReturn(keyPair); + DigitalIdentityClient identityClient = new DigitalIdentityClient( AN_SDK_ID, - validKeyPairSource, + keyPairSource, identityService ); String exMessage = "Fetch Share Receipt Error"; - when(identityService.fetchShareReceipt(A_RECEIPT_ID)).thenThrow(new DigitalIdentityException(exMessage)); + when(identityService.fetchShareReceipt(AN_SDK_ID, keyPair, A_RECEIPT_ID)) + .thenThrow(new DigitalIdentityException(exMessage)); DigitalIdentityException ex = assertThrows( DigitalIdentityException.class, diff --git a/yoti-sdk-api/src/test/java/com/yoti/json/ResourceMapperTest.java b/yoti-sdk-api/src/test/java/com/yoti/json/ResourceMapperTest.java index 9f09ade31..23e19e6ec 100644 --- a/yoti-sdk-api/src/test/java/com/yoti/json/ResourceMapperTest.java +++ b/yoti-sdk-api/src/test/java/com/yoti/json/ResourceMapperTest.java @@ -2,10 +2,10 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.junit.Assert.assertTrue; import java.net.URI; -import java.util.Date; +import java.time.OffsetDateTime; import java.util.HashMap; import java.util.Map; @@ -75,7 +75,7 @@ public void mapper_CreatesValidShareSessionRequestJson() throws Exception { String thirdPartyAttributeDefinition = "aDefinition"; Extension extension = new ThirdPartyAttributeExtensionBuilder() .withDefinition(thirdPartyAttributeDefinition) - .withExpiryDate(new Date()) + .withExpiryDate(OffsetDateTime.now()) .build(); String redirectUriValue = "aRedirectUri";