responseTypeReference = getResponseTypeReference();
+ if (responseTypeReference == null) {
+ return null;
+ }
+
M model = stepContext.getModel();
RequestContext requestContext = stepContext.getRequestContext();
@@ -328,9 +340,9 @@ protected void logDryRun(StepLogger stepLogger) {
try {
// Call the right method with the REST client
if (HttpMethod.GET.equals(requestContext.getHttpMethod())) {
- responseEntity = restClient.get(requestContext.getUri(), null, MapUtil.toMultiValueMap(headers), ParameterizedTypeReference.forType(getResponseTypeReference().getType()));
+ responseEntity = restClient.get(requestContext.getUri(), null, MapUtil.toMultiValueMap(headers), responseTypeReference);
} else {
- responseEntity = restClient.post(requestContext.getUri(), requestBytes, null, MapUtil.toMultiValueMap(headers), ParameterizedTypeReference.forType(getResponseTypeReference().getType()));
+ 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()));
diff --git a/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/ComputeOfflineSignatureStep.java b/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/ComputeOfflineSignatureStep.java
new file mode 100644
index 00000000..b8a29b80
--- /dev/null
+++ b/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/ComputeOfflineSignatureStep.java
@@ -0,0 +1,233 @@
+/*
+ * PowerAuth Command-line utility
+ * Copyright 2022 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.google.common.io.BaseEncoding;
+import io.getlime.security.powerauth.crypto.lib.enums.PowerAuthSignatureFormat;
+import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator;
+import io.getlime.security.powerauth.crypto.lib.util.KeyConvertor;
+import io.getlime.security.powerauth.crypto.lib.util.SignatureUtils;
+import io.getlime.security.powerauth.http.PowerAuthHttpBody;
+import io.getlime.security.powerauth.lib.cmd.consts.BackwardCompatibilityConst;
+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.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.StepContext;
+import io.getlime.security.powerauth.lib.cmd.steps.model.ComputeOfflineSignatureStepModel;
+import io.getlime.security.powerauth.lib.cmd.steps.pojo.ResultStatusObject;
+import io.getlime.security.powerauth.lib.cmd.util.CounterUtil;
+import io.getlime.security.powerauth.lib.cmd.util.EncryptedStorageUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.stereotype.Component;
+
+import javax.crypto.SecretKey;
+import java.io.Console;
+import java.nio.charset.StandardCharsets;
+import java.security.interfaces.ECPublicKey;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Step for computing offline PowerAuth signature.
+ *
+ * PowerAuth protocol versions:
+ *
+ * - 2.0
+ * - 2.1
+ * - 3.0
+ * - 3.1
+ *
+ *
+ * @author Roman Strobl, roman.strobl@wultra.com
+ */
+@Component
+public class ComputeOfflineSignatureStep extends AbstractBaseStep {
+
+ private static final KeyGenerator KEY_GENERATOR = new KeyGenerator();
+ private static final KeyConvertor KEY_CONVERTOR = new KeyConvertor();
+ private static final SignatureUtils SIGNATURE_UTILS = new SignatureUtils();
+
+ /**
+ * Constructor
+ * @param resultStatusService Result status service
+ * @param stepLoggerFactory Step logger factory
+ */
+ @Autowired
+ public ComputeOfflineSignatureStep(
+ ResultStatusService resultStatusService,
+ StepLoggerFactory stepLoggerFactory) {
+ super(PowerAuthStep.SIGNATURE_OFFLINE_COMPUTE, PowerAuthVersion.ALL_VERSIONS, resultStatusService, stepLoggerFactory);
+ }
+
+ /**
+ * Constructor for backward compatibility
+ */
+ public ComputeOfflineSignatureStep() {
+ this(
+ BackwardCompatibilityConst.RESULT_STATUS_SERVICE,
+ BackwardCompatibilityConst.STEP_LOGGER_FACTORY
+ );
+ }
+
+ @Override
+ public ParameterizedTypeReference getResponseTypeReference() {
+ // No response type, server is not called due to offline nature of the step
+ return null;
+ }
+
+ @Override
+ public StepContext prepareStepContext(StepLogger stepLogger, Map context) throws Exception {
+ final ComputeOfflineSignatureStepModel model = new ComputeOfflineSignatureStepModel();
+ model.fromMap(context);
+
+ final RequestContext requestContext = RequestContext.builder()
+ .uri(model.getUriString())
+ .build();
+
+ final StepContext stepContext =
+ buildStepContext(stepLogger, model, requestContext);
+
+ if (model.getQrCodeData() == null) {
+ stepLogger.writeError(getStep().id() + "-error-missing-qr-code-data", "Missing offline signature data", "Specify offline signature data which is encoded in QR code");
+ stepLogger.writeDoneFailed(getStep().id() + "-failed");
+ return null;
+ }
+
+ final String offlineData = unescape(model.getQrCodeData());
+ final Map inputMap = new HashMap<>();
+ inputMap.put("qrCodeData", offlineData);
+
+ stepLogger.writeItem(
+ getStep().id() + "-start",
+ "Offline Signature Computation Started",
+ null,
+ "OK",
+ inputMap
+ );
+
+ // Ask for the password to unlock knowledge factor key
+ final char[] password;
+ if (model.getPassword() == null) {
+ Console console = System.console();
+ password = console.readPassword("Enter your password to unlock the knowledge related key: ");
+ } else {
+ password = model.getPassword().toCharArray();
+ }
+
+ final String offlineSignature = calculateOfflineSignature(offlineData, stepLogger, model.getResultStatus(), password);
+ if (offlineSignature == null) {
+ return null;
+ }
+
+ final Map resultMap = new HashMap<>();
+ resultMap.put("offlineSignature", offlineSignature);
+
+ stepLogger.writeItem(
+ getStep().id() + "-finished",
+ "Offline Signature Computation Finished",
+ null,
+ "OK",
+ resultMap
+ );
+
+ incrementCounter(stepContext.getModel());
+
+ return stepContext;
+ }
+
+ private String unescape(String text) {
+ return text.replace("\\n", "\n");
+ }
+
+ private String calculateOfflineSignature(final String offlineData, final StepLogger stepLogger,
+ final ResultStatusObject resultStatusObject, final char[] password) {
+ // Split the offline data into individual lines, see: https://github.com/wultra/powerauth-webflow/blob/develop/docs/Off-line-Signatures-QR-Code.md
+ final String[] parts = offlineData.split("\n");
+ if (parts.length < 7) {
+ stepLogger.writeError(getStep().id() + "-error-invalid-qr-code-data", "Invalid QR code data", "Invalid QR code, expected 7 lines of data or more");
+ stepLogger.writeDoneFailed(getStep().id() + "-failed");
+ return null;
+ }
+ final String operationId = parts[0];
+ final String operationData = parts[3];
+ final String nonce = parts[5];
+ final String signatureLine = parts[parts.length - 1];
+
+ // 1 = KEY_SERVER_PRIVATE was used to sign data (personalized offline signature), otherwise return error
+ final String signatureType = signatureLine.substring(0, 1);
+ if (!"1".equals(signatureType)) {
+ stepLogger.writeError(getStep().id() + "-error-invalid-signature-type", "Invalid signature type", "Personalized offline signature expected, however other signature type is used");
+ stepLogger.writeDoneFailed(getStep().id() + "-failed");
+ return null;
+ }
+
+ try {
+ // Verify ECDSA signature from the offline data, return error in case of invalid signature
+ final String ecdsaSignature = signatureLine.substring(1);
+ final byte[] serverPublicKeyBytes = BaseEncoding.base64().decode(resultStatusObject.getServerPublicKey());
+ final ECPublicKey serverPublicKey = (ECPublicKey) KEY_CONVERTOR.convertBytesToPublicKey(serverPublicKeyBytes);
+ final String offlineDataWithoutSignature = offlineData.substring(0, offlineData.length() - ecdsaSignature.length());
+ final boolean dataSignatureValid = SIGNATURE_UTILS.validateECDSASignature(
+ offlineDataWithoutSignature.getBytes(StandardCharsets.UTF_8),
+ BaseEncoding.base64().decode(ecdsaSignature),
+ serverPublicKey);
+ if (!dataSignatureValid) {
+ stepLogger.writeError(getStep().id() + "-error-invalid-signature", "Invalid signature", "Invalid signature of offline data");
+ stepLogger.writeDoneFailed(getStep().id() + "-failed");
+ return null;
+ }
+
+ // Prepare data for PowerAuth offline signature calculation
+ final String dataForSignature = operationId + "&" + operationData;
+ final String signatureBaseString = PowerAuthHttpBody.getSignatureBaseString(
+ "POST",
+ "/operation/authorize/offline",
+ BaseEncoding.base64().decode(nonce),
+ dataForSignature.getBytes(StandardCharsets.UTF_8));
+
+ // Prepare keys for PowerAuth offline signature calculation
+ final byte[] signaturePossessionKeyBytes = BaseEncoding.base64().decode(resultStatusObject.getSignaturePossessionKey());
+ final byte[] signatureKnowledgeKeySalt = BaseEncoding.base64().decode(resultStatusObject.getSignatureKnowledgeKeySalt());
+ final byte[] signatureKnowledgeKeyEncryptedBytes = BaseEncoding.base64().decode(resultStatusObject.getSignatureKnowledgeKeyEncrypted());
+ final SecretKey signaturePossessionKey = KEY_CONVERTOR.convertBytesToSharedSecretKey(signaturePossessionKeyBytes);
+ final SecretKey signatureKnowledgeKey = EncryptedStorageUtil.getSignatureKnowledgeKey(
+ password,
+ signatureKnowledgeKeyEncryptedBytes,
+ signatureKnowledgeKeySalt,
+ KEY_GENERATOR);
+ final List signatureKeys = new ArrayList<>();
+ signatureKeys.add(signaturePossessionKey);
+ signatureKeys.add(signatureKnowledgeKey);
+
+ // Calculate signature of normalized signature base string with 'offline' constant used as application secret
+ return SIGNATURE_UTILS.computePowerAuthSignature((signatureBaseString + "&offline").getBytes(StandardCharsets.UTF_8),
+ signatureKeys,
+ CounterUtil.getCtrData(resultStatusObject, stepLogger),
+ PowerAuthSignatureFormat.DECIMAL);
+ } catch (Exception ex) {
+ stepLogger.writeError(getStep().id() + "-error-cryptography", "Cryptography error", ex.getMessage());
+ stepLogger.writeDoneFailed(getStep().id() + "-failed");
+ return null;
+ }
+ }
+}
diff --git a/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/VerifyTokenStep.java b/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/VerifyTokenStep.java
index d6e2463a..126eb792 100644
--- a/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/VerifyTokenStep.java
+++ b/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/VerifyTokenStep.java
@@ -102,8 +102,8 @@ public StepContext> prepareStepContext
buildStepContext(stepLogger, model, requestContext);
Map map = new HashMap<>();
- map.put("TOKEN_ID", model.getTokenId());
- map.put("TOKEN_SECRET", model.getTokenSecret());
+ map.put("tokenId", model.getTokenId());
+ map.put("tokenSecret", model.getTokenSecret());
stepLogger.writeItem(
"token-validate-start",
"Token Digest Validation Started",
diff --git a/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/model/ComputeOfflineSignatureStepModel.java b/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/model/ComputeOfflineSignatureStepModel.java
new file mode 100644
index 00000000..506eec69
--- /dev/null
+++ b/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/model/ComputeOfflineSignatureStepModel.java
@@ -0,0 +1,72 @@
+/*
+ * PowerAuth Command-line utility
+ * Copyright 2022 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.model;
+
+import io.getlime.security.powerauth.lib.cmd.steps.model.feature.DryRunCapable;
+import io.getlime.security.powerauth.lib.cmd.steps.model.feature.ResultStatusChangeable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.Map;
+
+/**
+ * Model representing parameters of the step for computing offline signatures.
+ *
+ * @author Roman Strobl, roman.strobl@wultra.com
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ComputeOfflineSignatureStepModel extends BaseStepModel implements ResultStatusChangeable, DryRunCapable {
+
+ /**
+ * File name of the file with stored activation status.
+ */
+ private String statusFileName;
+
+ /**
+ * QR code data.
+ */
+ private String qrCodeData;
+
+ /**
+ * Knowledge key password (PIN).
+ */
+ private String password;
+
+ @Override
+ public Map toMap() {
+ Map context = super.toMap();
+ context.put("STATUS_FILENAME", statusFileName);
+ context.put("QR_CODE_DATA", qrCodeData);
+ context.put("PASSWORD", password);
+ return context;
+ }
+
+ @Override
+ public void fromMap(Map context) {
+ super.fromMap(context);
+ setStatusFileName((String) context.get("STATUS_FILENAME"));
+ setQrCodeData((String) context.get("QR_CODE_DATA"));
+ setPassword((String) context.get("PASSWORD"));
+ }
+
+ @Override
+ public boolean isDryRun() {
+ return true;
+ }
+
+}
diff --git a/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/v2/PrepareActivationStep.java b/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/v2/PrepareActivationStep.java
index 47d57810..2839a82d 100755
--- a/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/v2/PrepareActivationStep.java
+++ b/powerauth-java-cmd-lib/src/main/java/io/getlime/security/powerauth/lib/cmd/steps/v2/PrepareActivationStep.java
@@ -78,7 +78,7 @@ public class PrepareActivationStep extends AbstractBaseStepV2 {
*/
@Autowired
public PrepareActivationStep(StepLogger stepLogger) {
- super(PowerAuthStep.ACTIVATION_CREATE, PowerAuthVersion.VERSION_2, stepLogger);
+ super(PowerAuthStep.ACTIVATION_CREATE, PowerAuthVersion.VERSION_2, Objects.requireNonNull(stepLogger, "stepLogger must not be null"));
}
/**
@@ -110,11 +110,9 @@ public ResultStatusObject execute(Map context) throws Exception
Pattern p = Pattern.compile("^[A-Z2-7]{5}-[A-Z2-7]{5}-[A-Z2-7]{5}-[A-Z2-7]{5}(#.*)?$");
Matcher m = p.matcher(model.getActivationCode());
if (!m.find()) {
- if (stepLogger != null) {
- stepLogger.writeError("activation-create-error-activation-code", "Activation failed", "Activation code has invalid format");
- stepLogger.writeDoneFailed("activation-create-failed");
- return null;
- }
+ stepLogger.writeError("activation-create-error-activation-code", "Activation failed", "Activation code has invalid format");
+ stepLogger.writeDoneFailed("activation-create-failed");
+ return null;
}
String activationIdShort = model.getActivationCode().substring(0, 11);
String activationOTP = model.getActivationCode().substring(12, 23);
diff --git a/powerauth-java-cmd/pom.xml b/powerauth-java-cmd/pom.xml
index a4f99d1e..89ce4b04 100644
--- a/powerauth-java-cmd/pom.xml
+++ b/powerauth-java-cmd/pom.xml
@@ -22,7 +22,6 @@
4.0.0
powerauth-java-cmd
- 1.3.0
powerauth-java-cmd
PowerAuth Reference Client Application connected to PowerAuth Standard RESTful API
@@ -31,15 +30,13 @@
io.getlime.security
powerauth-cmd-parent
- 1.3.0
- ../pom.xml
+ 1.4.0
io.getlime.security
powerauth-java-cmd-lib
- 1.3.0
log4j-to-slf4j
@@ -50,13 +47,15 @@
org.junit.jupiter
junit-jupiter-engine
- ${junit.version}
test
org.bouncycastle
- bcprov-jdk15on
- ${bc.version}
+ bcprov-jdk18on
+
+
+ ch.qos.logback
+ logback-classic
@@ -78,13 +77,6 @@
io.getlime.security.powerauth.app.cmd.Application
-
- org.apache.maven.plugins
- maven-deploy-plugin
-
- true
-
-
maven-surefire-plugin
${maven-surefire-plugin.version}
@@ -92,4 +84,28 @@
+
+
+ public-repository
+
+
+ !useInternalRepo
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-deploy-plugin
+
+ true
+
+
+
+
+
+
+
diff --git a/powerauth-java-cmd/src/main/java/io/getlime/security/powerauth/app/cmd/Application.java b/powerauth-java-cmd/src/main/java/io/getlime/security/powerauth/app/cmd/Application.java
index db5af222..0b9ddc84 100755
--- a/powerauth-java-cmd/src/main/java/io/getlime/security/powerauth/app/cmd/Application.java
+++ b/powerauth-java-cmd/src/main/java/io/getlime/security/powerauth/app/cmd/Application.java
@@ -104,6 +104,7 @@ public static void main(String[] args) {
options.addOption("R", "recovery-code", true, "Recovery code to be confirmed.");
options.addOption("P", "platform", true, "User device platform.");
options.addOption("D", "device-info", true, "Information about user device.");
+ options.addOption("q", "qr-code-data", true, "Data for offline signature encoded in QR code.");
options.addOption("v", "version", true, "PowerAuth protocol version.");
Option httpHeaderOption = Option.builder("H")
@@ -173,6 +174,11 @@ public static void main(String[] args) {
deviceInfo = "cmd-tool";
}
+ String qrCodeData = null;
+ if (cmd.hasOption("q")) {
+ qrCodeData = cmd.getOptionValue("q");
+ }
+
// Read values
String method = cmd.getOptionValue("m");
String uriString = cmd.getOptionValue("u");
@@ -448,6 +454,7 @@ public static void main(String[] args) {
model.setData(dataFileBytes);
stepExecutionService.execute(powerAuthStep, version, model);
+ break;
}
case UPGRADE_START: {
StartUpgradeStepModel model = new StartUpgradeStepModel();
@@ -520,6 +527,20 @@ public static void main(String[] args) {
model.setVersion(version);
stepExecutionService.execute(powerAuthStep, version, model);
+ break;
+ }
+
+ case SIGNATURE_OFFLINE_COMPUTE: {
+
+ ComputeOfflineSignatureStepModel model = new ComputeOfflineSignatureStepModel();
+ model.setStatusFileName(statusFileName);
+ model.setQrCodeData(qrCodeData);
+ model.setPassword(cmd.getOptionValue("p"));
+ model.setResultStatus(resultStatusObject);
+ model.setVersion(version);
+
+ stepExecutionService.execute(powerAuthStep, version, model);
+ break;
}
default: