From 9a735964161d951a48777ca4a42e316e8f4d4485 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juraj=20=C4=8Eurech?= Date: Tue, 3 Sep 2024 07:43:17 +0200 Subject: [PATCH] Android: Final support for ECIES V3.3 --- .../support/model/ServerVersion.java | 2 +- .../powerauth/ecies/EciesMetadata.java | 10 +- .../networking/client/HttpClient.java | 164 +++++++++--- .../networking/client/HttpRequestHelper.java | 2 +- .../networking/client/JsonSerialization.java | 2 + .../endpoints/CreateActivationEndpoint.java | 18 ++ .../endpoints/GetTemporaryKeyEndpoint.java | 42 +++ .../interfaces/ICustomEndpointOperation.java | 3 +- .../interfaces/IEndpointDefinition.java | 17 ++ .../networking/model/entity/JwtHeader.java | 41 +++ .../networking/model/entity/JwtObject.java | 36 +++ .../model/request/EciesEncryptedRequest.java | 17 ++ .../model/request/GetTemporaryKeyRequest.java | 51 ++++ .../response/GetTemporaryKeyResponse.java | 88 +++++++ .../security/powerauth/sdk/PowerAuthSDK.java | 228 +++++++++-------- .../sdk/impl/DefaultKeystoreService.java | 186 ++++++++++++++ .../sdk/impl/GetTemporaryKeyTask.java | 242 ++++++++++++++++++ .../sdk/impl/ICreateKeyListener.java | 40 +++ .../powerauth/sdk/impl/IKeystoreService.java | 47 ++++ .../sdk/impl/IPrivateCryptoHelper.java | 12 + .../default-instrumentation-tests.properties | 2 +- .../PowerAuth2/private/PA2KeystoreService.m | 47 ++-- .../PowerAuthServer/PowerAuthTestServerAPI.m | 6 +- proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h | 2 +- src/PowerAuth/crypto/ECC.cpp | 2 - src/PowerAuth/utils/DataReader.h | 2 +- 26 files changed, 1125 insertions(+), 184 deletions(-) create mode 100644 proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/GetTemporaryKeyEndpoint.java create mode 100644 proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtHeader.java create mode 100644 proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtObject.java create mode 100644 proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/GetTemporaryKeyRequest.java create mode 100644 proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/response/GetTemporaryKeyResponse.java create mode 100644 proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/DefaultKeystoreService.java create mode 100644 proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/GetTemporaryKeyTask.java create mode 100644 proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/ICreateKeyListener.java create mode 100644 proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IKeystoreService.java diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/model/ServerVersion.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/model/ServerVersion.java index 30673680..7de1b393 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/model/ServerVersion.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/model/ServerVersion.java @@ -43,7 +43,7 @@ public enum ServerVersion { /** * Contains constant for the latest PowerAuth Server version. */ - public static final ServerVersion LATEST = V1_8_0; + public static final ServerVersion LATEST = V1_9_0; /** * Server version represented as string. diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesMetadata.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesMetadata.java index c0be2220..25a3c3ae 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesMetadata.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesMetadata.java @@ -63,6 +63,13 @@ public EciesMetadata(@NonNull String applicationKey, @NonNull String temporaryKe return activationIdentifier; } + /** + * @return Identifier of temporary key. + */ + public @NonNull String getTemporaryKeyId() { + return temporaryKeyId; + } + // HTTP header /** @@ -76,8 +83,7 @@ public EciesMetadata(@NonNull String applicationKey, @NonNull String temporaryKe * @return String with HTTP request header's value. */ public @NonNull String getHttpHeaderValue() { - final String result = "PowerAuth version=\"3.3\" application_key=\"" + applicationKey + - "\" temporary_key_id=\"" + temporaryKeyId + "\""; + final String result = "PowerAuth version=\"3.3\" application_key=\"" + applicationKey + "\""; if (activationIdentifier != null) { return result + " activation_id=\"" + activationIdentifier + "\""; } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpClient.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpClient.java index db95a329..adc470a5 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpClient.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpClient.java @@ -21,6 +21,7 @@ import java.util.concurrent.Executor; +import io.getlime.security.powerauth.core.EciesEncryptorScope; import io.getlime.security.powerauth.networking.interfaces.ICancelable; import io.getlime.security.powerauth.networking.interfaces.IEndpointDefinition; import io.getlime.security.powerauth.networking.interfaces.IExecutorProvider; @@ -29,9 +30,7 @@ import io.getlime.security.powerauth.sdk.IPowerAuthTimeSynchronizationService; import io.getlime.security.powerauth.sdk.PowerAuthAuthentication; import io.getlime.security.powerauth.sdk.PowerAuthClientConfiguration; -import io.getlime.security.powerauth.sdk.impl.CompositeCancelableTask; -import io.getlime.security.powerauth.sdk.impl.ICallbackDispatcher; -import io.getlime.security.powerauth.sdk.impl.IPrivateCryptoHelper; +import io.getlime.security.powerauth.sdk.impl.*; /** * The {@code HttpClient} class provides a high level networking functionality, including @@ -48,6 +47,7 @@ public class HttpClient { private final @NonNull IExecutorProvider executorProvider; private final @NonNull ICallbackDispatcher callbackDispatcher; private IPowerAuthTimeSynchronizationService timeSynchronizationService; + private IKeystoreService keystoreService; /** * @param configuration HTTP client configuration @@ -89,7 +89,7 @@ public HttpClient( } /** - * Set time synchronization service to the HTTP client. + * Set time synchronization service to the HTTP client. If the service is already set, then throws {@link IllegalStateException}. * @param timeSynchronizationService Time synchronization service implementation. */ public void setTimeSynchronizationService(@NonNull IPowerAuthTimeSynchronizationService timeSynchronizationService) { @@ -99,6 +99,41 @@ public void setTimeSynchronizationService(@NonNull IPowerAuthTimeSynchronization this.timeSynchronizationService = timeSynchronizationService; } + /** + * Get time synchronization service associated to the HTTP client. If service is not set, then throws {@link IllegalStateException}. + * @return Implementation of {@link IPowerAuthTimeSynchronizationService}. + */ + @NonNull + IPowerAuthTimeSynchronizationService getTimeSynchronizationService() { + if (timeSynchronizationService == null) { + throw new IllegalStateException(); + } + return timeSynchronizationService; + } + + /** + * Set keystore service to the HTTP client. If the service is already set, then throws {@link IllegalStateException}. + * @param keystoreService Keystore service implementation. + */ + public void setKeystoreService(@Nullable IKeystoreService keystoreService) { + if (this.keystoreService != null) { + throw new IllegalStateException(); + } + this.keystoreService = keystoreService; + } + + /** + * Get keystore service associated to the HTTP client. If service is not set, then throws {@link IllegalStateException}. + * @return Implementation of {@link IKeystoreService}. + */ + @NonNull + IKeystoreService getKeystoreService() { + if (keystoreService == null) { + throw new IllegalStateException(); + } + return keystoreService; + } + /** * Posts a HTTP request with provided object to the REST endpoint. * @@ -139,44 +174,50 @@ public ICancelable post( @Nullable PowerAuthAuthentication authentication, @NonNull INetworkResponseListener listener) { - if (endpoint.isRequireSynchronizedTime()) { - // Get the time synchronization service. It supposed to be set by the PowerAuthSDK's builder in SDK construction. - final IPowerAuthTimeSynchronizationService tss = timeSynchronizationService; - if (tss == null) { - throw new IllegalStateException("Time synchronization service is not set."); - } - if (!tss.isTimeSynchronized()) { - // Endpoint require encryption and time is not synchronized yet. We have to create a composite task that cover both - // time synchronization and actual request execution. - final CompositeCancelableTask compositeTask = new CompositeCancelableTask(true); - compositeTask.setCancelCallback(() -> { - callbackDispatcher.dispatchCallback(listener::onCancel); + final IKeystoreService kss = getKeystoreService(); + final IPowerAuthTimeSynchronizationService tss = getTimeSynchronizationService(); + final int encryptorScope = endpoint.isEncryptedWithApplicationScope() ? EciesEncryptorScope.APPLICATION : EciesEncryptorScope.ACTIVATION; + final boolean requireTimeSynchronization = endpoint.isRequireSynchronizedTime() && !tss.isTimeSynchronized(); + final boolean requireEncryptionKey = endpoint.isEncrypted() && !kss.containsKeyForEncryptor(encryptorScope); + + if (requireTimeSynchronization || requireEncryptionKey) { + // Endpoint require encryption key or time synchronization. We have to create a composite task that cover + // multiple tasks including an actual request execution. + final CompositeCancelableTask compositeTask = new CompositeCancelableTask(true); + compositeTask.setCancelCallback(() -> { + callbackDispatcher.dispatchCallback(listener::onCancel); + }); + // Now determine what type of task should be executed before an actual task. + if (requireEncryptionKey) { + // Temporary encryption key must be acquired from the server. This operation also automatically + // synchronize the time. + if (helper == null) { + throw new IllegalArgumentException(); + } + final ICancelable getKeyTask = kss.createKeyForEncryptor(encryptorScope, helper, new ICreateKeyListener() { + @Override + public void onCreateKeySucceeded() { + // Encryption key successfully acquired, we can continue with the actual request. + compositePostImpl(object, endpoint, helper, authentication, compositeTask, listener); + } + + @Override + public void onCreateKeyFailed(@NonNull Throwable throwable) { + if (compositeTask.setCompleted()) { + listener.onNetworkError(throwable); + } + } }); + if (getKeyTask != null) { + compositeTask.addCancelable(getKeyTask); + } + } else { + // Only time synchronization is required final ICancelable synchronizationTask = tss.synchronizeTime(new ITimeSynchronizationListener() { @Override public void onTimeSynchronizationSucceeded() { // The time has been successfully synchronized, we can continue with the actual request. - final ICancelable actualTask = postImpl(object, endpoint, helper, authentication, new INetworkResponseListener() { - @Override - public void onNetworkResponse(@NonNull TResponse tResponse) { - if (compositeTask.setCompleted()) { - listener.onNetworkResponse(tResponse); - } - } - - @Override - public void onNetworkError(@NonNull Throwable throwable) { - if (compositeTask.setCompleted()) { - listener.onNetworkError(throwable); - } - } - - @Override - public void onCancel() { - // We can ignore the cancel, because it's handled already by the composite task. - } - }); - compositeTask.addCancelable(actualTask); + compositePostImpl(object, endpoint, helper, authentication, compositeTask, listener); } @Override @@ -189,14 +230,57 @@ public void onTimeSynchronizationFailed(@NonNull Throwable t) { if (synchronizationTask != null) { compositeTask.addCancelable(synchronizationTask); } - // Return composite task instead of original operation. - return compositeTask; } + // Return composite task instead of original operation. + return compositeTask; } - // Endpoint doesn't require time synchronization, or time is already synchronized. + + // Endpoint doesn't require time synchronization or encryption. return postImpl(object, endpoint, helper, authentication, listener); } + /** + * Function creates an asynchronous operation with HTTP request and includes the operation into composite task. + * @param object object to be serialized into POST request + * @param endpoint object defining the endpoint + * @param helper cryptographic helper + * @param authentication optional authentication object, if request has to be signed with PowerAuth signature + * @param compositeTask composite task reported back to the application + * @param listener response listener + * @param type of request object + * @param type of response object + */ + private void compositePostImpl( + @Nullable TRequest object, + @NonNull IEndpointDefinition endpoint, + @Nullable IPrivateCryptoHelper helper, + @Nullable PowerAuthAuthentication authentication, + @NonNull CompositeCancelableTask compositeTask, + @NonNull INetworkResponseListener listener) { + // Create actual HTTP + final ICancelable actualTask = postImpl(object, endpoint, helper, authentication, new INetworkResponseListener() { + @Override + public void onNetworkResponse(@NonNull TResponse tResponse) { + if (compositeTask.setCompleted()) { + listener.onNetworkResponse(tResponse); + } + } + + @Override + public void onNetworkError(@NonNull Throwable throwable) { + if (compositeTask.setCompleted()) { + listener.onNetworkError(throwable); + } + } + + @Override + public void onCancel() { + // We can ignore the cancel, because it's handled already by the composite task. + } + }); + compositeTask.addCancelable(actualTask); + } + /** * Internal implementation of HTTP post request. * diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpRequestHelper.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpRequestHelper.java index c87b1932..47347817 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpRequestHelper.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpRequestHelper.java @@ -177,7 +177,7 @@ RequestData buildRequest(@NonNull String baseUrl, @Nullable IPrivateCryptoHelper // Execute custom step before the request is serialized. ICustomEndpointOperation beforeRequestSerialization = endpoint.getBeforeRequestSerializationOperation(); if (beforeRequestSerialization != null) { - beforeRequestSerialization.customEndpointOperation(); + beforeRequestSerialization.customEndpointOperation(endpoint); } // Encrypt the request data if the endpoint has encryptor specified diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/JsonSerialization.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/JsonSerialization.java index eab81652..3d9123da 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/JsonSerialization.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/JsonSerialization.java @@ -160,6 +160,7 @@ public JsonObject parseResponseObject(@Nullable byte[] data) throws JsonParseExc @NonNull public byte[] encryptObject(@Nullable TRequest object, @NonNull EciesEncryptor encryptor) throws PowerAuthErrorException { final EciesEncryptedRequest request = encryptObjectToRequest(object, encryptor); + request.setTemporaryKeyId(encryptor.getMetadata().getTemporaryKeyId()); return serializeObject(request); } @@ -228,6 +229,7 @@ public EciesEncryptedRequest encryptObjectToRequest(@Nullable TReques } // 3. Construct final request object from the cryptogram final EciesEncryptedRequest request = new EciesEncryptedRequest(); + request.setTemporaryKeyId(encryptor.getMetadata().getTemporaryKeyId()); request.setEncryptedData(cryptogram.getBodyBase64()); request.setEphemeralPublicKey(cryptogram.getKeyBase64()); request.setMac(cryptogram.getMacBase64()); diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/CreateActivationEndpoint.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/CreateActivationEndpoint.java index 559b5759..979507fe 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/CreateActivationEndpoint.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/CreateActivationEndpoint.java @@ -21,6 +21,7 @@ import com.google.gson.reflect.TypeToken; +import io.getlime.security.powerauth.core.EciesEncryptor; import io.getlime.security.powerauth.ecies.EciesEncryptorId; import io.getlime.security.powerauth.networking.interfaces.ICustomEndpointOperation; import io.getlime.security.powerauth.networking.interfaces.IEndpointDefinition; @@ -29,6 +30,7 @@ public class CreateActivationEndpoint implements IEndpointDefinition { private final ICustomEndpointOperation beforeRequestSerialization; + private EciesEncryptor layer2Encryptor; /** * Construct endpoint with a custom serialization step executed before the request is serialized. @@ -66,4 +68,20 @@ public boolean isAvailableInProtocolUpgrade() { public ICustomEndpointOperation getBeforeRequestSerializationOperation() { return beforeRequestSerialization; } + + /** + * Get ECIES encryptor for layer-2 encryption. + * @return ECIES encryptor for layer-2 encryption. + */ + public EciesEncryptor getLayer2Encryptor() { + return layer2Encryptor; + } + + /** + * Set ECIES encryptor for layer-2 encryption. + * @param encryptor ECIES encryptor for layer-2 encryption. + */ + public void setLayer2Encryptor(EciesEncryptor encryptor) { + this.layer2Encryptor = encryptor; + } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/GetTemporaryKeyEndpoint.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/GetTemporaryKeyEndpoint.java new file mode 100644 index 00000000..6d3ae181 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/GetTemporaryKeyEndpoint.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.networking.endpoints; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.gson.reflect.TypeToken; +import io.getlime.security.powerauth.networking.interfaces.IEndpointDefinition; +import io.getlime.security.powerauth.networking.model.entity.JwtObject; + +public class GetTemporaryKeyEndpoint implements IEndpointDefinition { + @NonNull + @Override + public String getRelativePath() { + return "/pa/v3/keystore/create"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(JwtObject.class); + } + + @Override + public boolean isAvailableInProtocolUpgrade() { + return true; + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/ICustomEndpointOperation.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/ICustomEndpointOperation.java index fafa788e..a46aa431 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/ICustomEndpointOperation.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/ICustomEndpointOperation.java @@ -16,6 +16,7 @@ package io.getlime.security.powerauth.networking.interfaces; +import androidx.annotation.NonNull; import io.getlime.security.powerauth.exception.PowerAuthErrorException; /** @@ -23,5 +24,5 @@ */ @FunctionalInterface public interface ICustomEndpointOperation { - void customEndpointOperation() throws PowerAuthErrorException; + void customEndpointOperation(@NonNull IEndpointDefinition endpointDefinition) throws PowerAuthErrorException; } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/IEndpointDefinition.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/IEndpointDefinition.java index 20caeaf8..d06eb57f 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/IEndpointDefinition.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/IEndpointDefinition.java @@ -59,6 +59,23 @@ default EciesEncryptorId getEncryptorId() { return EciesEncryptorId.NONE; } + /** + * @return {@code true} if endpoint is using ECIES encryption. + */ + default boolean isEncrypted() { + return getEncryptorId() != EciesEncryptorId.NONE; + } + + /** + * @return {@code true} if endpoint is using application scoped encryptor. + */ + default boolean isEncryptedWithApplicationScope() { + final EciesEncryptorId encryptorId = getEncryptorId(); + return encryptorId == EciesEncryptorId.ACTIVATION_PAYLOAD || + encryptorId == EciesEncryptorId.ACTIVATION_REQUEST || + encryptorId == EciesEncryptorId.GENERIC_APPLICATION_SCOPE; + } + /** * @return Type of response object. By default, returns null. */ diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtHeader.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtHeader.java new file mode 100644 index 00000000..27fe5859 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtHeader.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.networking.model.entity; + +/** + * The JwtHeader class represents a header in JWT signature. + */ +public class JwtHeader { + /** + * Type of object. + */ + public final String typ; + /** + * Algorithm used in JWT. + */ + public final String alg; + + /** + * Construct object with JWT type and algorithm. + * @param typ Type of JWT. + * @param alg Algorithm used in JWT. + */ + public JwtHeader(String typ, String alg) { + this.typ = typ; + this.alg = alg; + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtObject.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtObject.java new file mode 100644 index 00000000..1945bf0e --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtObject.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.networking.model.entity; + +/** + * The JwtObject represents JWT request and response. + */ +public class JwtObject { + + /** + * Full JWT formatted string + */ + public final String jwt; + + /** + * Construct object with JWT formatted string. + * @param jwt JWT formatted string. + */ + public JwtObject(String jwt) { + this.jwt = jwt; + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/EciesEncryptedRequest.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/EciesEncryptedRequest.java index dce70de1..6b015f03 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/EciesEncryptedRequest.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/EciesEncryptedRequest.java @@ -22,6 +22,7 @@ */ public class EciesEncryptedRequest { + private String temporaryKeyId; private String ephemeralPublicKey; private String encryptedData; private String mac; @@ -106,4 +107,20 @@ public long getTimestamp() { public void setTimestamp(long timestamp) { this.timestamp = timestamp; } + + /** + * Get identifier of temporary key. + * @return Identifier of temporary key + */ + public String getTemporaryKeyId() { + return temporaryKeyId; + } + + /** + * Set identifier of temporary key. + * @param temporaryKeyId identifier of temporary key. + */ + public void setTemporaryKeyId(String temporaryKeyId) { + this.temporaryKeyId = temporaryKeyId; + } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/GetTemporaryKeyRequest.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/GetTemporaryKeyRequest.java new file mode 100644 index 00000000..07be449c --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/GetTemporaryKeyRequest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.networking.model.request; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Request object for endpoint returning a temporary key for ECIES encryption scheme. + */ +public class GetTemporaryKeyRequest { + + private final String applicationKey; + private final String activationId; + private final String challenge; + + public GetTemporaryKeyRequest(@NonNull String applicationKey, @Nullable String activationId, @NonNull String challenge) { + this.applicationKey = applicationKey; + this.activationId = activationId; + this.challenge = challenge; + } + + @NonNull + public String getApplicationKey() { + return applicationKey; + } + + @Nullable + public String getActivationId() { + return activationId; + } + + @NonNull + public String getChallenge() { + return challenge; + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/response/GetTemporaryKeyResponse.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/response/GetTemporaryKeyResponse.java new file mode 100644 index 00000000..9612dc85 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/response/GetTemporaryKeyResponse.java @@ -0,0 +1,88 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.networking.model.response; + +import com.google.gson.annotations.SerializedName; + +/** + * Response object for endpoint returning a temporary key for ECIES encryption scheme. + */ +public class GetTemporaryKeyResponse { + + private String applicationKey; + private String activationId; + private String challenge; + private String publicKey; + @SerializedName("sub") + private String keyId; + @SerializedName("exp_ms") + private long expiration; + @SerializedName("iat_ms") + private long serverTime; + + public String getApplicationKey() { + return applicationKey; + } + + public void setApplicationKey(String applicationKey) { + this.applicationKey = applicationKey; + } + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public String getChallenge() { + return challenge; + } + + public void setChallenge(String challenge) { + this.challenge = challenge; + } + + public String getPublicKey() { + return publicKey; + } + + public void setPublicKey(String publicKey) { + this.publicKey = publicKey; + } + + public String getKeyId() { + return keyId; + } + + public long getExpiration() { + return expiration; + } + + public void setExpiration(long expiration) { + this.expiration = expiration; + } + + public long getServerTime() { + return serverTime; + } + + public void setServerTime(long serverTime) { + this.serverTime = serverTime; + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java index 5795e388..5e04c7a1 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java @@ -77,6 +77,7 @@ public class PowerAuthSDK { private final @NonNull PowerAuthTokenStore mTokenStore; private final @NonNull IServerStatusProvider mServerStatusProvider; private final @NonNull TimeSynchronizationService mTimeSynchronizationService; + private final @NonNull IKeystoreService mKeystoreService; /** * A builder that collects configurations and arguments for {@link PowerAuthSDK}. @@ -213,6 +214,10 @@ public PowerAuthSDK build(@NonNull Context context) throws PowerAuthErrorExcepti // Prepare low-level Session object. final Session session = new Session(mConfiguration.getSessionSetup(), timeSynchronizationService); + // Prepare keystore service and conned it with HTTP client + final DefaultKeystoreService keystoreService = new DefaultKeystoreService(timeSynchronizationService, session, mCallbackDispatcher, sharedLock, httpClient); + httpClient.setKeystoreService(keystoreService); + // Create a final PowerAuthSDK instance final PowerAuthSDK instance = new PowerAuthSDK( sharedLock, @@ -227,7 +232,8 @@ public PowerAuthSDK build(@NonNull Context context) throws PowerAuthErrorExcepti tokenStoreKeychain, mCallbackDispatcher, serverStatusProvider, - timeSynchronizationService); + timeSynchronizationService, + keystoreService); // Register time service for automatic reset. PowerAuthAppLifecycleListener.getInstance().registerTimeSynchronizationService(context, timeSynchronizationService); @@ -251,8 +257,9 @@ public PowerAuthSDK build(@NonNull Context context) throws PowerAuthErrorExcepti * @param biometryKeychain Keychain that store biometry-related key. * @param tokenStoreKeychain Keychain that store tokens. * @param callbackDispatcher Dispatcher that handle callbacks back to application. - * @param serverStatusProvider Implementation of IServerStatusProvider. - * @param timeSynchronizationService Implementation of IPowerAuthTimeSynchronizationService. + * @param serverStatusProvider Implementation of {@link IServerStatusProvider}. + * @param timeSynchronizationService Implementation of {@link IPowerAuthTimeSynchronizationService}. + * @param keystoreService Implementation of {@link IKeystoreService}. */ private PowerAuthSDK( @NonNull ReentrantLock sharedLock, @@ -267,7 +274,8 @@ private PowerAuthSDK( @NonNull Keychain tokenStoreKeychain, @NonNull ICallbackDispatcher callbackDispatcher, @NonNull IServerStatusProvider serverStatusProvider, - @NonNull IPowerAuthTimeSynchronizationService timeSynchronizationService) { + @NonNull IPowerAuthTimeSynchronizationService timeSynchronizationService, + @NonNull IKeystoreService keystoreService) { this.mLock = sharedLock; this.mSession = session; this.mConfiguration = configuration; @@ -281,6 +289,7 @@ private PowerAuthSDK( this.mTokenStore = new PowerAuthTokenStore(this, tokenStoreKeychain, client); this.mServerStatusProvider = serverStatusProvider; this.mTimeSynchronizationService = (TimeSynchronizationService) timeSynchronizationService; + this.mKeystoreService = keystoreService; } /** @@ -319,6 +328,17 @@ public byte[] getDeviceRelatedKey() { return context == null ? null : deviceRelatedKey(context); } + @NonNull + @Override + public IKeystoreService getKeystoreService() { + return mKeystoreService; + } + + @NonNull + @Override + public Session getCoreSession() { + return mSession; + } }; } @@ -642,126 +662,113 @@ public void run() { final IPrivateCryptoHelper cryptoHelper = getCryptoHelper(null); final JsonSerialization serialization = new JsonSerialization(); - final EciesEncryptor encryptor; - try { - // Prepare cryptographic helper & Layer2 ECIES encryptor - encryptor = cryptoHelper.getEciesEncryptor(EciesEncryptorId.ACTIVATION_PAYLOAD); - - // Prepare low level activation parameters - final ActivationStep1Param step1Param; - if (activation.activationCode != null) { - step1Param = new ActivationStep1Param(activation.activationCode.activationCode, activation.activationCode.activationSignature); - } else { - step1Param = null; - } - - // Start the activation - final ActivationStep1Result step1Result = mSession.startActivation(step1Param); - if (step1Result.errorCode != ErrorCode.OK) { - // Looks like create activation failed - final int errorCode = step1Result.errorCode == ErrorCode.Encryption - ? PowerAuthErrorCodes.SIGNATURE_ERROR - : PowerAuthErrorCodes.INVALID_ACTIVATION_DATA; - dispatchCallback(new Runnable() { - @Override - public void run() { - listener.onActivationCreateFailed(new PowerAuthErrorException(errorCode)); - } - }); - return null; - } + // Prepare low level activation parameters + final ActivationStep1Param step1Param; + if (activation.activationCode != null) { + step1Param = new ActivationStep1Param(activation.activationCode.activationCode, activation.activationCode.activationSignature); + } else { + step1Param = null; + } - // Prepare level 2 payload - final ActivationLayer2Request privateData = new ActivationLayer2Request(); - privateData.setActivationName(activation.activationName); - privateData.setExtras(activation.extras); - privateData.setActivationOtp(activation.additionalActivationOtp); - privateData.setDevicePublicKey(step1Result.devicePublicKey); - privateData.setPlatform(PowerAuthSystem.getPlatform()); - privateData.setDeviceInfo(PowerAuthSystem.getDeviceInfo()); - - // Prepare level 1 payload - final ActivationLayer1Request request = new ActivationLayer1Request(); - request.setType(activation.activationType); - request.setIdentityAttributes(activation.identityAttributes); - request.setCustomAttributes(activation.customAttributes); - - // The create activation endpoint needs a custom object processing where we encrypt the inner data - // with a different encryptor. We have to do this in the HTTP client's queue to guarantee that time - // service is already synchronized. - final CreateActivationEndpoint endpointDefinition = new CreateActivationEndpoint(() -> { - // Set encrypted level 2 activation data to the request. - request.setActivationData(serialization.encryptObjectToRequest(privateData, encryptor)); + // Start the activation + final ActivationStep1Result step1Result = mSession.startActivation(step1Param); + if (step1Result.errorCode != ErrorCode.OK) { + // Looks like create activation failed + final int errorCode = step1Result.errorCode == ErrorCode.Encryption + ? PowerAuthErrorCodes.SIGNATURE_ERROR + : PowerAuthErrorCodes.INVALID_ACTIVATION_DATA; + dispatchCallback(new Runnable() { + @Override + public void run() { + listener.onActivationCreateFailed(new PowerAuthErrorException(errorCode)); + } }); + return null; + } - // Fire HTTP request - return mClient.post( - request, - endpointDefinition, - cryptoHelper, - new INetworkResponseListener() { - @Override - public void onNetworkResponse(@NonNull ActivationLayer1Response response) { - // Process response from the server - try { - // Try to decrypt Layer2 object from response - final ActivationLayer2Response layer2Response = serialization.decryptObjectFromResponse(response.getActivationData(), encryptor, TypeToken.get(ActivationLayer2Response.class)); - // Prepare Step2 param for low level session - final RecoveryData recoveryData; - if (layer2Response.getActivationRecovery() != null) { - final ActivationRecovery rd = layer2Response.getActivationRecovery(); - recoveryData = new RecoveryData(rd.getRecoveryCode(), rd.getPuk()); - } else { - recoveryData = null; - } - final ActivationStep2Param step2Param = new ActivationStep2Param(layer2Response.getActivationId(), layer2Response.getServerPublicKey(), layer2Response.getCtrData(), recoveryData); - // Validate the response - final ActivationStep2Result step2Result = mSession.validateActivationResponse(step2Param); - // - if (step2Result.errorCode == ErrorCode.OK) { - final UserInfo userInfo = response.getUserInfo() != null ? new UserInfo(response.getUserInfo()) : null; - final CreateActivationResult result = new CreateActivationResult(step2Result.activationFingerprint, response.getCustomAttributes(), recoveryData, userInfo); - setLastFetchedUserInfo(userInfo); - listener.onActivationCreateSucceed(result); - return; - } - throw new PowerAuthErrorException(PowerAuthErrorCodes.INVALID_ACTIVATION_DATA, "Invalid activation data received from the server."); + // Prepare level 2 payload + final ActivationLayer2Request privateData = new ActivationLayer2Request(); + privateData.setActivationName(activation.activationName); + privateData.setExtras(activation.extras); + privateData.setActivationOtp(activation.additionalActivationOtp); + privateData.setDevicePublicKey(step1Result.devicePublicKey); + privateData.setPlatform(PowerAuthSystem.getPlatform()); + privateData.setDeviceInfo(PowerAuthSystem.getDeviceInfo()); + + // Prepare level 1 payload + final ActivationLayer1Request request = new ActivationLayer1Request(); + request.setType(activation.activationType); + request.setIdentityAttributes(activation.identityAttributes); + request.setCustomAttributes(activation.customAttributes); + + // The create activation endpoint needs a custom object processing where we encrypt the inner data + // with a different encryptor. We have to do this in the HTTP client's queue to guarantee that time + // service is already synchronized. + final CreateActivationEndpoint endpointDefinition = new CreateActivationEndpoint((endpoint) -> { + // Set encrypted level 2 activation data to the request. + // Prepare cryptographic helper & Layer2 ECIES encryptor + final EciesEncryptor encryptor = cryptoHelper.getEciesEncryptor(EciesEncryptorId.ACTIVATION_PAYLOAD); + request.setActivationData(serialization.encryptObjectToRequest(privateData, encryptor)); + ((CreateActivationEndpoint) endpoint).setLayer2Encryptor(encryptor); + }); - } catch (PowerAuthErrorException e) { - // In case of error, reset the session & report that exception - mSession.resetSession(false); - listener.onActivationCreateFailed(e); + // Fire HTTP request + return mClient.post( + request, + endpointDefinition, + cryptoHelper, + new INetworkResponseListener<>() { + @Override + public void onNetworkResponse(@NonNull ActivationLayer1Response response) { + // Process response from the server + try { + // Try to decrypt Layer2 object from response + final EciesEncryptor encryptor = endpointDefinition.getLayer2Encryptor(); + final ActivationLayer2Response layer2Response = serialization.decryptObjectFromResponse(response.getActivationData(), encryptor, TypeToken.get(ActivationLayer2Response.class)); + // Prepare Step2 param for low level session + final RecoveryData recoveryData; + if (layer2Response.getActivationRecovery() != null) { + final ActivationRecovery rd = layer2Response.getActivationRecovery(); + recoveryData = new RecoveryData(rd.getRecoveryCode(), rd.getPuk()); + } else { + recoveryData = null; } - } + final ActivationStep2Param step2Param = new ActivationStep2Param(layer2Response.getActivationId(), layer2Response.getServerPublicKey(), layer2Response.getCtrData(), recoveryData); + // Validate the response + final ActivationStep2Result step2Result = mSession.validateActivationResponse(step2Param); + // + if (step2Result.errorCode == ErrorCode.OK) { + final UserInfo userInfo = response.getUserInfo() != null ? new UserInfo(response.getUserInfo()) : null; + final CreateActivationResult result = new CreateActivationResult(step2Result.activationFingerprint, response.getCustomAttributes(), recoveryData, userInfo); + setLastFetchedUserInfo(userInfo); + listener.onActivationCreateSucceed(result); + return; + } + throw new PowerAuthErrorException(PowerAuthErrorCodes.INVALID_ACTIVATION_DATA, "Invalid activation data received from the server."); - @Override - public void onNetworkError(@NonNull Throwable throwable) { + } catch (PowerAuthErrorException e) { // In case of error, reset the session & report that exception mSession.resetSession(false); - listener.onActivationCreateFailed(throwable); + listener.onActivationCreateFailed(e); } + } - @Override - public void onCancel() { - // In case of cancel, reset the session - mSession.resetSession(false); - } - }); + @Override + public void onNetworkError(@NonNull Throwable throwable) { + // In case of error, reset the session & report that exception + mSession.resetSession(false); + listener.onActivationCreateFailed(throwable); + } - } catch (final PowerAuthErrorException e) { - mSession.resetSession(false); - dispatchCallback(new Runnable() { - @Override - public void run() { - listener.onActivationCreateFailed(e); - } - }); - return null; - } + @Override + public void onCancel() { + // In case of cancel, reset the session + mSession.resetSession(false); + } + }); } - /** * Create a new standard activation with given name and activation code by calling a PowerAuth Standard RESTful API. * @@ -1842,7 +1849,6 @@ public void onFetchEncryptedVaultUnlockKeyFailed(Throwable t) { listener.onDataSignedFailed(t); } }); - } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/DefaultKeystoreService.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/DefaultKeystoreService.java new file mode 100644 index 00000000..8adf2b38 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/DefaultKeystoreService.java @@ -0,0 +1,186 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.sdk.impl; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.core.EciesEncryptorScope; +import io.getlime.security.powerauth.core.ErrorCode; +import io.getlime.security.powerauth.core.ICoreTimeService; +import io.getlime.security.powerauth.core.Session; +import io.getlime.security.powerauth.exception.PowerAuthErrorCodes; +import io.getlime.security.powerauth.exception.PowerAuthErrorException; +import io.getlime.security.powerauth.networking.client.HttpClient; +import io.getlime.security.powerauth.networking.interfaces.ICancelable; +import io.getlime.security.powerauth.networking.model.response.GetTemporaryKeyResponse; +import io.getlime.security.powerauth.system.PowerAuthLog; + +import java.util.concurrent.locks.ReentrantLock; + +/** + * The {@code DefaultKeystoreService} class provides temporary encryption keys for ECIES encryption acquired from the + * server. The key itself is stored in {@link io.getlime.security.powerauth.core.Session} instance and is available + * for further encryption operations. + */ +public class DefaultKeystoreService implements IKeystoreService, GetTemporaryKeyTask.TaskCompletion { + + private final @NonNull ReentrantLock lock; + private final @NonNull Session session; + private final @NonNull ICoreTimeService timeService; + private final @NonNull ICallbackDispatcher callbackDispatcher; + private final @NonNull HttpClient httpClient; + + private final PublicKeyInfo applicationScopePublicKeyInfo; + private final PublicKeyInfo activationScopePublicKeyInfo; + + private static final long EXPIRATION_THRESHOLD = 10_000; + + /** + * Service constructor. + * @param timeService Time synchronization service. + * @param session Instance of core Session. + * @param callbackDispatcher Callback dispatcher. + * @param sharedLock Reentrant lock shared across multiple SDK objects. + * @param httpClient HTTP client implementation. + */ + public DefaultKeystoreService( + @NonNull ICoreTimeService timeService, + @NonNull Session session, + @NonNull ICallbackDispatcher callbackDispatcher, + @NonNull ReentrantLock sharedLock, + @NonNull HttpClient httpClient) { + this.lock = sharedLock; + this.timeService = timeService; + this.session = session; + this.callbackDispatcher = callbackDispatcher; + this.httpClient = httpClient; + this.applicationScopePublicKeyInfo = new PublicKeyInfo(EciesEncryptorScope.APPLICATION); + this.activationScopePublicKeyInfo = new PublicKeyInfo(EciesEncryptorScope.ACTIVATION); + } + + @Override + public boolean containsKeyForEncryptor(int scope) { + try { + lock.lock(); + if (session.hasPublicKeyForEciesScope(scope)) { + final PublicKeyInfo publicKeyInfo = getPublicKeyInfoForScope(scope); + if (publicKeyInfo.expiration >= 0 && publicKeyInfo.expiration - EXPIRATION_THRESHOLD < timeService.getCurrentTime()) { + return true; + } + PowerAuthLog.d("Removing expired public key for ECIES encryptor " + scope); + publicKeyInfo.expiration = -1; + session.removePublicKeyForEciesScope(scope); + } + return false; + } finally { + lock.unlock(); + } + } + + @Override + @Nullable + public ICancelable createKeyForEncryptor(@EciesEncryptorScope int scope, @NonNull IPrivateCryptoHelper cryptoHelper, @NonNull ICreateKeyListener listener) { + if (scope == EciesEncryptorScope.ACTIVATION && !session.hasValidActivation()) { + callbackDispatcher.dispatchCallback(() -> listener.onCreateKeyFailed(new PowerAuthErrorException(PowerAuthErrorCodes.INVALID_ACTIVATION_STATE))); + return null; + } + try { + lock.lock(); + if (containsKeyForEncryptor(scope)) { + callbackDispatcher.dispatchCallback(listener::onCreateKeySucceeded); + return null; + } + final PublicKeyInfo publicKeyInfo = getPublicKeyInfoForScope(scope); + GetTemporaryKeyTask mainTask = publicKeyInfo.task; + if (mainTask == null) { + mainTask = new GetTemporaryKeyTask(scope, cryptoHelper, lock, callbackDispatcher, httpClient, this); + publicKeyInfo.task = mainTask; + publicKeyInfo.timeSynchronizationTask = timeService.startTimeSynchronizationTask(); + } + return mainTask.createChildTask(new ITaskCompletion<>() { + @Override + public void onSuccess(@NonNull GetTemporaryKeyResponse response) { + listener.onCreateKeySucceeded(); + } + + @Override + public void onFailure(@NonNull Throwable failure) { + listener.onCreateKeyFailed(failure); + } + }); + } finally { + lock.unlock(); + } + } + + @Override + public void onGetTemporaryKeyTaskCompletion(@NonNull GetTemporaryKeyTask task, @Nullable GetTemporaryKeyResponse response) { + final int scope = task.getScope(); + final PublicKeyInfo publicKeyInfo = getPublicKeyInfoForScope(scope); + publicKeyInfo.task = null; + if (response != null) { + final int errorCode = session.setPublicKeyForEciesScope(scope, response.getPublicKey(), response.getKeyId()); + if (errorCode == ErrorCode.OK) { + publicKeyInfo.expiration = response.getExpiration(); + timeService.completeTimeSynchronizationTask(publicKeyInfo.timeSynchronizationTask, response.getServerTime()); + PowerAuthLog.d("Saving public key for ECIES encryptor " + scope); + } else { + PowerAuthLog.e("Failed to update public key for ECIES encryption. Code = " + errorCode); + } + } + publicKeyInfo.timeSynchronizationTask = null; + } + + /** + * Get instance of {@link PublicKeyInfo} class depending on the scope. + * @param scope Scope of encryption. + * @return Instance of {@link PublicKeyInfo} class depending on the scope. + */ + @NonNull + private PublicKeyInfo getPublicKeyInfoForScope(@EciesEncryptorScope int scope) { + return scope == EciesEncryptorScope.APPLICATION ? applicationScopePublicKeyInfo : activationScopePublicKeyInfo; + } + + /** + * Internal class containing additional information about retrieved public key. + */ + private static class PublicKeyInfo { + /** + * Scope of the key. + */ + final @EciesEncryptorScope int scope; + /** + * If positive number, then contain timestamp when the key expires on the server. + */ + long expiration; + /** + * If not null, then service is currently retrieving the key from the server. + */ + GetTemporaryKeyTask task; + /** + * Time synchronization task. + */ + Object timeSynchronizationTask; + + PublicKeyInfo(@EciesEncryptorScope int scope) { + this.scope = scope; + this.expiration = -1; + this.task = null; + this.timeSynchronizationTask = null; + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/GetTemporaryKeyTask.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/GetTemporaryKeyTask.java new file mode 100644 index 00000000..d837cf5d --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/GetTemporaryKeyTask.java @@ -0,0 +1,242 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.sdk.impl; + +import android.text.TextUtils; +import android.util.Base64; +import android.util.Pair; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.gson.reflect.TypeToken; +import io.getlime.security.powerauth.core.*; +import io.getlime.security.powerauth.exception.PowerAuthErrorCodes; +import io.getlime.security.powerauth.exception.PowerAuthErrorException; +import io.getlime.security.powerauth.networking.client.HttpClient; +import io.getlime.security.powerauth.networking.client.JsonSerialization; +import io.getlime.security.powerauth.networking.endpoints.GetTemporaryKeyEndpoint; +import io.getlime.security.powerauth.networking.interfaces.ICancelable; +import io.getlime.security.powerauth.networking.interfaces.INetworkResponseListener; +import io.getlime.security.powerauth.networking.model.entity.JwtHeader; +import io.getlime.security.powerauth.networking.model.entity.JwtObject; +import io.getlime.security.powerauth.networking.model.request.GetTemporaryKeyRequest; +import io.getlime.security.powerauth.networking.model.response.GetTemporaryKeyResponse; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.locks.ReentrantLock; + +/** + * The {@code GetTemporaryKeyTask} class implements getting temporary encryption key from the server. + */ +public class GetTemporaryKeyTask extends GroupedTask { + + /** + * The task completion callback. + */ + public interface TaskCompletion { + /** + * Function is called once the {@code GetTemporaryKeyTask} finishes its job. + * @param task The completed task. + * @param response Response received from the server. If null, then task failed to get the response. + */ + void onGetTemporaryKeyTaskCompletion(@NonNull GetTemporaryKeyTask task, @Nullable GetTemporaryKeyResponse response); + } + + private final IPrivateCryptoHelper cryptoHelper; + private final HttpClient httpClient; + private final TaskCompletion taskCompletion; + private final @EciesEncryptorScope int scope; + private final JsonSerialization serialization; + + /** + * Construct task with required parameters. + * @param scope Scope of key to obtain from the server. + * @param cryptoHelper Instance of {@link IPrivateCryptoHelper} interface. + * @param sharedLock Reentrant lock shared across multiple SDK objects. + * @param dispatcher Callback dispatcher. + * @param httpClient HTTP client. + * @param completion Listener to call once the task is completed. + */ + public GetTemporaryKeyTask( + @EciesEncryptorScope int scope, + @NonNull IPrivateCryptoHelper cryptoHelper, + @NonNull ReentrantLock sharedLock, + @NonNull ICallbackDispatcher dispatcher, + @NonNull HttpClient httpClient, + @NonNull TaskCompletion completion) { + super("GetTemporaryKey", sharedLock, dispatcher); + this.scope = scope; + this.cryptoHelper = cryptoHelper; + this.httpClient = httpClient; + this.taskCompletion = completion; + this.serialization = new JsonSerialization(); + } + + /** + * Return the scope of the temporary key. + * @return Scope of the temporary key. + */ + public @EciesEncryptorScope int getScope() { + return scope; + } + + /** + * @return {@code true} if this task is configured to get the temporary key in application scope. + */ + private boolean isApplicationScope() { + return scope == EciesEncryptorScope.APPLICATION; + } + + @Override + public void onGroupedTaskStart() { + super.onGroupedTaskStart(); + try { + final Pair requestPair = prepareRequestJwt(); + if (requestPair == null) { + return; + } + ICancelable cancelable = httpClient.post(requestPair.first, new GetTemporaryKeyEndpoint(), cryptoHelper, new INetworkResponseListener<>() { + @Override + public void onNetworkResponse(@NonNull JwtObject jwtObject) { + try { + final GetTemporaryKeyResponse response = processResponseJwt(jwtObject); + validateResponse(requestPair.first, response); + complete(response); + } catch (Throwable t) { + complete(t); + } + } + + @Override + public void onNetworkError(@NonNull Throwable throwable) { + complete(throwable); + } + + @Override + public void onCancel() { + + } + }); + addCancelableOperation(cancelable); + } catch (Throwable t) { + complete(t); + } + } + + @Override + public void onGroupedTaskComplete(@Nullable GetTemporaryKeyResponse response, @Nullable Throwable failure) { + super.onGroupedTaskComplete(response, failure); + taskCompletion.onGetTemporaryKeyTaskCompletion(this, response); + } + + /** + * Prepare a pair of objects. The first object contains information before its encoded into JWT request. The second + * object is the same object, but encoded and signed as JWT. + * @return A pair of objects. The first object contains information before its encoded into JWT request. The second + * object is the same object, but encoded and signed as JWT. + */ + private Pair prepareRequestJwt() { + final Session session = cryptoHelper.getCoreSession(); + final SignatureUnlockKeys unlockKeys; + final String activationId; + final int signingKey; + if (isApplicationScope()) { + signingKey = SigningDataKey.HMAC_APPLICATION; + activationId = null; + unlockKeys = null; + } else { + signingKey = SigningDataKey.HMAC_ACTIVATION; + activationId = session.getActivationIdentifier(); + if (activationId == null) { + this.complete(new PowerAuthErrorException(PowerAuthErrorCodes.MISSING_ACTIVATION)); + return null; + } + unlockKeys = new SignatureUnlockKeys(cryptoHelper.getDeviceRelatedKey(), null, null); + } + // Prepare request data + final String applicationKey = session.getApplicationKey(); + final String challenge = Base64.encodeToString(CryptoUtils.randomBytes(18), Base64.NO_WRAP); + final GetTemporaryKeyRequest request = new GetTemporaryKeyRequest(applicationKey, activationId, challenge); + // Prepare JWT string + final String jwtHeader = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."; // {"alg":"HS256","typ":"JWT"} with dot separator + final String jwtPayload = serialization.serializeJwtObject(request); + final String jwtHeaderPlusPayload = jwtHeader + jwtPayload; + final SignedData dataToSign = new SignedData(jwtHeaderPlusPayload.getBytes(StandardCharsets.US_ASCII), null, signingKey, SignatureFormat.DEFAULT); + if (ErrorCode.OK != session.signDataWithHmacKey(dataToSign, unlockKeys)) { + this.complete(new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR)); + return null; + } + final String jwtSignature = Base64.encodeToString(dataToSign.signature, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); + final JwtObject jwtObject = new JwtObject(jwtHeaderPlusPayload + "." + jwtSignature); + return Pair.create(request, jwtObject); + } + + /** + * Process JWT response received from the server. + * @param response JWT response. + * @return Decoded payload extracted from the JWT response. + * @throws PowerAuthErrorException In case of failure. + */ + private GetTemporaryKeyResponse processResponseJwt(@NonNull JwtObject response) throws PowerAuthErrorException { + final String jwtString = response.jwt; + if (jwtString == null) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.NETWORK_ERROR, "Empty JWT response"); + } + final String[] jwtComponents = TextUtils.split(jwtString, "\\."); + if (jwtComponents.length != 3) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.NETWORK_ERROR, "Invalid JWT response"); + } + final String jwtHeader = jwtComponents[0]; + final String jwtPayload = jwtComponents[1]; + final String jwtSignature = jwtComponents[2]; + if (jwtHeader.isEmpty() || jwtPayload.isEmpty() || jwtSignature.isEmpty()) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.NETWORK_ERROR, "Invalid JWT response"); + } + final JwtHeader jwtHeaderObject = serialization.deserializeJwtObject(jwtHeader, TypeToken.get(JwtHeader.class)); + if (!"JWT".equals(jwtHeaderObject.typ)) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.NETWORK_ERROR, "Unsupported JWT type in response"); + } + if (!"ES256".equals(jwtHeaderObject.alg)) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.NETWORK_ERROR, "Unsupported JWT algorithm in response"); + } + final SignedData signedData = new SignedData( + (jwtHeader + "." + jwtPayload).getBytes(StandardCharsets.US_ASCII), + Base64.decode(jwtSignature, Base64.NO_WRAP| Base64.URL_SAFE | Base64.NO_PADDING), + isApplicationScope() ? SigningDataKey.ECDSA_MASTER_SERVER_KEY : SigningDataKey.ECDSA_PERSONALIZED_KEY, + SignatureFormat.ECDSA_JOSE); + if (ErrorCode.OK != cryptoHelper.getCoreSession().verifyServerSignedData(signedData)) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR, "Invalid signature in JWT response"); + } + return serialization.deserializeJwtObject(jwtPayload, TypeToken.get(GetTemporaryKeyResponse.class)); + } + + /** + * Validate whether values in response object match the important values form the request. + * @param request Request object. + * @param response Response object. + * @throws PowerAuthErrorException In case that important value doesn't match. + */ + private void validateResponse(@NonNull GetTemporaryKeyRequest request, @NonNull GetTemporaryKeyResponse response) throws PowerAuthErrorException { + boolean match = request.getChallenge().equals(response.getChallenge()); + match = match && request.getApplicationKey().equals(response.getApplicationKey()); + if (!isApplicationScope()) { + match = match && request.getActivationId() != null && request.getActivationId().equals(response.getActivationId()); + } + if (!match) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR, "JWT response doesn't match request"); + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/ICreateKeyListener.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/ICreateKeyListener.java new file mode 100644 index 00000000..35eef0ae --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/ICreateKeyListener.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.sdk.impl; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; + +/** + * Listener for getting temporary encryption key from the server. The actual information about the key is stored in + * the low level {@code Session} object. + */ +public interface ICreateKeyListener { + + /** + * The temporary encryption key has been successfully acquired from the server. + */ + @MainThread + void onCreateKeySucceeded(); + + /** + * Failed to acquire the temporary encryption key from the server. + * @param throwable Failure to report. + */ + @MainThread + void onCreateKeyFailed(@NonNull Throwable throwable); +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IKeystoreService.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IKeystoreService.java new file mode 100644 index 00000000..c0c83b90 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IKeystoreService.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.sdk.impl; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.core.EciesEncryptorScope; +import io.getlime.security.powerauth.networking.interfaces.ICancelable; + +/** + * The {@code IKeystoreService} is interface for getting temporary encryption keys for ECIES encryption from the server. + * The key itself is stored in {@link io.getlime.security.powerauth.core.Session} instance and is available for further + * encryption operations. + */ +public interface IKeystoreService { + /** + * Determine whether the service contains key for the requested encryption scope. + * @param scope The scope of the key. + * @return {@code true} if service contains a valid key for the requested encryption scope. + */ + boolean containsKeyForEncryptor(@EciesEncryptorScope int scope); + + /** + * Create a key for the requested encryptor scope. If the already exist and is valid, then does nothing. + * @param scope The scope of the key. + * @param cryptoHelper Implementation of {@link IPrivateCryptoHelper} interface. + * @param listener The listener where the result of the operation will be notified. + * @return Cancelable operation if communication with the server is required, or {@code null} if the result of + * the call has been determined immediately. + */ + @Nullable + ICancelable createKeyForEncryptor(@EciesEncryptorScope int scope, @NonNull IPrivateCryptoHelper cryptoHelper, @NonNull ICreateKeyListener listener); +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IPrivateCryptoHelper.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IPrivateCryptoHelper.java index b11a359a..8e7271ee 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IPrivateCryptoHelper.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IPrivateCryptoHelper.java @@ -20,6 +20,7 @@ import androidx.annotation.Nullable; import io.getlime.security.powerauth.core.EciesEncryptor; +import io.getlime.security.powerauth.core.Session; import io.getlime.security.powerauth.ecies.EciesEncryptorId; import io.getlime.security.powerauth.exception.PowerAuthErrorException; import io.getlime.security.powerauth.sdk.PowerAuthAuthentication; @@ -66,4 +67,15 @@ public interface IPrivateCryptoHelper { * {@link android.content.Context} object is not available for the crypto helper. */ @Nullable byte[] getDeviceRelatedKey(); + + /** + * @return Object implementing {@link IKeystoreService} interface. + */ + @NonNull IKeystoreService getKeystoreService(); + + /** + * @return Core {@link Session} object associated with this helper. + */ + @NonNull Session getCoreSession(); + } diff --git a/proj-android/configs/default-instrumentation-tests.properties b/proj-android/configs/default-instrumentation-tests.properties index 759a030a..f58da028 100644 --- a/proj-android/configs/default-instrumentation-tests.properties +++ b/proj-android/configs/default-instrumentation-tests.properties @@ -11,7 +11,7 @@ test.powerauth.restApiUrl=http://localhost:8080/enrollment-server test.powerauth.serverApiUrl=http://localhost:8080/powerauth-java-server test.powerauth.serverAuthUser= test.powerauth.serverAuthPass= -test.powerauth.serverVersion=1.5 +test.powerauth.serverVersion=1.9 test.powerauth.serverAutoCommit= test.powerauth.appName=AutomaticTest-Android test.powerauth.appVersion=default diff --git a/proj-xcode/PowerAuth2/private/PA2KeystoreService.m b/proj-xcode/PowerAuth2/private/PA2KeystoreService.m index 5592b45d..9c2cda94 100644 --- a/proj-xcode/PowerAuth2/private/PA2KeystoreService.m +++ b/proj-xcode/PowerAuth2/private/PA2KeystoreService.m @@ -20,6 +20,8 @@ #import "PA2GetTemporaryKeyResponse.h" #import +#define PUBLIC_KEY_EXPIRATION_THRESHOLD 10.0 + #pragma mark - Service data @interface PA2PublicKeyInfo : NSObject @@ -76,30 +78,31 @@ - (instancetype) initWithHttpClient:(PA2HttpClient*)httpClient callback(PA2MakeError(PowerAuthErrorCode_MissingActivation, nil)); return nil; } - if ([self hasKeyForEncryptorScope:encryptorScope]) { - callback(nil); - return nil; - } [_lock lock]; - - id task; - PA2PublicKeyInfo * pki = [self pkiForScope:encryptorScope]; - PA2GetTemporaryKeyTask * mainTask = pki.task; - if (!mainTask) { - mainTask = [[PA2GetTemporaryKeyTask alloc] initWithHttpClient:_httpClient - sessionProvider:_sessionInterface - sharedLock:_lock - applicationKey:_applicationKey - deviceRelatedKey:_deviceRelatedKey - encryptorScope:encryptorScope - delegate:self]; - pki.task = mainTask; - pki.timeSynchronizationTask = [_timeService startTimeSynchronizationTask]; + id task = nil; + if ([self hasKeyForEncryptorScope:encryptorScope]) { + // Key already exist + callback(nil); + } else { + // Key must be received from the server + PA2PublicKeyInfo * pki = [self pkiForScope:encryptorScope]; + PA2GetTemporaryKeyTask * mainTask = pki.task; + if (!mainTask) { + mainTask = [[PA2GetTemporaryKeyTask alloc] initWithHttpClient:_httpClient + sessionProvider:_sessionInterface + sharedLock:_lock + applicationKey:_applicationKey + deviceRelatedKey:_deviceRelatedKey + encryptorScope:encryptorScope + delegate:self]; + pki.task = mainTask; + pki.timeSynchronizationTask = [_timeService startTimeSynchronizationTask]; + } + task = [mainTask createChildTask:^(PA2GetTemporaryKeyResponse * _Nullable result, NSError * _Nullable error) { + callback(error); + }]; } - task = [mainTask createChildTask:^(PA2GetTemporaryKeyResponse * _Nullable result, NSError * _Nullable error) { - callback(error); - }]; [_lock unlock]; return task; } @@ -120,7 +123,7 @@ - (BOOL) hasKeyForEncryptorScope:(PowerAuthCoreEciesEncryptorScope)encryptorScop PA2PublicKeyInfo * pki = [self pkiForScope:encryptorScope]; NSTimeInterval expiration = pki.expiration; keyIsSet = expiration >= 0.0; - keyIsExpired = expiration < [_timeService currentTime]; + keyIsExpired = expiration - PUBLIC_KEY_EXPIRATION_THRESHOLD < [_timeService currentTime]; if (keyIsExpired) { pki.expiration = -1; } diff --git a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerAPI.m b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerAPI.m index ff001a57..baed7318 100644 --- a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerAPI.m +++ b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerAPI.m @@ -94,7 +94,11 @@ - (BOOL) validateConnection } if (!_appVersion.supported) { NSLog(@"Application version '%@' is not supported", _appVersion.applicationVersionName); - return NO; + if (![self supportApplicationVersion:_appVersion.applicationVersionId]) { + NSLog(@"Failed to set application version '%@' supported", _appVersion.applicationVersionName); + return NO; + } + _appVersion.supported = YES; } _hasValidConnection = YES; diff --git a/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h b/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h index 5986040d..943a526e 100644 --- a/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h +++ b/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h @@ -350,7 +350,7 @@ typedef NS_ENUM(int, PowerAuthCoreSignatureFormat) { */ @interface PowerAuthCoreSignedData : NSObject /** - A signign key to use. + A signing key to use. */ @property (nonatomic, assign) PowerAuthCoreSigningDataKey signingDataKey; /** diff --git a/src/PowerAuth/crypto/ECC.cpp b/src/PowerAuth/crypto/ECC.cpp index 81a015c2..cbd450d1 100644 --- a/src/PowerAuth/crypto/ECC.cpp +++ b/src/PowerAuth/crypto/ECC.cpp @@ -25,8 +25,6 @@ #include "../utils/DataReader.h" #include "../utils/DataWriter.h" -#include -#include namespace io { diff --git a/src/PowerAuth/utils/DataReader.h b/src/PowerAuth/utils/DataReader.h index 6c746a68..40ef8e26 100644 --- a/src/PowerAuth/utils/DataReader.h +++ b/src/PowerAuth/utils/DataReader.h @@ -145,7 +145,7 @@ namespace utils */ bool readCount(size_t & out_value); /** - Reads count in ASN.1 format from the data stream. This is cimplementary method + Reads count in ASN.1 format from the data stream. This is complementary method to DataWriter::writeAsn1Count(). */ bool readAsn1Count(size_t & out_value);