Skip to content

Commit

Permalink
Android: Final support for ECIES V3.3
Browse files Browse the repository at this point in the history
  • Loading branch information
hvge committed Sep 3, 2024
1 parent a67f0af commit 9a73596
Show file tree
Hide file tree
Showing 26 changed files with 1,125 additions and 184 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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 + "\"";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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.
*
Expand Down Expand Up @@ -139,44 +174,50 @@ public <TRequest, TResponse> ICancelable post(
@Nullable PowerAuthAuthentication authentication,
@NonNull INetworkResponseListener<TResponse> 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<TResponse>() {
@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
Expand All @@ -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 <TRequest> type of request object
* @param <TResponse> type of response object
*/
private <TRequest, TResponse> void compositePostImpl(
@Nullable TRequest object,
@NonNull IEndpointDefinition<TResponse> endpoint,
@Nullable IPrivateCryptoHelper helper,
@Nullable PowerAuthAuthentication authentication,
@NonNull CompositeCancelableTask compositeTask,
@NonNull INetworkResponseListener<TResponse> listener) {
// Create actual HTTP
final ICancelable actualTask = postImpl(object, endpoint, helper, authentication, new INetworkResponseListener<TResponse>() {
@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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ public JsonObject parseResponseObject(@Nullable byte[] data) throws JsonParseExc
@NonNull
public <TRequest> byte[] encryptObject(@Nullable TRequest object, @NonNull EciesEncryptor encryptor) throws PowerAuthErrorException {
final EciesEncryptedRequest request = encryptObjectToRequest(object, encryptor);
request.setTemporaryKeyId(encryptor.getMetadata().getTemporaryKeyId());
return serializeObject(request);
}

Expand Down Expand Up @@ -228,6 +229,7 @@ public <TRequest> 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());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -29,6 +30,7 @@
public class CreateActivationEndpoint implements IEndpointDefinition<ActivationLayer1Response> {

private final ICustomEndpointOperation beforeRequestSerialization;
private EciesEncryptor layer2Encryptor;

/**
* Construct endpoint with a custom serialization step executed before the request is serialized.
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<JwtObject> {
@NonNull
@Override
public String getRelativePath() {
return "/pa/v3/keystore/create";
}

@Nullable
@Override
public TypeToken<JwtObject> getResponseType() {
return TypeToken.get(JwtObject.class);
}

@Override
public boolean isAvailableInProtocolUpgrade() {
return true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@

package io.getlime.security.powerauth.networking.interfaces;

import androidx.annotation.NonNull;
import io.getlime.security.powerauth.exception.PowerAuthErrorException;

/**
* Defines custom operation executed while endpoint is processed.
*/
@FunctionalInterface
public interface ICustomEndpointOperation {
void customEndpointOperation() throws PowerAuthErrorException;
void customEndpointOperation(@NonNull IEndpointDefinition endpointDefinition) throws PowerAuthErrorException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Loading

0 comments on commit 9a73596

Please sign in to comment.