
io.getlime.security.powerauth.lib.cmd.steps.AbstractBaseStep Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of powerauth-java-cmd-lib Show documentation
Show all versions of powerauth-java-cmd-lib Show documentation
PowerAuth Command-line Utility - Java Library
The newest version!
/*
* PowerAuth Command-line utility
* Copyright 2023 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.lib.cmd.steps;
import com.wultra.core.rest.client.base.RestClient;
import com.wultra.core.rest.client.base.RestClientException;
import io.getlime.security.powerauth.crypto.lib.encryptor.ClientEncryptor;
import io.getlime.security.powerauth.crypto.lib.encryptor.EncryptorFactory;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.exception.EciesException;
import io.getlime.security.powerauth.crypto.lib.encryptor.model.*;
import io.getlime.security.powerauth.crypto.lib.encryptor.model.v3.ClientEncryptorSecrets;
import io.getlime.security.powerauth.crypto.lib.util.KeyConvertor;
import io.getlime.security.powerauth.lib.cmd.consts.PowerAuthStep;
import io.getlime.security.powerauth.lib.cmd.consts.PowerAuthVersion;
import io.getlime.security.powerauth.lib.cmd.logging.DisabledStepLogger;
import io.getlime.security.powerauth.lib.cmd.logging.StepLogger;
import io.getlime.security.powerauth.lib.cmd.logging.StepLoggerFactory;
import io.getlime.security.powerauth.lib.cmd.status.ResultStatusService;
import io.getlime.security.powerauth.lib.cmd.steps.context.RequestContext;
import io.getlime.security.powerauth.lib.cmd.steps.context.ResponseContext;
import io.getlime.security.powerauth.lib.cmd.steps.context.StepContext;
import io.getlime.security.powerauth.lib.cmd.steps.context.security.SimpleSecurityContext;
import io.getlime.security.powerauth.lib.cmd.steps.model.data.BaseStepData;
import io.getlime.security.powerauth.lib.cmd.steps.model.feature.DryRunCapable;
import io.getlime.security.powerauth.lib.cmd.steps.model.feature.ResultStatusChangeable;
import io.getlime.security.powerauth.lib.cmd.steps.pojo.ResultStatusObject;
import io.getlime.security.powerauth.lib.cmd.util.*;
import io.getlime.security.powerauth.rest.api.model.request.EciesEncryptedRequest;
import io.getlime.security.powerauth.rest.api.model.response.EciesEncryptedResponse;
import jakarta.annotation.Nullable;
import lombok.Getter;
import org.json.simple.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import java.security.PublicKey;
import java.util.*;
import static io.getlime.security.powerauth.lib.cmd.util.TemporaryKeyUtil.TEMPORARY_KEY_ID;
import static io.getlime.security.powerauth.lib.cmd.util.TemporaryKeyUtil.TEMPORARY_PUBLIC_KEY;
/**
* Abstract step with common execution patterns and methods
*
* @param Model data type
* @param Response type
* @author Lukas Lukovsky, [email protected]
*/
public abstract class AbstractBaseStep implements BaseStep {
private static final Logger logger = LoggerFactory.getLogger(AbstractBaseStep.class);
private static final KeyConvertor KEY_CONVERTOR = new KeyConvertor();
/**
* Corresponding PowerAuth step
*/
@Getter
private final PowerAuthStep step;
/**
* Supported versions of PowerAuth by this step
*/
@Getter
private final List supportedVersions;
/**
* Result status service
*/
protected final ResultStatusService resultStatusService;
/**
* Step logger
*/
protected final StepLoggerFactory stepLoggerFactory;
private static final EncryptorFactory ENCRYPTOR_FACTORY = new EncryptorFactory();
/**
* Constructor
*
* @param step Corresponding PowerAuth step
* @param supportedVersions Supported versions of PowerAuth
* @param resultStatusService Result status service
* @param stepLoggerFactory Step logger factory
*/
public AbstractBaseStep(PowerAuthStep step,
List supportedVersions,
ResultStatusService resultStatusService,
StepLoggerFactory stepLoggerFactory) {
this.step = step;
this.supportedVersions = List.copyOf(supportedVersions);
this.resultStatusService = resultStatusService;
this.stepLoggerFactory = stepLoggerFactory;
}
/**
* Prepares a context for this step execution
*
* @param stepLogger Step logger
* @param context Context data
* @return Step context
* @throws Exception when an error during context preparation occurred.
*/
public abstract StepContext prepareStepContext(StepLogger stepLogger, Map context) throws Exception;
/**
* @return Type reference of the response object
*/
protected abstract ParameterizedTypeReference getResponseTypeReference();
/**
* Execute this step with given logger and context objects.
*
* Keeps backward compatibility with former approaches of step instantiation and execution
*
* @param stepLogger Step logger.
* @param context Context objects.
* @return Result status object (with current activation status), null in case of failure.
* @throws Exception In case of a failure.
*/
public final ResultStatusObject execute(StepLogger stepLogger, Map context) throws Exception {
if (stepLogger == null) {
stepLogger = DisabledStepLogger.INSTANCE;
}
stepLogger.writeItem(
getStep().id() + "-start",
getStep().description() + " Started",
null,
"OK",
null
);
final StepContext stepContext;
try {
stepContext = prepareStepContext(stepLogger, context);
if (stepContext == null) {
return null;
}
} catch (EciesException e) {
stepLogger.writeError(getStep().id() + "-error-encryption", e);
stepLogger.writeDoneFailed(getStep().id() + "-failed");
return null;
}
try {
ResponseContext responseContext = callServer(stepContext);
if (responseContext != null) {
stepContext.setResponseContext(responseContext);
processResponse(stepContext);
stepLogger.writeDoneOK(getStep().id() + "-success");
} else if (!isDryRun(stepContext.getModel())) {
stepContext.getStepLogger().writeDoneFailed(getStep().id() + "-failed");
}
} catch (Exception exception) {
stepLogger.writeError(getStep().id() + "-error-generic", exception);
stepLogger.writeDoneFailed(getStep().id() + "-failed");
return null;
}
final JSONObject resultStatusObject = stepContext.getModel().getResultStatusObject();
if (resultStatusObject == null) {
return null;
} else {
return ResultStatusObject.fromJsonObject(resultStatusObject);
}
}
/**
* Prepares encryptor and encrypts request data with given encryptor.
* The encrypted request is then added to the request context of this step.
*
* @param stepContext Context of this step
* @param applicationKey Application key.
* @param applicationSecret Application secret
* @param encryptorId Encryptor identifier
* @param data Request data for the encryption
* @param scope Encryptor scope
* @throws Exception when an error during encryption of the request data occurred
*/
public void addEncryptedRequest(StepContext stepContext, String applicationKey, String applicationSecret, EncryptorId encryptorId, byte[] data, EncryptorScope scope) throws Exception {
M model = stepContext.getModel();
final SimpleSecurityContext securityContext = (SimpleSecurityContext) stepContext.getSecurityContext();
final ResultStatusObject resultStatusObject = model.getResultStatus();
fetchTemporaryKey(stepContext, scope);
final ClientEncryptor encryptor;
if (securityContext == null) {
final String temporaryKeyId = (String) stepContext.getAttributes().get(TEMPORARY_KEY_ID);
final String temporaryPublicKey = (String) stepContext.getAttributes().get(TEMPORARY_PUBLIC_KEY);
final PublicKey encryptionPublicKey = temporaryKeyId == null ?
resultStatusObject.getServerPublicKeyObject() :
KEY_CONVERTOR.convertBytesToPublicKey(Base64.getDecoder().decode(temporaryPublicKey));
final byte[] transportMasterKeyBytes = Base64.getDecoder().decode(resultStatusObject.getTransportMasterKey());
final EncryptorParameters encryptorParameters = new EncryptorParameters(model.getVersion().value(), applicationKey, resultStatusObject.getActivationId(), temporaryKeyId);
final ClientEncryptorSecrets encryptorSecrets = new ClientEncryptorSecrets(encryptionPublicKey, applicationSecret, transportMasterKeyBytes);
encryptor = ENCRYPTOR_FACTORY.getClientEncryptor(encryptorId, encryptorParameters, encryptorSecrets);
stepContext.setSecurityContext(SimpleSecurityContext.builder()
.encryptor(encryptor)
.build());
} else {
encryptor = securityContext.getEncryptor();
}
addEncryptedRequest(stepContext, encryptor, data);
}
/**
* Encrypts request data with given encryptor.
* The encrypted request is then added to the request context of this step.
*
* @param stepContext Context of this step
* @param encryptor Encryptor to use
* @param data Request data for the encryption
* @throws Exception when an error during encryption of the request data occurred
*/
public void addEncryptedRequest(StepContext stepContext, ClientEncryptor encryptor, byte[] data) throws Exception {
SimpleSecurityContext securityContext = (SimpleSecurityContext) stepContext.getSecurityContext();
if (securityContext == null) {
stepContext.setSecurityContext(
SimpleSecurityContext.builder()
.encryptor(encryptor)
.build()
);
} else if (securityContext.getEncryptor() != encryptor) {
throw new Exception("Different encryptor is already set to security context");
}
final EncryptedRequest encryptedRequest = encryptor.encryptRequest(data);
final EciesEncryptedRequest requestObject = SecurityUtil.createEncryptedRequest(encryptedRequest);
stepContext.getRequestContext().setRequestObject(requestObject);
}
/**
* Fetch temporary key for current request, if applicable.
* @param stepContext Step context.
* @param scope ECIES scope.
* @throws Exception In case request fails.
*/
public void fetchTemporaryKey(StepContext stepContext, EncryptorScope scope) throws Exception {
TemporaryKeyUtil.fetchTemporaryKey(getStep(), stepContext, scope);
}
/**
* Decrypts an object from a response
*
* @param stepContext Step context
* @param cls Class type of the decrypted object
* @param Class of the decrypted object
* @return Decrypted object from the provided response
*/
public T decryptResponse(StepContext, EciesEncryptedResponse> stepContext, Class cls) {
try {
final SimpleSecurityContext securityContext = (SimpleSecurityContext) stepContext.getSecurityContext();
final EciesEncryptedResponse encryptedResponse = stepContext.getResponseContext().getResponseBodyObject();
final byte[] decryptedBytes = securityContext.getEncryptor().decryptResponse(new EncryptedResponse(
encryptedResponse.getEncryptedData(),
encryptedResponse.getMac(),
encryptedResponse.getNonce(),
encryptedResponse.getTimestamp()
));
final T responsePayload = RestClientConfiguration.defaultMapper().readValue(decryptedBytes, cls);
stepContext.getResponseContext().setResponsePayloadDecrypted(responsePayload);
stepContext.getStepLogger().writeItem(
getStep().id() + "-response-decrypt",
"Decrypted Response",
"Following data were decrypted",
"OK",
responsePayload
);
return responsePayload;
} catch (Exception ex) {
logger.debug(ex.getMessage(), ex);
return null;
}
}
/**
* Optional processing of the response data
*
* @param stepContext Step context
* @throws Exception when an error during response processing occurred
*/
public void processResponse(StepContext stepContext) throws Exception { }
/**
* Processing of the response data bytes
*
* @param stepContext Step context
* @param responseBody Response body bytes
* @param responseObjectClass Response object class
* @throws Exception when an error during response processing occurred
*/
public final void processResponse(StepContext stepContext, byte[] responseBody, Class responseObjectClass) throws Exception {
final R responseBodyObject = HttpUtil.fromBytes(responseBody, responseObjectClass);
final ResponseEntity responseEntity = ResponseEntity.ofNullable(responseBodyObject);
addResponseContext(stepContext, responseEntity);
processResponse(stepContext);
}
/**
* Builds a step context instance from a model and a request context
*
* @param stepLogger Step logger
* @param model Data model
* @param requestContext Request context
* @return Step context instance
*/
protected final StepContext buildStepContext(StepLogger stepLogger, M model, RequestContext requestContext) {
StepContext context = new StepContext<>();
context.setModel(model);
context.setRequestContext(requestContext);
context.setStep(getStep());
context.setStepLogger(stepLogger);
return context;
}
/**
* Increments the counter (the signature already used hash based counter)
* @param model Model
* @param Type of the model with result status
* @throws Exception when an error during saving the model occurred
*/
protected void incrementCounter(RS model) throws Exception {
CounterUtil.incrementCounter(model);
resultStatusService.save(model);
}
/**
* Optional way to log special messages when in a dry run (no real service call)
*
* @param stepLogger Step logger
*/
protected void logDryRun(StepLogger stepLogger) {
stepLogger.writeItem(
getStep().id() + "-dry-run",
"Dry run",
"The request was just dry-run, no external service call",
"OK",
null
);
}
/**
* Calls the server and prepares response context with the response data
*/
private @Nullable ResponseContext callServer(StepContext stepContext) throws Exception {
if (stepContext == null) {
return null;
}
final ParameterizedTypeReference responseTypeReference = getResponseTypeReference();
if (responseTypeReference == null) {
return null;
}
M model = stepContext.getModel();
RequestContext requestContext = stepContext.getRequestContext();
Map headers = new HashMap<>();
headers.put(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE);
headers.put(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
// put all headers from request context (includes e.g. authorization header)
headers.putAll(requestContext.getHttpHeaders());
if (model.getHeaders() != null && !model.getHeaders().isEmpty()) {
headers.putAll(model.getHeaders());
}
byte[] requestBytes = HttpUtil.toRequestBytes(requestContext.getRequestObject());
stepContext.getStepLogger().writeServerCall(step.id() + "-request-sent", requestContext.getUri(), requestContext.getHttpMethod().name(), requestContext.getRequestObject(), requestBytes, headers);
// In the case of a dry run the execution ends here
if (isDryRun(model)) {
logDryRun(stepContext.getStepLogger());
stepContext.getStepLogger().writeDoneOK(getStep().id() + "-success");
return null;
}
RestClient restClient = RestClientFactory.getRestClient();
if (restClient == null) {
stepContext.getStepLogger().writeError(step.id() + "-error-rest-client", "Unable to prepare a REST client");
return null;
}
ResponseEntity responseEntity;
try {
// Call the right method with the REST client
if (HttpMethod.GET.equals(requestContext.getHttpMethod())) {
responseEntity = restClient.get(requestContext.getUri(), null, MapUtil.toMultiValueMap(headers), responseTypeReference);
} else {
responseEntity = restClient.post(requestContext.getUri(), requestBytes, null, MapUtil.toMultiValueMap(headers), responseTypeReference);
}
} catch (RestClientException ex) {
stepContext.getStepLogger().writeServerCallError(step.id() + "-error-server-call", ex.getStatusCode().value(), ex.getResponse(), HttpUtil.flattenHttpHeaders(ex.getResponseHeaders()));
return null;
}
return addResponseContext(stepContext, responseEntity);
}
private ResponseContext addResponseContext(StepContext stepContext, ResponseEntity responseEntity) {
R responseBodyObject = Objects.requireNonNull(responseEntity.getBody());
stepContext.getStepLogger().writeServerCallOK(step.id() + "-response-received", responseBodyObject, HttpUtil.flattenHttpHeaders(responseEntity.getHeaders()));
ResponseContext responseContext = ResponseContext.builder()
.responseBodyObject(responseBodyObject)
.responseEntity(responseEntity)
.build();
stepContext.setResponseContext(responseContext);
return responseContext;
}
private boolean isDryRun(M model) {
return model instanceof DryRunCapable && ((DryRunCapable) model).isDryRun();
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy