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); }