diff --git a/docs/PowerAuth-Server-1.5.0.md b/docs/PowerAuth-Server-1.5.0.md
index 6566b70c8..1e6d8b65d 100644
--- a/docs/PowerAuth-Server-1.5.0.md
+++ b/docs/PowerAuth-Server-1.5.0.md
@@ -87,6 +87,36 @@ ALTER TABLE PA_OPERATION ADD COLUMN TOTP_SEED VARCHAR2(24 CHAR);
ALTER TABLE PA_OPERATION_TEMPLATE ADD COLUMN PROXIMITY_CHECK_ENABLED NUMBER(1, 0) DEFAULT 0 NOT NULL;
```
+### Added Table for Detecting Replay Attacks
+
+A new table `pa_unique_values` was added to store unique values sent in requests, so that replay attacks are prevented.
+
+#### PostgreSQL
+
+```sql
+CREATE TABLE pa_unique_value (
+ unique_value VARCHAR(255) NOT NULL PRIMARY KEY,
+ type INTEGER NOT NULL,
+ timestamp_expires TIMESTAMP NOT NULL
+);
+
+CREATE INDEX pa_unique_value_expiration ON pa_unique_value(timestamp_expires);
+```
+
+#### Oracle
+
+```sql
+--
+-- DDL for Table PA_UNIQUE_VALUE
+--
+CREATE TABLE PA_UNIQUE_VALUE (
+ unique_value VARCHAR2(255 CHAR) NOT NULL PRIMARY KEY,
+ type NUMBER(10,0) NOT NULL,
+ timestamp_expires TIMESTAMP NOT NULL
+);
+
+CREATE INDEX pa_unique_value_expiration ON pa_unique_value(timestamp_expires);
+```
### Drop MySQL Support
diff --git a/docs/WebServices-Methods.md b/docs/WebServices-Methods.md
index 01d44f67f..eb269b19c 100644
--- a/docs/WebServices-Methods.md
+++ b/docs/WebServices-Methods.md
@@ -59,7 +59,7 @@ The following `v3` methods are published using the service:
- [getCallbackUrlList](#method-getcallbackurllist)
- [removeCallbackUrl](#method-removecallbackurl)
- End-To-End Encryption
- - [getEciesDecryptor](#method-geteciesdecryptor)
+ - [getEciesDecryptor](#method-geteciesdecryptor)
- Activation Versioning
- [startUpgrade](#method-startupgrade)
- [commitUpgrade](#method-commitupgrade)
diff --git a/docs/db/changelog/changesets/powerauth-java-server/1.5.x/20230723-add-table-unique-value.xml b/docs/db/changelog/changesets/powerauth-java-server/1.5.x/20230723-add-table-unique-value.xml
new file mode 100644
index 000000000..521d69126
--- /dev/null
+++ b/docs/db/changelog/changesets/powerauth-java-server/1.5.x/20230723-add-table-unique-value.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+ Create a new table pa_unique_value
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Create a new index on pa_unique_value(timestamp_expires)
+
+
+
+
+
+
diff --git a/docs/db/changelog/changesets/powerauth-java-server/1.5.x/db.changelog-version.xml b/docs/db/changelog/changesets/powerauth-java-server/1.5.x/db.changelog-version.xml
index 44e12c7ba..20cffd003 100644
--- a/docs/db/changelog/changesets/powerauth-java-server/1.5.x/db.changelog-version.xml
+++ b/docs/db/changelog/changesets/powerauth-java-server/1.5.x/db.changelog-version.xml
@@ -5,5 +5,6 @@
+
\ No newline at end of file
diff --git a/docs/sql/oracle/create_schema.sql b/docs/sql/oracle/create_schema.sql
index 3f7a5a59d..12ff93713 100755
--- a/docs/sql/oracle/create_schema.sql
+++ b/docs/sql/oracle/create_schema.sql
@@ -254,6 +254,17 @@ CREATE TABLE pa_operation_application (
CONSTRAINT pa_operation_application_pk PRIMARY KEY ("APPLICATION_ID", "OPERATION_ID")
);
+--
+-- DDL for Table PA_UNIQUE_VALUE
+--
+CREATE TABLE PA_UNIQUE_VALUE (
+ unique_value VARCHAR2(255 CHAR) NOT NULL PRIMARY KEY,
+ type NUMBER(10,0) NOT NULL,
+ timestamp_expires TIMESTAMP NOT NULL
+);
+
+CREATE INDEX pa_unique_value_expiration ON pa_unique_value(timestamp_expires);
+
--
-- DDL for Table SHEDLOCK
--
diff --git a/docs/sql/oracle/delete_schema.sql b/docs/sql/oracle/delete_schema.sql
index bcc0f2326..ea5bec2e7 100755
--- a/docs/sql/oracle/delete_schema.sql
+++ b/docs/sql/oracle/delete_schema.sql
@@ -15,6 +15,7 @@ DROP TABLE "PA_RECOVERY_PUK" CASCADE CONSTRAINTS;
DROP TABLE "PA_RECOVERY_CONFIG" CASCADE CONSTRAINTS;
DROP TABLE "PA_OPERATION" CASCADE CONSTRAINTS;
DROP TABLE "PA_OPERATION_TEMPLATE" CASCADE CONSTRAINTS;
+DROP TABLE "PA_UNIQUE_VALUE" CASCADE CONSTRAINTS;
-- Optionally drop the shedlock table
-- DROP TABLE "shedlock" CASCADE CONSTRAINTS;
diff --git a/docs/sql/postgresql/create_schema.sql b/docs/sql/postgresql/create_schema.sql
index fb0531ea9..0fa54a1e7 100644
--- a/docs/sql/postgresql/create_schema.sql
+++ b/docs/sql/postgresql/create_schema.sql
@@ -255,6 +255,15 @@ CREATE TABLE pa_operation_application (
CONSTRAINT pa_operation_application_pk PRIMARY KEY (application_id, operation_id)
);
+--
+-- DDL for Table PA_UNIQUE_VALUE
+--
+CREATE TABLE pa_unique_value (
+ unique_value VARCHAR(255) NOT NULL PRIMARY KEY,
+ type INTEGER NOT NULL,
+ timestamp_expires TIMESTAMP NOT NULL
+);
+
--
-- DDL for Table SHEDLOCK
--
@@ -407,6 +416,8 @@ CREATE INDEX pa_operation_status_exp ON pa_operation(timestamp_expires, status);
CREATE INDEX pa_operation_template_name_idx ON pa_operation_template(template_name);
+CREATE INDEX pa_unique_value_expiration ON pa_unique_value(timestamp_expires);
+
--
-- Auditing indexes.
--
diff --git a/docs/sql/postgresql/delete_schema.sql b/docs/sql/postgresql/delete_schema.sql
index b6c470d14..2b63f1272 100644
--- a/docs/sql/postgresql/delete_schema.sql
+++ b/docs/sql/postgresql/delete_schema.sql
@@ -15,6 +15,7 @@ DROP TABLE IF EXISTS "pa_recovery_puk" CASCADE;
DROP TABLE IF EXISTS "pa_recovery_code" CASCADE;
DROP TABLE IF EXISTS "pa_operation" CASCADE;
DROP TABLE IF EXISTS "pa_operation_template" CASCADE;
+DROP TABLE IF EXISTS "pa_unique_value" CASCADE;
-- Optionally drop the shedlock table
-- DROP TABLE IF EXISTS "shedlock" CASCADE;
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/PowerAuthClient.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/PowerAuthClient.java
index ce19bdda2..d99cfa1ac 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/PowerAuthClient.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/PowerAuthClient.java
@@ -183,10 +183,13 @@ InitActivationResponse initActivation(String userId, String applicationId, Long
* @param encryptedData Encrypted data for ECIES.
* @param mac Mac of key and data for ECIES.
* @param nonce Nonce for ECIES.
+ * @param protocolVersion Crypto protocol version.
+ * @param timestamp Unix timestamp in milliseconds for ECIES.
* @return {@link PrepareActivationResponse}
* @throws PowerAuthClientException In case REST API call fails.
*/
- PrepareActivationResponse prepareActivation(String activationCode, String applicationKey, boolean shouldGenerateRecoveryCodes, String ephemeralPublicKey, String encryptedData, String mac, String nonce) throws PowerAuthClientException;
+ PrepareActivationResponse prepareActivation(String activationCode, String applicationKey, boolean shouldGenerateRecoveryCodes, String ephemeralPublicKey,
+ String encryptedData, String mac, String nonce, String protocolVersion, Long timestamp) throws PowerAuthClientException;
/**
* Create a new activation directly, using the createActivation method of the PowerAuth Server
@@ -221,12 +224,14 @@ InitActivationResponse initActivation(String userId, String applicationId, Long
* @param encryptedData Encrypted data for ECIES.
* @param mac Mac of key and data for ECIES.
* @param nonce Nonce for ECIES.
+ * @param protocolVersion Crypto protocol version.
+ * @param timestamp Unix timestamp in milliseconds for ECIES.
* @return {@link CreateActivationResponse}
* @throws PowerAuthClientException In case REST API call fails.
*/
CreateActivationResponse createActivation(String userId, Date timestampActivationExpire, Long maxFailureCount,
String applicationKey, String ephemeralPublicKey, String encryptedData,
- String mac, String nonce) throws PowerAuthClientException;
+ String mac, String nonce, String protocolVersion, Long timestamp) throws PowerAuthClientException;
/**
* Call the updateActivationOtp method of PowerAuth 3.1 Server interface.
@@ -639,12 +644,14 @@ CreateActivationResponse createActivation(String userId, Date timestampActivatio
* @param encryptedData Encrypted data for ECIES.
* @param mac MAC of key and data for ECIES.
* @param nonce Nonce for ECIES.
+ * @param timestamp Unix timestamp in milliseconds for ECIES.
* @return {@link VaultUnlockResponse}
* @throws PowerAuthClientException In case REST API call fails.
*/
VaultUnlockResponse unlockVault(String activationId, String applicationKey, String signature,
SignatureType signatureType, String signatureVersion, String signedData,
- String ephemeralPublicKey, String encryptedData, String mac, String nonce) throws PowerAuthClientException;
+ String ephemeralPublicKey, String encryptedData, String mac, String nonce,
+ Long timestamp) throws PowerAuthClientException;
/**
* Call the verifyECDSASignature method of the PowerAuth 3.0 Server interface.
@@ -1232,12 +1239,15 @@ VaultUnlockResponse unlockVault(String activationId, String applicationKey, Stri
* @param encryptedData Encrypted request data.
* @param mac MAC computed for request key and data.
* @param nonce Nonce for ECIES.
+ * @param protocolVersion Crypto protocol version.
+ * @param timestamp Unix timestamp in milliseconds for ECIES.
* @param signatureType Type of the signature used for validating the create request.
* @return Response with created token.
* @throws PowerAuthClientException In case REST API call fails.
*/
CreateTokenResponse createToken(String activationId, String applicationKey, String ephemeralPublicKey,
- String encryptedData, String mac, String nonce, SignatureType signatureType) throws PowerAuthClientException;
+ String encryptedData, String mac, String nonce, String protocolVersion,
+ Long timestamp, SignatureType signatureType) throws PowerAuthClientException;
/**
* Validate credentials used for basic token-based authentication.
@@ -1326,11 +1336,15 @@ CreateTokenResponse createToken(String activationId, String applicationKey, Stri
*
* @param activationId Activation ID.
* @param applicationKey Application key.
- * @param ephemeralPublicKey Ephemeral key for ECIES.
+ * @param ephemeralPublicKey Ephemeral public key for ECIES.
+ * @param nonce ECIES nonce.
+ * @param protocolVersion Crypto protocol version.
+ * @param timestamp Unix timestamp in milliseconds for ECIES.
* @return ECIES decryptor parameters.
* @throws PowerAuthClientException In case REST API call fails.
*/
- GetEciesDecryptorResponse getEciesDecryptor(String activationId, String applicationKey, String ephemeralPublicKey) throws PowerAuthClientException;
+ GetEciesDecryptorResponse getEciesDecryptor(String activationId, String applicationKey, String ephemeralPublicKey,
+ String nonce, String protocolVersion, Long timestamp) throws PowerAuthClientException;
/**
* Start upgrade of activations to version 3.
@@ -1361,11 +1375,14 @@ CreateTokenResponse createToken(String activationId, String applicationKey, Stri
* @param encryptedData Encrypted request data.
* @param mac MAC computed for request key and data.
* @param nonce Nonce for ECIES.
+ * @param protocolVersion Crypto protocol version.
+ * @param timestamp Unix timestamp in milliseconds for ECIES.
* @return Start upgrade response.
* @throws PowerAuthClientException In case REST API call fails.
*/
StartUpgradeResponse startUpgrade(String activationId, String applicationKey, String ephemeralPublicKey,
- String encryptedData, String mac, String nonce) throws PowerAuthClientException;
+ String encryptedData, String mac, String nonce,
+ String protocolVersion, Long timestamp) throws PowerAuthClientException;
/**
* Commit upgrade of activations to version 3.
@@ -1455,11 +1472,14 @@ StartUpgradeResponse startUpgrade(String activationId, String applicationKey, St
* @param encryptedData Encrypted data for ECIES.
* @param mac MAC of key and data for ECIES.
* @param nonce Nonce for ECIES.
+ * @param protocolVersion Crypto protocol version.
+ * @param timestamp Unix timestamp in milliseconds for ECIES.
* @return Confirm recovery code response.
* @throws PowerAuthClientException In case REST API call fails.
*/
ConfirmRecoveryCodeResponse confirmRecoveryCode(String activationId, String applicationKey, String ephemeralPublicKey,
- String encryptedData, String mac, String nonce) throws PowerAuthClientException;
+ String encryptedData, String mac, String nonce,
+ String protocolVersion, Long timestamp) throws PowerAuthClientException;
/**
* Lookup recovery codes.
@@ -1552,12 +1572,15 @@ LookupRecoveryCodesResponse lookupRecoveryCodes(String userId, String activation
* @param ephemeralPublicKey Ephemeral key for ECIES.
* @param encryptedData Encrypted data for ECIES.
* @param mac MAC of key and data for ECIES.
- * @param nonce nonce for ECIES.
+ * @param nonce Nonce for ECIES.
+ * @param protocolVersion Crypto protocol version.
+ * @param timestamp Unix timestamp in milliseconds for ECIES.
* @return Create activation using recovery code response.
* @throws PowerAuthClientException In case REST API call fails.
*/
RecoveryCodeActivationResponse createActivationUsingRecoveryCode(String recoveryCode, String puk, String applicationKey, Long maxFailureCount,
- String ephemeralPublicKey, String encryptedData, String mac, String nonce) throws PowerAuthClientException;
+ String ephemeralPublicKey, String encryptedData, String mac, String nonce,
+ String protocolVersion, Long timestamp) throws PowerAuthClientException;
/**
* Get recovery configuration.
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/ConfirmRecoveryCodeRequest.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/ConfirmRecoveryCodeRequest.java
index 820838f09..2fdf58a1b 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/ConfirmRecoveryCodeRequest.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/ConfirmRecoveryCodeRequest.java
@@ -36,5 +36,7 @@ public class ConfirmRecoveryCodeRequest {
private String mac;
@ToString.Exclude
private String nonce;
+ private Long timestamp;
+ private String protocolVersion;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/CreateActivationRequest.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/CreateActivationRequest.java
index b090757a6..2bee8bbd8 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/CreateActivationRequest.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/CreateActivationRequest.java
@@ -43,5 +43,7 @@ public class CreateActivationRequest {
private String nonce;
@ToString.Exclude
private String activationOtp;
+ private String protocolVersion;
+ private Long timestamp;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/CreateTokenRequest.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/CreateTokenRequest.java
index 30ef7c3da..9ba4c6c08 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/CreateTokenRequest.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/CreateTokenRequest.java
@@ -38,5 +38,7 @@ public class CreateTokenRequest {
@ToString.Exclude
private String nonce;
private SignatureType signatureType;
+ private String protocolVersion;
+ private Long timestamp;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/GetEciesDecryptorRequest.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/GetEciesDecryptorRequest.java
index c2f64003e..8207edaf5 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/GetEciesDecryptorRequest.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/GetEciesDecryptorRequest.java
@@ -19,6 +19,7 @@
package com.wultra.security.powerauth.client.model.request;
import lombok.Data;
+import lombok.ToString;
/**
* Model class representing request for ECIES decryptor initialization.
@@ -31,5 +32,9 @@ public class GetEciesDecryptorRequest {
private String activationId;
private String applicationKey;
private String ephemeralPublicKey;
+ @ToString.Exclude
+ private String nonce;
+ private String protocolVersion;
+ private Long timestamp;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/PrepareActivationRequest.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/PrepareActivationRequest.java
index 1f0598365..5ea059309 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/PrepareActivationRequest.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/PrepareActivationRequest.java
@@ -37,5 +37,7 @@ public class PrepareActivationRequest {
private String mac;
@ToString.Exclude
private String nonce;
+ private String protocolVersion;
+ private Long timestamp;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/RecoveryCodeActivationRequest.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/RecoveryCodeActivationRequest.java
index 2d398702c..74d9c5879 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/RecoveryCodeActivationRequest.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/RecoveryCodeActivationRequest.java
@@ -42,5 +42,7 @@ public class RecoveryCodeActivationRequest {
private String nonce;
@ToString.Exclude
private String activationOtp;
+ private String protocolVersion;
+ private Long timestamp;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/StartUpgradeRequest.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/StartUpgradeRequest.java
index 11098cb09..538fc6b18 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/StartUpgradeRequest.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/StartUpgradeRequest.java
@@ -36,5 +36,7 @@ public class StartUpgradeRequest {
private String mac;
@ToString.Exclude
private String nonce;
+ private String protocolVersion;
+ private Long timestamp;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/VaultUnlockRequest.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/VaultUnlockRequest.java
index 363ea71e1..494da0b80 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/VaultUnlockRequest.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/request/VaultUnlockRequest.java
@@ -42,5 +42,6 @@ public class VaultUnlockRequest {
private String mac;
@ToString.Exclude
private String nonce;
+ private Long timestamp;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/ConfirmRecoveryCodeResponse.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/ConfirmRecoveryCodeResponse.java
index 4ea18d5d3..8fd2cd43d 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/ConfirmRecoveryCodeResponse.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/ConfirmRecoveryCodeResponse.java
@@ -19,6 +19,7 @@
package com.wultra.security.powerauth.client.model.response;
import lombok.Data;
+import lombok.ToString;
/**
* Model class representing response with recovery code confirmation.
@@ -32,5 +33,9 @@ public class ConfirmRecoveryCodeResponse {
private String userId;
private String encryptedData;
private String mac;
+ private String ephemeralPublicKey;
+ @ToString.Exclude
+ private String nonce;
+ private Long timestamp;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/CreateActivationResponse.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/CreateActivationResponse.java
index 5e8cd8a87..3c29e4dc9 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/CreateActivationResponse.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/CreateActivationResponse.java
@@ -20,6 +20,7 @@
import com.wultra.security.powerauth.client.model.enumeration.ActivationStatus;
import lombok.Data;
+import lombok.ToString;
/**
* Model class representing response with created activation.
@@ -34,6 +35,10 @@ public class CreateActivationResponse {
private String applicationId;
private String encryptedData;
private String mac;
+ private String ephemeralPublicKey;
+ @ToString.Exclude
+ private String nonce;
+ private Long timestamp;
private ActivationStatus activationStatus;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/CreateRecoveryCodeResponse.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/CreateRecoveryCodeResponse.java
index bbd2e18a0..f9217dada 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/CreateRecoveryCodeResponse.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/CreateRecoveryCodeResponse.java
@@ -21,6 +21,7 @@
import com.wultra.security.powerauth.client.model.entity.RecoveryCodePuk;
import com.wultra.security.powerauth.client.model.enumeration.RecoveryCodeStatus;
import lombok.Data;
+import lombok.ToString;
import java.util.ArrayList;
import java.util.List;
@@ -33,6 +34,7 @@
@Data
public class CreateRecoveryCodeResponse {
+ @ToString.Exclude
private String nonce;
private String userId;
private long recoveryCodeId;
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/CreateTokenResponse.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/CreateTokenResponse.java
index c726de503..4a4460748 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/CreateTokenResponse.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/CreateTokenResponse.java
@@ -19,6 +19,7 @@
package com.wultra.security.powerauth.client.model.response;
import lombok.Data;
+import lombok.ToString;
/**
* Model class representing response with newly created HMAC token.
@@ -30,5 +31,9 @@ public class CreateTokenResponse {
private String encryptedData;
private String mac;
+ private String ephemeralPublicKey;
+ @ToString.Exclude
+ private String nonce;
+ private Long timestamp;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/PrepareActivationResponse.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/PrepareActivationResponse.java
index 6c8d0af78..26aad370d 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/PrepareActivationResponse.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/PrepareActivationResponse.java
@@ -20,6 +20,7 @@
import com.wultra.security.powerauth.client.model.enumeration.ActivationStatus;
import lombok.Data;
+import lombok.ToString;
/**
* Model class representing response with prepared activation.
@@ -34,6 +35,10 @@ public class PrepareActivationResponse {
private String applicationId;
private String encryptedData;
private String mac;
+ private String ephemeralPublicKey;
+ @ToString.Exclude
+ private String nonce;
+ private Long timestamp;
private ActivationStatus activationStatus;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/RecoveryCodeActivationResponse.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/RecoveryCodeActivationResponse.java
index f865f5bc8..67db80d6d 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/RecoveryCodeActivationResponse.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/RecoveryCodeActivationResponse.java
@@ -20,6 +20,7 @@
import com.wultra.security.powerauth.client.model.enumeration.ActivationStatus;
import lombok.Data;
+import lombok.ToString;
/**
* Model class representing response with activation via recovery code.
@@ -34,6 +35,10 @@ public class RecoveryCodeActivationResponse {
private String applicationId;
private String encryptedData;
private String mac;
+ private String ephemeralPublicKey;
+ @ToString.Exclude
+ private String nonce;
+ private Long timestamp;
private ActivationStatus activationStatus;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/StartUpgradeResponse.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/StartUpgradeResponse.java
index e384c2331..97725d7ce 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/StartUpgradeResponse.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/StartUpgradeResponse.java
@@ -19,6 +19,7 @@
package com.wultra.security.powerauth.client.model.response;
import lombok.Data;
+import lombok.ToString;
/**
* Model class representing response with information about started protocol upgrade.
@@ -30,5 +31,9 @@ public class StartUpgradeResponse {
private String encryptedData;
private String mac;
+ private String ephemeralPublicKey;
+ @ToString.Exclude
+ private String nonce;
+ private Long timestamp;
}
diff --git a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/VaultUnlockResponse.java b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/VaultUnlockResponse.java
index 6d1788add..0f3ded004 100644
--- a/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/VaultUnlockResponse.java
+++ b/powerauth-client-model/src/main/java/com/wultra/security/powerauth/client/model/response/VaultUnlockResponse.java
@@ -19,6 +19,7 @@
package com.wultra.security.powerauth.client.model.response;
import lombok.Data;
+import lombok.ToString;
/**
* Model class representing response with vault unlock data required by the client.
@@ -30,6 +31,10 @@ public class VaultUnlockResponse {
private String encryptedData;
private String mac;
+ private String ephemeralPublicKey;
private boolean signatureValid;
+ @ToString.Exclude
+ private String nonce;
+ private Long timestamp;
}
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/configuration/PowerAuthServiceConfiguration.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/configuration/PowerAuthServiceConfiguration.java
index f32ea4d93..995555f95 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/configuration/PowerAuthServiceConfiguration.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/configuration/PowerAuthServiceConfiguration.java
@@ -155,6 +155,13 @@ public class PowerAuthServiceConfiguration {
@Max(8)
private int offlineSignatureComponentLength;
+ /**
+ * Expiration of timestamps for ECIES and MAC token requests.
+ */
+ @Value("${powerauth.service.crypto.requestExpirationInMilliseconds}")
+ @Min(0)
+ private int requestExpirationInMilliseconds;
+
/**
* Whether HTTP proxy is enabled for outgoing HTTP requests.
*/
@@ -484,6 +491,22 @@ public void setOfflineSignatureComponentLength(int offlineSignatureComponentLeng
this.offlineSignatureComponentLength = offlineSignatureComponentLength;
}
+ /**
+ * Get ECIES request expiration in milliseconds.
+ * @return ECIES request expiration in milliseconds.
+ */
+ public int getRequestExpirationInMilliseconds() {
+ return requestExpirationInMilliseconds;
+ }
+
+ /**
+ * Set ECIES request expiration in milliseconds.
+ * @param requestExpirationInMilliseconds ECIES request expiration in milliseconds.
+ */
+ public void setRequestExpirationInMilliseconds(int requestExpirationInMilliseconds) {
+ this.requestExpirationInMilliseconds = requestExpirationInMilliseconds;
+ }
+
/**
* Get whether HTTP proxy is enabled.
* @return Whether HTTP proxy is enabled.
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/entity/UniqueValueEntity.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/entity/UniqueValueEntity.java
new file mode 100644
index 000000000..fb41007bd
--- /dev/null
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/entity/UniqueValueEntity.java
@@ -0,0 +1,96 @@
+/*
+ * PowerAuth Server and related software components
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package io.getlime.security.powerauth.app.server.database.model.entity;
+
+import io.getlime.security.powerauth.app.server.database.model.enumeration.UniqueValueType;
+import jakarta.persistence.*;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.Objects;
+
+/**
+ * Database entity for unique cryptographic values.
+ *
+ * @author Roman Strobl, roman.strobl@wultra.com
+ */
+@Entity
+@Table(name = "pa_unique_value")
+public class UniqueValueEntity implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = -7766414923668550268L;
+
+ @Id
+ @Column(name = "unique_value", nullable = false, updatable = false)
+ private String uniqueValue;
+
+ @Enumerated
+ @Column(name = "type", nullable = false, updatable = false)
+ private UniqueValueType type;
+
+ @Column(name = "timestamp_expires", nullable = false, updatable = false)
+ private Date timestampExpires;
+
+ public String getUniqueValue() {
+ return uniqueValue;
+ }
+
+ public void setUniqueValue(String uniqueValue) {
+ this.uniqueValue = uniqueValue;
+ }
+
+ public UniqueValueType getType() {
+ return type;
+ }
+
+ public void setType(UniqueValueType type) {
+ this.type = type;
+ }
+
+ public Date getTimestampExpires() {
+ return timestampExpires;
+ }
+
+ public void setTimestampExpires(Date timestampExpires) {
+ this.timestampExpires = timestampExpires;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ UniqueValueEntity that = (UniqueValueEntity) o;
+ return uniqueValue.equals(that.uniqueValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(uniqueValue);
+ }
+
+ @Override
+ public String toString() {
+ return "UniqueValueEntity{" +
+ "uniqueValue='" + uniqueValue + '\'' +
+ ", type=" + type +
+ ", timestampExpires=" + timestampExpires +
+ '}';
+ }
+}
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/enumeration/UniqueValueType.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/enumeration/UniqueValueType.java
new file mode 100644
index 000000000..86469145d
--- /dev/null
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/model/enumeration/UniqueValueType.java
@@ -0,0 +1,33 @@
+/*
+ * PowerAuth Server and related software components
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package io.getlime.security.powerauth.app.server.database.model.enumeration;
+
+/**
+ * Enum representing unique value types.
+ *
+ * @author Roman Strobl, roman.strobl@wultra.com
+ */
+public enum UniqueValueType {
+
+ MAC_TOKEN,
+
+ ECIES_APPLICATION_SCOPE,
+
+ ECIES_ACTIVATION_SCOPE
+
+}
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/repository/UniqueValueRepository.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/repository/UniqueValueRepository.java
new file mode 100644
index 000000000..8a41cb68f
--- /dev/null
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/database/repository/UniqueValueRepository.java
@@ -0,0 +1,36 @@
+/*
+ * PowerAuth Server and related software components
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+package io.getlime.security.powerauth.app.server.database.repository;
+
+import io.getlime.security.powerauth.app.server.database.model.entity.UniqueValueEntity;
+import org.springframework.data.repository.CrudRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Date;
+
+/**
+ * Repository for accessing stored tokens for token-based authentication.
+ *
+ * @author Petr Dvorak, petr@wultra.com
+ */
+@Repository
+public interface UniqueValueRepository extends CrudRepository {
+
+ int deleteAllByTimestampExpiresBefore(Date timestampExpires);
+
+}
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/PowerAuthService.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/PowerAuthService.java
index 954095a77..7e48b1deb 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/PowerAuthService.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/PowerAuthService.java
@@ -34,6 +34,10 @@
import io.getlime.security.powerauth.app.server.service.i18n.LocalizationProvider;
import io.getlime.security.powerauth.app.server.service.model.ServiceError;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesCryptogram;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesParameters;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesPayload;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesScope;
+import io.getlime.security.powerauth.crypto.lib.util.EciesUtils;
import io.getlime.security.powerauth.crypto.lib.util.KeyConvertor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -302,9 +306,14 @@ public PrepareActivationResponse prepareActivation(PrepareActivationRequest requ
final byte[] mac = Base64.getDecoder().decode(request.getMac());
final byte[] encryptedData = Base64.getDecoder().decode(request.getEncryptedData());
final byte[] nonce = request.getNonce() != null ? Base64.getDecoder().decode(request.getNonce()) : null;
- final EciesCryptogram cryptogram = new EciesCryptogram(ephemeralPublicKey, mac, encryptedData, nonce);
+ final String version = request.getProtocolVersion();
+ final Long timestamp = "3.2".equals(version) ? request.getTimestamp() : null;
+ final byte[] associatedData = "3.2".equals(version) ? EciesUtils.deriveAssociatedData(EciesScope.APPLICATION_SCOPE, version, applicationKey, null) : null;
+ final EciesCryptogram eciesCryptogram = EciesCryptogram.builder().ephemeralPublicKey(ephemeralPublicKey).mac(mac).encryptedData(encryptedData).build();
+ final EciesParameters eciesParameters = EciesParameters.builder().nonce(nonce).associatedData(associatedData).timestamp(timestamp).build();
+ final EciesPayload eciesPayload = new EciesPayload(eciesCryptogram, eciesParameters);
logger.info("PrepareActivationRequest received, activation code: {}", activationCode);
- final PrepareActivationResponse response = behavior.getActivationServiceBehavior().prepareActivation(activationCode, applicationKey, shouldGenerateRecoveryCodes, cryptogram, keyConvertor);
+ final PrepareActivationResponse response = behavior.getActivationServiceBehavior().prepareActivation(activationCode, applicationKey, shouldGenerateRecoveryCodes, eciesPayload, version, keyConvertor);
logger.info("PrepareActivationRequest succeeded");
return response;
} catch (GenericServiceException ex) {
@@ -338,7 +347,12 @@ public CreateActivationResponse createActivation(CreateActivationRequest request
final byte[] mac = Base64.getDecoder().decode(request.getMac());
final byte[] encryptedData = Base64.getDecoder().decode(request.getEncryptedData());
final byte[] nonce = request.getNonce() != null ? Base64.getDecoder().decode(request.getNonce()) : null;
- final EciesCryptogram cryptogram = new EciesCryptogram(ephemeralPublicKey, mac, encryptedData, nonce);
+ final String version = request.getProtocolVersion();
+ final Long timestamp = "3.2".equals(version) ? request.getTimestamp() : null;
+ final byte[] associatedData = "3.2".equals(version) ? EciesUtils.deriveAssociatedData(EciesScope.APPLICATION_SCOPE, version, applicationKey, null) : null;
+ final EciesCryptogram eciesCryptogram = EciesCryptogram.builder().ephemeralPublicKey(ephemeralPublicKey).mac(mac).encryptedData(encryptedData).build();
+ final EciesParameters eciesParameters = EciesParameters.builder().nonce(nonce).associatedData(associatedData).timestamp(timestamp).build();
+ final EciesPayload eciesPayload = new EciesPayload(eciesCryptogram, eciesParameters);
logger.info("CreateActivationRequest received, user ID: {}", userId);
final CreateActivationResponse response = behavior.getActivationServiceBehavior().createActivation(
userId,
@@ -346,8 +360,9 @@ public CreateActivationResponse createActivation(CreateActivationRequest request
shouldGenerateRecoveryCodes,
maxFailedCount,
applicationKey,
- cryptogram,
+ eciesPayload,
activationOtp,
+ request.getProtocolVersion(),
keyConvertor
);
logger.info("CreateActivationRequest succeeded");
@@ -665,10 +680,14 @@ public VaultUnlockResponse vaultUnlock(VaultUnlockRequest request) throws Generi
}
// Convert received ECIES request data to cryptogram
- final EciesCryptogram cryptogram = new EciesCryptogram(ephemeralPublicKey, mac, encryptedData, nonce);
+ final Long timestamp = "3.2".equals(signatureVersion) ? request.getTimestamp() : null;
+ final byte[] associatedData = "3.2".equals(signatureVersion) ? EciesUtils.deriveAssociatedData(EciesScope.ACTIVATION_SCOPE, signatureVersion, applicationKey, activationId) : null;
+ final EciesCryptogram eciesCryptogram = EciesCryptogram.builder().ephemeralPublicKey(ephemeralPublicKey).mac(mac).encryptedData(encryptedData).build();
+ final EciesParameters eciesParameters = EciesParameters.builder().nonce(nonce).associatedData(associatedData).timestamp(timestamp).build();
+ final EciesPayload eciesPayload = new EciesPayload(eciesCryptogram, eciesParameters);
final VaultUnlockResponse response = behavior.getVaultUnlockServiceBehavior().unlockVault(activationId, applicationKey,
- signature, signatureType, signatureVersion, signedData, cryptogram, keyConvertor);
+ signature, signatureType, signatureVersion, signedData, eciesPayload, keyConvertor);
logger.info("VaultUnlockRequest succeeded");
return response;
} catch (GenericServiceException ex) {
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/ActivationServiceBehavior.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/ActivationServiceBehavior.java
index 17342b3b3..5c0985fe2 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/ActivationServiceBehavior.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/ActivationServiceBehavior.java
@@ -34,6 +34,7 @@
import io.getlime.security.powerauth.app.server.database.model.entity.*;
import io.getlime.security.powerauth.app.server.database.model.enumeration.*;
import io.getlime.security.powerauth.app.server.database.repository.*;
+import io.getlime.security.powerauth.app.server.service.replay.ReplayVerificationService;
import io.getlime.security.powerauth.app.server.service.exceptions.GenericServiceException;
import io.getlime.security.powerauth.app.server.service.i18n.LocalizationProvider;
import io.getlime.security.powerauth.app.server.service.model.ActivationRecovery;
@@ -41,10 +42,10 @@
import io.getlime.security.powerauth.app.server.service.model.request.ActivationLayer2Request;
import io.getlime.security.powerauth.app.server.service.model.response.ActivationLayer2Response;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesDecryptor;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesEncryptor;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesFactory;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.exception.EciesException;
-import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesCryptogram;
-import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesSharedInfo1;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.*;
import io.getlime.security.powerauth.crypto.lib.generator.HashBasedCounter;
import io.getlime.security.powerauth.crypto.lib.generator.IdentifierGenerator;
import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator;
@@ -52,6 +53,7 @@
import io.getlime.security.powerauth.crypto.lib.model.RecoveryInfo;
import io.getlime.security.powerauth.crypto.lib.model.exception.CryptoProviderException;
import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
+import io.getlime.security.powerauth.crypto.lib.util.EciesUtils;
import io.getlime.security.powerauth.crypto.lib.util.KeyConvertor;
import io.getlime.security.powerauth.crypto.lib.util.PasswordHash;
import io.getlime.security.powerauth.crypto.server.activation.PowerAuthServerActivation;
@@ -102,6 +104,8 @@ public class ActivationServiceBehavior {
private final PowerAuthServiceConfiguration powerAuthServiceConfiguration;
+ private final ReplayVerificationService eciesReplayPersistenceService;
+
// Prepare converters
private final ActivationStatusConverter activationStatusConverter = new ActivationStatusConverter();
private final ActivationOtpValidationConverter activationOtpValidationConverter = new ActivationOtpValidationConverter();
@@ -112,14 +116,16 @@ public class ActivationServiceBehavior {
private final EciesFactory eciesFactory = new EciesFactory();
private final ObjectMapper objectMapper;
private final IdentifierGenerator identifierGenerator = new IdentifierGenerator();
+ private final KeyGenerator keyGenerator = new KeyGenerator();
// Prepare logger
private static final Logger logger = LoggerFactory.getLogger(ActivationServiceBehavior.class);
@Autowired
- public ActivationServiceBehavior(RepositoryCatalogue repositoryCatalogue, PowerAuthServiceConfiguration powerAuthServiceConfiguration, ObjectMapper objectMapper) {
+ public ActivationServiceBehavior(RepositoryCatalogue repositoryCatalogue, PowerAuthServiceConfiguration powerAuthServiceConfiguration, ReplayVerificationService eciesreplayPersistenceService, ObjectMapper objectMapper) {
this.repositoryCatalogue = repositoryCatalogue;
this.powerAuthServiceConfiguration = powerAuthServiceConfiguration;
+ this.eciesReplayPersistenceService = eciesreplayPersistenceService;
this.objectMapper = objectMapper;
}
@@ -619,7 +625,7 @@ public GetActivationStatusResponse getActivationStatus(String activationId, Stri
*
* @param applicationId Application ID
* @param userId User ID
- * @param maxFailureCount Maximum failed attempt count (5)
+ * @param maxFailureCount Maximum failed attempt count (5)
* @param activationExpireTimestamp Timestamp after which activation can no longer be completed
* @param activationOtpValidation Activation OTP validation mode
* @param activationOtp Activation OTP
@@ -817,12 +823,15 @@ public InitActivationResponse initActivation(String applicationId, String userId
*
* @param activationCode Activation code.
* @param applicationKey Application key.
- * @param shouldGenerateRecoveryCodes Flag indicating if recovery codes shoud be generated. If null is provided, the system settings are used.
- * @param eciesCryptogram Ecies cryptogram.
+ * @param shouldGenerateRecoveryCodes Flag indicating if recovery codes should be generated. If null is provided, the system settings are used.
+ * @param eciesPayload ECIES payload.
+ * @param version Protocol version.
+ * @param keyConversion Key convertor.
* @return ECIES encrypted activation information.
* @throws GenericServiceException If invalid values are provided.
*/
- public PrepareActivationResponse prepareActivation(String activationCode, String applicationKey, boolean shouldGenerateRecoveryCodes, EciesCryptogram eciesCryptogram, KeyConvertor keyConversion) throws GenericServiceException {
+ public PrepareActivationResponse prepareActivation(String activationCode, String applicationKey, boolean shouldGenerateRecoveryCodes,
+ EciesPayload eciesPayload, String version, KeyConvertor keyConversion) throws GenericServiceException {
try {
// Get current timestamp
final Date timestamp = new Date();
@@ -856,6 +865,16 @@ public PrepareActivationResponse prepareActivation(String activationCode, String
throw localizationProvider.buildExceptionForCode(ServiceError.NO_MASTER_SERVER_KEYPAIR);
}
+ if (eciesPayload.getParameters().getTimestamp() != null) {
+ // Check ECIES request for replay attacks and persist unique value from request
+ eciesReplayPersistenceService.checkAndPersistUniqueValue(
+ UniqueValueType.ECIES_APPLICATION_SCOPE,
+ new Date(eciesPayload.getParameters().getTimestamp()),
+ eciesPayload.getCryptogram().getEphemeralPublicKey(),
+ eciesPayload.getParameters().getNonce(),
+ null);
+ }
+
final String masterPrivateKeyBase64 = masterKeyPairEntity.getMasterKeyPrivateBase64();
final PrivateKey privateKey = keyConversion.convertBytesToPrivateKey(Base64.getDecoder().decode(masterPrivateKeyBase64));
@@ -863,10 +882,12 @@ public PrepareActivationResponse prepareActivation(String activationCode, String
final byte[] applicationSecret = applicationVersion.getApplicationSecret().getBytes(StandardCharsets.UTF_8);
// Get ecies decryptor
- final EciesDecryptor eciesDecryptor = eciesFactory.getEciesDecryptorForApplication((ECPrivateKey) privateKey, applicationSecret, EciesSharedInfo1.ACTIVATION_LAYER_2);
+ final EciesDecryptor eciesDecryptor = eciesFactory.getEciesDecryptorForApplication(
+ (ECPrivateKey) privateKey, applicationSecret, EciesSharedInfo1.ACTIVATION_LAYER_2,
+ eciesPayload.getParameters(), eciesPayload.getCryptogram().getEphemeralPublicKey());
// Decrypt activation data
- final byte[] activationData = eciesDecryptor.decryptRequest(eciesCryptogram);
+ final byte[] activationData = eciesDecryptor.decrypt(eciesPayload);
// Convert JSON data to activation layer 2 request object
final ActivationLayer2Request request;
@@ -956,9 +977,16 @@ public PrepareActivationResponse prepareActivation(String activationCode, String
final byte[] responseData = objectMapper.writeValueAsBytes(layer2Response);
// Encrypt response data
- final EciesCryptogram responseCryptogram = eciesDecryptor.encryptResponse(responseData);
- final String encryptedData = Base64.getEncoder().encodeToString(responseCryptogram.getEncryptedData());
- final String mac = Base64.getEncoder().encodeToString(responseCryptogram.getMac());
+ final byte[] nonceBytesResponse = ("3.2".equals(version) || "3.1".equals(version)) ? keyGenerator.generateRandomBytes(16) : null;
+ final Long timestampResponse = "3.2".equals(version) ? new Date().getTime() : null;
+ final EciesParameters parametersResponse = EciesParameters.builder().nonce(nonceBytesResponse).associatedData(eciesPayload.getParameters().getAssociatedData()).timestamp(timestampResponse).build();
+ final EciesEncryptor encryptorResponse = eciesFactory.getEciesEncryptor(EciesScope.APPLICATION_SCOPE,
+ eciesDecryptor.getEnvelopeKey(), applicationSecret, null, parametersResponse);
+
+ final EciesPayload responseEciesPayload = encryptorResponse.encrypt(responseData, parametersResponse);
+ final String encryptedData = Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getEncryptedData());
+ final String mac = Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getMac());
+ final String ephemeralPublicKey = Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getEphemeralPublicKey());
// Persist activation report and notify listeners
activationHistoryServiceBehavior.saveActivationAndLogChange(activation);
@@ -971,6 +999,9 @@ public PrepareActivationResponse prepareActivation(String activationCode, String
encryptedResponse.setApplicationId(applicationId);
encryptedResponse.setEncryptedData(encryptedData);
encryptedResponse.setMac(mac);
+ encryptedResponse.setEphemeralPublicKey(ephemeralPublicKey);
+ encryptedResponse.setNonce(nonceBytesResponse != null ? Base64.getEncoder().encodeToString(nonceBytesResponse) : null);
+ encryptedResponse.setTimestamp(timestampResponse);
encryptedResponse.setActivationStatus(activationStatusConverter.convert(activationStatus));
return encryptedResponse;
} catch (InvalidKeySpecException ex) {
@@ -1005,8 +1036,9 @@ public PrepareActivationResponse prepareActivation(String activationCode, String
* @param shouldGenerateRecoveryCodes Flag indicating if recovery codes should be generated. If null is provided, system settings are used.
* @param maxFailureCount Maximum failed attempt count (default = 5)
* @param applicationKey Application key
- * @param eciesCryptogram ECIES cryptogram
+ * @param eciesPayload ECIES payload
* @param keyConversion Utility class for key conversion
+ * @param version Crypto protocol version
* @param activationOtp Additional activation OTP
* @return ECIES encrypted activation information
* @throws GenericServiceException In case create activation fails
@@ -1017,8 +1049,9 @@ public CreateActivationResponse createActivation(
boolean shouldGenerateRecoveryCodes,
Long maxFailureCount,
String applicationKey,
- EciesCryptogram eciesCryptogram,
+ EciesPayload eciesPayload,
String activationOtp,
+ String version,
KeyConvertor keyConversion) throws GenericServiceException {
try {
// Get current timestamp
@@ -1074,6 +1107,16 @@ public CreateActivationResponse createActivation(
throw localizationProvider.buildRollbackingExceptionForCode(ServiceError.NO_MASTER_SERVER_KEYPAIR);
}
+ if (eciesPayload.getParameters().getTimestamp() != null) {
+ // Check ECIES request for replay attacks and persist unique value from request
+ eciesReplayPersistenceService.checkAndPersistUniqueValue(
+ UniqueValueType.ECIES_APPLICATION_SCOPE,
+ new Date(eciesPayload.getParameters().getTimestamp()),
+ eciesPayload.getCryptogram().getEphemeralPublicKey(),
+ eciesPayload.getParameters().getNonce(),
+ null);
+ }
+
final String masterPrivateKeyBase64 = masterKeyPairEntity.getMasterKeyPrivateBase64();
final PrivateKey privateKey = keyConversion.convertBytesToPrivateKey(Base64.getDecoder().decode(masterPrivateKeyBase64));
@@ -1081,10 +1124,12 @@ public CreateActivationResponse createActivation(
final byte[] applicationSecret = applicationVersion.getApplicationSecret().getBytes(StandardCharsets.UTF_8);
// Get ecies decryptor
- final EciesDecryptor eciesDecryptor = eciesFactory.getEciesDecryptorForApplication((ECPrivateKey) privateKey, applicationSecret, EciesSharedInfo1.ACTIVATION_LAYER_2);
+ final EciesDecryptor eciesDecryptor = eciesFactory.getEciesDecryptorForApplication(
+ (ECPrivateKey) privateKey, applicationSecret, EciesSharedInfo1.ACTIVATION_LAYER_2,
+ eciesPayload.getParameters(), eciesPayload.getCryptogram().getEphemeralPublicKey());
// Decrypt activation data
- final byte[] activationData = eciesDecryptor.decryptRequest(eciesCryptogram);
+ final byte[] activationData = eciesDecryptor.decrypt(eciesPayload);
// Convert JSON data to activation layer 2 request object
ActivationLayer2Request request;
@@ -1144,9 +1189,17 @@ public CreateActivationResponse createActivation(
final byte[] responseData = objectMapper.writeValueAsBytes(layer2Response);
// Encrypt response data
- final EciesCryptogram responseCryptogram = eciesDecryptor.encryptResponse(responseData);
- final String encryptedData = Base64.getEncoder().encodeToString(responseCryptogram.getEncryptedData());
- final String mac = Base64.getEncoder().encodeToString(responseCryptogram.getMac());
+ final byte[] nonceBytesResponse = ("3.2".equals(version) || "3.1".equals(version)) ? keyGenerator.generateRandomBytes(16) : null;
+ final Long timestampResponse = "3.2".equals(version) ? new Date().getTime() : null;
+ final byte[] associatedData = "3.2".equals(version) ? EciesUtils.deriveAssociatedData(EciesScope.APPLICATION_SCOPE, version, applicationKey, null) : null;
+ final EciesParameters parametersResponse = EciesParameters.builder().nonce(nonceBytesResponse).associatedData(associatedData).timestamp(timestampResponse).build();
+ final EciesEncryptor encryptorResponse = eciesFactory.getEciesEncryptor(EciesScope.APPLICATION_SCOPE,
+ eciesDecryptor.getEnvelopeKey(), applicationSecret, null, parametersResponse);
+
+ final EciesPayload responseEciesPayload = encryptorResponse.encrypt(responseData, parametersResponse);
+ final String encryptedData = Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getEncryptedData());
+ final String mac = Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getMac());
+ final String ephemeralPublicKey = Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getEphemeralPublicKey());
// Generate encrypted response
final CreateActivationResponse encryptedResponse = new CreateActivationResponse();
@@ -1155,6 +1208,9 @@ public CreateActivationResponse createActivation(
encryptedResponse.setApplicationId(applicationId);
encryptedResponse.setEncryptedData(encryptedData);
encryptedResponse.setMac(mac);
+ encryptedResponse.setEphemeralPublicKey(ephemeralPublicKey);
+ encryptedResponse.setNonce(nonceBytesResponse != null ? Base64.getEncoder().encodeToString(nonceBytesResponse) : null);
+ encryptedResponse.setTimestamp(timestampResponse);
encryptedResponse.setActivationStatus(activationStatusConverter.convert(activation.getActivationStatus()));
return encryptedResponse;
} catch (InvalidKeySpecException ex) {
@@ -1556,7 +1612,12 @@ public RecoveryCodeActivationResponse createActivationUsingRecoveryCode(Recovery
final byte[] encryptedDataBytes = Base64.getDecoder().decode(encryptedData);
final byte[] macBytes = Base64.getDecoder().decode(mac);
final byte[] nonceBytes = request.getNonce() != null ? Base64.getDecoder().decode(request.getNonce()) : null;
- final EciesCryptogram eciesCryptogram = new EciesCryptogram(ephemeralPublicKeyBytes, macBytes, encryptedDataBytes, nonceBytes);
+ final String version = request.getProtocolVersion();
+ final Long timestamp = "3.2".equals(version) ? request.getTimestamp() : null;
+ final byte[] associatedData = "3.2".equals(version) ? EciesUtils.deriveAssociatedData(EciesScope.APPLICATION_SCOPE, version, applicationKey, null) : null;
+ final EciesCryptogram eciesCryptogram = EciesCryptogram.builder().ephemeralPublicKey(ephemeralPublicKeyBytes).mac(macBytes).encryptedData(encryptedDataBytes).build();
+ final EciesParameters eciesParameters = EciesParameters.builder().nonce(nonceBytes).associatedData(associatedData).timestamp(timestamp).build();
+ final EciesPayload eciesPayload = new EciesPayload(eciesCryptogram, eciesParameters);
// Prepare repositories
final ActivationRepository activationRepository = repositoryCatalogue.getActivationRepository();
@@ -1597,6 +1658,16 @@ public RecoveryCodeActivationResponse createActivationUsingRecoveryCode(Recovery
throw localizationProvider.buildExceptionForCode(ServiceError.NO_MASTER_SERVER_KEYPAIR);
}
+ if (eciesPayload.getParameters().getTimestamp() != null) {
+ // Check ECIES request for replay attacks and persist unique value from request
+ eciesReplayPersistenceService.checkAndPersistUniqueValue(
+ UniqueValueType.ECIES_APPLICATION_SCOPE,
+ new Date(eciesPayload.getParameters().getTimestamp()),
+ ephemeralPublicKeyBytes,
+ nonceBytes,
+ null);
+ }
+
final String masterPrivateKeyBase64 = masterKeyPairEntity.getMasterKeyPrivateBase64();
final PrivateKey privateKey = keyConversion.convertBytesToPrivateKey(Base64.getDecoder().decode(masterPrivateKeyBase64));
@@ -1604,10 +1675,12 @@ public RecoveryCodeActivationResponse createActivationUsingRecoveryCode(Recovery
final byte[] applicationSecret = applicationVersion.getApplicationSecret().getBytes(StandardCharsets.UTF_8);
// Get ecies decryptor
- final EciesDecryptor eciesDecryptor = eciesFactory.getEciesDecryptorForApplication((ECPrivateKey) privateKey, applicationSecret, EciesSharedInfo1.ACTIVATION_LAYER_2);
+ final EciesDecryptor eciesDecryptor = eciesFactory.getEciesDecryptorForApplication(
+ (ECPrivateKey) privateKey, applicationSecret, EciesSharedInfo1.ACTIVATION_LAYER_2,
+ eciesParameters, ephemeralPublicKeyBytes);
// Decrypt activation data
- final byte[] activationData = eciesDecryptor.decryptRequest(eciesCryptogram);
+ final byte[] activationData = eciesDecryptor.decrypt(eciesPayload);
// Convert JSON data to activation layer 2 request object
ActivationLayer2Request layer2Request;
@@ -1791,9 +1864,16 @@ public RecoveryCodeActivationResponse createActivationUsingRecoveryCode(Recovery
final byte[] responseData = objectMapper.writeValueAsBytes(layer2Response);
// Encrypt response data
- final EciesCryptogram responseCryptogram = eciesDecryptor.encryptResponse(responseData);
- final String encryptedDataResponse = Base64.getEncoder().encodeToString(responseCryptogram.getEncryptedData());
- final String macResponse = Base64.getEncoder().encodeToString(responseCryptogram.getMac());
+ final byte[] nonceBytesResponse = ("3.2".equals(version) || "3.1".equals(version)) ? keyGenerator.generateRandomBytes(16) : null;
+ final Long timestampResponse = "3.2".equals(version) ? new Date().getTime() : null;
+ final EciesParameters parametersResponse = EciesParameters.builder().nonce(nonceBytesResponse).associatedData(eciesPayload.getParameters().getAssociatedData()).timestamp(timestampResponse).build();
+ final EciesEncryptor encryptorResponse = eciesFactory.getEciesEncryptor(EciesScope.APPLICATION_SCOPE,
+ eciesDecryptor.getEnvelopeKey(), applicationSecret, null, parametersResponse);
+
+ final EciesPayload responseEciesPayload = encryptorResponse.encrypt(responseData, parametersResponse);
+ final String encryptedDataResponse = Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getEncryptedData());
+ final String macResponse = Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getMac());
+ final String ephemeralPublicKeyResponse = Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getEphemeralPublicKey());
final RecoveryCodeActivationResponse encryptedResponse = new RecoveryCodeActivationResponse();
encryptedResponse.setActivationId(activation.getActivationId());
@@ -1801,6 +1881,9 @@ public RecoveryCodeActivationResponse createActivationUsingRecoveryCode(Recovery
encryptedResponse.setApplicationId(applicationId);
encryptedResponse.setEncryptedData(encryptedDataResponse);
encryptedResponse.setMac(macResponse);
+ encryptedResponse.setEphemeralPublicKey(ephemeralPublicKeyResponse);
+ encryptedResponse.setNonce(nonceBytesResponse != null ? Base64.getEncoder().encodeToString(nonceBytesResponse) : null);
+ encryptedResponse.setTimestamp(timestampResponse);
encryptedResponse.setActivationStatus(activationStatusConverter.convert(activation.getActivationStatus()));
return encryptedResponse;
} catch (InvalidKeySpecException ex) {
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/EciesEncryptionBehavior.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/EciesEncryptionBehavior.java
index 558546338..b4a647612 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/EciesEncryptionBehavior.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/EciesEncryptionBehavior.java
@@ -35,9 +35,12 @@
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesEnvelopeKey;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesFactory;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.exception.EciesException;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesParameters;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesScope;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesSharedInfo1;
import io.getlime.security.powerauth.crypto.lib.model.exception.CryptoProviderException;
import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
+import io.getlime.security.powerauth.crypto.lib.util.EciesUtils;
import io.getlime.security.powerauth.crypto.lib.util.KeyConvertor;
import io.getlime.security.powerauth.crypto.server.keyfactory.PowerAuthServerKeyFactory;
import org.slf4j.Logger;
@@ -145,11 +148,19 @@ private GetEciesDecryptorResponse getEciesDecryptorParametersForApplication(GetE
// Get application secret
final byte[] applicationSecret = applicationVersion.getApplicationSecret().getBytes(StandardCharsets.UTF_8);
+ final String applicationKey = request.getApplicationKey();
+ final byte[] nonceBytes = request.getNonce() != null ? Base64.getDecoder().decode(request.getNonce()) : null;
+ final String version = request.getProtocolVersion();
+ final Long timestamp = "3.2".equals(version) ? request.getTimestamp() : null;
+ final byte[] associatedData = "3.2".equals(version) ? EciesUtils.deriveAssociatedData(EciesScope.APPLICATION_SCOPE, version, applicationKey, null) : null;
+ final EciesParameters eciesParameters = EciesParameters.builder().nonce(nonceBytes).timestamp(timestamp).associatedData(associatedData).build();
+ final byte[] ephemeralPublicKeyBytes = Base64.getDecoder().decode(request.getEphemeralPublicKey());
// Get decryptor for the application
- final EciesDecryptor decryptor = eciesFactory.getEciesDecryptorForApplication((ECPrivateKey) privateKey, applicationSecret, EciesSharedInfo1.APPLICATION_SCOPE_GENERIC);
+ final EciesDecryptor decryptor = eciesFactory.getEciesDecryptorForApplication(
+ (ECPrivateKey) privateKey, applicationSecret, EciesSharedInfo1.APPLICATION_SCOPE_GENERIC,
+ eciesParameters, ephemeralPublicKeyBytes);
// Initialize decryptor with ephemeral public key
- final byte[] ephemeralPublicKeyBytes = Base64.getDecoder().decode(request.getEphemeralPublicKey());
decryptor.initEnvelopeKey(ephemeralPublicKeyBytes);
// Extract envelope key and sharedInfo2 parameters to allow decryption on intermediate server
@@ -237,11 +248,21 @@ private GetEciesDecryptorResponse getEciesDecryptorParametersForActivation(GetEc
final SecretKey transportKey = powerAuthServerKeyFactory.deriveTransportKey(serverPrivateKey, devicePublicKey);
final byte[] transportKeyBytes = keyConversion.convertSharedSecretKeyToBytes(transportKey);
+ final Long timestamp = request.getTimestamp();
+ final String version = request.getProtocolVersion();
+ final String applicationKey = request.getApplicationKey();
+ final String activationId = request.getActivationId();
+ final byte[] nonceBytes = request.getNonce() != null ? Base64.getDecoder().decode(request.getNonce()) : null;
+ final byte[] associatedData = EciesUtils.deriveAssociatedData(EciesScope.ACTIVATION_SCOPE, version, applicationKey, activationId);
+ final EciesParameters eciesParameters = EciesParameters.builder().nonce(nonceBytes).timestamp(timestamp).associatedData(associatedData).build();
+ final byte[] ephemeralPublicKeyBytes = Base64.getDecoder().decode(request.getEphemeralPublicKey());
+
// Get decryptor for the activation
- final EciesDecryptor decryptor = eciesFactory.getEciesDecryptorForActivation((ECPrivateKey) serverPrivateKey, applicationSecret, transportKeyBytes, EciesSharedInfo1.ACTIVATION_SCOPE_GENERIC);
+ final EciesDecryptor decryptor = eciesFactory.getEciesDecryptorForActivation(
+ (ECPrivateKey) serverPrivateKey, applicationSecret, transportKeyBytes, EciesSharedInfo1.ACTIVATION_SCOPE_GENERIC,
+ eciesParameters, ephemeralPublicKeyBytes);
// Initialize decryptor with ephemeral public key
- final byte[] ephemeralPublicKeyBytes = Base64.getDecoder().decode(request.getEphemeralPublicKey());
decryptor.initEnvelopeKey(ephemeralPublicKeyBytes);
// Extract envelope key and sharedInfo2 parameters to allow decryption on intermediate server
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/RecoveryServiceBehavior.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/RecoveryServiceBehavior.java
index 9308346cf..850805864 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/RecoveryServiceBehavior.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/RecoveryServiceBehavior.java
@@ -27,28 +27,27 @@
import io.getlime.security.powerauth.app.server.database.RepositoryCatalogue;
import io.getlime.security.powerauth.app.server.database.model.*;
import io.getlime.security.powerauth.app.server.database.model.entity.*;
-import io.getlime.security.powerauth.app.server.database.model.enumeration.ActivationStatus;
-import io.getlime.security.powerauth.app.server.database.model.enumeration.EncryptionMode;
-import io.getlime.security.powerauth.app.server.database.model.enumeration.RecoveryCodeStatus;
-import io.getlime.security.powerauth.app.server.database.model.enumeration.RecoveryPukStatus;
+import io.getlime.security.powerauth.app.server.database.model.enumeration.*;
import io.getlime.security.powerauth.app.server.database.repository.ApplicationRepository;
import io.getlime.security.powerauth.app.server.database.repository.RecoveryCodeRepository;
import io.getlime.security.powerauth.app.server.database.repository.RecoveryConfigRepository;
+import io.getlime.security.powerauth.app.server.service.replay.ReplayVerificationService;
import io.getlime.security.powerauth.app.server.service.exceptions.GenericServiceException;
import io.getlime.security.powerauth.app.server.service.i18n.LocalizationProvider;
import io.getlime.security.powerauth.app.server.service.model.ServiceError;
import io.getlime.security.powerauth.app.server.service.model.request.ConfirmRecoveryRequestPayload;
import io.getlime.security.powerauth.app.server.service.model.response.ConfirmRecoveryResponsePayload;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesDecryptor;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesEncryptor;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesFactory;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.exception.EciesException;
-import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesCryptogram;
-import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesSharedInfo1;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.*;
import io.getlime.security.powerauth.crypto.lib.generator.IdentifierGenerator;
import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator;
import io.getlime.security.powerauth.crypto.lib.model.RecoveryInfo;
import io.getlime.security.powerauth.crypto.lib.model.exception.CryptoProviderException;
import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
+import io.getlime.security.powerauth.crypto.lib.util.EciesUtils;
import io.getlime.security.powerauth.crypto.lib.util.KeyConvertor;
import io.getlime.security.powerauth.crypto.lib.util.PasswordHash;
import io.getlime.security.powerauth.crypto.server.keyfactory.PowerAuthServerKeyFactory;
@@ -87,6 +86,7 @@ public class RecoveryServiceBehavior {
private final RepositoryCatalogue repositoryCatalogue;
private final ServerPrivateKeyConverter serverPrivateKeyConverter;
private final RecoveryPrivateKeyConverter recoveryPrivateKeyConverter;
+ private final ReplayVerificationService eciesreplayPersistenceService;
// Business logic implementation classes
private final PowerAuthServerKeyFactory powerAuthServerKeyFactory = new PowerAuthServerKeyFactory();
@@ -105,12 +105,13 @@ public class RecoveryServiceBehavior {
public RecoveryServiceBehavior(LocalizationProvider localizationProvider,
PowerAuthServiceConfiguration powerAuthServiceConfiguration, RepositoryCatalogue repositoryCatalogue,
ServerPrivateKeyConverter serverPrivateKeyConverter, RecoveryPrivateKeyConverter recoveryPrivateKeyConverter,
- ObjectMapper objectMapper, RecoveryPukConverter recoveryPukConverter) {
+ ReplayVerificationService eciesreplayPersistenceService, ObjectMapper objectMapper, RecoveryPukConverter recoveryPukConverter) {
this.localizationProvider = localizationProvider;
this.powerAuthServiceConfiguration = powerAuthServiceConfiguration;
this.repositoryCatalogue = repositoryCatalogue;
this.serverPrivateKeyConverter = serverPrivateKeyConverter;
this.recoveryPrivateKeyConverter = recoveryPrivateKeyConverter;
+ this.eciesreplayPersistenceService = eciesreplayPersistenceService;
this.objectMapper = objectMapper;
this.recoveryPukConverter = recoveryPukConverter;
}
@@ -280,7 +281,7 @@ public ConfirmRecoveryCodeResponse confirmRecoveryCode(ConfirmRecoveryCodeReques
final String ephemeralPublicKey = request.getEphemeralPublicKey();
final String encryptedData = request.getEncryptedData();
final String mac = request.getMac();
- final String nonce = request.getNonce();
+ final String nonceRequest = request.getNonce();
final RecoveryCodeRepository recoveryCodeRepository = repositoryCatalogue.getRecoveryCodeRepository();
final RecoveryConfigRepository recoveryConfigRepository = repositoryCatalogue.getRecoveryConfigRepository();
@@ -325,16 +326,31 @@ public ConfirmRecoveryCodeResponse confirmRecoveryCode(ConfirmRecoveryCodeReques
final byte[] transportKeyBytes = keyConversion.convertSharedSecretKeyToBytes(transportKey);
// Get decryptor for the activation
- final EciesDecryptor decryptor = eciesFactory.getEciesDecryptorForActivation((ECPrivateKey) serverPrivateKey,
- applicationSecret, transportKeyBytes, EciesSharedInfo1.CONFIRM_RECOVERY_CODE);
-
- // Decrypt request data
final byte[] ephemeralPublicKeyBytes = Base64.getDecoder().decode(ephemeralPublicKey);
+ final byte[] nonceBytesRequest = nonceRequest != null ? Base64.getDecoder().decode(nonceRequest) : null;
+ final String version = request.getProtocolVersion();
+ final Long timestampRequest = "3.2".equals(version) ? request.getTimestamp() : null;
+ final byte[] associatedData = "3.2".equals(version) ? EciesUtils.deriveAssociatedData(EciesScope.ACTIVATION_SCOPE, version, applicationKey, activationId) : null;
+ // Decrypt request data
final byte[] encryptedDataBytes = Base64.getDecoder().decode(encryptedData);
final byte[] macBytes = Base64.getDecoder().decode(mac);
- final byte[] nonceBytes = nonce != null ? Base64.getDecoder().decode(nonce) : null;
- final EciesCryptogram cryptogram = new EciesCryptogram(ephemeralPublicKeyBytes, macBytes, encryptedDataBytes, nonceBytes);
- final byte[] decryptedData = decryptor.decryptRequest(cryptogram);
+ final EciesCryptogram cryptogramRequest = EciesCryptogram.builder().ephemeralPublicKey(ephemeralPublicKeyBytes).mac(macBytes).encryptedData(encryptedDataBytes).build();
+ final EciesParameters parametersRequest = EciesParameters.builder().nonce(nonceBytesRequest).associatedData(associatedData).timestamp(timestampRequest).build();
+ final EciesPayload payloadRequest = new EciesPayload(cryptogramRequest, parametersRequest);
+ final EciesDecryptor decryptor = eciesFactory.getEciesDecryptorForActivation((ECPrivateKey) serverPrivateKey,
+ applicationSecret, transportKeyBytes, EciesSharedInfo1.CONFIRM_RECOVERY_CODE, parametersRequest, ephemeralPublicKeyBytes);
+
+ // Check ECIES request for replay attacks and persist unique value from request
+ if (request.getTimestamp() != null) {
+ eciesreplayPersistenceService.checkAndPersistUniqueValue(
+ UniqueValueType.ECIES_ACTIVATION_SCOPE,
+ new Date(request.getTimestamp()),
+ ephemeralPublicKeyBytes,
+ nonceBytesRequest,
+ activationId);
+ }
+
+ final byte[] decryptedData = decryptor.decrypt(payloadRequest);
// Convert JSON data to confirm recovery request object
final ConfirmRecoveryRequestPayload requestPayload = objectMapper.readValue(decryptedData, ConfirmRecoveryRequestPayload.class);
@@ -383,10 +399,17 @@ public ConfirmRecoveryCodeResponse confirmRecoveryCode(ConfirmRecoveryCodeReques
// Convert response payload
final byte[] responseBytes = objectMapper.writeValueAsBytes(responsePayload);
- // Encrypt response using previously created ECIES decryptor
- final EciesCryptogram eciesResponse = decryptor.encryptResponse(responseBytes);
- final String encryptedDataResponse = Base64.getEncoder().encodeToString(eciesResponse.getEncryptedData());
- final String macResponse = Base64.getEncoder().encodeToString(eciesResponse.getMac());
+ // Encrypt response using ECIES encryptor
+ final byte[] nonceBytesResponse = ("3.2".equals(version) || "3.1".equals(version)) ? keyGenerator.generateRandomBytes(16) : null;
+ final Long timestampResponse = "3.2".equals(version) ? new Date().getTime() : null;
+
+ final EciesParameters parametersResponse = EciesParameters.builder().nonce(nonceBytesResponse).associatedData(associatedData).timestamp(timestampResponse).build();
+ final EciesEncryptor encryptorResponse = eciesFactory.getEciesEncryptor(EciesScope.ACTIVATION_SCOPE,
+ decryptor.getEnvelopeKey(), applicationSecret, transportKeyBytes, parametersResponse);
+
+ final EciesPayload eciesResponse = encryptorResponse.encrypt(responseBytes, parametersResponse);
+ final String encryptedDataResponse = Base64.getEncoder().encodeToString(eciesResponse.getCryptogram().getEncryptedData());
+ final String macResponse = Base64.getEncoder().encodeToString(eciesResponse.getCryptogram().getMac());
// Return response
final ConfirmRecoveryCodeResponse response = new ConfirmRecoveryCodeResponse();
@@ -394,6 +417,9 @@ public ConfirmRecoveryCodeResponse confirmRecoveryCode(ConfirmRecoveryCodeReques
response.setUserId(recoveryCodeEntity.getUserId());
response.setEncryptedData(encryptedDataResponse);
response.setMac(macResponse);
+ response.setEphemeralPublicKey(ephemeralPublicKey);
+ response.setNonce(nonceBytesResponse != null ? Base64.getEncoder().encodeToString(nonceBytesResponse) : null);
+ response.setTimestamp(timestampResponse);
// Confirm recovery code and persist it
if (inCreatedState) {
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/TokenBehavior.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/TokenBehavior.java
index f6fbf0cde..a52d7643a 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/TokenBehavior.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/TokenBehavior.java
@@ -37,17 +37,21 @@
import io.getlime.security.powerauth.app.server.database.model.entity.ActivationRecordEntity;
import io.getlime.security.powerauth.app.server.database.model.entity.ApplicationVersionEntity;
import io.getlime.security.powerauth.app.server.database.model.entity.TokenEntity;
+import io.getlime.security.powerauth.app.server.database.model.enumeration.UniqueValueType;
+import io.getlime.security.powerauth.app.server.service.replay.ReplayVerificationService;
import io.getlime.security.powerauth.app.server.service.exceptions.GenericServiceException;
import io.getlime.security.powerauth.app.server.service.i18n.LocalizationProvider;
import io.getlime.security.powerauth.app.server.service.model.ServiceError;
import io.getlime.security.powerauth.app.server.service.model.TokenInfo;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesDecryptor;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesEncryptor;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesFactory;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.exception.EciesException;
-import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesCryptogram;
-import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesSharedInfo1;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.*;
+import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator;
import io.getlime.security.powerauth.crypto.lib.model.exception.CryptoProviderException;
import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
+import io.getlime.security.powerauth.crypto.lib.util.EciesUtils;
import io.getlime.security.powerauth.crypto.lib.util.KeyConvertor;
import io.getlime.security.powerauth.crypto.server.keyfactory.PowerAuthServerKeyFactory;
import io.getlime.security.powerauth.crypto.server.token.ServerTokenGenerator;
@@ -66,6 +70,7 @@
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
import java.util.Calendar;
+import java.util.Date;
import java.util.Optional;
/**
@@ -80,6 +85,7 @@ public class TokenBehavior {
private final LocalizationProvider localizationProvider;
private final PowerAuthServiceConfiguration powerAuthServiceConfiguration;
private final ServerPrivateKeyConverter serverPrivateKeyConverter;
+ private final ReplayVerificationService eciesreplayPersistenceService;
// Business logic implementation classes
private final ServerTokenGenerator tokenGenerator = new ServerTokenGenerator();
@@ -90,6 +96,7 @@ public class TokenBehavior {
// Helper classes
private final SignatureTypeConverter signatureTypeConverter = new SignatureTypeConverter();
private final ActivationStatusConverter activationStatusConverter = new ActivationStatusConverter();
+ private final KeyGenerator keyGenerator = new KeyGenerator();
private final ObjectMapper objectMapper;
@@ -97,11 +104,12 @@ public class TokenBehavior {
private static final Logger logger = LoggerFactory.getLogger(TokenBehavior.class);
@Autowired
- public TokenBehavior(RepositoryCatalogue repositoryCatalogue, LocalizationProvider localizationProvider, PowerAuthServiceConfiguration powerAuthServiceConfiguration, ServerPrivateKeyConverter serverPrivateKeyConverter, ObjectMapper objectMapper) {
+ public TokenBehavior(RepositoryCatalogue repositoryCatalogue, LocalizationProvider localizationProvider, PowerAuthServiceConfiguration powerAuthServiceConfiguration, ServerPrivateKeyConverter serverPrivateKeyConverter, ReplayVerificationService eciesreplayPersistenceService, ObjectMapper objectMapper) {
this.repositoryCatalogue = repositoryCatalogue;
this.localizationProvider = localizationProvider;
this.powerAuthServiceConfiguration = powerAuthServiceConfiguration;
this.serverPrivateKeyConverter = serverPrivateKeyConverter;
+ this.eciesreplayPersistenceService = eciesreplayPersistenceService;
this.objectMapper = objectMapper;
}
@@ -125,16 +133,23 @@ public CreateTokenResponse createToken(CreateTokenRequest request, KeyConvertor
final byte[] encryptedData = Base64.getDecoder().decode(request.getEncryptedData());
final byte[] mac = Base64.getDecoder().decode(request.getMac());
final byte[] nonce = request.getNonce() != null ? Base64.getDecoder().decode(request.getNonce()) : null;
- final SignatureType signatureType = request.getSignatureType();
+ final String version = request.getProtocolVersion();
+ final Long timestamp = "3.2".equals(version) ? request.getTimestamp() : null;
+ final byte[] associatedData = "3.2".equals(version) ? EciesUtils.deriveAssociatedData(EciesScope.ACTIVATION_SCOPE, version, applicationKey, activationId) : null;
+ final EciesCryptogram eciesCryptogram = EciesCryptogram.builder().ephemeralPublicKey(ephemeralPublicKey).mac(mac).encryptedData(encryptedData).build();
+ final EciesParameters eciesParameters = EciesParameters.builder().nonce(nonce).associatedData(associatedData).timestamp(timestamp).build();
+ final EciesPayload eciesPayload = new EciesPayload(eciesCryptogram, eciesParameters);
- // Convert received ECIES request data to cryptogram
- final EciesCryptogram cryptogram = new EciesCryptogram(ephemeralPublicKey, mac, encryptedData, nonce);
+ final SignatureType signatureType = request.getSignatureType();
- final EciesCryptogram encryptedCryptogram = createToken(activationId, applicationKey, cryptogram, signatureType.name(), keyConversion);
+ final EciesPayload responseEciesPayload = createToken(activationId, applicationKey, eciesPayload, signatureType.name(), version, keyConversion);
final CreateTokenResponse response = new CreateTokenResponse();
- response.setMac(Base64.getEncoder().encodeToString(encryptedCryptogram.getMac()));
- response.setEncryptedData(Base64.getEncoder().encodeToString(encryptedCryptogram.getEncryptedData()));
+ response.setMac(Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getMac()));
+ response.setEncryptedData(Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getEncryptedData()));
+ response.setEphemeralPublicKey(Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getEphemeralPublicKey()));
+ response.setNonce(responseEciesPayload.getParameters().getNonce() != null ? Base64.getEncoder().encodeToString(responseEciesPayload.getParameters().getNonce()) : null);
+ response.setTimestamp(responseEciesPayload.getParameters().getTimestamp());
return response;
}
@@ -143,13 +158,15 @@ public CreateTokenResponse createToken(CreateTokenRequest request, KeyConvertor
*
* @param activationId Activation ID.
* @param applicationKey Application key.
- * @param cryptogram ECIES cryptogram.
+ * @param eciesPayload ECIES payload.
* @param signatureType Signature type.
+ * @param version Protocol version.
* @param keyConversion Key conversion utility class.
* @return Response with a newly created token information (ECIES encrypted).
* @throws GenericServiceException In case a business error occurs.
*/
- private EciesCryptogram createToken(String activationId, String applicationKey, EciesCryptogram cryptogram, String signatureType, KeyConvertor keyConversion) throws GenericServiceException {
+ private EciesPayload createToken(String activationId, String applicationKey, EciesPayload eciesPayload,
+ String signatureType, String version, KeyConvertor keyConversion) throws GenericServiceException {
try {
// Lookup the activation
final ActivationRecordEntity activation = repositoryCatalogue.getActivationRepository().findActivationWithoutLock(activationId);
@@ -166,6 +183,16 @@ private EciesCryptogram createToken(String activationId, String applicationKey,
throw localizationProvider.buildExceptionForCode(ServiceError.ACTIVATION_INCORRECT_STATE);
}
+ if (eciesPayload.getParameters().getTimestamp() != null) {
+ // Check ECIES request for replay attacks and persist unique value from request
+ eciesreplayPersistenceService.checkAndPersistUniqueValue(
+ UniqueValueType.ECIES_ACTIVATION_SCOPE,
+ new Date(eciesPayload.getParameters().getTimestamp()),
+ eciesPayload.getCryptogram().getEphemeralPublicKey(),
+ eciesPayload.getParameters().getNonce(),
+ activationId);
+ }
+
// Get the server private key, decrypt it if required
final String serverPrivateKeyFromEntity = activation.getServerPrivateKeyBase64();
final EncryptionMode serverPrivateKeyEncryptionMode = activation.getServerPrivateKeyEncryption();
@@ -185,11 +212,12 @@ private EciesCryptogram createToken(String activationId, String applicationKey,
final byte[] transportKeyBytes = keyConversion.convertSharedSecretKeyToBytes(transportKey);
// Get decryptor for the activation
- final EciesDecryptor decryptor = eciesFactory.getEciesDecryptorForActivation((ECPrivateKey) serverPrivateKey,
- applicationSecret, transportKeyBytes, EciesSharedInfo1.CREATE_TOKEN);
+ final EciesDecryptor decryptor = eciesFactory.getEciesDecryptorForActivation(
+ (ECPrivateKey) serverPrivateKey, applicationSecret, transportKeyBytes, EciesSharedInfo1.CREATE_TOKEN,
+ eciesPayload.getParameters(), eciesPayload.getCryptogram().getEphemeralPublicKey());
// Try to decrypt request data, the data must not be empty. Currently only '{}' is sent in request data.
- final byte[] decryptedData = decryptor.decryptRequest(cryptogram);
+ final byte[] decryptedData = decryptor.decrypt(eciesPayload);
if (decryptedData.length == 0) {
logger.warn("Invalid decrypted request data");
// Rollback is not required, error occurs before writing to database
@@ -220,7 +248,13 @@ private EciesCryptogram createToken(String activationId, String applicationKey,
final byte[] tokenBytes = objectMapper.writeValueAsBytes(tokenInfo);
// Encrypt response using previously created ECIES decryptor
- final EciesCryptogram response = decryptor.encryptResponse(tokenBytes);
+ final byte[] nonceBytesResponse = ("3.1".equals(version) || "3.2".equals(version)) ? keyGenerator.generateRandomBytes(16) : null;
+ final Long timestampResponse = "3.2".equals(version) ? new Date().getTime() : null;
+ final EciesParameters parametersResponse = EciesParameters.builder().nonce(nonceBytesResponse).associatedData(eciesPayload.getParameters().getAssociatedData()).timestamp(timestampResponse).build();
+ final EciesEncryptor encryptorResponse = eciesFactory.getEciesEncryptor(EciesScope.ACTIVATION_SCOPE,
+ decryptor.getEnvelopeKey(), applicationSecret, transportKeyBytes, parametersResponse);
+
+ final EciesPayload response = encryptorResponse.encrypt(tokenBytes, parametersResponse);
// Create a new token
final TokenEntity token = new TokenEntity();
@@ -288,6 +322,13 @@ public ValidateTokenResponse validateToken(ValidateTokenRequest request) throws
logger.info("Activation is not ACTIVE, activation ID: {}", activation.getActivationId());
isTokenValid = false;
} else {
+ // Check MAC token verification request for replay attacks and persist unique value from request
+ eciesreplayPersistenceService.checkAndPersistUniqueValue(
+ UniqueValueType.MAC_TOKEN,
+ new Date(request.getTimestamp()),
+ nonce,
+ token.getTokenId());
+ // Validate MAC token
isTokenValid = tokenVerifier.validateTokenDigest(nonce, timestamp, tokenSecret, tokenDigest);
}
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/UpgradeServiceBehavior.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/UpgradeServiceBehavior.java
index d066c9359..a27b40d81 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/UpgradeServiceBehavior.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/UpgradeServiceBehavior.java
@@ -31,18 +31,22 @@
import io.getlime.security.powerauth.app.server.database.model.ServerPrivateKey;
import io.getlime.security.powerauth.app.server.database.model.entity.ActivationRecordEntity;
import io.getlime.security.powerauth.app.server.database.model.entity.ApplicationVersionEntity;
+import io.getlime.security.powerauth.app.server.database.model.enumeration.UniqueValueType;
+import io.getlime.security.powerauth.app.server.service.replay.ReplayVerificationService;
import io.getlime.security.powerauth.app.server.service.exceptions.GenericServiceException;
import io.getlime.security.powerauth.app.server.service.i18n.LocalizationProvider;
import io.getlime.security.powerauth.app.server.service.model.ServiceError;
import io.getlime.security.powerauth.app.server.service.model.response.UpgradeResponsePayload;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesDecryptor;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesEncryptor;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesFactory;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.exception.EciesException;
-import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesCryptogram;
-import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesSharedInfo1;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.*;
import io.getlime.security.powerauth.crypto.lib.generator.HashBasedCounter;
+import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator;
import io.getlime.security.powerauth.crypto.lib.model.exception.CryptoProviderException;
import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
+import io.getlime.security.powerauth.crypto.lib.util.EciesUtils;
import io.getlime.security.powerauth.crypto.lib.util.KeyConvertor;
import io.getlime.security.powerauth.crypto.server.keyfactory.PowerAuthServerKeyFactory;
import org.slf4j.Logger;
@@ -58,6 +62,7 @@
import java.security.interfaces.ECPrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
+import java.util.Date;
/**
* Behavior class implementing the activation upgrade process.
@@ -70,6 +75,7 @@ public class UpgradeServiceBehavior {
private final RepositoryCatalogue repositoryCatalogue;
private final LocalizationProvider localizationProvider;
private final ServerPrivateKeyConverter serverPrivateKeyConverter;
+ private final ReplayVerificationService eciesreplayPersistenceService;
// Helper classes
private final EciesFactory eciesFactory = new EciesFactory();
@@ -77,6 +83,7 @@ public class UpgradeServiceBehavior {
private final PowerAuthServerKeyFactory powerAuthServerKeyFactory = new PowerAuthServerKeyFactory();
private final ObjectMapper objectMapper;
private final ActivationHistoryServiceBehavior activationHistoryServiceBehavior;
+ private final KeyGenerator keyGenerator = new KeyGenerator();
// Prepare logger
private static final Logger logger = LoggerFactory.getLogger(UpgradeServiceBehavior.class);
@@ -86,12 +93,14 @@ public UpgradeServiceBehavior(
final RepositoryCatalogue repositoryCatalogue,
final LocalizationProvider localizationProvider,
final ServerPrivateKeyConverter serverPrivateKeyConverter,
+ final ReplayVerificationService eciesreplayPersistenceService,
final ObjectMapper objectMapper,
final ActivationHistoryServiceBehavior activationHistoryServiceBehavior) {
this.repositoryCatalogue = repositoryCatalogue;
this.localizationProvider = localizationProvider;
this.serverPrivateKeyConverter = serverPrivateKeyConverter;
+ this.eciesreplayPersistenceService = eciesreplayPersistenceService;
this.objectMapper = objectMapper;
this.activationHistoryServiceBehavior = activationHistoryServiceBehavior;
}
@@ -120,7 +129,22 @@ public StartUpgradeResponse startUpgrade(StartUpgradeRequest request) throws Gen
final byte[] encryptedDataBytes = Base64.getDecoder().decode(encryptedData);
final byte[] macBytes = Base64.getDecoder().decode(mac);
final byte[] nonceBytes = nonce != null ? Base64.getDecoder().decode(nonce) : null;
- final EciesCryptogram cryptogram = new EciesCryptogram(ephemeralPublicKeyBytes, macBytes, encryptedDataBytes, nonceBytes);
+ final String version = request.getProtocolVersion();
+ final Long timestamp = "3.2".equals(version) ? request.getTimestamp() : null;
+ final byte[] associatedData = "3.2".equals(version) ? EciesUtils.deriveAssociatedData(EciesScope.ACTIVATION_SCOPE, version, applicationKey, activationId) : null;
+ final EciesCryptogram eciesCryptogram = EciesCryptogram.builder().ephemeralPublicKey(ephemeralPublicKeyBytes).mac(macBytes).encryptedData(encryptedDataBytes).build();
+ final EciesParameters eciesParameters = EciesParameters.builder().nonce(nonceBytes).associatedData(associatedData).timestamp(timestamp).build();
+ final EciesPayload eciesPayload = new EciesPayload(eciesCryptogram, eciesParameters);
+
+ if (eciesPayload.getParameters().getTimestamp() != null) {
+ // Check ECIES request for replay attacks and persist unique value from request
+ eciesreplayPersistenceService.checkAndPersistUniqueValue(
+ UniqueValueType.ECIES_ACTIVATION_SCOPE,
+ new Date(eciesPayload.getParameters().getTimestamp()),
+ ephemeralPublicKeyBytes,
+ nonceBytes,
+ activationId);
+ }
// Lookup the activation
final ActivationRecordEntity activation = repositoryCatalogue.getActivationRepository().findActivationWithLock(activationId);
@@ -166,10 +190,12 @@ public StartUpgradeResponse startUpgrade(StartUpgradeRequest request) throws Gen
final byte[] transportKeyBytes = keyConvertor.convertSharedSecretKeyToBytes(transportKey);
// Get decryptor for the application
- final EciesDecryptor decryptor = eciesFactory.getEciesDecryptorForActivation((ECPrivateKey) serverPrivateKey, applicationSecret, transportKeyBytes, EciesSharedInfo1.UPGRADE);
+ final EciesDecryptor decryptor = eciesFactory.getEciesDecryptorForActivation(
+ (ECPrivateKey) serverPrivateKey, applicationSecret, transportKeyBytes, EciesSharedInfo1.UPGRADE,
+ eciesParameters, ephemeralPublicKeyBytes);
// Try to decrypt request data, the data must not be empty. Currently only '{}' is sent in request data.
- final byte[] decryptedData = decryptor.decryptRequest(cryptogram);
+ final byte[] decryptedData = decryptor.decrypt(eciesPayload);
if (decryptedData.length == 0) {
logger.warn("Invalid decrypted request data");
// Rollback is not required, error occurs before writing to database
@@ -200,10 +226,19 @@ public StartUpgradeResponse startUpgrade(StartUpgradeRequest request) throws Gen
// Encrypt response payload and return it
final byte[] payloadBytes = objectMapper.writeValueAsBytes(payload);
- final EciesCryptogram cryptogramResponse = decryptor.encryptResponse(payloadBytes);
+ final byte[] nonceBytesResponse = ("3.2".equals(version) || "3.1".equals(version)) ? keyGenerator.generateRandomBytes(16) : null;
+ final Long timestampResponse = "3.2".equals(version) ? new Date().getTime() : null;
+ final EciesParameters parametersResponse = EciesParameters.builder().nonce(nonceBytesResponse).associatedData(eciesPayload.getParameters().getAssociatedData()).timestamp(timestampResponse).build();
+ final EciesEncryptor encryptorResponse = eciesFactory.getEciesEncryptor(EciesScope.ACTIVATION_SCOPE,
+ decryptor.getEnvelopeKey(), applicationSecret, transportKeyBytes, parametersResponse);
+
+ final EciesPayload payloadResponse = encryptorResponse.encrypt(payloadBytes, parametersResponse);
final StartUpgradeResponse response = new StartUpgradeResponse();
- response.setEncryptedData(Base64.getEncoder().encodeToString(cryptogramResponse.getEncryptedData()));
- response.setMac(Base64.getEncoder().encodeToString(cryptogramResponse.getMac()));
+ response.setEncryptedData(Base64.getEncoder().encodeToString(payloadResponse.getCryptogram().getEncryptedData()));
+ response.setMac(Base64.getEncoder().encodeToString(payloadResponse.getCryptogram().getMac()));
+ response.setEphemeralPublicKey(Base64.getEncoder().encodeToString(payloadResponse.getCryptogram().getEphemeralPublicKey()));
+ response.setNonce(nonceBytesResponse != null ? Base64.getEncoder().encodeToString(nonceBytesResponse) : null);
+ response.setTimestamp(timestampResponse);
// Save activation as last step to avoid rollbacks
if (activationShouldBeSaved) {
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/VaultUnlockServiceBehavior.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/VaultUnlockServiceBehavior.java
index 001435ae0..49f9dead3 100644
--- a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/VaultUnlockServiceBehavior.java
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/behavior/tasks/VaultUnlockServiceBehavior.java
@@ -31,6 +31,8 @@
import io.getlime.security.powerauth.app.server.database.model.ServerPrivateKey;
import io.getlime.security.powerauth.app.server.database.model.entity.ActivationRecordEntity;
import io.getlime.security.powerauth.app.server.database.model.entity.ApplicationVersionEntity;
+import io.getlime.security.powerauth.app.server.database.model.enumeration.UniqueValueType;
+import io.getlime.security.powerauth.app.server.service.replay.ReplayVerificationService;
import io.getlime.security.powerauth.app.server.service.behavior.ServiceBehaviorCatalogue;
import io.getlime.security.powerauth.app.server.service.exceptions.GenericServiceException;
import io.getlime.security.powerauth.app.server.service.i18n.LocalizationProvider;
@@ -38,10 +40,11 @@
import io.getlime.security.powerauth.app.server.service.model.request.VaultUnlockRequestPayload;
import io.getlime.security.powerauth.app.server.service.model.response.VaultUnlockResponsePayload;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesDecryptor;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesEncryptor;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesFactory;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.exception.EciesException;
-import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesCryptogram;
-import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesSharedInfo1;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.*;
+import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator;
import io.getlime.security.powerauth.crypto.lib.model.exception.CryptoProviderException;
import io.getlime.security.powerauth.crypto.lib.model.exception.GenericCryptoException;
import io.getlime.security.powerauth.crypto.lib.util.KeyConvertor;
@@ -60,10 +63,7 @@
import java.security.PublicKey;
import java.security.interfaces.ECPrivateKey;
import java.security.spec.InvalidKeySpecException;
-import java.util.ArrayList;
-import java.util.Base64;
-import java.util.List;
-import java.util.Objects;
+import java.util.*;
/**
* Behavior class implementing the vault unlock related processes. The class separates the
@@ -83,9 +83,11 @@ public class VaultUnlockServiceBehavior {
private final LocalizationProvider localizationProvider;
private final ServerPrivateKeyConverter serverPrivateKeyConverter;
private final ServiceBehaviorCatalogue behavior;
+ private final ReplayVerificationService eciesreplayPersistenceService;
// Helper classes
private final EciesFactory eciesFactory = new EciesFactory();
+ private final KeyGenerator keyGenerator = new KeyGenerator();
private final PowerAuthServerVault powerAuthServerVault = new PowerAuthServerVault();
private final ObjectMapper objectMapper;
private final PowerAuthServerKeyFactory powerAuthServerKeyFactory = new PowerAuthServerKeyFactory();
@@ -94,11 +96,12 @@ public class VaultUnlockServiceBehavior {
private static final Logger logger = LoggerFactory.getLogger(VaultUnlockServiceBehavior.class);
@Autowired
- public VaultUnlockServiceBehavior(RepositoryCatalogue repositoryCatalogue, LocalizationProvider localizationProvider, ServerPrivateKeyConverter serverPrivateKeyConverter, ServiceBehaviorCatalogue behavior, ObjectMapper objectMapper) {
+ public VaultUnlockServiceBehavior(RepositoryCatalogue repositoryCatalogue, LocalizationProvider localizationProvider, ServerPrivateKeyConverter serverPrivateKeyConverter, ServiceBehaviorCatalogue behavior, ReplayVerificationService eciesreplayPersistenceService, ObjectMapper objectMapper) {
this.repositoryCatalogue = repositoryCatalogue;
this.localizationProvider = localizationProvider;
this.serverPrivateKeyConverter = serverPrivateKeyConverter;
this.behavior = behavior;
+ this.eciesreplayPersistenceService = eciesreplayPersistenceService;
this.objectMapper = objectMapper;
}
@@ -113,13 +116,13 @@ public VaultUnlockServiceBehavior(RepositoryCatalogue repositoryCatalogue, Local
* @param signature PowerAuth signature.
* @param signatureType PowerAuth signature type.
* @param signatureVersion PowerAuth signature version.
- * @param cryptogram ECIES cryptogram.
+ * @param eciesPayload ECIES payload.
* @param keyConversion Key conversion utilities.
* @return Vault unlock response with a properly encrypted vault unlock key.
* @throws GenericServiceException In case server private key decryption fails.
*/
public VaultUnlockResponse unlockVault(String activationId, String applicationKey, String signature, SignatureType signatureType, String signatureVersion,
- String signedData, EciesCryptogram cryptogram, KeyConvertor keyConversion)
+ String signedData, EciesPayload eciesPayload, KeyConvertor keyConversion)
throws GenericServiceException {
try {
// Lookup the activation
@@ -152,6 +155,16 @@ public VaultUnlockResponse unlockVault(String activationId, String applicationKe
return response;
}
+ if (eciesPayload.getParameters().getTimestamp() != null) {
+ // Check ECIES request for replay attacks and persist unique value from request
+ eciesreplayPersistenceService.checkAndPersistUniqueValue(
+ UniqueValueType.ECIES_ACTIVATION_SCOPE,
+ new Date(eciesPayload.getParameters().getTimestamp()),
+ eciesPayload.getCryptogram().getEphemeralPublicKey(),
+ eciesPayload.getParameters().getNonce(),
+ activationId);
+ }
+
// Get application secret and transport key used in sharedInfo2 parameter of ECIES
final byte[] applicationSecret = applicationVersion.getApplicationSecret().getBytes(StandardCharsets.UTF_8);
final byte[] devicePublicKeyBytes = Base64.getDecoder().decode(activation.getDevicePublicKeyBase64());
@@ -161,10 +174,11 @@ public VaultUnlockResponse unlockVault(String activationId, String applicationKe
// Get decryptor for the activation
final EciesDecryptor decryptor = eciesFactory.getEciesDecryptorForActivation((ECPrivateKey) serverPrivateKey,
- applicationSecret, transportKeyBytes, EciesSharedInfo1.VAULT_UNLOCK);
+ applicationSecret, transportKeyBytes, EciesSharedInfo1.VAULT_UNLOCK, eciesPayload.getParameters(),
+ eciesPayload.getCryptogram().getEphemeralPublicKey());
// Decrypt request to obtain vault unlock reason
- final byte[] decryptedData = decryptor.decryptRequest(cryptogram);
+ final byte[] decryptedData = decryptor.decrypt(eciesPayload);
// Convert JSON data to vault unlock request object
VaultUnlockRequestPayload request;
@@ -211,14 +225,24 @@ public VaultUnlockResponse unlockVault(String activationId, String applicationKe
final byte[] reponsePayloadBytes = objectMapper.writeValueAsBytes(responsePayload);
// Encrypt response payload
- final EciesCryptogram responseCryptogram = decryptor.encryptResponse(reponsePayloadBytes);
- final String responseData = Base64.getEncoder().encodeToString(responseCryptogram.getEncryptedData());
- final String responseMac = Base64.getEncoder().encodeToString(responseCryptogram.getMac());
+ final byte[] nonceBytesResponse = ("3.2".equals(signatureVersion) || "3.1".equals(signatureVersion)) ? keyGenerator.generateRandomBytes(16) : null;
+ final Long timestampResponse = "3.2".equals(signatureVersion) ? new Date().getTime() : null;
+ final EciesParameters parametersResponse = EciesParameters.builder().nonce(nonceBytesResponse).associatedData(eciesPayload.getParameters().getAssociatedData()).timestamp(timestampResponse).build();
+ final EciesEncryptor encryptorResponse = eciesFactory.getEciesEncryptor(EciesScope.ACTIVATION_SCOPE,
+ decryptor.getEnvelopeKey(), applicationSecret, transportKeyBytes, parametersResponse);
+
+ final EciesPayload responseEciesPayload = encryptorResponse.encrypt(reponsePayloadBytes, parametersResponse);
+ final String dataResponse = Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getEncryptedData());
+ final String macResponse = Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getMac());
+ final String ephemeralPublicKeyResponse = Base64.getEncoder().encodeToString(responseEciesPayload.getCryptogram().getEphemeralPublicKey());
// Return vault unlock response, set signature validity
final VaultUnlockResponse response = new VaultUnlockResponse();
- response.setEncryptedData(responseData);
- response.setMac(responseMac);
+ response.setEncryptedData(dataResponse);
+ response.setMac(macResponse);
+ response.setEphemeralPublicKey(ephemeralPublicKeyResponse);
+ response.setNonce(nonceBytesResponse != null ? Base64.getEncoder().encodeToString(nonceBytesResponse) : null);
+ response.setTimestamp(timestampResponse);
response.setSignatureValid(signatureResponse.isSignatureValid());
return response;
} catch (InvalidKeyException | InvalidKeySpecException ex) {
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/replay/ReplayExpirationService.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/replay/ReplayExpirationService.java
new file mode 100644
index 000000000..a317079b5
--- /dev/null
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/replay/ReplayExpirationService.java
@@ -0,0 +1,55 @@
+/*
+ * PowerAuth Server and related software components
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package io.getlime.security.powerauth.app.server.service.replay;
+
+import lombok.extern.slf4j.Slf4j;
+import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Service for expiring database records related to prevention against replay attacks.
+ *
+ * @author Roman Strobl, roman.strobl@wultra.com
+ */
+@Service
+@Slf4j
+public class ReplayExpirationService {
+
+ private final ReplayPersistenceService replayPersistenceService;
+
+ /**
+ * Service constructor.
+ * @param replayPersistenceService Replay persistence service.
+ */
+ @Autowired
+ public ReplayExpirationService(ReplayPersistenceService replayPersistenceService) {
+ this.replayPersistenceService = replayPersistenceService;
+ }
+
+ @Scheduled(fixedRateString = "${powerauth.service.scheduled.job.uniqueValueCleanup:60000}")
+ @SchedulerLock(name = "expireUniqueValuesTask")
+ @Transactional
+ public void processExpirations() {
+ replayPersistenceService.deleteExpiredUniqueValues();
+ }
+}
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/replay/ReplayPersistenceService.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/replay/ReplayPersistenceService.java
new file mode 100644
index 000000000..f7b6fd50b
--- /dev/null
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/replay/ReplayPersistenceService.java
@@ -0,0 +1,94 @@
+/*
+ * PowerAuth Server and related software components
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package io.getlime.security.powerauth.app.server.service.replay;
+
+import io.getlime.security.powerauth.app.server.configuration.PowerAuthServiceConfiguration;
+import io.getlime.security.powerauth.app.server.database.model.entity.UniqueValueEntity;
+import io.getlime.security.powerauth.app.server.database.model.enumeration.UniqueValueType;
+import io.getlime.security.powerauth.app.server.database.repository.UniqueValueRepository;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+
+/**
+ * Service for checking unique cryptography values to prevent replay attacks.
+ *
+ * @author Roman Strobl, roman.strobl@wultra.com
+ */
+@Service
+@Slf4j
+public class ReplayPersistenceService {
+
+ private final UniqueValueRepository uniqueValueRepository;
+ private final PowerAuthServiceConfiguration config;
+
+ /**
+ * Service constructor.
+ * @param uniqueValueRepository Unique value repository.
+ * @param config PowerAuth service configuration.
+ */
+ @Autowired
+ public ReplayPersistenceService(UniqueValueRepository uniqueValueRepository, PowerAuthServiceConfiguration config) {
+ this.uniqueValueRepository = uniqueValueRepository;
+ this.config = config;
+ }
+
+ /**
+ * Check whether unique value exists in the database.
+ * @param uniqueValue Unique value to check.
+ * @return Whether unique value exists.
+ */
+ public boolean uniqueValueExists(final String uniqueValue) {
+ return uniqueValueRepository.findById(uniqueValue).isPresent();
+ }
+
+ /**
+ * Persist a unique value into the database.
+ * @param type Unique value type.
+ * @param uniqueValue Unique value.
+ * @return Whether unique value was added successfully.
+ */
+ public boolean persistUniqueValue(final UniqueValueType type, final String uniqueValue) {
+ final Instant expiration = Instant.now().plus(config.getRequestExpirationInMilliseconds(), ChronoUnit.MILLIS);
+ final UniqueValueEntity uniqueVal = new UniqueValueEntity();
+ uniqueVal.setType(type);
+ uniqueVal.setUniqueValue(uniqueValue);
+ uniqueVal.setTimestampExpires(Date.from(expiration));
+ try {
+ uniqueValueRepository.save(uniqueVal);
+ return true;
+ } catch (Exception ex) {
+ logger.warn("Could not persist unique value: " + uniqueValue, ex);
+ return false;
+ }
+ }
+
+ /**
+ * Remove expired unique values in the database.
+ */
+ public void deleteExpiredUniqueValues() {
+ int expiredCount = uniqueValueRepository.deleteAllByTimestampExpiresBefore(Date.from(Instant.now()));
+ logger.info("Removed {} expired unique values", expiredCount);
+ }
+}
diff --git a/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/replay/ReplayVerificationService.java b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/replay/ReplayVerificationService.java
new file mode 100644
index 000000000..e998f46a5
--- /dev/null
+++ b/powerauth-java-server/src/main/java/io/getlime/security/powerauth/app/server/service/replay/ReplayVerificationService.java
@@ -0,0 +1,121 @@
+/*
+ * PowerAuth Server and related software components
+ * Copyright (C) 2023 Wultra s.r.o.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published
+ * by the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package io.getlime.security.powerauth.app.server.service.replay;
+
+import io.getlime.security.powerauth.app.server.configuration.PowerAuthServiceConfiguration;
+import io.getlime.security.powerauth.app.server.database.model.enumeration.UniqueValueType;
+import io.getlime.security.powerauth.app.server.service.exceptions.GenericServiceException;
+import io.getlime.security.powerauth.app.server.service.i18n.LocalizationProvider;
+import io.getlime.security.powerauth.app.server.service.model.ServiceError;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Base64;
+import java.util.Date;
+
+/**
+ * Service for checking unique cryptography values to prevent replay attacks.
+ *
+ * @author Roman Strobl, roman.strobl@wultra.com
+ */
+@Service
+@Slf4j
+public class ReplayVerificationService {
+
+ private final ReplayPersistenceService replayPersistenceService;
+ private final LocalizationProvider localizationProvider;
+ private final PowerAuthServiceConfiguration config;
+
+ /**
+ * Service constructor.
+ * @param replayPersistenceService Replay persistence service.
+ * @param localizationProvider Localization provider.
+ * @param powerAuthServiceConfiguration PowerAuth service configuration.
+ */
+ @Autowired
+ public ReplayVerificationService(ReplayPersistenceService replayPersistenceService, LocalizationProvider localizationProvider, PowerAuthServiceConfiguration powerAuthServiceConfiguration) {
+ this.replayPersistenceService = replayPersistenceService;
+ this.localizationProvider = localizationProvider;
+ this.config = powerAuthServiceConfiguration;
+ }
+
+ /**
+ * Check whether unique value exists for MAC Token request.
+ * @param type Unique value type.
+ * @param requestTimestamp Request timestamp.
+ * @param nonceBytes Nonce bytes.
+ * @param identifier Identifier for the record.
+ * @throws GenericServiceException Thrown in case unique value exists.
+ */
+ public void checkAndPersistUniqueValue(UniqueValueType type, Date requestTimestamp, byte[] nonceBytes, String identifier) throws GenericServiceException {
+ checkAndPersistUniqueValue(type, requestTimestamp, new byte[0], nonceBytes, identifier);
+ }
+
+ /**
+ * Check whether unique value exists for ECIES request.
+ * @param type Unique value type.
+ * @param requestTimestamp Request timestamp.
+ * @param ephemeralPublicKeyBytes Ephemeral public key bytes.
+ * @param nonceBytes Nonce bytes.
+ * @param identifier Identifier for the record.
+ * @throws GenericServiceException Thrown in case unique value exists.
+ */
+ public void checkAndPersistUniqueValue(UniqueValueType type, Date requestTimestamp, byte[] ephemeralPublicKeyBytes, byte[] nonceBytes, String identifier) throws GenericServiceException {
+ final Date expiration = Date.from(Instant.now().plus(config.getRequestExpirationInMilliseconds(), ChronoUnit.MILLIS));
+ if (requestTimestamp.after(expiration)) {
+ // Rollback is not required, error occurs before writing to database
+ logger.warn("Expired ECIES request received, timestamp: {}", requestTimestamp);
+ throw localizationProvider.buildExceptionForCode(ServiceError.INVALID_REQUEST);
+ }
+
+ final byte[] identifierBytes = identifier != null ? identifier.getBytes(StandardCharsets.UTF_8) : new byte[0];
+ final ByteBuffer uniqueValBuffer = ByteBuffer.allocate(
+ (ephemeralPublicKeyBytes != null ? ephemeralPublicKeyBytes.length : 0)
+ + (nonceBytes != null ? nonceBytes.length : 0)
+ + identifierBytes.length);
+ if (ephemeralPublicKeyBytes != null) {
+ uniqueValBuffer.put(ephemeralPublicKeyBytes);
+ }
+ if (nonceBytes != null) {
+ uniqueValBuffer.put(nonceBytes);
+ }
+ if (identifier != null) {
+ uniqueValBuffer.put(identifierBytes);
+ }
+ final String uniqueValue = Base64.getEncoder().encodeToString(uniqueValBuffer.array());
+ if (replayPersistenceService.uniqueValueExists(uniqueValue)) {
+ logger.warn("Duplicate request not allowed to prevent replay attacks");
+ // Rollback is not required, error occurs before writing to database
+ throw localizationProvider.buildExceptionForCode(ServiceError.INVALID_REQUEST);
+ }
+ if (!replayPersistenceService.persistUniqueValue(type, uniqueValue)) {
+ logger.warn("Unique value could not be persisted");
+ // Rollback is not required, error occurs before writing to database
+ throw localizationProvider.buildExceptionForCode(ServiceError.GENERIC_CRYPTOGRAPHY_ERROR);
+ }
+ }
+
+
+}
diff --git a/powerauth-java-server/src/main/resources/application.properties b/powerauth-java-server/src/main/resources/application.properties
index c7959e7fd..dfe6aa125 100644
--- a/powerauth-java-server/src/main/resources/application.properties
+++ b/powerauth-java-server/src/main/resources/application.properties
@@ -55,6 +55,7 @@ powerauth.service.crypto.activationValidityInMilliseconds=300000
powerauth.service.crypto.signatureMaxFailedAttempts=5
powerauth.service.crypto.signatureValidationLookahead=20
powerauth.service.crypto.offlineSignatureComponentLength=8
+powerauth.service.crypto.requestExpirationInMilliseconds=7200000
# HTTP Proxy Settings
powerauth.service.http.proxy.enabled=false
@@ -80,6 +81,7 @@ powerauth.service.secureVault.enableBiometricAuthentication=false
powerauth.service.scheduled.job.operationCleanup=5000
powerauth.service.scheduled.job.activationsCleanup=5000
powerauth.service.scheduled.job.activationsCleanup.lookBackInMilliseconds=3600000
+powerauth.service.scheduled.job.uniqueValueCleanup=60000
# Database Lock Timeout Configuration
spring.jpa.properties.jakarta.persistence.lock.timeout=10000
diff --git a/powerauth-java-server/src/test/java/io/getlime/security/powerauth/app/server/VerifySignatureConcurrencyTest.java b/powerauth-java-server/src/test/java/io/getlime/security/powerauth/app/server/VerifySignatureConcurrencyTest.java
index 0187c24c6..88a981bb7 100644
--- a/powerauth-java-server/src/test/java/io/getlime/security/powerauth/app/server/VerifySignatureConcurrencyTest.java
+++ b/powerauth-java-server/src/test/java/io/getlime/security/powerauth/app/server/VerifySignatureConcurrencyTest.java
@@ -6,12 +6,11 @@
import com.wultra.security.powerauth.client.model.response.*;
import io.getlime.security.powerauth.app.server.service.model.request.ActivationLayer2Request;
import io.getlime.security.powerauth.app.server.service.PowerAuthService;
-import io.getlime.security.powerauth.crypto.client.activation.PowerAuthClientActivation;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesEncryptor;
import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.EciesFactory;
-import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesCryptogram;
-import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.EciesSharedInfo1;
+import io.getlime.security.powerauth.crypto.lib.encryptor.ecies.model.*;
import io.getlime.security.powerauth.crypto.lib.generator.KeyGenerator;
+import io.getlime.security.powerauth.crypto.lib.util.EciesUtils;
import io.getlime.security.powerauth.crypto.lib.util.KeyConvertor;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -62,9 +61,6 @@ public void testVerifySignatureConcurrent() throws Exception {
PublicKey publicKey = keyPair.getPublic();
byte[] publicKeyBytes = keyConvertor.convertPublicKeyToBytes(publicKey);
- // Compute application signature
- new PowerAuthClientActivation();
-
// Generate expiration time
Calendar expiration = Calendar.getInstance();
expiration.add(Calendar.MINUTE, 5);
@@ -79,10 +75,19 @@ public void testVerifySignatureConcurrent() throws Exception {
ECPublicKey masterPublicKey = (ECPublicKey) keyConvertor.convertBytesToPublicKey(Base64.getDecoder().decode(detailResponse.getMasterPublicKey()));
- EciesEncryptor eciesEncryptor = new EciesFactory().getEciesEncryptorForApplication(masterPublicKey, createApplicationVersionResponse.getApplicationSecret().getBytes(StandardCharsets.UTF_8), EciesSharedInfo1.ACTIVATION_LAYER_2);
+ final String version = "3.2";
+ final String applicationKey = createApplicationVersionResponse.getApplicationKey();
+ final byte[] associatedData = EciesUtils.deriveAssociatedData(EciesScope.APPLICATION_SCOPE, version, applicationKey, null);
+ final Long timestamp = new Date().getTime();
+ final byte[] nonceBytes = new KeyGenerator().generateRandomBytes(16);
+ final EciesParameters eciesParameters = EciesParameters.builder().nonce(nonceBytes).associatedData(associatedData).timestamp(timestamp).build();
+
+ EciesEncryptor eciesEncryptor = new EciesFactory().getEciesEncryptorForApplication(masterPublicKey,
+ createApplicationVersionResponse.getApplicationSecret().getBytes(StandardCharsets.UTF_8),
+ EciesSharedInfo1.ACTIVATION_LAYER_2, eciesParameters);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
new ObjectMapper().writeValue(baos, requestL2);
- EciesCryptogram eciesCryptogram = eciesEncryptor.encryptRequest(baos.toByteArray(), true);
+ EciesPayload eciesPayload = eciesEncryptor.encrypt(baos.toByteArray(), eciesParameters);
// Create activation
CreateActivationRequest createActivationRequest = new CreateActivationRequest();
@@ -90,10 +95,12 @@ public void testVerifySignatureConcurrent() throws Exception {
createActivationRequest.setTimestampActivationExpire(expiration.getTime());
createActivationRequest.setMaxFailureCount(5L);
createActivationRequest.setApplicationKey(createApplicationVersionResponse.getApplicationKey());
- createActivationRequest.setEncryptedData(Base64.getEncoder().encodeToString(eciesCryptogram.getEncryptedData()));
- createActivationRequest.setMac(Base64.getEncoder().encodeToString(eciesCryptogram.getMac()));
- createActivationRequest.setEphemeralPublicKey(Base64.getEncoder().encodeToString(eciesCryptogram.getEphemeralPublicKey()));
- createActivationRequest.setNonce(Base64.getEncoder().encodeToString(eciesCryptogram.getNonce()));
+ createActivationRequest.setEncryptedData(Base64.getEncoder().encodeToString(eciesPayload.getCryptogram().getEncryptedData()));
+ createActivationRequest.setMac(Base64.getEncoder().encodeToString(eciesPayload.getCryptogram().getMac()));
+ createActivationRequest.setEphemeralPublicKey(Base64.getEncoder().encodeToString(eciesPayload.getCryptogram().getEphemeralPublicKey()));
+ createActivationRequest.setNonce(Base64.getEncoder().encodeToString(eciesPayload.getParameters().getNonce()));
+ createActivationRequest.setTimestamp(timestamp);
+ createActivationRequest.setProtocolVersion("3.2");
CreateActivationResponse createActivationResponse = powerAuthService.createActivation(createActivationRequest);
// Commit activation
diff --git a/powerauth-java-server/src/test/resources/application.properties b/powerauth-java-server/src/test/resources/application.properties
index fcc99773f..45e662f12 100644
--- a/powerauth-java-server/src/test/resources/application.properties
+++ b/powerauth-java-server/src/test/resources/application.properties
@@ -42,6 +42,7 @@ powerauth.service.crypto.activationValidityInMilliseconds=120000
powerauth.service.crypto.signatureMaxFailedAttempts=5
powerauth.service.crypto.signatureValidationLookahead=20
powerauth.service.crypto.offlineSignatureComponentLength=8
+powerauth.service.crypto.requestExpirationInMilliseconds=7200000
# HTTP Proxy Settings
powerauth.service.http.proxy.enabled=false
diff --git a/powerauth-rest-client-spring/src/main/java/com/wultra/security/powerauth/rest/client/PowerAuthRestClient.java b/powerauth-rest-client-spring/src/main/java/com/wultra/security/powerauth/rest/client/PowerAuthRestClient.java
index a96282012..2568b7706 100644
--- a/powerauth-rest-client-spring/src/main/java/com/wultra/security/powerauth/rest/client/PowerAuthRestClient.java
+++ b/powerauth-rest-client-spring/src/main/java/com/wultra/security/powerauth/rest/client/PowerAuthRestClient.java
@@ -247,7 +247,7 @@ public PrepareActivationResponse prepareActivation(PrepareActivationRequest requ
}
@Override
- public PrepareActivationResponse prepareActivation(String activationCode, String applicationKey, boolean shouldGenerateRecoveryCodes, String ephemeralPublicKey, String encryptedData, String mac, String nonce) throws PowerAuthClientException {
+ public PrepareActivationResponse prepareActivation(String activationCode, String applicationKey, boolean shouldGenerateRecoveryCodes, String ephemeralPublicKey, String encryptedData, String mac, String nonce, String protocolVersion, Long timestamp) throws PowerAuthClientException {
final PrepareActivationRequest request = new PrepareActivationRequest();
request.setActivationCode(activationCode);
request.setApplicationKey(applicationKey);
@@ -256,6 +256,8 @@ public PrepareActivationResponse prepareActivation(String activationCode, String
request.setEncryptedData(encryptedData);
request.setMac(mac);
request.setNonce(nonce);
+ request.setProtocolVersion(protocolVersion);
+ request.setTimestamp(timestamp);
return prepareActivation(request, EMPTY_MULTI_MAP, EMPTY_MULTI_MAP);
}
@@ -272,7 +274,7 @@ public CreateActivationResponse createActivation(CreateActivationRequest request
@Override
public CreateActivationResponse createActivation(String userId, Date timestampActivationExpire, Long maxFailureCount,
String applicationKey, String ephemeralPublicKey, String encryptedData,
- String mac, String nonce) throws PowerAuthClientException {
+ String mac, String nonce, String protocolVersion, Long timestamp) throws PowerAuthClientException {
final CreateActivationRequest request = new CreateActivationRequest();
request.setUserId(userId);
if (timestampActivationExpire != null) {
@@ -286,6 +288,8 @@ public CreateActivationResponse createActivation(String userId, Date timestampAc
request.setEncryptedData(encryptedData);
request.setMac(mac);
request.setNonce(nonce);
+ request.setProtocolVersion(protocolVersion);
+ request.setTimestamp(timestamp);
return createActivation(request, EMPTY_MULTI_MAP, EMPTY_MULTI_MAP);
}
@@ -545,7 +549,8 @@ public VaultUnlockResponse unlockVault(VaultUnlockRequest request, MultiValueMap
@Override
public VaultUnlockResponse unlockVault(String activationId, String applicationKey, String signature,
SignatureType signatureType, String signatureVersion, String signedData,
- String ephemeralPublicKey, String encryptedData, String mac, String nonce) throws PowerAuthClientException {
+ String ephemeralPublicKey, String encryptedData, String mac, String nonce,
+ Long timestamp) throws PowerAuthClientException {
final VaultUnlockRequest request = new VaultUnlockRequest();
request.setActivationId(activationId);
request.setApplicationKey(applicationKey);
@@ -557,6 +562,7 @@ public VaultUnlockResponse unlockVault(String activationId, String applicationKe
request.setEncryptedData(encryptedData);
request.setMac(mac);
request.setNonce(nonce);
+ request.setTimestamp(timestamp);
return unlockVault(request, EMPTY_MULTI_MAP, EMPTY_MULTI_MAP);
}
@@ -912,7 +918,8 @@ public CreateTokenResponse createToken(CreateTokenRequest request, MultiValueMap
@Override
public CreateTokenResponse createToken(String activationId, String applicationKey, String ephemeralPublicKey,
- String encryptedData, String mac, String nonce, SignatureType signatureType) throws PowerAuthClientException {
+ String encryptedData, String mac, String nonce, String protocolVersion,
+ Long timestamp, SignatureType signatureType) throws PowerAuthClientException {
final CreateTokenRequest request = new CreateTokenRequest();
request.setActivationId(activationId);
request.setApplicationKey(applicationKey);
@@ -920,6 +927,8 @@ public CreateTokenResponse createToken(String activationId, String applicationKe
request.setMac(mac);
request.setEphemeralPublicKey(ephemeralPublicKey);
request.setNonce(nonce);
+ request.setProtocolVersion(protocolVersion);
+ request.setTimestamp(timestamp);
request.setSignatureType(signatureType);
return createToken(request, EMPTY_MULTI_MAP, EMPTY_MULTI_MAP);
}
@@ -973,11 +982,15 @@ public GetEciesDecryptorResponse getEciesDecryptor(GetEciesDecryptorRequest requ
}
@Override
- public GetEciesDecryptorResponse getEciesDecryptor(String activationId, String applicationKey, String ephemeralPublicKey) throws PowerAuthClientException {
+ public GetEciesDecryptorResponse getEciesDecryptor(String activationId, String applicationKey, String ephemeralPublicKey,
+ String nonce, String protocolVersion, Long timestamp) throws PowerAuthClientException {
final GetEciesDecryptorRequest request = new GetEciesDecryptorRequest();
request.setActivationId(activationId);
request.setApplicationKey(applicationKey);
request.setEphemeralPublicKey(ephemeralPublicKey);
+ request.setNonce(nonce);
+ request.setProtocolVersion(protocolVersion);
+ request.setTimestamp(timestamp);
return getEciesDecryptor(request, EMPTY_MULTI_MAP, EMPTY_MULTI_MAP);
}
@@ -993,7 +1006,8 @@ public StartUpgradeResponse startUpgrade(StartUpgradeRequest request, MultiValue
@Override
public StartUpgradeResponse startUpgrade(String activationId, String applicationKey, String ephemeralPublicKey,
- String encryptedData, String mac, String nonce) throws PowerAuthClientException {
+ String encryptedData, String mac, String nonce, String protocolVersion,
+ Long timestamp) throws PowerAuthClientException {
final StartUpgradeRequest request = new StartUpgradeRequest();
request.setActivationId(activationId);
request.setApplicationKey(applicationKey);
@@ -1001,6 +1015,8 @@ public StartUpgradeResponse startUpgrade(String activationId, String application
request.setEncryptedData(encryptedData);
request.setMac(mac);
request.setNonce(nonce);
+ request.setProtocolVersion(protocolVersion);
+ request.setTimestamp(timestamp);
return startUpgrade(request, EMPTY_MULTI_MAP, EMPTY_MULTI_MAP);
}
@@ -1053,7 +1069,8 @@ public ConfirmRecoveryCodeResponse confirmRecoveryCode(ConfirmRecoveryCodeReques
@Override
public ConfirmRecoveryCodeResponse confirmRecoveryCode(String activationId, String applicationKey, String ephemeralPublicKey,
- String encryptedData, String mac, String nonce) throws PowerAuthClientException {
+ String encryptedData, String mac, String nonce, String protocolVersion,
+ Long timestamp) throws PowerAuthClientException {
final ConfirmRecoveryCodeRequest request = new ConfirmRecoveryCodeRequest();
request.setActivationId(activationId);
request.setApplicationKey(applicationKey);
@@ -1061,6 +1078,8 @@ public ConfirmRecoveryCodeResponse confirmRecoveryCode(String activationId, Stri
request.setEncryptedData(encryptedData);
request.setMac(mac);
request.setNonce(nonce);
+ request.setProtocolVersion(protocolVersion);
+ request.setTimestamp(timestamp);
return confirmRecoveryCode(request, EMPTY_MULTI_MAP, EMPTY_MULTI_MAP);
}
@@ -1115,7 +1134,8 @@ public RecoveryCodeActivationResponse createActivationUsingRecoveryCode(Recovery
@Override
public RecoveryCodeActivationResponse createActivationUsingRecoveryCode(String recoveryCode, String puk, String applicationKey, Long maxFailureCount,
- String ephemeralPublicKey, String encryptedData, String mac, String nonce) throws PowerAuthClientException {
+ String ephemeralPublicKey, String encryptedData, String mac, String nonce,
+ String protocolVersion, Long timestamp) throws PowerAuthClientException {
final RecoveryCodeActivationRequest request = new RecoveryCodeActivationRequest();
request.setRecoveryCode(recoveryCode);
request.setPuk(puk);
@@ -1127,6 +1147,8 @@ public RecoveryCodeActivationResponse createActivationUsingRecoveryCode(String r
request.setEncryptedData(encryptedData);
request.setMac(mac);
request.setNonce(nonce);
+ request.setProtocolVersion(protocolVersion);
+ request.setTimestamp(timestamp);
return createActivationUsingRecoveryCode(request, EMPTY_MULTI_MAP, EMPTY_MULTI_MAP);
}