diff --git a/README.md b/README.md index e7bbc374..fd7f71bd 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Related projects If you need to upgrade PowerAuth Mobile SDK to a newer version, you can check following migration guides: +- [Migration from version `1.8.x` to `1.9.x`](docs/Migration-from-1.8-to-1.9.md) - [Migration from version `1.7.x` to `1.8.x`](docs/Migration-from-1.7-to-1.8.md) - [Migration from version `1.6.x` to `1.7.x`](docs/Migration-from-1.6-to-1.7.md) - [Migration from version `1.5.x` to `1.6.x`](docs/Migration-from-1.5-to-1.6.md) @@ -32,11 +33,12 @@ If you need to upgrade PowerAuth Mobile SDK to a newer version, you can check fo | Mobile SDK | Protocol | PowerAuth Server | Support Status | |------------|----------|---------------------|-----------------------------------| -| `1.8.x` | `V3.2` | `1.5+` | Fully supported | -| `1.7.x` | `V3.1` | `0.24+` | Security & Functionality bugfixes | +| `1.9.x` | `V3.3` | `1.9+` | Fully supported | +| `1.8.x` | `V3.2` | `1.5+` | Security & Functionality bugfixes | +| `1.7.x` | `V3.1` | `0.24+` | Security bugfixes | | `1.6.x` | `V3.1` | `0.24+` | Security bugfixes | | `1.5.x` | `V3.1` | `0.24+` | Security bugfixes | -| `1.4.x` | `V3.1` | `0.24+` | Security bugfixes | +| `1.4.x` | `V3.1` | `0.24+` | Not supported | | `1.3.x` | `V3.1` | `0.23+` | Not supported | | `1.2.x` | `V3.0` | `0.22+` | Not supported | | `1.1.x` | `V3.0` | `0.21+` | Not supported | diff --git a/docs/Migration-from-1.8-to-1.9.md b/docs/Migration-from-1.8-to-1.9.md new file mode 100644 index 00000000..bd082cfe --- /dev/null +++ b/docs/Migration-from-1.8-to-1.9.md @@ -0,0 +1,84 @@ +# Migration from 1.8.x to 1.9.x + +PowerAuth Mobile SDK in version `1.9.0` provides the following improvements: + +- Added support for PowerAuth protocol version 3.3, including the following improvements: + - The PowerAuth protocol is no longer use EC key-pairs for encryption and signature calculation (dual use problem.) + - The End-To-End encryption is now using a temporary keys to improve the forward secrecy of our ECIES scheme. +- Simplified construction of encrypted request and response. Check updated [Android](PowerAuth-SDK-for-Android.md#end-to-end-encryption) or [iOS](PowerAuth-SDK-for-iOS.md#end-to-end-encryption) documentation for more details. + +### Compatibility with PowerAuth Server + +- This release is fully compatible with PowerAuth Server version `1.9.0` and newer. + +## Android + +### API changes + +- The following methods are now deprecated in the `PowerAuthSDK` class: + - Synchronous method `getEciesEncryptorForApplicationScope()` is replaced with asynchronous variant that guarantees the temporary encryption key is prepared. + - Synchronous method `getEciesEncryptorForActivationScope()` is replaced with asynchronous variant that guarantees the temporary encryption key is prepared. + +- Removed all interfaces deprecated in release `1.8.x` + +### Other changes + +#### End-To-End Encryption + +Encrypted request now contains a new property `temporaryKeyId` with type `String`, please update your model objects. For example: + +```json +{ + "temporaryKeyId" : "UUID", + "ephemeralPublicKey" : "BASE64-DATA-BLOB", + "encryptedData": "BASE64-DATA-BLOB", + "mac" : "BASE64-DATA-BLOB", + "nonce" : "BASE64-NONCE", + "timestamp" : 1694172789256 +} +``` + +You can use new `EciesCryptogram.toEncryptedRequest()` method to convert cryptogram into easily serializable request object. It's also no longer necessary to synchronize the time with the server, because as the new asynchronous `getEciesEncryptorFor{*}Scope()` methods do that automatically. + +## iOS & tvOS + +### API changes + +- The following methods in `PowerAuthSDK` are now deprecated: + - Synchronous function `eciesEncryptorForApplicationScope()` is replaced with asynchronous variant that guarantees the temporary encryption key is prepared. + - Synchronous function `eciesEncryptorForActivationScope()` is replaced with asynchronous variant that guarantees the temporary encryption key is prepared. + +- Removed all interfaces deprecated in release `1.8.x` + +### Other changes + +#### End-To-End Encryption + +Encrypted request now contains a new property `temporaryKeyId` with type `String`, please update your model objects. For example: + +```json +{ + "temporaryKeyId" : "UUID", + "ephemeralPublicKey" : "BASE64-DATA-BLOB", + "encryptedData": "BASE64-DATA-BLOB", + "mac" : "BASE64-DATA-BLOB", + "nonce" : "BASE64-NONCE", + "timestamp" : 1694172789256 +} +``` + +You can use the new `PowerAuthCoreEciesCryptogram.requestPayload()` function to prepare a dictionary containing all request parameters. It's also no longer necessary to synchronize the time with the server, because as the new asynchronous `eciesEncryptorFor{*}Scope()` functions do that automatically. + +## iOS & tvOS App Extensions + +- Removed all interfaces deprecated in release `1.8.x` + +## Known Bugs + +The PowerAuth SDKs for iOS and tvOS App Extensions, as well as for watchOS, do not use time synchronized with the server for token-based authentication. To avoid any compatibility issues with the server, the authentication headers generated in your App Extension or on watchOS still use the older protocol version 3.1. This issue will be fixed in a future SDK update. + +You can watch the following related issues: + +- [wultra/powerauth-mobile-sdk#551](https://github.com/wultra/powerauth-mobile-sdk/issues/551) +- [wultra/powerauth-mobile-watch-sdk#7](https://github.com/wultra/powerauth-mobile-watch-sdk/issues/7) +- [wultra/powerauth-mobile-extensions-sdk#7](https://github.com/wultra/powerauth-mobile-extensions-sdk/issues/7) \ No newline at end of file diff --git a/docs/PowerAuth-SDK-for-Android.md b/docs/PowerAuth-SDK-for-Android.md index 47f29bce..42d283ac 100644 --- a/docs/PowerAuth-SDK-for-Android.md +++ b/docs/PowerAuth-SDK-for-Android.md @@ -2091,29 +2091,29 @@ The following steps are typically required for a full E2EE request and response 1. Acquire the right encryptor from the `PowerAuthSDK` instance. For example: ```kotlin // Encryptor for "application" scope. - val encryptor = powerAuthSDK.eciesEncryptorForApplicationScope + val cancelable = powerAuthSDK.eciesEncryptorForApplicationScope(object : IGetEciesEncryptorListener { + override fun onGetEciesEncryptorSuccess(encryptor: EciesEncryptor) { + // Success + } + + override fun onGetEciesEncryptorFailed(t: Throwable) { + // Failure + } + }) // ...or similar, for an "activation" scope. - val encryptor = powerAuthSDK.getEciesEncryptorForActivationScope(context) + val cancelable = powerAuthSDK.getEciesEncryptorForActivationScope(context, object : IGetEciesEncryptorListener { + override fun onGetEciesEncryptorSuccess(encryptor: EciesEncryptor) { + // Success + } + + override fun onGetEciesEncryptorFailed(t: Throwable) { + // Failure + } + }) ``` 1. Serialize your request payload, if needed, into a sequence of bytes. This step typically means that you need to serialize your model object into a JSON-formatted sequence of bytes. -1. Make sure that the PowerAuth SDK instance has [time synchronized with the server](#synchronized-time): - ```kotlin - val timeService = powerAuthSDK.timeSynchronizationService - if (!timeService.isTimeSynchronized) { - timeService.synchronizeTime(object : ITimeSynchronizationListener { - override fun onTimeSynchronizationSucceeded() { - // Success - } - - override fun onTimeSynchronizationFailed(t: Throwable) { - // Failure - } - }) - } - ``` - 1. Encrypt your payload: ```kotlin val cryptogram = encryptor.encryptRequest(payloadData) @@ -2122,16 +2122,15 @@ The following steps are typically required for a full E2EE request and response } ``` -1. Construct a JSON from the provided cryptogram object. The dictionary with the following keys is expected: - - `ephemeralPublicKey` property fill with `cryptogram.getKeyBase64()` - - `encryptedData` property fill with `cryptogram.getBodyBase64()` - - `mac` property fill with `cryptogram.getMacBase64()` - - `nonce` property fill with `cryptogram.getNonceBase64()` - - `timestamp` property fill with `cryptogram.getTimestamp()` - +1. Construct a JSON from the provided cryptogram object: + ```kotlin + val requestObject = cryptogram.toEncryptedRequest() + val requestJson = Gson().toJson(requestObject) + ``` So, the final request JSON should look like this: ```json { + "temporaryKeyId" : "UUID", "ephemeralPublicKey" : "BASE64-DATA-BLOB", "encryptedData" : "BASE64-DATA-BLOB", "mac" : "BASE64-DATA-BLOB", @@ -2152,7 +2151,7 @@ The following steps are typically required for a full E2EE request and response 1. Fire your HTTP request and wait for a response - In case that non-200 HTTP status code is received, then the error processing is identical to a standard RESTful response defined in our protocol. So, you can expect a JSON object with `"error"` and `"message"` properties in the response. -1. Decrypt the response. The received JSON typically looks like this: +1. Decrypt the response. The received JSON response typically looks like this: ```json { "encryptedData" : "BASE64-DATA-BLOB", @@ -2161,10 +2160,13 @@ The following steps are typically required for a full E2EE request and response "timestamp" : 1694172789256 } ``` - - So, you need to create yet another "cryptogram" object, but with only two properties set: + So, you need to create yet another "cryptogram" object: ```kotlin - val responseCryptogram = EciesCryptogram(response.encryptedData, response.mac) + val responseObject = Gson().fromJson(responseData, EciesEncryptedResponse::class.java) + val responseCryptogram = EciesCryptogram.fromEncryptedResponse(responseObject) + if (responseCryptogram == null) { + // failure + } val responseData = encryptor.decryptResponse(responseCryptogram) if (responseData == null) { // failed to decrypt response data diff --git a/docs/PowerAuth-SDK-for-iOS.md b/docs/PowerAuth-SDK-for-iOS.md index 20fbad28..06df8556 100644 --- a/docs/PowerAuth-SDK-for-iOS.md +++ b/docs/PowerAuth-SDK-for-iOS.md @@ -1255,20 +1255,20 @@ The following steps are typically required for a full E2EE request and response import PowerAuthCore // Encryptor for "application" scope. - guard let encryptor = powerAuthSDK.eciesEncryptorForApplicationScope() else { ...failure... } + sdk.eciesEncryptorForApplicationScope { encryptor, error in + if let encryptor { + // success + } else { + // failure + } + } // ...or similar, for an "activation" scope. - guard let encryptor = powerAuthSDK.eciesEncryptorForActivationScope() else { ...failure... } - ``` - -1. Make sure that the PowerAuth SDK instance has [time synchronized with the server](#synchronized-time): - ```swift - let timeService = powerAuthSDK.timeSynchronizationService - if !timeService.isTimeSynchronized { - timeService.synchronizeTime(callback: { error in - if error != nil { - // failure - } - }, callbackQueue: .main) + sdk.eciesEncryptorForActivationScope { encryptor, error in + if let encryptor { + // success + } else { + // failure + } } ``` @@ -1279,18 +1279,16 @@ The following steps are typically required for a full E2EE request and response guard let cryptogram = encryptor.encryptRequest(payloadData) else { ...failure... } ``` -1. Construct a JSON from the provided cryptogram object. The dictionary with the following keys is expected: - - `ephemeralPublicKey` property fill with `cryptogram.keyBase64` - - `encryptedData` property fill with `cryptogram.bodyBase64` - - `mac` property fill with `cryptogram.macBase64` - - `nonce` property fill with `cryptogram.nonceBase64` - - `timestamp` property fill with `cryptogram.timestamp` - +1. Construct a JSON from the provided cryptogram object: + ```swift + guard let requestBody = try? JSONSerialization.data(withJSONObject: cryptogram.requestPayload()) else { ...failure... } + ``` So, the final request JSON should look like this: ```json { + "temporaryKeyId" : "UUID", "ephemeralPublicKey" : "BASE64-DATA-BLOB", - "encryptedData": "BASE64-DATA-BLOB", + "encryptedData" : "BASE64-DATA-BLOB", "mac" : "BASE64-DATA-BLOB", "nonce" : "BASE64-NONCE", "timestamp" : 1694172789256 @@ -1309,7 +1307,7 @@ The following steps are typically required for a full E2EE request and response 1. Fire your HTTP request and wait for a response - In case that non-200 HTTP status code is received, then the error processing is identical to a standard RESTful response defined in our protocol. So, you can expect a JSON object with `"error"` and `"message"` properties in the response. -1. Decrypt the response. The received JSON typically looks like this: +1. Decrypt the response. The received JSON response typically looks like this: ```json { "encryptedData": "BASE64-DATA-BLOB", @@ -1318,14 +1316,10 @@ The following steps are typically required for a full E2EE request and response "timestamp": 1694172789256 } ``` - So, you need to create yet another "cryptogram" object, but with only two properties set: + So, you need to create yet another "cryptogram" object: ```swift - let responseCryptogram = PowerAuthCoreEciesCryptogram() - responseCryptogram.bodyBase64 = response.getEncryptedData() - responseCryptogram.macBase64 = response.getMac() - responseCryptogram.nonceBase64 = response.getNonce() - responseCryptogram.timestamp = response.getTimestamp() - + guard let response = try? JSONSerialization.jsonObject(with: responseBody) else { ...failure... } + guard let responseCryptogram = PowerAuthCoreEciesCryptogram(responsePayload: response) else { ...not a dictionary... } guard let responseData = encryptor.decryptResponse(responseCryptogram) else { ... failed to decrypt data ... } ``` diff --git a/docs/Readme.md b/docs/Readme.md index e5ef7492..ace5d819 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -17,6 +17,7 @@ Related projects If you need to upgrade PowerAuth Mobile SDK to a newer version, you can check the following migration guides: +- [Migration from version `1.8.x` to `1.9.x`](Migration-from-1.8-to-1.9.md) - [Migration from version `1.7.x` to `1.8.x`](Migration-from-1.7-to-1.8.md) - [Migration from version `1.6.x` to `1.7.x`](Migration-from-1.6-to-1.7.md) - [Migration from version `1.5.x` to `1.6.x`](Migration-from-1.5-to-1.6.md) @@ -32,11 +33,12 @@ If you need to upgrade PowerAuth Mobile SDK to a newer version, you can check th | Mobile SDK | Protocol | PowerAuth Server | Support Status | |------------|----------|---------------------|-----------------------------------| -| `1.8.x` | `V3.2` | `1.5+` | Fully supported | -| `1.7.x` | `V3.1` | `0.24+` | Security & Functionality bugfixes | +| `1.9.x` | `V3.3` | `1.9+` | Fully supported | +| `1.8.x` | `V3.2` | `1.5+` | Security & Functionality bugfixes | +| `1.7.x` | `V3.1` | `0.24+` | Security bugfixes | | `1.6.x` | `V3.1` | `0.24+` | Security bugfixes | | `1.5.x` | `V3.1` | `0.24+` | Security bugfixes | -| `1.4.x` | `V3.1` | `0.24+` | Security bugfixes | +| `1.4.x` | `V3.1` | `0.24+` | Not supported | | `1.3.x` | `V3.1` | `0.23+` | Not supported | | `1.2.x` | `V3.0` | `0.22+` | Not supported | | `1.1.x` | `V3.0` | `0.21+` | Not supported | diff --git a/include/PowerAuth/ECIES.h b/include/PowerAuth/ECIES.h index 62e0563c..e9b29bfb 100644 --- a/include/PowerAuth/ECIES.h +++ b/include/PowerAuth/ECIES.h @@ -394,7 +394,7 @@ namespace powerAuth public: /// Build associated data from provided parameters. If activationId is not available (e.g. for ECIES in application scope) /// then you can provide an empty string. - static cc7::ByteArray buildAssociatedData(const std::string & applicationKey, const std::string & activationId); + static cc7::ByteArray buildAssociatedData(const std::string & applicationKey, const std::string & temporaryKeyId, const std::string & activationId); }; diff --git a/include/PowerAuth/PublicTypes.h b/include/PowerAuth/PublicTypes.h index 9c1862e0..529e7f02 100644 --- a/include/PowerAuth/PublicTypes.h +++ b/include/PowerAuth/PublicTypes.h @@ -156,7 +156,7 @@ namespace powerAuth }; /** - Returns textual representation for given protocol version. For example, for `Version_V3` returns "3.2". + Returns textual representation for given protocol version. For example, for `Version_V3` returns "3.3". You can use `Version_NA` to get the lastest supported version. */ extern std::string Version_GetMaxSupportedHttpProtocolVersion(Version protocol_version); @@ -375,7 +375,39 @@ namespace powerAuth /** `KEY_SERVER_PRIVATE` key was used for signature calculation */ - ECDSA_PersonalizedKey = 1 + ECDSA_PersonalizedKey = 1, + /** + `APP_SECRET` key is used for HMAC-SHA256 signature calculation. + */ + HMAC_Application = 2, + /** + `KEY_TRANSPORT` key is used for HMAC-SHA256 signature calculation. + */ + HMAC_Activation = 3 + }; + + enum SignatureFormat + { + /** + If default signature is used, then `ECDSA_DER` is used for ECDSA signature. + The raw bytes are always used for HMAC signatures. + */ + Default = 0, + /** + ECDSA signature in DER format is expected at input, or produced at output: + ``` + ASN.1 notation: + ECDSASignature ::= SEQUENCE { + r INTEGER, + s INTEGER + } + ``` + */ + ECDSA_DER = 1, + /** + ECDSA signature in JOSE format is epxpected at input, or produced at output. + */ + ECDSA_JOSE = 2, }; /** @@ -383,21 +415,40 @@ namespace powerAuth */ SigningKey signingKey; /** - An arbitrary data + Format of signature expected at input, or produced at output. + */ + SignatureFormat signatureFormat; + /** + An arbitrary data. */ cc7::ByteArray data; /** - A signagure calculated for data + A signagure calculated for data. */ cc7::ByteArray signature; /** - Default constructor + Default constructor. */ - SignedData(SigningKey signingKey = ECDSA_MasterServerKey) : - signingKey(signingKey) + SignedData(SigningKey signingKey = ECDSA_MasterServerKey, SignatureFormat signatureFormat = Default) : + signingKey(signingKey), + signatureFormat(signatureFormat) { } + + /** + Determine whether the signing key is set to one from ECDSA variants. + */ + bool isEcdsaSignature() const { + return signingKey == ECDSA_PersonalizedKey || signingKey == ECDSA_MasterServerKey; + } + + /** + Determine whether the signing key is set to one from HMAC viarants. + */ + bool isHmacSignature() const { + return signingKey == HMAC_Activation || signingKey == HMAC_Application; + } }; diff --git a/include/PowerAuth/Session.h b/include/PowerAuth/Session.h index ec26e661..05969c9e 100644 --- a/include/PowerAuth/Session.h +++ b/include/PowerAuth/Session.h @@ -38,6 +38,7 @@ namespace powerAuth { struct PersistentData; struct ActivationData; + struct SessionData; } /** @@ -86,9 +87,10 @@ namespace powerAuth /** Resets session into its initial state. The existing session's setup and the external encryption - key is preserved by this call. + key is preserved by this call. If |full_reset| parameter is true, then also resets data not relevant + to the activation state. For example, ECIES public key for application scope. */ - void resetSession(); + void resetSession(bool full_reset = true); // MARK: - State probing - @@ -305,6 +307,20 @@ namespace powerAuth EC_WrongParam if data structure doesn't contain signature */ ErrorCode verifyServerSignedData(const SignedData & data) const; + + /** + Calculates HMAC-SHA256 signature with using key specified in |data|. The output signature is + also stored to provided data object. If `HMAC_Activation` key is requested, then |keys| must + contain possession factor unlock key and the session must have valid activation. + + Returns EC_Ok, if operation succeeded and signature is computed. + EC_Encryption if cryptographic operation failed. + EC_WrongState if session contains invalid setup, or valid activation is required + for the requested key. + EC_WrongParam if keys structure doesn't contain possession factor unlock key + and the key is required. + */ + ErrorCode signDataWithHmacKey(SignedData & data, const SignatureUnlockKeys & keys) const; // MARK: - Signature keys management - @@ -413,7 +429,8 @@ namespace powerAuth EC_WrongParam, if some required parameter is missing */ ErrorCode signDataWithDevicePrivateKey(const std::string & c_vault_key, const SignatureUnlockKeys & keys, - const cc7::ByteRange & data, cc7::ByteArray & out_signature); + const cc7::ByteRange & data, SignedData::SignatureFormat out_format, + cc7::ByteArray & out_signature); private: @@ -493,7 +510,37 @@ namespace powerAuth */ ErrorCode getEciesEncryptor(ECIESEncryptorScope scope, const SignatureUnlockKeys & keys, const cc7::ByteRange & sharedInfo1, ECIESEncryptor & out_encryptor) const; - + /** + Sets a server's public key and its identifier for ECIES encryption. The scope of the encryption is + determined by |scope| parameter. + + Returns EC_Ok if operation succeeded. + EC_WrongState if activation scope is used and the session has no valid activation, or + if session object has no valid setup. + EC_WrongParam if public key is empty, or doesn't contain Base64 encoded data, or + if key identifier is empty. + */ + ErrorCode setPublicKeyForEciesScope(ECIESEncryptorScope scope, const std::string & public_key, const std::string & key_id); + + /** + Removes a server's public key and its identifier store for the given scope. It's safe to call this + function if key for given scope is not set. + */ + void removePublicKeyForEciesScope(ECIESEncryptorScope scope); + + /** + Determines whether session contains stored server's public key for ECIES scope. + */ + bool hasPublicKeyForEciesScope(ECIESEncryptorScope scope) const; + + /** + Returns identifier of server's public key for given scope. If no key is set, or if function is called in + wrong state, then returns empty string. + */ + std::string getPublicKeyIdForEciesScope(ECIESEncryptorScope scope) const; + + public: + // MARK: - Utilities for generic keys - /** @@ -664,6 +711,12 @@ namespace powerAuth */ protocol::ActivationData * _ad; + /** + Pointer to private session data structure. The pointer is valid during the whole + Session object lifetime. + */ + protocol::SessionData * _sd; + /** Commits a |new_pd| and |new_state| as a new valid session state. Check documentation in method's implementation for details. diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/PowerAuthServerApi.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/PowerAuthServerApi.java index 8ca342aa..5d1dfdbd 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/PowerAuthServerApi.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/PowerAuthServerApi.java @@ -301,10 +301,11 @@ public interface PowerAuthServerApi { * @param activationId Activation identifier. * @param data Signed data. * @param signature Signature for data. + * @param format Signature format. Use "DER" (default if not provided) or "JOSE". * @return {@code true} if signature is valid. * @throws Exception In case of failure. */ - boolean verifyEcdsaSignature(@NonNull String activationId, @NonNull String data, @NonNull String signature) throws Exception; + boolean verifyEcdsaSignature(@NonNull String activationId, @NonNull String data, @NonNull String signature, @Nullable String format) throws Exception; /** * Create a payload for offline QR code, signed with non-personalized private key. diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/client/PowerAuthClientFactory.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/client/PowerAuthClientFactory.java index 9a3cc19d..9b921b69 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/client/PowerAuthClientFactory.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/client/PowerAuthClientFactory.java @@ -24,6 +24,7 @@ import io.getlime.security.powerauth.integration.support.v10.PowerAuthClientV3_ServerV10; import io.getlime.security.powerauth.integration.support.v13.PowerAuthClientV3_ServerV13; import io.getlime.security.powerauth.integration.support.v15.PowerAuthClientV3_ServerV15; +import io.getlime.security.powerauth.integration.support.v19.PowerAuthClientV3_ServerV19; /** * The {@code PowerAuthClientFactory} provides client that communicate with PowerAuth Server API, @@ -46,8 +47,10 @@ public PowerAuthServerApi createApiClient(@NonNull PowerAuthTestConfig testConfi api = new PowerAuthClientV3_ServerV10(testConfig.getServerApiUrl(), testConfig.getAuthorizationHeaderValue(), ServerVersion.V1_0_0, ServerVersion.V1_2_5); } else if (numVer >= ServerVersion.V1_3_0.numericVersion && numVer < ServerVersion.V1_5_0.numericVersion) { api = new PowerAuthClientV3_ServerV13(testConfig.getServerApiUrl(), testConfig.getAuthorizationHeaderValue(), ServerVersion.V1_3_0, ServerVersion.V1_4_0); - } else if (numVer >= ServerVersion.V1_5_0.numericVersion && numVer <= ServerVersion.LATEST.numericVersion) { - api = new PowerAuthClientV3_ServerV15(testConfig.getServerApiUrl(), testConfig.getAuthorizationHeaderValue(), ServerVersion.V1_5_0, null); + } else if (numVer >= ServerVersion.V1_5_0.numericVersion && numVer <= ServerVersion.V1_8_0.numericVersion) { + api = new PowerAuthClientV3_ServerV15(testConfig.getServerApiUrl(), testConfig.getAuthorizationHeaderValue(), ServerVersion.V1_5_0, ServerVersion.V1_8_0); + } else if (numVer >= ServerVersion.V1_9_0.numericVersion && numVer <= ServerVersion.LATEST.numericVersion) { + api = new PowerAuthClientV3_ServerV19(testConfig.getServerApiUrl(), testConfig.getAuthorizationHeaderValue(), ServerVersion.V1_9_0, null); } if (api == null) { throw new Exception("Missing implementation for server API, for server version " + testConfig.getServerVersion().version); diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/model/ProtocolVersion.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/model/ProtocolVersion.java index 532278d5..340495df 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/model/ProtocolVersion.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/model/ProtocolVersion.java @@ -23,7 +23,8 @@ public enum ProtocolVersion { V2_1(21, "2.1"), V3(30, "3.0"), V3_1(31, "3.1"), - V3_2(32, "3.2"); + V3_2(32, "3.2"), + V3_3(33, "3.3"); public final int version; public final String versionForHeader; diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/model/ServerVersion.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/model/ServerVersion.java index 66f92899..7de1b393 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/model/ServerVersion.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/model/ServerVersion.java @@ -36,13 +36,14 @@ public enum ServerVersion { V1_6_0("1.6", 1006000, ProtocolVersion.V3_2), V1_7_0("1.7", 1007000, ProtocolVersion.V3_2), V1_8_0("1.8", 1008000, ProtocolVersion.V3_2), + V1_9_0("1.9", 1009000, ProtocolVersion.V3_3), ; /** * Contains constant for the latest PowerAuth Server version. */ - public static final ServerVersion LATEST = V1_8_0; + public static final ServerVersion LATEST = V1_9_0; /** * Server version represented as string. diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v10/PowerAuthClientV3_ServerV10.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v10/PowerAuthClientV3_ServerV10.java index 7a6c4db6..3d95e9fe 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v10/PowerAuthClientV3_ServerV10.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v10/PowerAuthClientV3_ServerV10.java @@ -319,7 +319,10 @@ public SignatureInfo verifyOfflineSignature(@NonNull SignatureData signatureData } @Override - public boolean verifyEcdsaSignature(@NonNull String activationId, @NonNull String data, @NonNull String signature) throws Exception { + public boolean verifyEcdsaSignature(@NonNull String activationId, @NonNull String data, @NonNull String signature, @Nullable String format) throws Exception { + if (format != null && !"DER".equals(format)) { + throw new IllegalArgumentException("Unsupported format: " + format); + } final VerifyEcdsaSignatureEndpoint.Request request = new VerifyEcdsaSignatureEndpoint.Request(); request.setActivationId(activationId); request.setData(data); diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v13/PowerAuthClientV3_ServerV13.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v13/PowerAuthClientV3_ServerV13.java index b950c62b..1ae00013 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v13/PowerAuthClientV3_ServerV13.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v13/PowerAuthClientV3_ServerV13.java @@ -352,7 +352,10 @@ public SignatureInfo verifyOfflineSignature(@NonNull SignatureData signatureData } @Override - public boolean verifyEcdsaSignature(@NonNull String activationId, @NonNull String data, @NonNull String signature) throws Exception { + public boolean verifyEcdsaSignature(@NonNull String activationId, @NonNull String data, @NonNull String signature, @Nullable String format) throws Exception { + if (format != null && !"DER".equals(format)) { + throw new IllegalArgumentException("Unsupported format: " + format); + } final VerifyEcdsaSignatureEndpoint.Request request = new VerifyEcdsaSignatureEndpoint.Request(); request.setActivationId(activationId); request.setData(data); diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v15/PowerAuthClientV3_ServerV15.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v15/PowerAuthClientV3_ServerV15.java index 6a48e856..bb04c2f6 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v15/PowerAuthClientV3_ServerV15.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v15/PowerAuthClientV3_ServerV15.java @@ -353,7 +353,10 @@ public SignatureInfo verifyOfflineSignature(@NonNull SignatureData signatureData } @Override - public boolean verifyEcdsaSignature(@NonNull String activationId, @NonNull String data, @NonNull String signature) throws Exception { + public boolean verifyEcdsaSignature(@NonNull String activationId, @NonNull String data, @NonNull String signature, @Nullable String format) throws Exception { + if (format != null && !"DER".equals(format)) { + throw new IllegalArgumentException("Unsupported format: " + format); + } final VerifyEcdsaSignatureEndpoint.Request request = new VerifyEcdsaSignatureEndpoint.Request(); request.setActivationId(activationId); request.setData(data); diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/PowerAuthClientV3_ServerV19.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/PowerAuthClientV3_ServerV19.java new file mode 100644 index 00000000..3e4f88ee --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/PowerAuthClientV3_ServerV19.java @@ -0,0 +1,383 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19; + +import java.util.Collections; +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.PowerAuthServerApi; +import io.getlime.security.powerauth.integration.support.client.HttpRestClient; +import io.getlime.security.powerauth.integration.support.model.Activation; +import io.getlime.security.powerauth.integration.support.model.ActivationDetail; +import io.getlime.security.powerauth.integration.support.model.ActivationOtpValidation; +import io.getlime.security.powerauth.integration.support.model.ActivationStatus; +import io.getlime.security.powerauth.integration.support.model.Application; +import io.getlime.security.powerauth.integration.support.model.ApplicationDetail; +import io.getlime.security.powerauth.integration.support.model.ApplicationVersion; +import io.getlime.security.powerauth.integration.support.model.OfflineSignaturePayload; +import io.getlime.security.powerauth.integration.support.model.RecoveryConfig; +import io.getlime.security.powerauth.integration.support.model.ServerConstants; +import io.getlime.security.powerauth.integration.support.model.ServerVersion; +import io.getlime.security.powerauth.integration.support.model.SignatureData; +import io.getlime.security.powerauth.integration.support.model.SignatureInfo; +import io.getlime.security.powerauth.integration.support.model.TokenInfo; +import io.getlime.security.powerauth.integration.support.v19.endpoints.BlockActivationEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.CommitActivationEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.CreateApplicationEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.CreateApplicationVersionEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.CreateNonPersonalizedOfflineSignaturePayloadEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.CreatePersonalizedOfflineSignaturePayloadEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.GetActivationStatusEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.GetApplicationDetailEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.GetApplicationListEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.GetRecoveryConfigEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.GetSystemStatusEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.InitActivationEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.RemoveActivationEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.SetApplicationVersionSupportedEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.UnblockActivationEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.UpdateActivationOtpEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.UpdateRecoveryConfigEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.ValidateTokenEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.VerifyEcdsaSignatureEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.VerifyOfflineSignatureEndpoint; +import io.getlime.security.powerauth.integration.support.v19.endpoints.VerifyOnlineSignatureEndpoint; + +public class PowerAuthClientV3_ServerV19 implements PowerAuthServerApi { + + private final @NonNull HttpRestClient restClient; + private final @NonNull ServerVersion minSupportedVersion; + private final @NonNull ServerVersion maxSupportedVersion; + private ServerVersion currentServerVersion; + + /** + * Create REST client that communicate with PowerAuth Server RESTful API. + * + * @param serverApiUrl URL to PowerAuth Server. + * @param authorization Optional authorization header value, if PowerAuth Server require authorization. + * @param minSupportedVersion Minimum supported server version. If {@code null} is provided, then {@link ServerVersion#LATEST} is used. + * @param maxSupportedVersion Maximum supported server version. If {@code null} is provided, then {@link ServerVersion#LATEST} is used. + */ + public PowerAuthClientV3_ServerV19(@NonNull String serverApiUrl, @Nullable String authorization, @Nullable ServerVersion minSupportedVersion, @Nullable ServerVersion maxSupportedVersion) throws Exception { + this.restClient = new HttpRestClient(serverApiUrl, authorization); + this.minSupportedVersion = minSupportedVersion == null ? ServerVersion.LATEST : minSupportedVersion; + this.maxSupportedVersion = maxSupportedVersion == null ? ServerVersion.LATEST : maxSupportedVersion; + if (this.minSupportedVersion.numericVersion > this.maxSupportedVersion.numericVersion) { + throw new Exception("Minimum supported server version is higher that maximum."); + } + } + + @Override + public void validateConnection() throws Exception { + final GetSystemStatusEndpoint.Response response = restClient.send(null, new GetSystemStatusEndpoint()); + String version = response.getVersion(); + if (version == null) { + throw new Exception("Missing version in system status response."); + } + currentServerVersion = ServerVersion.versionFromString(version, true); + if (currentServerVersion.numericVersion < minSupportedVersion.numericVersion || currentServerVersion.numericVersion > maxSupportedVersion.numericVersion) { + throw new Exception("Unsupported server version " + response.getVersion()); + } + } + + @NonNull + @Override + public ServerVersion getServerVersion() throws Exception { + if (currentServerVersion == null) { + validateConnection(); + if (currentServerVersion == null) { + throw new Exception("Cannot determine server version."); + } + } + return currentServerVersion; + } + + @Nullable + @Override + public Application findApplicationByName(@NonNull String applicationName) throws Exception { + final GetApplicationListEndpoint.Response response = restClient.send(null, new GetApplicationListEndpoint()); + if (response != null && response.getApplications() != null) { + for (Application app : response.getApplications()) { + // If V1.3 server has been migrated from older version, then contains previous application names in form of identifier. + // There's no such application name in the new model. + if (applicationName.equals(app.getApplicationId())) { + return app; + } + } + } + return null; + } + + @Nullable + @Override + public ApplicationVersion findApplicationVersionByName(@NonNull ApplicationDetail applicationDetail, @NonNull String applicationVersionName) throws Exception { + if (applicationDetail.getVersions() != null) { + for (ApplicationVersion version: applicationDetail.getVersions()) { + // If V1.3 server has been migrated from older version, then contains previous version names in form of identifier. + // There's no such application version name in the new model. + if (applicationVersionName.equals(version.getApplicationVersionId())) { + return version; + } + } + } + return null; + } + + @NonNull + @Override + public List getApplicationList() throws Exception { + final GetApplicationListEndpoint.Response response = restClient.send(null, new GetApplicationListEndpoint()); + return response.getApplications() != null ? response.getApplications() : Collections.emptyList(); + } + + @NonNull + @Override + public Application createApplication(@NonNull String applicationName) throws Exception { + final CreateApplicationEndpoint.Request request = new CreateApplicationEndpoint.Request(); + request.setApplicationId(applicationName); + return restClient.send(request, new CreateApplicationEndpoint()); + } + + @NonNull + @Override + public ApplicationDetail getApplicationDetailByName(@NonNull String applicationName) throws Exception { + final GetApplicationDetailEndpoint.Request request = new GetApplicationDetailEndpoint.Request(); + request.setApplicationName(applicationName); + return restClient.send(request, new GetApplicationDetailEndpoint()); + } + + @NonNull + @Override + public ApplicationDetail getApplicationDetailById(String applicationId) throws Exception { + final GetApplicationDetailEndpoint.Request request = new GetApplicationDetailEndpoint.Request(); + request.setApplicationId(applicationId); + return restClient.send(request, new GetApplicationDetailEndpoint()); + } + + @NonNull + @Override + public ApplicationVersion createApplicationVersion(String applicationId, @NonNull String versionName) throws Exception { + final CreateApplicationVersionEndpoint.Request request = new CreateApplicationVersionEndpoint.Request(); + request.setApplicationId(applicationId); + request.setApplicationVersionId(versionName); + return restClient.send(request, new CreateApplicationVersionEndpoint()); + } + + @Override + public void setApplicationVersionSupported(String applicationVersionId, boolean supported) throws Exception { + final SetApplicationVersionSupportedEndpoint.Request request = new SetApplicationVersionSupportedEndpoint.Request(); + request.setApplicationVersionId(applicationVersionId); + final SetApplicationVersionSupportedEndpoint.Response response = restClient.send(request, new SetApplicationVersionSupportedEndpoint(supported)); + if (response.isSupported() != supported) { + throw new Exception("Application version is still " + (supported ? "unsupported" : "supported") + " after successful response."); + } + } + + @NonNull + @Override + public RecoveryConfig getRecoveryConfig(String applicationId) throws Exception { + final GetRecoveryConfigEndpoint.Request request = new GetRecoveryConfigEndpoint.Request(); + request.setApplicationId(applicationId); + return restClient.send(request, new GetRecoveryConfigEndpoint()); + } + + @Override + public void updateRecoveryConfig(@NonNull RecoveryConfig recoveryConfig) throws Exception { + final UpdateRecoveryConfigEndpoint.Request request = new UpdateRecoveryConfigEndpoint.Request(recoveryConfig); + final UpdateRecoveryConfigEndpoint.Response response = restClient.send(request, new UpdateRecoveryConfigEndpoint()); + if (!response.isUpdated()) { + throw new Exception("Recovery config for application " + recoveryConfig.getApplicationId() + " is not updated after successful response."); + } + } + + @NonNull + @Override + public Activation activationInit(@NonNull Application application, @NonNull String userId, @Nullable String otp, @Nullable ActivationOtpValidation otpValidation, @Nullable Long maxFailureCount) throws Exception { + if ((otp != null && otpValidation == null) || (otp == null) && (otpValidation != null)) { + throw new Exception("Invalid combination of activation OTP and OTP validation."); + } + final InitActivationEndpoint.Request request = new InitActivationEndpoint.Request(); + request.setApplicationId(application.getApplicationId()); + request.setUserId(userId); + request.setActivationOtp(otp); + request.setActivationOtpValidation(otpValidation); + request.setMaxFailureCount(maxFailureCount != null ? maxFailureCount : ServerConstants.DEFAULT_MAX_FAILURE_ATTEMPTS); + return restClient.send(request, new InitActivationEndpoint()); + } + + @NonNull + @Override + public Activation activationInit(@NonNull Application application, @NonNull String userId) throws Exception { + return activationInit(application, userId, null, null, null); + } + + @Override + public void updateActivationOtp(@NonNull String activationId, @NonNull String otp, @Nullable String externalUserId) throws Exception { + final UpdateActivationOtpEndpoint.Request request = new UpdateActivationOtpEndpoint.Request(); + request.setActivationId(activationId); + request.setActivationOtp(otp); + request.setExternalUserId(externalUserId != null ? externalUserId : ServerConstants.DEFAULT_EXTERNAL_USER_ID); + final UpdateActivationOtpEndpoint.Response response = restClient.send(request, new UpdateActivationOtpEndpoint()); + if (!response.isUpdated()) { + throw new Exception("Ativation OTP for activation " + activationId + " is not updated after request success."); + } + } + + @Override + public void updateActivationOtp(@NonNull Activation activation, @NonNull String otp) throws Exception { + updateActivationOtp(activation.getActivationId(), otp, ServerConstants.DEFAULT_EXTERNAL_USER_ID); + } + + @Override + public void activationCommit(@NonNull String activationId, @Nullable String otp, @Nullable String externalUserId) throws Exception { + final CommitActivationEndpoint.Request request = new CommitActivationEndpoint.Request(); + request.setActivationId(activationId); + request.setActivationOtp(otp); + request.setExternalUserId(externalUserId != null ? externalUserId : ServerConstants.DEFAULT_EXTERNAL_USER_ID); + final CommitActivationEndpoint.Response response = restClient.send(request, new CommitActivationEndpoint()); + if (!response.isActivated()) { + throw new Exception("Activation " + activationId + " is not activated after commit after successful response."); + } + } + + @Override + public void activationCommit(@NonNull Activation activation) throws Exception { + activationCommit(activation.getActivationId(), null, ServerConstants.DEFAULT_EXTERNAL_USER_ID); + } + + @Override + public void activationRemove(@NonNull String activationId, @Nullable String externalUserId, boolean revokeRecoveryCodes) throws Exception { + final RemoveActivationEndpoint.Request request = new RemoveActivationEndpoint.Request(); + request.setActivationId(activationId); + request.setExternalUserId(externalUserId); + request.setRevokeRecoveryCodes(revokeRecoveryCodes); + final RemoveActivationEndpoint.Response response = restClient.send(request, new RemoveActivationEndpoint()); + if (!response.isRemoved()) { + throw new Exception("Activation " + activationId + " is not removed after request success."); + } + } + + @Override + public void activationRemove(@NonNull Activation activation) throws Exception { + activationRemove(activation.getActivationId(), ServerConstants.DEFAULT_EXTERNAL_USER_ID, true); + } + + @Override + public void activationBlock(@NonNull String activationId, @Nullable String reason, @Nullable String externalUserId) throws Exception { + final BlockActivationEndpoint.Request request = new BlockActivationEndpoint.Request(); + request.setActivationId(activationId); + request.setReason(reason != null ? reason : ServerConstants.BLOCKED_REASON_NOT_SPECIFIED); + request.setExternalUserId(externalUserId != null ? externalUserId : ServerConstants.DEFAULT_EXTERNAL_USER_ID); + final BlockActivationEndpoint.Response response = restClient.send(request, new BlockActivationEndpoint()); + if (response.getActivationStatus() != ActivationStatus.BLOCKED) { + throw new Exception("Activation " + activationId + " is not blocked after block request success."); + } + } + + @Override + public void activationBlock(@NonNull Activation activation) throws Exception { + activationBlock(activation.getActivationId(), ServerConstants.BLOCKED_REASON_NOT_SPECIFIED, ServerConstants.DEFAULT_EXTERNAL_USER_ID); + } + + @Override + public void activationUnblock(@NonNull String activationId, @Nullable String externalUserId) throws Exception { + final UnblockActivationEndpoint.Request request = new UnblockActivationEndpoint.Request(); + request.setActivationId(activationId); + request.setExternalUserId(externalUserId != null ? externalUserId : ServerConstants.DEFAULT_EXTERNAL_USER_ID); + final UnblockActivationEndpoint.Response response = restClient.send(request, new UnblockActivationEndpoint()); + if (response.getActivationStatus() != ActivationStatus.ACTIVE) { + throw new Exception("Activation " + activationId + " is not active after unblock request success."); + } + } + + @Override + public void activationUnblock(@NonNull Activation activation) throws Exception { + activationUnblock(activation.getActivationId(), ServerConstants.DEFAULT_EXTERNAL_USER_ID); + } + + @NonNull + @Override + public ActivationDetail getActivationDetail(@NonNull String activationId, @Nullable String challenge) throws Exception { + final GetActivationStatusEndpoint.Request request = new GetActivationStatusEndpoint.Request(); + request.setActivationId(activationId); + request.setChallenge(challenge); + return restClient.send(request, new GetActivationStatusEndpoint()); + } + + @NonNull + @Override + public ActivationDetail getActivationDetail(@NonNull Activation activation) throws Exception { + return getActivationDetail(activation.getActivationId(), null); + } + + @NonNull + @Override + public TokenInfo validateToken(@NonNull String tokenId, @NonNull String tokenDigest, @NonNull String nonce, long timestamp, @NonNull String protocolVersion) throws Exception { + final ValidateTokenEndpoint.Request request = new ValidateTokenEndpoint.Request(); + request.setTokenId(tokenId); + request.setTokenDigest(tokenDigest); + request.setNonce(nonce); + request.setTimestamp(timestamp); + request.setProtocolVersion(protocolVersion); + return restClient.send(request, new ValidateTokenEndpoint()); + } + + @NonNull + @Override + public SignatureInfo verifyOnlineSignature(@NonNull SignatureData signatureData) throws Exception { + final VerifyOnlineSignatureEndpoint.Request request = new VerifyOnlineSignatureEndpoint.Request(signatureData); + return restClient.send(request, new VerifyOnlineSignatureEndpoint()); + } + + @NonNull + @Override + public SignatureInfo verifyOfflineSignature(@NonNull SignatureData signatureData) throws Exception { + final VerifyOfflineSignatureEndpoint.Request request = new VerifyOfflineSignatureEndpoint.Request(signatureData); + return restClient.send(request, new VerifyOfflineSignatureEndpoint()); + } + + @Override + public boolean verifyEcdsaSignature(@NonNull String activationId, @NonNull String data, @NonNull String signature, @Nullable String format) throws Exception { + final VerifyEcdsaSignatureEndpoint.Request request = new VerifyEcdsaSignatureEndpoint.Request(); + request.setActivationId(activationId); + request.setData(data); + request.setSignature(signature); + request.setSignatureFormat(format); + final VerifyEcdsaSignatureEndpoint.Response response = restClient.send(request, new VerifyEcdsaSignatureEndpoint()); + return response.isSignatureValid(); + } + + @NonNull + @Override + public OfflineSignaturePayload createNonPersonalizedOfflineSignaturePayload(String applicationId, @NonNull String data) throws Exception { + final CreateNonPersonalizedOfflineSignaturePayloadEndpoint.Request request = new CreateNonPersonalizedOfflineSignaturePayloadEndpoint.Request(); + request.setApplicationId(applicationId); + request.setData(data); + return restClient.send(request, new CreateNonPersonalizedOfflineSignaturePayloadEndpoint()); + } + + @NonNull + @Override + public OfflineSignaturePayload createPersonalizedOfflineSignaturePayload(@NonNull String activationId, @NonNull String data) throws Exception { + final CreatePersonalizedOfflineSignaturePayloadEndpoint.Request request = new CreatePersonalizedOfflineSignaturePayloadEndpoint.Request(); + request.setActivationId(activationId); + request.setData(data); + return restClient.send(request, new CreatePersonalizedOfflineSignaturePayloadEndpoint()); + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/BlockActivationEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/BlockActivationEndpoint.java new file mode 100644 index 00000000..a4234660 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/BlockActivationEndpoint.java @@ -0,0 +1,96 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.ActivationStatus; + +public class BlockActivationEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/activation/block"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + + private String activationId; + private String externalUserId; + private String reason; + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public String getExternalUserId() { + return externalUserId; + } + + public void setExternalUserId(String externalUserId) { + this.externalUserId = externalUserId; + } + + public String getReason() { + return reason; + } + + public void setReason(String reason) { + this.reason = reason; + } + } + + // Response + + public static class Response { + + private String activationId; + private ActivationStatus activationStatus; + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public ActivationStatus getActivationStatus() { + return activationStatus; + } + + public void setActivationStatus(ActivationStatus activationStatus) { + this.activationStatus = activationStatus; + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CommitActivationEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CommitActivationEndpoint.java new file mode 100644 index 00000000..b9c4804b --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CommitActivationEndpoint.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; + +public class CommitActivationEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/activation/commit"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + + private String activationId; + private String activationOtp; + private String externalUserId; + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public String getActivationOtp() { + return activationOtp; + } + + public void setActivationOtp(String activationOtp) { + this.activationOtp = activationOtp; + } + + public String getExternalUserId() { + return externalUserId; + } + + public void setExternalUserId(String externalUserId) { + this.externalUserId = externalUserId; + } + } + + // Response + + public static class Response { + + private boolean activated; + private String activationId; + + public boolean isActivated() { + return activated; + } + + public void setActivated(boolean activated) { + this.activated = activated; + } + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CreateApplicationEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CreateApplicationEndpoint.java new file mode 100644 index 00000000..d18bbbd6 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CreateApplicationEndpoint.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.Application; + +public class CreateApplicationEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/application/create"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + + private String applicationId; + + public String getApplicationId() { + return applicationId; + } + + public void setApplicationId(String applicationName) { + this.applicationId = applicationName; + } + } + + // Response + + public static class Response extends Application { + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CreateApplicationVersionEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CreateApplicationVersionEndpoint.java new file mode 100644 index 00000000..488af324 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CreateApplicationVersionEndpoint.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.ApplicationVersion; + +public class CreateApplicationVersionEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/application/version/create"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + + private String applicationId; + private String applicationVersionId; + + public String getApplicationId() { + return applicationId; + } + + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + public String getApplicationVersionId() { + return applicationVersionId; + } + + public void setApplicationVersionId(String applicationVersionName) { + this.applicationVersionId = applicationVersionName; + } + } + + // Response + + public static class Response extends ApplicationVersion { + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CreateNonPersonalizedOfflineSignaturePayloadEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CreateNonPersonalizedOfflineSignaturePayloadEndpoint.java new file mode 100644 index 00000000..cf8c82f2 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CreateNonPersonalizedOfflineSignaturePayloadEndpoint.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.OfflineSignaturePayload; + +public class CreateNonPersonalizedOfflineSignaturePayloadEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/signature/offline/non-personalized/create"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + + private String applicationId; + private String data; + + public String getApplicationId() { + return applicationId; + } + + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } + + // Response + + public static class Response extends OfflineSignaturePayload { + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CreatePersonalizedOfflineSignaturePayloadEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CreatePersonalizedOfflineSignaturePayloadEndpoint.java new file mode 100644 index 00000000..187d5487 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/CreatePersonalizedOfflineSignaturePayloadEndpoint.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.OfflineSignaturePayload; + +public class CreatePersonalizedOfflineSignaturePayloadEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/signature/offline/personalized/create"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + private String activationId; + private String data; + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } + + // Response + + public static class Response extends OfflineSignaturePayload { + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/EmptyRequestObject.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/EmptyRequestObject.java new file mode 100644 index 00000000..7627a791 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/EmptyRequestObject.java @@ -0,0 +1,21 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +public class EmptyRequestObject { + // Empty request object +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetActivationStatusEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetActivationStatusEndpoint.java new file mode 100644 index 00000000..68113b04 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetActivationStatusEndpoint.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.ActivationDetail; + +public class GetActivationStatusEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/activation/status"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + + private String activationId; + private String challenge; + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public String getChallenge() { + return challenge; + } + + public void setChallenge(String challenge) { + this.challenge = challenge; + } + } + + // Response + + public static class Response extends ActivationDetail { + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetApplicationDetailEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetApplicationDetailEndpoint.java new file mode 100644 index 00000000..6265801c --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetApplicationDetailEndpoint.java @@ -0,0 +1,67 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.ApplicationDetail; + +public class GetApplicationDetailEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/application/detail"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + private String applicationId; + private String applicationName; + + public String getApplicationId() { + return applicationId; + } + + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + public String getApplicationName() { + return applicationName; + } + + public void setApplicationName(String applicationName) { + this.applicationName = applicationName; + } + } + + // Response + + public static class Response extends ApplicationDetail { + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetApplicationListEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetApplicationListEndpoint.java new file mode 100644 index 00000000..95554b70 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetApplicationListEndpoint.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.Application; + +public class GetApplicationListEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/application/list"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Empty request + + // Response + + public static class Response { + private List applications; + + public List getApplications() { + return applications; + } + + public void setApplications(List applications) { + this.applications = applications; + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetRecoveryConfigEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetRecoveryConfigEndpoint.java new file mode 100644 index 00000000..9055c3bb --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetRecoveryConfigEndpoint.java @@ -0,0 +1,59 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.RecoveryConfig; + +public class GetRecoveryConfigEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/recovery/config/detail"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + + private String applicationId; + + public String getApplicationId() { + return applicationId; + } + + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + } + + // Response + + public static class Response extends RecoveryConfig { + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetSystemStatusEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetSystemStatusEndpoint.java new file mode 100644 index 00000000..8be1ab95 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/GetSystemStatusEndpoint.java @@ -0,0 +1,100 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; + +public class GetSystemStatusEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/status"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Empty request + + // Response + + public static class Response { + + private String status; + private String applicationName; + private String applicationDisplayName; + private String applicationEnvironment; + private String timestamp; + private String version; + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getApplicationName() { + return applicationName; + } + + public void setApplicationName(String applicationName) { + this.applicationName = applicationName; + } + + public String getApplicationDisplayName() { + return applicationDisplayName; + } + + public void setApplicationDisplayName(String applicationDisplayName) { + this.applicationDisplayName = applicationDisplayName; + } + + public String getApplicationEnvironment() { + return applicationEnvironment; + } + + public void setApplicationEnvironment(String applicationEnvironment) { + this.applicationEnvironment = applicationEnvironment; + } + + public String getTimestamp() { + return timestamp; + } + + public void setTimestamp(String timestamp) { + this.timestamp = timestamp; + } + + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/InitActivationEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/InitActivationEndpoint.java new file mode 100644 index 00000000..f9921a89 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/InitActivationEndpoint.java @@ -0,0 +1,96 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.Activation; +import io.getlime.security.powerauth.integration.support.model.ActivationOtpValidation; + +public class InitActivationEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/activation/init"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + + private String activationOtp; + private ActivationOtpValidation activationOtpValidation; + private String applicationId; + private long maxFailureCount; + private String userId; + + public String getActivationOtp() { + return activationOtp; + } + + public void setActivationOtp(String activationOtp) { + this.activationOtp = activationOtp; + } + + public ActivationOtpValidation getActivationOtpValidation() { + return activationOtpValidation; + } + + public void setActivationOtpValidation(ActivationOtpValidation activationOtpValidation) { + this.activationOtpValidation = activationOtpValidation; + } + + public String getApplicationId() { + return applicationId; + } + + public void setApplicationId(String applicationId) { + this.applicationId = applicationId; + } + + public long getMaxFailureCount() { + return maxFailureCount; + } + + public void setMaxFailureCount(long maxFailureCount) { + this.maxFailureCount = maxFailureCount; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + } + + // Response + + public static class Response extends Activation { + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/RemoveActivationEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/RemoveActivationEndpoint.java new file mode 100644 index 00000000..d9642a07 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/RemoveActivationEndpoint.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; + +public class RemoveActivationEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/activation/remove"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + + private String activationId; + private String externalUserId; + private boolean revokeRecoveryCodes; + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public String getExternalUserId() { + return externalUserId; + } + + public void setExternalUserId(String externalUserId) { + this.externalUserId = externalUserId; + } + + public boolean isRevokeRecoveryCodes() { + return revokeRecoveryCodes; + } + + public void setRevokeRecoveryCodes(boolean revokeRecoveryCodes) { + this.revokeRecoveryCodes = revokeRecoveryCodes; + } + } + + // Response + + public static class Response { + + private String activationId; + private boolean removed; + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public boolean isRemoved() { + return removed; + } + + public void setRemoved(boolean removed) { + this.removed = removed; + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/SetApplicationVersionSupportedEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/SetApplicationVersionSupportedEndpoint.java new file mode 100644 index 00000000..7095a148 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/SetApplicationVersionSupportedEndpoint.java @@ -0,0 +1,83 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; + +public class SetApplicationVersionSupportedEndpoint implements IServerApiEndpoint { + + private final boolean supported; + + public SetApplicationVersionSupportedEndpoint(boolean supported) { + this.supported = supported; + } + + @NonNull + @Override + public String getRelativePath() { + return supported ? "/rest/v3/application/version/support" : "/rest/v3/application/version/unsupport"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + + private String applicationVersionId; + + public String getApplicationVersionId() { + return applicationVersionId; + } + + public void setApplicationVersionId(String applicationVersionId) { + this.applicationVersionId = applicationVersionId; + } + } + + // Response + + public static class Response { + + private String applicationVersionId; + private boolean supported; + + public String getApplicationVersionId() { + return applicationVersionId; + } + + public void setApplicationVersionId(String applicationVersionId) { + this.applicationVersionId = applicationVersionId; + } + + public boolean isSupported() { + return supported; + } + + public void setSupported(boolean supported) { + this.supported = supported; + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/UnblockActivationEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/UnblockActivationEndpoint.java new file mode 100644 index 00000000..b20d9dd8 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/UnblockActivationEndpoint.java @@ -0,0 +1,87 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.ActivationStatus; + +public class UnblockActivationEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/activation/unblock"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(UnblockActivationEndpoint.Response.class); + } + + // Request + + public static class Request { + + private String activationId; + private String externalUserId; + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public String getExternalUserId() { + return externalUserId; + } + + public void setExternalUserId(String externalUserId) { + this.externalUserId = externalUserId; + } + } + + // Response + + public static class Response { + + private String activationId; + private ActivationStatus activationStatus; + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public ActivationStatus getActivationStatus() { + return activationStatus; + } + + public void setActivationStatus(ActivationStatus activationStatus) { + this.activationStatus = activationStatus; + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/UpdateActivationOtpEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/UpdateActivationOtpEndpoint.java new file mode 100644 index 00000000..5a9aef35 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/UpdateActivationOtpEndpoint.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; + +public class UpdateActivationOtpEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/activation/otp/update"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + + private String activationId; + private String activationOtp; + private String externalUserId; + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public String getActivationOtp() { + return activationOtp; + } + + public void setActivationOtp(String activationOtp) { + this.activationOtp = activationOtp; + } + + public String getExternalUserId() { + return externalUserId; + } + + public void setExternalUserId(String externalUserId) { + this.externalUserId = externalUserId; + } + } + + // Response + + public static class Response { + + private String activationId; + private boolean updated; + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public boolean isUpdated() { + return updated; + } + + public void setUpdated(boolean updated) { + this.updated = updated; + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/UpdateRecoveryConfigEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/UpdateRecoveryConfigEndpoint.java new file mode 100644 index 00000000..4a33cd8e --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/UpdateRecoveryConfigEndpoint.java @@ -0,0 +1,71 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.RecoveryConfig; + +public class UpdateRecoveryConfigEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/recovery/config/update"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request extends RecoveryConfig { + + /** + * Create request from given recovery config. + * @param config Recovery config. + */ + public Request(@NonNull RecoveryConfig config) { + setApplicationId(config.getApplicationId()); + setActivationRecoveryEnabled(config.isActivationRecoveryEnabled()); + setRecoveryPostcardEnabled(config.isRecoveryPostcardEnabled()); + setAllowMultipleRecoveryCodes(config.getAllowMultipleRecoveryCodes()); + setRemotePostcardPublicKey(config.getRemotePostcardPublicKey()); + } + } + + // Response + + public static class Response { + + private boolean updated; + + public boolean isUpdated() { + return updated; + } + + public void setUpdated(boolean updated) { + this.updated = updated; + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/ValidateTokenEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/ValidateTokenEndpoint.java new file mode 100644 index 00000000..ce1e48e6 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/ValidateTokenEndpoint.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.TokenInfo; + +public class ValidateTokenEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/token/validate"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + // Request + + public static class Request { + + private String tokenId; + private String tokenDigest; + private String nonce; + private long timestamp; + private String protocolVersion; + + public String getTokenId() { + return tokenId; + } + + public void setTokenId(String tokenId) { + this.tokenId = tokenId; + } + + public String getTokenDigest() { + return tokenDigest; + } + + public void setTokenDigest(String tokenDigest) { + this.tokenDigest = tokenDigest; + } + + public String getNonce() { + return nonce; + } + + public void setNonce(String nonce) { + this.nonce = nonce; + } + + public long getTimestamp() { + return timestamp; + } + + public void setTimestamp(long timestamp) { + this.timestamp = timestamp; + } + + public String getProtocolVersion() { + return protocolVersion; + } + + public void setProtocolVersion(String protocolVersion) { + this.protocolVersion = protocolVersion; + } + } + + // Response + + public static class Response extends TokenInfo { + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/VerifyEcdsaSignatureEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/VerifyEcdsaSignatureEndpoint.java new file mode 100644 index 00000000..b64d72c0 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/VerifyEcdsaSignatureEndpoint.java @@ -0,0 +1,91 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; + +public class VerifyEcdsaSignatureEndpoint implements IServerApiEndpoint { + + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/signature/ecdsa/verify"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + public static class Request { + + private String activationId; + private String data; + private String signature; + private String signatureFormat; + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public String getSignature() { + return signature; + } + + public void setSignature(String signature) { + this.signature = signature; + } + + public String getSignatureFormat() { + return signatureFormat; + } + + public void setSignatureFormat(String signatureFormat) { + this.signatureFormat = signatureFormat; + } + } + + public static class Response { + + private boolean signatureValid; + + public boolean isSignatureValid() { + return signatureValid; + } + + public void setSignatureValid(boolean signatureValid) { + this.signatureValid = signatureValid; + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/VerifyOfflineSignatureEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/VerifyOfflineSignatureEndpoint.java new file mode 100644 index 00000000..eb4ac6c5 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/VerifyOfflineSignatureEndpoint.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.SignatureData; +import io.getlime.security.powerauth.integration.support.model.SignatureInfo; + +public class VerifyOfflineSignatureEndpoint implements IServerApiEndpoint { + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/signature/offline/verify"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + public static class Request { + + private String activationId; + private String data; + private String signature; + private boolean allowBiometry; + + public Request(@NonNull SignatureData sd) { + activationId = sd.getActivationId(); + data = sd.getData(); + signature = sd.getSignature(); + allowBiometry = sd.getAllowBiometry() != null ? sd.getAllowBiometry() : false; + } + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public String getSignature() { + return signature; + } + + public void setSignature(String signature) { + this.signature = signature; + } + + public boolean isAllowBiometry() { + return allowBiometry; + } + + public void setAllowBiometry(boolean allowBiometry) { + this.allowBiometry = allowBiometry; + } + } + + public static class Response extends SignatureInfo { + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/VerifyOnlineSignatureEndpoint.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/VerifyOnlineSignatureEndpoint.java new file mode 100644 index 00000000..fc5f704c --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/support/v19/endpoints/VerifyOnlineSignatureEndpoint.java @@ -0,0 +1,120 @@ +/* + * Copyright 2020 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.integration.support.v19.endpoints; + +import com.google.gson.reflect.TypeToken; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.integration.support.client.IServerApiEndpoint; +import io.getlime.security.powerauth.integration.support.model.SignatureData; +import io.getlime.security.powerauth.integration.support.model.SignatureInfo; +import io.getlime.security.powerauth.integration.support.model.SignatureType; + +public class VerifyOnlineSignatureEndpoint implements IServerApiEndpoint { + @NonNull + @Override + public String getRelativePath() { + return "/rest/v3/signature/verify"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(Response.class); + } + + public static class Request { + + private String activationId; + private String applicationKey; + private String data; + private String signature; + private SignatureType signatureType; + private String signatureVersion; + private Long forcedSignatureVersion; + + public Request(@NonNull SignatureData sd) { + activationId = sd.getActivationId(); + applicationKey = sd.getApplicationKey(); + data = sd.getData(); + signature = sd.getSignature(); + signatureType = sd.getSignatureType(); + signatureVersion = sd.getSignatureVersion(); + forcedSignatureVersion = sd.getForcedSignatureVersion(); + } + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public String getApplicationKey() { + return applicationKey; + } + + public void setApplicationKey(String applicationKey) { + this.applicationKey = applicationKey; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public String getSignature() { + return signature; + } + + public void setSignature(String signature) { + this.signature = signature; + } + + public SignatureType getSignatureType() { + return signatureType; + } + + public void setSignatureType(SignatureType signatureType) { + this.signatureType = signatureType; + } + + public String getSignatureVersion() { + return signatureVersion; + } + + public void setSignatureVersion(String signatureVersion) { + this.signatureVersion = signatureVersion; + } + + public Long getForcedSignatureVersion() { + return forcedSignatureVersion; + } + + public void setForcedSignatureVersion(Long forcedSignatureVersion) { + this.forcedSignatureVersion = forcedSignatureVersion; + } + } + + public static class Response extends SignatureInfo { + } +} diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/EcdsaSignatureTest.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/EcdsaSignatureTest.java index 67757de6..19c7de4c 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/EcdsaSignatureTest.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/EcdsaSignatureTest.java @@ -91,7 +91,7 @@ public void onDataSignedFailed(@NonNull Throwable t) { final String signatureForVerification = Base64.encodeToString(signatureForData, Base64.NO_WRAP); // Now validate that signature on the server. - boolean result = testHelper.getServerApi().verifyEcdsaSignature(activationHelper.getActivation().getActivationId(), dataForVerification, signatureForVerification); + boolean result = testHelper.getServerApi().verifyEcdsaSignature(activationHelper.getActivation().getActivationId(), dataForVerification, signatureForVerification, null); assertTrue(result); } diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/StandardActivationTest.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/StandardActivationTest.java index 92aa0bfa..f15eb814 100644 --- a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/StandardActivationTest.java +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/integration/tests/StandardActivationTest.java @@ -29,6 +29,7 @@ import org.junit.Test; import org.junit.runner.RunWith; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -483,17 +484,26 @@ public void onJwtSignatureFailed(@NonNull Throwable t) { final String jwtClaims = jwtComponents[1]; final String jwtSignature = jwtComponents[2]; // Validate header - Map headerObject = jsonSerialization.deserializeObject(Base64.decode(jwtHeader, Base64.NO_WRAP), new TypeToken>() {}); + Map headerObject = jsonSerialization.deserializeObject(Base64.decode(jwtHeader, Base64.NO_WRAP | Base64.URL_SAFE | Base64.NO_PADDING), new TypeToken>() {}); assertEquals("JWT", headerObject.get("typ")); assertEquals("ES256", headerObject.get("alg")); // Validate claims - Map claimsObject = jsonSerialization.deserializeObject(Base64.decode(jwtClaims, Base64.NO_WRAP), new TypeToken>() {}); + Map claimsObject = jsonSerialization.deserializeObject(Base64.decode(jwtClaims, Base64.NO_WRAP | Base64.URL_SAFE | Base64.NO_PADDING), new TypeToken>() {}); assertEquals(originalClaims.keySet().size(), claimsObject.keySet().size()); claimsObject.forEach((key, value) -> { assertEquals(originalClaims.get(key), value); }); + // Prepare signed data + final String jwtSignedDatasBase64 = Base64.encodeToString((jwtHeader + "." + jwtClaims).getBytes(StandardCharsets.US_ASCII), Base64.NO_WRAP); + // Decode signature and encode back to Base64 + final String jwtSignatureBase64 = Base64.encodeToString( + Base64.decode(jwtSignature, Base64.NO_WRAP | Base64.URL_SAFE | Base64.NO_PADDING), + Base64.NO_WRAP + ); + // Validate signature - boolean result = testHelper.getServerApi().verifyEcdsaSignature(activationHelper.getActivation().getActivationId(), jwtClaims, jwtSignature); + // Note that signature format is supported from PAS 1.9+ + boolean result = testHelper.getServerApi().verifyEcdsaSignature(activationHelper.getActivation().getActivationId(), jwtSignedDatasBase64, jwtSignatureBase64, "JOSE"); assertTrue(result); } } diff --git a/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/networking/client/JsonSerializationTest.java b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/networking/client/JsonSerializationTest.java new file mode 100644 index 00000000..fc88e973 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/androidTest/java/io/getlime/security/powerauth/networking/client/JsonSerializationTest.java @@ -0,0 +1,49 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.networking.client; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.google.gson.reflect.TypeToken; +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +@RunWith(AndroidJUnit4.class) +public class JsonSerializationTest { + + public static class TestModel { + public String text; + TestModel(String text) { + this.text = text; + } + } + + @Test + public void testJwtSerialize() throws Exception { + final JsonSerialization serialization = new JsonSerialization(); + TestModel data = serialization.deserializeJwtObject("eyJ0ZXh0Ijoixb7DtMW-w6QifQ", new TypeToken<>(){}); + assertEquals("žôžä", data.text); + String serializedData = serialization.serializeJwtObject(data); + assertEquals("eyJ0ZXh0Ijoixb7DtMW-w6QifQ", serializedData); + data = serialization.deserializeJwtObject("eyJ0ZXh0Ijoi8J-SqT8_In0", new TypeToken<>(){}); + assertEquals("\uD83D\uDCA9??", data.text); + serializedData = serialization.serializeJwtObject(data); + assertEquals("eyJ0ZXh0Ijoi8J-SqT8_In0", serializedData); + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/EciesCryptogram.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/EciesCryptogram.java index 5f3507ca..b229c9e2 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/EciesCryptogram.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/EciesCryptogram.java @@ -17,12 +17,19 @@ package io.getlime.security.powerauth.core; import android.util.Base64; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.networking.model.request.EciesEncryptedRequest; +import io.getlime.security.powerauth.networking.model.response.EciesEncryptedResponse; +import io.getlime.security.powerauth.system.PowerAuthLog; /** * The EciesCryptogram object represents cryptogram transmitted over the network. */ public class EciesCryptogram { - + /** + * An identifier of temporary key. + */ + public final String temporaryKeyId; /** * An encrypted data */ @@ -93,6 +100,7 @@ public String getNonceBase64() { * object initialization. */ public EciesCryptogram() { + this.temporaryKeyId = null; this.body = null; this.mac = null; this.key = null; @@ -103,13 +111,15 @@ public EciesCryptogram() { /** * Constructs a cryptogram with body, mac and key. The key can and nonce be null for responses received * from the server. + * @param temporaryKeyId Identifier of temporary encryption key. * @param body encrypted data * @param mac MAC computed for encrypted data * @param key An optional ephemeral key * @param nonce An optional nonce. * @param timestamp Timestamp with milliseconds precision. */ - public EciesCryptogram(byte[] body, byte[] mac, byte[] key, byte[] nonce, long timestamp) { + public EciesCryptogram(String temporaryKeyId, byte[] body, byte[] mac, byte[] key, byte[] nonce, long timestamp) { + this.temporaryKeyId = temporaryKeyId; this.body = body; this.mac = mac; this.key = key; @@ -126,11 +136,50 @@ public EciesCryptogram(byte[] body, byte[] mac, byte[] key, byte[] nonce, long t * @param nonceBase64 An optional nonce in Base64 format. * @param timestamp Timestamp with milliseconds precision. */ - public EciesCryptogram(String bodyBase64, String macBase64, String keyBase64, String nonceBase64, long timestamp) { + public EciesCryptogram(String temporaryKeyId, String bodyBase64, String macBase64, String keyBase64, String nonceBase64, long timestamp) { + this.temporaryKeyId = temporaryKeyId; this.body = (bodyBase64 != null) ? Base64.decode(bodyBase64, Base64.NO_WRAP) : null; this.mac = (macBase64 != null) ? Base64.decode(macBase64, Base64.NO_WRAP) : null; this.key = (keyBase64 != null) ? Base64.decode(keyBase64, Base64.NO_WRAP) : null; this.nonce = (nonceBase64 != null) ? Base64.decode(nonceBase64, Base64.NO_WRAP) : null; this.timestamp = timestamp; } + + /** + * Convert cryptogram into encrypted request object. + * @return New instance of {@link EciesEncryptedRequest} object with all parameters set from the cryptogram. + */ + public EciesEncryptedRequest toEncryptedRequest() { + final EciesEncryptedRequest request = new EciesEncryptedRequest(); + request.setTemporaryKeyId(temporaryKeyId); + request.setEncryptedData(getBodyBase64()); + request.setMac(getMacBase64()); + request.setNonce(getNonceBase64()); + request.setEphemeralPublicKey(getKeyBase64()); + request.setTimestamp(timestamp); + return request; + } + + /** + * Construct cryptogram from {@link EciesEncryptedResponse} received from the server. + * @param response Encrypted response object received from the server. + * @return Cryptogram with response received from the server, or {@code null} in case some required parameter is missing. + */ + @Nullable + public static EciesCryptogram fromEncryptedResponse(EciesEncryptedResponse response) { + if (response != null) { + try { + return new EciesCryptogram( + null, + response.getEncryptedData(), + response.getMac(), + null, + response.getNonce(), + response.getTimestamp()); + } catch (IllegalArgumentException e) { + PowerAuthLog.e("Failed to parse encrypted response: " + e.getMessage()); + } + } + return null; + } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/Session.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/Session.java index c7424758..516d2d6d 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/Session.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/Session.java @@ -103,8 +103,11 @@ public SessionSetup getSessionSetup() { /** * Resets session into its initial state. The existing session's setup and EEK is preserved * after the call. + * + * @param fullReset If {@code true}, then also resets data not relevant to the activation state. For example, ECIES public + * key for application scope. */ - public native void resetSession(); + public native void resetSession(boolean fullReset); /** * Returns true if dynamic library was compiled with a debug features. It is highly recommended @@ -353,6 +356,17 @@ public byte[] prepareKeyValueDictionaryForDataSigning(Map keyVal @ErrorCode public native int verifyServerSignedData(SignedData signedData); + /** + * Calculates HMAC-SHA256 signature with using key specified in {@link SignedData} object. The output signature is + * also stored to provided data object. If {@link SigningDataKey#HMAC_ACTIVATION} key is requested, then unlock keys + * must object must contain possession factor unlock key and the session must have valid activation. + * @param dataToSign Object containing data to sign and the key to use for the signature calculation. + * @param unlockKeys Required for {@link SigningDataKey#HMAC_ACTIVATION} key. + * @return integer comparable to constants available at {@link ErrorCode} class. + */ + @ErrorCode + public native int signDataWithHmacKey(SignedData dataToSign, SignatureUnlockKeys unlockKeys); + // // Signature keys management // @@ -463,10 +477,11 @@ public byte[] prepareKeyValueDictionaryForDataSigning(Map keyVal * @param cVaultKey encrypted vault key * @param unlockKeys unlock keys object with required possession factor * @param data data to be signed + * @param signatureFormat Format of produced signature. * * @return array of bytes with calculated signature or null in case of failure. */ - public native byte[] signDataWithDevicePrivateKey(String cVaultKey, SignatureUnlockKeys unlockKeys, byte[] data); + public native byte[] signDataWithDevicePrivateKey(String cVaultKey, SignatureUnlockKeys unlockKeys, byte[] data, @SignatureFormat int signatureFormat); // // External encryption key @@ -535,7 +550,7 @@ public byte[] prepareKeyValueDictionaryForDataSigning(Map keyVal * * @return {@link EciesEncryptor} object or nil in case of error */ - public EciesEncryptor getEciesEncryptor(int scope, SignatureUnlockKeys unlockKeys, byte[] sharedInfo1) { + public EciesEncryptor getEciesEncryptor(@EciesEncryptorScope int scope, SignatureUnlockKeys unlockKeys, byte[] sharedInfo1) { return getEciesEncryptorImpl(scope, unlockKeys, sharedInfo1, timeService); } @@ -551,7 +566,44 @@ public EciesEncryptor getEciesEncryptor(int scope, SignatureUnlockKeys unlockKey * * @return {@link EciesEncryptor} object or nil in case of error */ - private native EciesEncryptor getEciesEncryptorImpl(int scope, SignatureUnlockKeys unlockKeys, byte[] sharedInfo1, ICoreTimeService timeService); + private native EciesEncryptor getEciesEncryptorImpl(@EciesEncryptorScope int scope, SignatureUnlockKeys unlockKeys, byte[] sharedInfo1, ICoreTimeService timeService); + + /** + * Sets a server's public key and its identifier for ECIES encryption. + * @param scope Scope of the server's public key. + * @param publicKey Public key encoded as Base64 string. + * @param publicKeyId Identifier of new public key. + * @return
    + *
  • {@link ErrorCode#OK} in case of success.
  • + *
  • {@link ErrorCode#WrongParam} if null or empty string is provided.
  • + *
  • {@link ErrorCode#WrongState} if activation scope is used and the session has no valid activation.
  • + *
+ */ + @ErrorCode + public native int setPublicKeyForEciesScope(@EciesEncryptorScope int scope, @NonNull String publicKey, @NonNull String publicKeyId); + + /** + * Removes a server's public key and its identifier store for the given scope. It's safe to call this + * function if key for given scope is not set. + * @param scope Scope of the key to remove. + */ + public native void removePublicKeyForEciesScope(@EciesEncryptorScope int scope); + + /** + * Determines whether session contains stored server's public key for ECIES scope. + * @param scope Scope of the key. + * @return {@code true} if session has stored server's public key for the requested scope. + */ + public native boolean hasPublicKeyForEciesScope(@EciesEncryptorScope int scope); + + /** + * Returns identifier of server's public key for given scope. + * @param scope Scope of the key. + * @return Identifier of the stored server's public key, or {@code null} if no key is stored, or method is called + * in the wrong activation state. + */ + @Nullable + public native String getPublicKeyIdForEciesScope(@EciesEncryptorScope int scope); // // Utilities @@ -656,7 +708,7 @@ public EciesEncryptor getEciesEncryptor(int scope, SignatureUnlockKeys unlockKey /** * Return textual representation for given protocol version. For example, for {@link ProtocolVersion#V3} - * returns {@code "3.2"}. You can use {@link ProtocolVersion#NA} to get the latest supported version. + * returns {@code "3.3"}. You can use {@link ProtocolVersion#NA} to get the latest supported version. * * @param version Version to convert to string * @return Textual representation for given protocol version. @@ -668,7 +720,7 @@ public static String getMaxSupportedHttpProtocolVersion(ProtocolVersion version) /** * Return textual representation for given integer value of protocol version. For example, - * for {@link ProtocolVersion#V3} returns {@code "3.2"}. You can use {@link ProtocolVersion#NA} + * for {@link ProtocolVersion#V3} returns {@code "3.3"}. You can use {@link ProtocolVersion#NA} * to get the latest supported version. * * @param protocolVersionValue Integer value from {@link ProtocolVersion} enum. diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignatureFormat.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignatureFormat.java new file mode 100644 index 00000000..3385988e --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignatureFormat.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.core; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import static io.getlime.security.powerauth.core.SignatureFormat.DEFAULT; +import static io.getlime.security.powerauth.core.SignatureFormat.ECDSA_DER; +import static io.getlime.security.powerauth.core.SignatureFormat.ECDSA_JOSE; + +/** + * The {@code SignatureFormat} enumeration defines signature type expected at input, or produced at output. + */ +@Retention(RetentionPolicy.SOURCE) +@IntDef({DEFAULT, ECDSA_DER, ECDSA_JOSE}) +public @interface SignatureFormat { + /** + * If default signature is used, then `ECDSA_DER` is used for ECDSA signature. The raw bytes are always used for + * HMAC signatures. + */ + int DEFAULT = 0; + /** + * ECDSA signature in DER format is expected at input, or produced at output: + *
+     *  ASN.1 notation:
+     *  ECDSASignature ::= SEQUENCE {
+     *     r   INTEGER,
+     *     s   INTEGER
+     * }
+     * 
+ */ + int ECDSA_DER = 1; + /** + * ECDSA signature in JOSE format is epxpected at input, or produced at output. + */ + int ECDSA_JOSE = 2; +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignedData.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignedData.java index 944b326b..68e57703 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignedData.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SignedData.java @@ -30,20 +30,26 @@ public class SignedData { */ public final byte[] signature; /** - * If true, then the master server's public key is used for validation, otherwise - * the personalized server's public key is used. + * Key to use for signature validation or computation. */ - public final boolean useMasterKey; + @SigningDataKey + public final int signingKey; + /** + * Format of signature expected at input, or produced at output. + */ + @SignatureFormat + public final int signatureFormat; /** * @param data data protected with signature * @param signature signature calculated for data - * @param useMasterKey If true, then the master server's public key is used for validation, otherwise - * the personalized server's public key is used. + * @param signingKey Key used to sign data, or will be used for the signature calculation. + * @param signatureFormat Format of signature expected at input, or produced at output. */ - public SignedData(byte[] data, byte[] signature, boolean useMasterKey) { + public SignedData(byte[] data, byte[] signature, @SigningDataKey int signingKey, @SignatureFormat int signatureFormat) { this.data = data; this.signature = signature; - this.useMasterKey = useMasterKey; + this.signingKey = signingKey; + this.signatureFormat = signatureFormat; } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SigningDataKey.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SigningDataKey.java new file mode 100644 index 00000000..b4290966 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/core/SigningDataKey.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.core; + +import androidx.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import static io.getlime.security.powerauth.core.SigningDataKey.ECDSA_MASTER_SERVER_KEY; +import static io.getlime.security.powerauth.core.SigningDataKey.ECDSA_PERSONALIZED_KEY; +import static io.getlime.security.powerauth.core.SigningDataKey.HMAC_APPLICATION; +import static io.getlime.security.powerauth.core.SigningDataKey.HMAC_ACTIVATION; + +/** + * The SigningDataKey defines key type for signature validation or calculation. + */ +@Retention(RetentionPolicy.SOURCE) +@IntDef({ECDSA_MASTER_SERVER_KEY, ECDSA_PERSONALIZED_KEY, HMAC_APPLICATION, HMAC_ACTIVATION}) +public @interface SigningDataKey { + /** + * {@code KEY_SERVER_MASTER_PRIVATE} key was used for signature calculation. + */ + int ECDSA_MASTER_SERVER_KEY = 0; + /** + * {@code KEY_SERVER_PRIVATE} key was used for signature calculation. + */ + int ECDSA_PERSONALIZED_KEY = 1; + /** + * {@code APP_SECRET} key is used for HMAC-SHA256 signature calculation. + */ + int HMAC_APPLICATION = 2; + /** + * {@code KEY_TRANSPORT} key is used for HMAC-SHA256 signature calculation. + */ + int HMAC_ACTIVATION = 3; +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesEncryptorFactory.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesEncryptorFactory.java index b1f8105c..617ddd19 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesEncryptorFactory.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesEncryptorFactory.java @@ -80,6 +80,10 @@ public EciesEncryptorFactory(@NonNull Session session, @Nullable byte[] possessi final byte[] sharedInfo1Bytes = sharedInfo1 != null ? sharedInfo1.getBytes(Charset.defaultCharset()) : null; final SignatureUnlockKeys unlockKeys; final String activationId; + final String temporaryKeyId = mSession.getPublicKeyIdForEciesScope(scope); + if (temporaryKeyId == null) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR, "Temporary key for ECIES is not set"); + } if (scope == EciesEncryptorScope.ACTIVATION) { if (mPossessionUnlockKey == null) { throw new PowerAuthErrorException(PowerAuthErrorCodes.WRONG_PARAMETER, "Device related key is missing for activation scoped encryptor"); @@ -97,7 +101,7 @@ public EciesEncryptorFactory(@NonNull Session session, @Nullable byte[] possessi if (encryptor == null) { throw new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR, "Failed to create ECIES encryptor"); } - encryptor.setMetadata(new EciesMetadata(mSession.getApplicationKey(), activationId)); + encryptor.setMetadata(new EciesMetadata(mSession.getApplicationKey(), temporaryKeyId, activationId)); return encryptor; } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesMetadata.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesMetadata.java index ae21a149..3fbb75d2 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesMetadata.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/ecies/EciesMetadata.java @@ -27,33 +27,50 @@ public class EciesMetadata { private final @NonNull String applicationKey; + private final @NonNull String temporaryKeyId; private final @Nullable String activationIdentifier; /** * @param applicationKey Base64 string with an application key cryptographic constant + * @param temporaryKeyId Temporary encryption key identifier * @param activationIdentifier String with an activation identifier */ - public EciesMetadata(@NonNull String applicationKey, @Nullable String activationIdentifier) { + public EciesMetadata(@NonNull String applicationKey, @NonNull String temporaryKeyId, @Nullable String activationIdentifier) { this.applicationKey = applicationKey; + this.temporaryKeyId = temporaryKeyId; this.activationIdentifier = activationIdentifier; } // Getters /** - * @return Base64 string with an application key cryptographic constant + * @return Base64 string with an application key cryptographic constant. */ public @NonNull String getActivationKey() { return applicationKey; } /** - * @return Base64 String with an activation identifier + * @return Application key identifier. + */ + public @NonNull String getApplicationKey() { + return applicationKey; + } + + /** + * @return Base64 String with an activation identifier. */ public @Nullable String getActivationIdentifier() { return activationIdentifier; } + /** + * @return Identifier of temporary key. + */ + public @NonNull String getTemporaryKeyId() { + return temporaryKeyId; + } + // HTTP header /** @@ -67,7 +84,7 @@ public EciesMetadata(@NonNull String applicationKey, @Nullable String activation * @return String with HTTP request header's value. */ public @NonNull String getHttpHeaderValue() { - final String result = "PowerAuth version=\"3.2\" application_key=\"" + applicationKey + "\""; + final String result = "PowerAuth version=\"3.3\" application_key=\"" + applicationKey + "\""; if (activationIdentifier != null) { return result + " activation_id=\"" + activationIdentifier + "\""; } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpClient.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpClient.java index db95a329..adc470a5 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpClient.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpClient.java @@ -21,6 +21,7 @@ import java.util.concurrent.Executor; +import io.getlime.security.powerauth.core.EciesEncryptorScope; import io.getlime.security.powerauth.networking.interfaces.ICancelable; import io.getlime.security.powerauth.networking.interfaces.IEndpointDefinition; import io.getlime.security.powerauth.networking.interfaces.IExecutorProvider; @@ -29,9 +30,7 @@ import io.getlime.security.powerauth.sdk.IPowerAuthTimeSynchronizationService; import io.getlime.security.powerauth.sdk.PowerAuthAuthentication; import io.getlime.security.powerauth.sdk.PowerAuthClientConfiguration; -import io.getlime.security.powerauth.sdk.impl.CompositeCancelableTask; -import io.getlime.security.powerauth.sdk.impl.ICallbackDispatcher; -import io.getlime.security.powerauth.sdk.impl.IPrivateCryptoHelper; +import io.getlime.security.powerauth.sdk.impl.*; /** * The {@code HttpClient} class provides a high level networking functionality, including @@ -48,6 +47,7 @@ public class HttpClient { private final @NonNull IExecutorProvider executorProvider; private final @NonNull ICallbackDispatcher callbackDispatcher; private IPowerAuthTimeSynchronizationService timeSynchronizationService; + private IKeystoreService keystoreService; /** * @param configuration HTTP client configuration @@ -89,7 +89,7 @@ public HttpClient( } /** - * Set time synchronization service to the HTTP client. + * Set time synchronization service to the HTTP client. If the service is already set, then throws {@link IllegalStateException}. * @param timeSynchronizationService Time synchronization service implementation. */ public void setTimeSynchronizationService(@NonNull IPowerAuthTimeSynchronizationService timeSynchronizationService) { @@ -99,6 +99,41 @@ public void setTimeSynchronizationService(@NonNull IPowerAuthTimeSynchronization this.timeSynchronizationService = timeSynchronizationService; } + /** + * Get time synchronization service associated to the HTTP client. If service is not set, then throws {@link IllegalStateException}. + * @return Implementation of {@link IPowerAuthTimeSynchronizationService}. + */ + @NonNull + IPowerAuthTimeSynchronizationService getTimeSynchronizationService() { + if (timeSynchronizationService == null) { + throw new IllegalStateException(); + } + return timeSynchronizationService; + } + + /** + * Set keystore service to the HTTP client. If the service is already set, then throws {@link IllegalStateException}. + * @param keystoreService Keystore service implementation. + */ + public void setKeystoreService(@Nullable IKeystoreService keystoreService) { + if (this.keystoreService != null) { + throw new IllegalStateException(); + } + this.keystoreService = keystoreService; + } + + /** + * Get keystore service associated to the HTTP client. If service is not set, then throws {@link IllegalStateException}. + * @return Implementation of {@link IKeystoreService}. + */ + @NonNull + IKeystoreService getKeystoreService() { + if (keystoreService == null) { + throw new IllegalStateException(); + } + return keystoreService; + } + /** * Posts a HTTP request with provided object to the REST endpoint. * @@ -139,44 +174,50 @@ public ICancelable post( @Nullable PowerAuthAuthentication authentication, @NonNull INetworkResponseListener listener) { - if (endpoint.isRequireSynchronizedTime()) { - // Get the time synchronization service. It supposed to be set by the PowerAuthSDK's builder in SDK construction. - final IPowerAuthTimeSynchronizationService tss = timeSynchronizationService; - if (tss == null) { - throw new IllegalStateException("Time synchronization service is not set."); - } - if (!tss.isTimeSynchronized()) { - // Endpoint require encryption and time is not synchronized yet. We have to create a composite task that cover both - // time synchronization and actual request execution. - final CompositeCancelableTask compositeTask = new CompositeCancelableTask(true); - compositeTask.setCancelCallback(() -> { - callbackDispatcher.dispatchCallback(listener::onCancel); + final IKeystoreService kss = getKeystoreService(); + final IPowerAuthTimeSynchronizationService tss = getTimeSynchronizationService(); + final int encryptorScope = endpoint.isEncryptedWithApplicationScope() ? EciesEncryptorScope.APPLICATION : EciesEncryptorScope.ACTIVATION; + final boolean requireTimeSynchronization = endpoint.isRequireSynchronizedTime() && !tss.isTimeSynchronized(); + final boolean requireEncryptionKey = endpoint.isEncrypted() && !kss.containsKeyForEncryptor(encryptorScope); + + if (requireTimeSynchronization || requireEncryptionKey) { + // Endpoint require encryption key or time synchronization. We have to create a composite task that cover + // multiple tasks including an actual request execution. + final CompositeCancelableTask compositeTask = new CompositeCancelableTask(true); + compositeTask.setCancelCallback(() -> { + callbackDispatcher.dispatchCallback(listener::onCancel); + }); + // Now determine what type of task should be executed before an actual task. + if (requireEncryptionKey) { + // Temporary encryption key must be acquired from the server. This operation also automatically + // synchronize the time. + if (helper == null) { + throw new IllegalArgumentException(); + } + final ICancelable getKeyTask = kss.createKeyForEncryptor(encryptorScope, helper, new ICreateKeyListener() { + @Override + public void onCreateKeySucceeded() { + // Encryption key successfully acquired, we can continue with the actual request. + compositePostImpl(object, endpoint, helper, authentication, compositeTask, listener); + } + + @Override + public void onCreateKeyFailed(@NonNull Throwable throwable) { + if (compositeTask.setCompleted()) { + listener.onNetworkError(throwable); + } + } }); + if (getKeyTask != null) { + compositeTask.addCancelable(getKeyTask); + } + } else { + // Only time synchronization is required final ICancelable synchronizationTask = tss.synchronizeTime(new ITimeSynchronizationListener() { @Override public void onTimeSynchronizationSucceeded() { // The time has been successfully synchronized, we can continue with the actual request. - final ICancelable actualTask = postImpl(object, endpoint, helper, authentication, new INetworkResponseListener() { - @Override - public void onNetworkResponse(@NonNull TResponse tResponse) { - if (compositeTask.setCompleted()) { - listener.onNetworkResponse(tResponse); - } - } - - @Override - public void onNetworkError(@NonNull Throwable throwable) { - if (compositeTask.setCompleted()) { - listener.onNetworkError(throwable); - } - } - - @Override - public void onCancel() { - // We can ignore the cancel, because it's handled already by the composite task. - } - }); - compositeTask.addCancelable(actualTask); + compositePostImpl(object, endpoint, helper, authentication, compositeTask, listener); } @Override @@ -189,14 +230,57 @@ public void onTimeSynchronizationFailed(@NonNull Throwable t) { if (synchronizationTask != null) { compositeTask.addCancelable(synchronizationTask); } - // Return composite task instead of original operation. - return compositeTask; } + // Return composite task instead of original operation. + return compositeTask; } - // Endpoint doesn't require time synchronization, or time is already synchronized. + + // Endpoint doesn't require time synchronization or encryption. return postImpl(object, endpoint, helper, authentication, listener); } + /** + * Function creates an asynchronous operation with HTTP request and includes the operation into composite task. + * @param object object to be serialized into POST request + * @param endpoint object defining the endpoint + * @param helper cryptographic helper + * @param authentication optional authentication object, if request has to be signed with PowerAuth signature + * @param compositeTask composite task reported back to the application + * @param listener response listener + * @param type of request object + * @param type of response object + */ + private void compositePostImpl( + @Nullable TRequest object, + @NonNull IEndpointDefinition endpoint, + @Nullable IPrivateCryptoHelper helper, + @Nullable PowerAuthAuthentication authentication, + @NonNull CompositeCancelableTask compositeTask, + @NonNull INetworkResponseListener listener) { + // Create actual HTTP + final ICancelable actualTask = postImpl(object, endpoint, helper, authentication, new INetworkResponseListener() { + @Override + public void onNetworkResponse(@NonNull TResponse tResponse) { + if (compositeTask.setCompleted()) { + listener.onNetworkResponse(tResponse); + } + } + + @Override + public void onNetworkError(@NonNull Throwable throwable) { + if (compositeTask.setCompleted()) { + listener.onNetworkError(throwable); + } + } + + @Override + public void onCancel() { + // We can ignore the cancel, because it's handled already by the composite task. + } + }); + compositeTask.addCancelable(actualTask); + } + /** * Internal implementation of HTTP post request. * diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpRequestHelper.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpRequestHelper.java index c87b1932..47347817 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpRequestHelper.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/HttpRequestHelper.java @@ -177,7 +177,7 @@ RequestData buildRequest(@NonNull String baseUrl, @Nullable IPrivateCryptoHelper // Execute custom step before the request is serialized. ICustomEndpointOperation beforeRequestSerialization = endpoint.getBeforeRequestSerializationOperation(); if (beforeRequestSerialization != null) { - beforeRequestSerialization.customEndpointOperation(); + beforeRequestSerialization.customEndpointOperation(endpoint); } // Encrypt the request data if the endpoint has encryptor specified diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/JsonSerialization.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/JsonSerialization.java index 1a5ffc8d..8ab99bfa 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/JsonSerialization.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/client/JsonSerialization.java @@ -19,6 +19,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import android.util.Base64; + import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; @@ -175,7 +177,10 @@ public byte[] decryptData(@Nullable byte[] data, @NonNull EciesEncryptor decrypt // 1. Deserialize bytes into response object final EciesEncryptedResponse response = deserializeObject(data, TypeToken.get(EciesEncryptedResponse.class)); // 2. Construct cryptogram with data & mac (response doesn't contain ephemeral key) - final EciesCryptogram cryptogram = new EciesCryptogram(response.getEncryptedData(), response.getMac(), null, response.getNonce(), response.getTimestamp()); + final EciesCryptogram cryptogram = EciesCryptogram.fromEncryptedResponse(response); + if (cryptogram == null) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR, "Invalid encrypted response received."); + } // 3. Decrypt the response final byte[] plainData = decryptor.decryptResponse(cryptogram); if (plainData == null) { @@ -225,13 +230,7 @@ public EciesEncryptedRequest encryptObjectToRequest(@Nullable TReques throw new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR, "Failed to encrypt object data."); } // 3. Construct final request object from the cryptogram - final EciesEncryptedRequest request = new EciesEncryptedRequest(); - request.setEncryptedData(cryptogram.getBodyBase64()); - request.setEphemeralPublicKey(cryptogram.getKeyBase64()); - request.setMac(cryptogram.getMacBase64()); - request.setNonce(cryptogram.getNonceBase64()); - request.setTimestamp(cryptogram.timestamp); - return request; + return cryptogram.toEncryptedRequest(); } @@ -252,7 +251,10 @@ public TResponse decryptObjectFromResponse(@Nullable EciesEncryptedR throw new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR, "Empty response cannot be decrypted."); } // 1. Convert response into cryptogram object - final EciesCryptogram cryptogram = new EciesCryptogram(response.getEncryptedData(), response.getMac(), null, response.getNonce(), response.getTimestamp()); + final EciesCryptogram cryptogram = EciesCryptogram.fromEncryptedResponse(response); + if (cryptogram == null) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR, "Invalid encrypted response received."); + } // 2. Try to decrypt the response final byte[] plainData = decryptor.decryptResponse(cryptogram); if (plainData == null) { @@ -262,6 +264,42 @@ public TResponse decryptObjectFromResponse(@Nullable EciesEncryptedR return deserializeObject(plainData, type); } + // JWT + + /** + * Serialize object into Base64Url encoded string. + * @param object Object to serialize. + * @return Object serialized into Base64Jwt encoded string. + * @param Type of object. + */ + @NonNull + public String serializeJwtObject(@Nullable TRequest object) { + byte[] data = serializeObject(object); + return Base64.encodeToString(data, Base64.NO_WRAP | Base64.URL_SAFE | Base64.NO_PADDING); + } + + /** + * Deserialize object from Base64Url encoded string. + * @param data String with serialized object. + * @param type Type of object to deserialize. + * @return Deserialized object. + * @param Type of object. + * @throws PowerAuthErrorException In case that string doesn't contain JWT encoded data. + */ + @NonNull + public TResponse deserializeJwtObject(@Nullable String data, @NonNull TypeToken type) throws PowerAuthErrorException { + if (data == null) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.NETWORK_ERROR, "Failed to deserialize JWT object."); + } + final byte[] objectBytes; + try { + objectBytes = Base64.decode(data, Base64.NO_WRAP| Base64.URL_SAFE | Base64.NO_PADDING); + } catch (IllegalArgumentException e) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.NETWORK_ERROR, "Failed to deserialize JWT object.", e); + } + return deserializeObject(objectBytes, type); + } + // Lazy initialized GSON & JsonParser /** diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/CreateActivationEndpoint.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/CreateActivationEndpoint.java index 559b5759..979507fe 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/CreateActivationEndpoint.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/CreateActivationEndpoint.java @@ -21,6 +21,7 @@ import com.google.gson.reflect.TypeToken; +import io.getlime.security.powerauth.core.EciesEncryptor; import io.getlime.security.powerauth.ecies.EciesEncryptorId; import io.getlime.security.powerauth.networking.interfaces.ICustomEndpointOperation; import io.getlime.security.powerauth.networking.interfaces.IEndpointDefinition; @@ -29,6 +30,7 @@ public class CreateActivationEndpoint implements IEndpointDefinition { private final ICustomEndpointOperation beforeRequestSerialization; + private EciesEncryptor layer2Encryptor; /** * Construct endpoint with a custom serialization step executed before the request is serialized. @@ -66,4 +68,20 @@ public boolean isAvailableInProtocolUpgrade() { public ICustomEndpointOperation getBeforeRequestSerializationOperation() { return beforeRequestSerialization; } + + /** + * Get ECIES encryptor for layer-2 encryption. + * @return ECIES encryptor for layer-2 encryption. + */ + public EciesEncryptor getLayer2Encryptor() { + return layer2Encryptor; + } + + /** + * Set ECIES encryptor for layer-2 encryption. + * @param encryptor ECIES encryptor for layer-2 encryption. + */ + public void setLayer2Encryptor(EciesEncryptor encryptor) { + this.layer2Encryptor = encryptor; + } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/GetTemporaryKeyEndpoint.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/GetTemporaryKeyEndpoint.java new file mode 100644 index 00000000..6d3ae181 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/endpoints/GetTemporaryKeyEndpoint.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.networking.endpoints; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.gson.reflect.TypeToken; +import io.getlime.security.powerauth.networking.interfaces.IEndpointDefinition; +import io.getlime.security.powerauth.networking.model.entity.JwtObject; + +public class GetTemporaryKeyEndpoint implements IEndpointDefinition { + @NonNull + @Override + public String getRelativePath() { + return "/pa/v3/keystore/create"; + } + + @Nullable + @Override + public TypeToken getResponseType() { + return TypeToken.get(JwtObject.class); + } + + @Override + public boolean isAvailableInProtocolUpgrade() { + return true; + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/ICustomEndpointOperation.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/ICustomEndpointOperation.java index fafa788e..a46aa431 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/ICustomEndpointOperation.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/ICustomEndpointOperation.java @@ -16,6 +16,7 @@ package io.getlime.security.powerauth.networking.interfaces; +import androidx.annotation.NonNull; import io.getlime.security.powerauth.exception.PowerAuthErrorException; /** @@ -23,5 +24,5 @@ */ @FunctionalInterface public interface ICustomEndpointOperation { - void customEndpointOperation() throws PowerAuthErrorException; + void customEndpointOperation(@NonNull IEndpointDefinition endpointDefinition) throws PowerAuthErrorException; } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/IEndpointDefinition.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/IEndpointDefinition.java index 20caeaf8..d06eb57f 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/IEndpointDefinition.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/interfaces/IEndpointDefinition.java @@ -59,6 +59,23 @@ default EciesEncryptorId getEncryptorId() { return EciesEncryptorId.NONE; } + /** + * @return {@code true} if endpoint is using ECIES encryption. + */ + default boolean isEncrypted() { + return getEncryptorId() != EciesEncryptorId.NONE; + } + + /** + * @return {@code true} if endpoint is using application scoped encryptor. + */ + default boolean isEncryptedWithApplicationScope() { + final EciesEncryptorId encryptorId = getEncryptorId(); + return encryptorId == EciesEncryptorId.ACTIVATION_PAYLOAD || + encryptorId == EciesEncryptorId.ACTIVATION_REQUEST || + encryptorId == EciesEncryptorId.GENERIC_APPLICATION_SCOPE; + } + /** * @return Type of response object. By default, returns null. */ diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtHeader.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtHeader.java new file mode 100644 index 00000000..27fe5859 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtHeader.java @@ -0,0 +1,41 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.networking.model.entity; + +/** + * The JwtHeader class represents a header in JWT signature. + */ +public class JwtHeader { + /** + * Type of object. + */ + public final String typ; + /** + * Algorithm used in JWT. + */ + public final String alg; + + /** + * Construct object with JWT type and algorithm. + * @param typ Type of JWT. + * @param alg Algorithm used in JWT. + */ + public JwtHeader(String typ, String alg) { + this.typ = typ; + this.alg = alg; + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtObject.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtObject.java new file mode 100644 index 00000000..1945bf0e --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/entity/JwtObject.java @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.networking.model.entity; + +/** + * The JwtObject represents JWT request and response. + */ +public class JwtObject { + + /** + * Full JWT formatted string + */ + public final String jwt; + + /** + * Construct object with JWT formatted string. + * @param jwt JWT formatted string. + */ + public JwtObject(String jwt) { + this.jwt = jwt; + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/EciesEncryptedRequest.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/EciesEncryptedRequest.java index dce70de1..6b015f03 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/EciesEncryptedRequest.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/EciesEncryptedRequest.java @@ -22,6 +22,7 @@ */ public class EciesEncryptedRequest { + private String temporaryKeyId; private String ephemeralPublicKey; private String encryptedData; private String mac; @@ -106,4 +107,20 @@ public long getTimestamp() { public void setTimestamp(long timestamp) { this.timestamp = timestamp; } + + /** + * Get identifier of temporary key. + * @return Identifier of temporary key + */ + public String getTemporaryKeyId() { + return temporaryKeyId; + } + + /** + * Set identifier of temporary key. + * @param temporaryKeyId identifier of temporary key. + */ + public void setTemporaryKeyId(String temporaryKeyId) { + this.temporaryKeyId = temporaryKeyId; + } } diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/GetTemporaryKeyRequest.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/GetTemporaryKeyRequest.java new file mode 100644 index 00000000..07be449c --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/request/GetTemporaryKeyRequest.java @@ -0,0 +1,51 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.networking.model.request; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * Request object for endpoint returning a temporary key for ECIES encryption scheme. + */ +public class GetTemporaryKeyRequest { + + private final String applicationKey; + private final String activationId; + private final String challenge; + + public GetTemporaryKeyRequest(@NonNull String applicationKey, @Nullable String activationId, @NonNull String challenge) { + this.applicationKey = applicationKey; + this.activationId = activationId; + this.challenge = challenge; + } + + @NonNull + public String getApplicationKey() { + return applicationKey; + } + + @Nullable + public String getActivationId() { + return activationId; + } + + @NonNull + public String getChallenge() { + return challenge; + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/response/GetTemporaryKeyResponse.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/response/GetTemporaryKeyResponse.java new file mode 100644 index 00000000..9612dc85 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/model/response/GetTemporaryKeyResponse.java @@ -0,0 +1,88 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.networking.model.response; + +import com.google.gson.annotations.SerializedName; + +/** + * Response object for endpoint returning a temporary key for ECIES encryption scheme. + */ +public class GetTemporaryKeyResponse { + + private String applicationKey; + private String activationId; + private String challenge; + private String publicKey; + @SerializedName("sub") + private String keyId; + @SerializedName("exp_ms") + private long expiration; + @SerializedName("iat_ms") + private long serverTime; + + public String getApplicationKey() { + return applicationKey; + } + + public void setApplicationKey(String applicationKey) { + this.applicationKey = applicationKey; + } + + public String getActivationId() { + return activationId; + } + + public void setActivationId(String activationId) { + this.activationId = activationId; + } + + public String getChallenge() { + return challenge; + } + + public void setChallenge(String challenge) { + this.challenge = challenge; + } + + public String getPublicKey() { + return publicKey; + } + + public void setPublicKey(String publicKey) { + this.publicKey = publicKey; + } + + public String getKeyId() { + return keyId; + } + + public long getExpiration() { + return expiration; + } + + public void setExpiration(long expiration) { + this.expiration = expiration; + } + + public long getServerTime() { + return serverTime; + } + + public void setServerTime(long serverTime) { + this.serverTime = serverTime; + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IGetEciesEncryptorListener.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IGetEciesEncryptorListener.java new file mode 100644 index 00000000..8c3e0f30 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/networking/response/IGetEciesEncryptorListener.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.networking.response; + +import androidx.annotation.NonNull; +import io.getlime.security.powerauth.core.EciesEncryptor; + +/** + * Listener for getting ECIES encryptor for general application purposes. + */ +public interface IGetEciesEncryptorListener { + /** + * Called when encryptor has been successfully created. + * @param encryptor {@link EciesEncryptor} object configured for the requested scope. + */ + void onGetEciesEncryptorSuccess(@NonNull EciesEncryptor encryptor); + + /** + * Called when operation fails. + * @param t Error that occurred during the operation. + */ + void onGetEciesEncryptorFailed(@NonNull Throwable t); +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java index 4928a528..a42db602 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/PowerAuthSDK.java @@ -28,7 +28,7 @@ import com.google.gson.reflect.TypeToken; -import java.util.HashMap; +import java.nio.charset.StandardCharsets; import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.locks.ReentrantLock; @@ -77,6 +77,7 @@ public class PowerAuthSDK { private final @NonNull PowerAuthTokenStore mTokenStore; private final @NonNull IServerStatusProvider mServerStatusProvider; private final @NonNull TimeSynchronizationService mTimeSynchronizationService; + private final @NonNull IKeystoreService mKeystoreService; /** * A builder that collects configurations and arguments for {@link PowerAuthSDK}. @@ -213,6 +214,10 @@ public PowerAuthSDK build(@NonNull Context context) throws PowerAuthErrorExcepti // Prepare low-level Session object. final Session session = new Session(mConfiguration.getSessionSetup(), timeSynchronizationService); + // Prepare keystore service and conned it with HTTP client + final DefaultKeystoreService keystoreService = new DefaultKeystoreService(timeSynchronizationService, session, mCallbackDispatcher, sharedLock, httpClient); + httpClient.setKeystoreService(keystoreService); + // Create a final PowerAuthSDK instance final PowerAuthSDK instance = new PowerAuthSDK( sharedLock, @@ -227,7 +232,8 @@ public PowerAuthSDK build(@NonNull Context context) throws PowerAuthErrorExcepti tokenStoreKeychain, mCallbackDispatcher, serverStatusProvider, - timeSynchronizationService); + timeSynchronizationService, + keystoreService); // Register time service for automatic reset. PowerAuthAppLifecycleListener.getInstance().registerTimeSynchronizationService(context, timeSynchronizationService); @@ -251,8 +257,9 @@ public PowerAuthSDK build(@NonNull Context context) throws PowerAuthErrorExcepti * @param biometryKeychain Keychain that store biometry-related key. * @param tokenStoreKeychain Keychain that store tokens. * @param callbackDispatcher Dispatcher that handle callbacks back to application. - * @param serverStatusProvider Implementation of IServerStatusProvider. - * @param timeSynchronizationService Implementation of IPowerAuthTimeSynchronizationService. + * @param serverStatusProvider Implementation of {@link IServerStatusProvider}. + * @param timeSynchronizationService Implementation of {@link IPowerAuthTimeSynchronizationService}. + * @param keystoreService Implementation of {@link IKeystoreService}. */ private PowerAuthSDK( @NonNull ReentrantLock sharedLock, @@ -267,7 +274,8 @@ private PowerAuthSDK( @NonNull Keychain tokenStoreKeychain, @NonNull ICallbackDispatcher callbackDispatcher, @NonNull IServerStatusProvider serverStatusProvider, - @NonNull IPowerAuthTimeSynchronizationService timeSynchronizationService) { + @NonNull IPowerAuthTimeSynchronizationService timeSynchronizationService, + @NonNull IKeystoreService keystoreService) { this.mLock = sharedLock; this.mSession = session; this.mConfiguration = configuration; @@ -281,6 +289,7 @@ private PowerAuthSDK( this.mTokenStore = new PowerAuthTokenStore(this, tokenStoreKeychain, client); this.mServerStatusProvider = serverStatusProvider; this.mTimeSynchronizationService = (TimeSynchronizationService) timeSynchronizationService; + this.mKeystoreService = keystoreService; } /** @@ -319,6 +328,17 @@ public byte[] getDeviceRelatedKey() { return context == null ? null : deviceRelatedKey(context); } + @NonNull + @Override + public IKeystoreService getKeystoreService() { + return mKeystoreService; + } + + @NonNull + @Override + public Session getCoreSession() { + return mSession; + } }; } @@ -553,7 +573,7 @@ public void saveSerializedState() { */ @CheckResult public boolean restoreState(byte[] state) { - mSession.resetSession(); + mSession.resetSession(false); final int result = mSession.deserializeState(state); return result == ErrorCode.OK; } @@ -642,126 +662,113 @@ public void run() { final IPrivateCryptoHelper cryptoHelper = getCryptoHelper(null); final JsonSerialization serialization = new JsonSerialization(); - final EciesEncryptor encryptor; - - try { - // Prepare cryptographic helper & Layer2 ECIES encryptor - encryptor = cryptoHelper.getEciesEncryptor(EciesEncryptorId.ACTIVATION_PAYLOAD); - - // Prepare low level activation parameters - final ActivationStep1Param step1Param; - if (activation.activationCode != null) { - step1Param = new ActivationStep1Param(activation.activationCode.activationCode, activation.activationCode.activationSignature); - } else { - step1Param = null; - } - // Start the activation - final ActivationStep1Result step1Result = mSession.startActivation(step1Param); - if (step1Result.errorCode != ErrorCode.OK) { - // Looks like create activation failed - final int errorCode = step1Result.errorCode == ErrorCode.Encryption - ? PowerAuthErrorCodes.SIGNATURE_ERROR - : PowerAuthErrorCodes.INVALID_ACTIVATION_DATA; - dispatchCallback(new Runnable() { - @Override - public void run() { - listener.onActivationCreateFailed(new PowerAuthErrorException(errorCode)); - } - }); - return null; - } + // Prepare low level activation parameters + final ActivationStep1Param step1Param; + if (activation.activationCode != null) { + step1Param = new ActivationStep1Param(activation.activationCode.activationCode, activation.activationCode.activationSignature); + } else { + step1Param = null; + } - // Prepare level 2 payload - final ActivationLayer2Request privateData = new ActivationLayer2Request(); - privateData.setActivationName(activation.activationName); - privateData.setExtras(activation.extras); - privateData.setActivationOtp(activation.additionalActivationOtp); - privateData.setDevicePublicKey(step1Result.devicePublicKey); - privateData.setPlatform(PowerAuthSystem.getPlatform()); - privateData.setDeviceInfo(PowerAuthSystem.getDeviceInfo()); - - // Prepare level 1 payload - final ActivationLayer1Request request = new ActivationLayer1Request(); - request.setType(activation.activationType); - request.setIdentityAttributes(activation.identityAttributes); - request.setCustomAttributes(activation.customAttributes); - - // The create activation endpoint needs a custom object processing where we encrypt the inner data - // with a different encryptor. We have to do this in the HTTP client's queue to guarantee that time - // service is already synchronized. - final CreateActivationEndpoint endpointDefinition = new CreateActivationEndpoint(() -> { - // Set encrypted level 2 activation data to the request. - request.setActivationData(serialization.encryptObjectToRequest(privateData, encryptor)); + // Start the activation + final ActivationStep1Result step1Result = mSession.startActivation(step1Param); + if (step1Result.errorCode != ErrorCode.OK) { + // Looks like create activation failed + final int errorCode = step1Result.errorCode == ErrorCode.Encryption + ? PowerAuthErrorCodes.SIGNATURE_ERROR + : PowerAuthErrorCodes.INVALID_ACTIVATION_DATA; + dispatchCallback(new Runnable() { + @Override + public void run() { + listener.onActivationCreateFailed(new PowerAuthErrorException(errorCode)); + } }); + return null; + } - // Fire HTTP request - return mClient.post( - request, - endpointDefinition, - cryptoHelper, - new INetworkResponseListener() { - @Override - public void onNetworkResponse(@NonNull ActivationLayer1Response response) { - // Process response from the server - try { - // Try to decrypt Layer2 object from response - final ActivationLayer2Response layer2Response = serialization.decryptObjectFromResponse(response.getActivationData(), encryptor, TypeToken.get(ActivationLayer2Response.class)); - // Prepare Step2 param for low level session - final RecoveryData recoveryData; - if (layer2Response.getActivationRecovery() != null) { - final ActivationRecovery rd = layer2Response.getActivationRecovery(); - recoveryData = new RecoveryData(rd.getRecoveryCode(), rd.getPuk()); - } else { - recoveryData = null; - } - final ActivationStep2Param step2Param = new ActivationStep2Param(layer2Response.getActivationId(), layer2Response.getServerPublicKey(), layer2Response.getCtrData(), recoveryData); - // Validate the response - final ActivationStep2Result step2Result = mSession.validateActivationResponse(step2Param); - // - if (step2Result.errorCode == ErrorCode.OK) { - final UserInfo userInfo = response.getUserInfo() != null ? new UserInfo(response.getUserInfo()) : null; - final CreateActivationResult result = new CreateActivationResult(step2Result.activationFingerprint, response.getCustomAttributes(), recoveryData, userInfo); - setLastFetchedUserInfo(userInfo); - listener.onActivationCreateSucceed(result); - return; - } - throw new PowerAuthErrorException(PowerAuthErrorCodes.INVALID_ACTIVATION_DATA, "Invalid activation data received from the server."); + // Prepare level 2 payload + final ActivationLayer2Request privateData = new ActivationLayer2Request(); + privateData.setActivationName(activation.activationName); + privateData.setExtras(activation.extras); + privateData.setActivationOtp(activation.additionalActivationOtp); + privateData.setDevicePublicKey(step1Result.devicePublicKey); + privateData.setPlatform(PowerAuthSystem.getPlatform()); + privateData.setDeviceInfo(PowerAuthSystem.getDeviceInfo()); + + // Prepare level 1 payload + final ActivationLayer1Request request = new ActivationLayer1Request(); + request.setType(activation.activationType); + request.setIdentityAttributes(activation.identityAttributes); + request.setCustomAttributes(activation.customAttributes); + + // The create activation endpoint needs a custom object processing where we encrypt the inner data + // with a different encryptor. We have to do this in the HTTP client's queue to guarantee that time + // service is already synchronized. + final CreateActivationEndpoint endpointDefinition = new CreateActivationEndpoint((endpoint) -> { + // Set encrypted level 2 activation data to the request. + // Prepare cryptographic helper & Layer2 ECIES encryptor + final EciesEncryptor encryptor = cryptoHelper.getEciesEncryptor(EciesEncryptorId.ACTIVATION_PAYLOAD); + request.setActivationData(serialization.encryptObjectToRequest(privateData, encryptor)); + ((CreateActivationEndpoint) endpoint).setLayer2Encryptor(encryptor); + }); - } catch (PowerAuthErrorException e) { - // In case of error, reset the session & report that exception - mSession.resetSession(); - listener.onActivationCreateFailed(e); + // Fire HTTP request + return mClient.post( + request, + endpointDefinition, + cryptoHelper, + new INetworkResponseListener<>() { + @Override + public void onNetworkResponse(@NonNull ActivationLayer1Response response) { + // Process response from the server + try { + // Try to decrypt Layer2 object from response + final EciesEncryptor encryptor = endpointDefinition.getLayer2Encryptor(); + final ActivationLayer2Response layer2Response = serialization.decryptObjectFromResponse(response.getActivationData(), encryptor, TypeToken.get(ActivationLayer2Response.class)); + // Prepare Step2 param for low level session + final RecoveryData recoveryData; + if (layer2Response.getActivationRecovery() != null) { + final ActivationRecovery rd = layer2Response.getActivationRecovery(); + recoveryData = new RecoveryData(rd.getRecoveryCode(), rd.getPuk()); + } else { + recoveryData = null; } - } + final ActivationStep2Param step2Param = new ActivationStep2Param(layer2Response.getActivationId(), layer2Response.getServerPublicKey(), layer2Response.getCtrData(), recoveryData); + // Validate the response + final ActivationStep2Result step2Result = mSession.validateActivationResponse(step2Param); + // + if (step2Result.errorCode == ErrorCode.OK) { + final UserInfo userInfo = response.getUserInfo() != null ? new UserInfo(response.getUserInfo()) : null; + final CreateActivationResult result = new CreateActivationResult(step2Result.activationFingerprint, response.getCustomAttributes(), recoveryData, userInfo); + setLastFetchedUserInfo(userInfo); + listener.onActivationCreateSucceed(result); + return; + } + throw new PowerAuthErrorException(PowerAuthErrorCodes.INVALID_ACTIVATION_DATA, "Invalid activation data received from the server."); - @Override - public void onNetworkError(@NonNull Throwable throwable) { + } catch (PowerAuthErrorException e) { // In case of error, reset the session & report that exception - mSession.resetSession(); - listener.onActivationCreateFailed(throwable); + mSession.resetSession(false); + listener.onActivationCreateFailed(e); } + } - @Override - public void onCancel() { - // In case of cancel, reset the session - mSession.resetSession(); - } - }); + @Override + public void onNetworkError(@NonNull Throwable throwable) { + // In case of error, reset the session & report that exception + mSession.resetSession(false); + listener.onActivationCreateFailed(throwable); + } - } catch (final PowerAuthErrorException e) { - mSession.resetSession(); - dispatchCallback(new Runnable() { - @Override - public void run() { - listener.onActivationCreateFailed(e); - } - }); - return null; - } + @Override + public void onCancel() { + // In case of cancel, reset the session + mSession.resetSession(false); + } + }); } - /** * Create a new standard activation with given name and activation code by calling a PowerAuth Standard RESTful API. * @@ -1628,7 +1635,7 @@ public void removeActivationLocal(@NonNull Context context, boolean removeShared getTokenStore().removeAllLocalTokens(context); // Reset C++ session - mSession.resetSession(); + mSession.resetSession(false); // Serialize will notify state listener saveSerializedState(); // Cancel possible pending activation status task @@ -1789,7 +1796,8 @@ public boolean verifyServerSignedData(byte[] data, byte[] signature, boolean use checkForValidSetup(); // Verify signature - SignedData signedData = new SignedData(data, signature, useMasterKey); + final int signingKey = useMasterKey ? SigningDataKey.ECDSA_MASTER_SERVER_KEY : SigningDataKey.ECDSA_PERSONALIZED_KEY; + final SignedData signedData = new SignedData(data, signature, signingKey, SignatureFormat.ECDSA_DER); return mSession.verifyServerSignedData(signedData) == ErrorCode.OK; } @@ -1803,7 +1811,20 @@ public boolean verifyServerSignedData(byte[] data, byte[] signature, boolean use */ public @Nullable ICancelable signDataWithDevicePrivateKey(@NonNull final Context context, @NonNull PowerAuthAuthentication authentication, @NonNull final byte[] data, @NonNull final IDataSignatureListener listener) { + return signDataWithDevicePrivateKeyImpl(context, authentication, data, SignatureFormat.ECDSA_DER, listener); + } + /** + * Sign provided data with a private key that is stored in secure vault. + * @param context Context. + * @param authentication Authentication object for vault unlock request. + * @param data Data to be signed. + * @param signatureFormat Format of output signature. + * @param listener Listener with callbacks to signature status. + * @return Async task associated with vault unlock request. + */ + private @Nullable + ICancelable signDataWithDevicePrivateKeyImpl(@NonNull final Context context, @NonNull PowerAuthAuthentication authentication, @NonNull final byte[] data, @SignatureFormat int signatureFormat, @NonNull final IDataSignatureListener listener) { // Fetch vault encryption key using vault unlock request. return this.fetchEncryptedVaultUnlockKey(context, authentication, VaultUnlockReason.SIGN_WITH_DEVICE_PRIVATE_KEY, new IFetchEncryptedVaultUnlockKeyListener() { @Override @@ -1811,7 +1832,7 @@ public void onFetchEncryptedVaultUnlockKeySucceed(String encryptedEncryptionKey) if (encryptedEncryptionKey != null) { // Let's sign the data SignatureUnlockKeys keys = new SignatureUnlockKeys(deviceRelatedKey(context), null, null); - byte[] signature = mSession.signDataWithDevicePrivateKey(encryptedEncryptionKey, keys, data); + byte[] signature = mSession.signDataWithDevicePrivateKey(encryptedEncryptionKey, keys, data, signatureFormat); // Propagate error if (signature != null) { listener.onDataSignedSucceed(signature); @@ -1830,6 +1851,7 @@ public void onFetchEncryptedVaultUnlockKeyFailed(Throwable t) { }); } + /** * Change the password using local re-encryption, do not validate old password by calling any endpoint. * @@ -2530,22 +2552,16 @@ public void onBiometricDialogFailed(@NonNull PowerAuthErrorException error) { @Nullable public ICancelable signJwtWithDevicePrivateKey(@NonNull Context context, @NonNull PowerAuthAuthentication authentication, @NonNull Map claims, @NonNull IJwtSignatureListener listener) { final JsonSerialization serialization = new JsonSerialization(); - final byte[] serializedClaims = serialization.serializeObject(claims); - return signDataWithDevicePrivateKey(context, authentication, serializedClaims, new IDataSignatureListener() { + final String jwtHeader = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9"; // {"alg":"ES256","typ":"JWT"} + final String jwtClaims = serialization.serializeJwtObject(claims); + final String jwtHeaderAndClaims = jwtHeader + "." + jwtClaims; + return signDataWithDevicePrivateKeyImpl(context, authentication, jwtHeaderAndClaims.getBytes(StandardCharsets.US_ASCII), SignatureFormat.ECDSA_JOSE, new IDataSignatureListener() { @Override public void onDataSignedSucceed(@NonNull byte[] signature) { - // Prepare header - final HashMap header = new HashMap<>(); - header.put("alg", "ES256"); - header.put("typ", "JWT"); - final byte[] headerData = serialization.serializeObject(header); - final String headerBase64 = Base64.encodeToString(headerData, Base64.NO_WRAP); - // Prepare claims data - final String claimsBase64 = Base64.encodeToString(serializedClaims, Base64.NO_WRAP); // Encoded signature - final String signatureBase64 = Base64.encodeToString(signature, Base64.NO_WRAP); + final String jwtSignature = Base64.encodeToString(signature, Base64.NO_WRAP | Base64.URL_SAFE | Base64.NO_PADDING); // Construct final JWT - final String jwt = headerBase64 + "." + claimsBase64 + "." + signatureBase64; + final String jwt = jwtHeaderAndClaims + "." + jwtSignature; listener.onJwtSignatureSucceed(jwt); } @@ -2560,15 +2576,83 @@ public void onDataSignedFailed(@NonNull Throwable t) { /** * Creates a new instance of ECIES encryptor suited for application's general end-to-end encryption purposes. - * The returned encryptor is cryptographically bounded to the PowerAuth configuration, so it can be used + * The returned encryptor is cryptographically bound to the PowerAuth configuration, so it can be used + * with or without a valid activation. The encryptor also contains an associated {@link io.getlime.security.powerauth.ecies.EciesMetadata} + * object, allowing you to properly setup HTTP header for the request. + * + * @param listener Listener with the callback methods. + * @return {@link ICancelable} operation in case that the temporary encryption key needs to be acquired from the server. If the key is already + * present, then returns {@code null}. + */ + public @Nullable ICancelable getEciesEncryptorForApplicationScope(@NonNull IGetEciesEncryptorListener listener) { + return createEciesEncryptor(null, listener, true); + } + + /** + * Creates a new instance of ECIES encryptor suited for application's general end-to-end encryption purposes. + * The returned encryptor is cryptographically bound to a device's activation, so it can be used only + * when this instance has a valid activation. The encryptor also contains an associated {@link io.getlime.security.powerauth.ecies.EciesMetadata} + * object, allowing you to properly setup HTTP header for the request. + *

+ * Note that the created encryptor has no reference to this instance of {@link PowerAuthSDK}. This means + * that if the instance will lose its activation in the future, then the encryptor will still be capable + * to encrypt, or decrypt the data. This is an expected behavior, so if you plan to keep the encryptor for + * multiple requests, then it's up to you to release its instance after you change the state of {@code PowerAuthSDK}. + * + * @param context Android {@link Context} object + * @param listener Listener with the callback methods. + * @return {@link ICancelable} operation in case that the temporary encryption key needs to be acquired from the server. If the key is already + * present, then returns {@code null}. + */ + public @Nullable ICancelable getEciesEncryptorForActivationScope(@NonNull Context context, @NonNull IGetEciesEncryptorListener listener) { + return createEciesEncryptor(context, listener, false); + } + + /** + * Create application or activation scoped ECIES encryptor. + * @param context Android context, required for activation scoped encryptor. + * @param listener Listener with the callback methods. + * @param applicationScope If {@code true} then encryptor in application scope is created. + * @return {@link ICancelable} operation in case that the temporary encryption key needs to be acquired from the server. If the key is already + * present, then returns {@code null}. + */ + private @Nullable ICancelable createEciesEncryptor(@Nullable final Context context, @NonNull final IGetEciesEncryptorListener listener, final boolean applicationScope) { + final IPrivateCryptoHelper helper = getCryptoHelper(context); + return mKeystoreService.createKeyForEncryptor(applicationScope ? EciesEncryptorScope.APPLICATION : EciesEncryptorScope.ACTIVATION, helper, new ICreateKeyListener() { + @Override + public void onCreateKeySucceeded() { + try { + final EciesEncryptor encryptor = helper.getEciesEncryptor(applicationScope ? EciesEncryptorId.GENERIC_APPLICATION_SCOPE : EciesEncryptorId.GENERIC_ACTIVATION_SCOPE); + listener.onGetEciesEncryptorSuccess(encryptor); + } catch (PowerAuthErrorException exception) { + listener.onGetEciesEncryptorFailed(exception); + } + } + + @Override + public void onCreateKeyFailed(@NonNull Throwable throwable) { + listener.onGetEciesEncryptorFailed(throwable); + } + }); + } + + /** + * Creates a new instance of ECIES encryptor suited for application's general end-to-end encryption purposes. + * The returned encryptor is cryptographically bound to the PowerAuth configuration, so it can be used * with or without a valid activation. The encryptor also contains an associated {@link io.getlime.security.powerauth.ecies.EciesMetadata} * object, allowing you to properly setup HTTP header for the request. + *

+ * Note that this method is deprecated because doesn't guarantee that encryptor is provided, or the temporary encryption + * key is still valid. You should use new {@link #getEciesEncryptorForApplicationScope(IGetEciesEncryptorListener)} + * as a replacement. * * @return New instance of {@link EciesEncryptor} object with an associated {@link io.getlime.security.powerauth.ecies.EciesMetadata}. * @throws PowerAuthErrorException if {@link PowerAuthConfiguration} contains an invalid configuration. * You can call {@link PowerAuthErrorException#getPowerAuthErrorCode()} to get a more * detailed information about the failure. + * @deprecated Use {@link #getEciesEncryptorForApplicationScope(IGetEciesEncryptorListener)} as a replacement. */ + @Deprecated // 1.9.0 public @Nullable EciesEncryptor getEciesEncryptorForApplicationScope() throws PowerAuthErrorException { final IPrivateCryptoHelper helper = getCryptoHelper(null); return helper.getEciesEncryptor(EciesEncryptorId.GENERIC_APPLICATION_SCOPE); @@ -2576,7 +2660,7 @@ public void onDataSignedFailed(@NonNull Throwable t) { /** * Creates a new instance of ECIES encryptor suited for application's general end-to-end encryption purposes. - * The returned encryptor is cryptographically bounded to a device's activation, so it can be used only + * The returned encryptor is cryptographically bound to a device's activation, so it can be used only * when this instance has a valid activation. The encryptor also contains an associated {@link io.getlime.security.powerauth.ecies.EciesMetadata} * object, allowing you to properly setup HTTP header for the request. *

@@ -2584,13 +2668,19 @@ public void onDataSignedFailed(@NonNull Throwable t) { * that if the instance will loose its activation in the future, then the encryptor will still be capable * to encrypt, or decrypt the data. This is an expected behavior, so if you plan to keep the encryptor for * multiple requests, then it's up to you to release its instance after you change the state of {@code PowerAuthSDK}. + *

+ * Note that this method is deprecated because doesn't guarantee that encryptor is provided, or the temporary encryption + * key is still valid. You should use new {@link #getEciesEncryptorForActivationScope(Context, IGetEciesEncryptorListener)} + * as a replacement. * * @param context Android {@link Context} object * @return New instance of {@link EciesEncryptor} object with an associated {@link io.getlime.security.powerauth.ecies.EciesMetadata}. * @throws PowerAuthErrorException if {@link PowerAuthConfiguration} contains an invalid configuration or there's * no activation. You can call {@link PowerAuthErrorException#getPowerAuthErrorCode()} to get a more * detailed information about the failure. + * @deprecated Use {@link #getEciesEncryptorForActivationScope(Context, IGetEciesEncryptorListener)} as a replacement. */ + @Deprecated // 1.9.0 public @Nullable EciesEncryptor getEciesEncryptorForActivationScope(@NonNull final Context context) throws PowerAuthErrorException { final IPrivateCryptoHelper helper = getCryptoHelper(context); return helper.getEciesEncryptor(EciesEncryptorId.GENERIC_ACTIVATION_SCOPE); diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/DefaultKeystoreService.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/DefaultKeystoreService.java new file mode 100644 index 00000000..eefac1c7 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/DefaultKeystoreService.java @@ -0,0 +1,190 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.sdk.impl; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.core.EciesEncryptorScope; +import io.getlime.security.powerauth.core.ErrorCode; +import io.getlime.security.powerauth.core.ICoreTimeService; +import io.getlime.security.powerauth.core.Session; +import io.getlime.security.powerauth.exception.PowerAuthErrorCodes; +import io.getlime.security.powerauth.exception.PowerAuthErrorException; +import io.getlime.security.powerauth.networking.client.HttpClient; +import io.getlime.security.powerauth.networking.interfaces.ICancelable; +import io.getlime.security.powerauth.networking.model.response.GetTemporaryKeyResponse; +import io.getlime.security.powerauth.system.PowerAuthLog; + +import java.util.concurrent.locks.ReentrantLock; + +/** + * The {@code DefaultKeystoreService} class provides temporary encryption keys for ECIES encryption acquired from the + * server. The key itself is stored in {@link io.getlime.security.powerauth.core.Session} instance and is available + * for further encryption operations. + */ +public class DefaultKeystoreService implements IKeystoreService, GetTemporaryKeyTask.TaskCompletion { + + private final @NonNull ReentrantLock lock; + private final @NonNull Session session; + private final @NonNull ICoreTimeService timeService; + private final @NonNull ICallbackDispatcher callbackDispatcher; + private final @NonNull HttpClient httpClient; + + private final PublicKeyInfo applicationScopePublicKeyInfo; + private final PublicKeyInfo activationScopePublicKeyInfo; + + /** + * We don't want to use the key that's close to its expiration on the server. This constant specifies for how much + * we move the expiration time to backward. + */ + private static final long EXPIRATION_THRESHOLD = 10_000; + + /** + * Service constructor. + * @param timeService Time synchronization service. + * @param session Instance of core Session. + * @param callbackDispatcher Callback dispatcher. + * @param sharedLock Reentrant lock shared across multiple SDK objects. + * @param httpClient HTTP client implementation. + */ + public DefaultKeystoreService( + @NonNull ICoreTimeService timeService, + @NonNull Session session, + @NonNull ICallbackDispatcher callbackDispatcher, + @NonNull ReentrantLock sharedLock, + @NonNull HttpClient httpClient) { + this.lock = sharedLock; + this.timeService = timeService; + this.session = session; + this.callbackDispatcher = callbackDispatcher; + this.httpClient = httpClient; + this.applicationScopePublicKeyInfo = new PublicKeyInfo(EciesEncryptorScope.APPLICATION); + this.activationScopePublicKeyInfo = new PublicKeyInfo(EciesEncryptorScope.ACTIVATION); + } + + @Override + public boolean containsKeyForEncryptor(int scope) { + try { + lock.lock(); + if (session.hasPublicKeyForEciesScope(scope)) { + final PublicKeyInfo publicKeyInfo = getPublicKeyInfoForScope(scope); + if (publicKeyInfo.expiration >= 0 && publicKeyInfo.expiration - EXPIRATION_THRESHOLD < timeService.getCurrentTime()) { + return true; + } + PowerAuthLog.d("Removing expired public key for ECIES encryptor " + scope); + publicKeyInfo.expiration = -1; + session.removePublicKeyForEciesScope(scope); + } + return false; + } finally { + lock.unlock(); + } + } + + @Override + @Nullable + public ICancelable createKeyForEncryptor(@EciesEncryptorScope int scope, @NonNull IPrivateCryptoHelper cryptoHelper, @NonNull ICreateKeyListener listener) { + if (scope == EciesEncryptorScope.ACTIVATION && !session.hasValidActivation()) { + callbackDispatcher.dispatchCallback(() -> listener.onCreateKeyFailed(new PowerAuthErrorException(PowerAuthErrorCodes.INVALID_ACTIVATION_STATE))); + return null; + } + try { + lock.lock(); + if (containsKeyForEncryptor(scope)) { + callbackDispatcher.dispatchCallback(listener::onCreateKeySucceeded); + return null; + } + final PublicKeyInfo publicKeyInfo = getPublicKeyInfoForScope(scope); + GetTemporaryKeyTask mainTask = publicKeyInfo.task; + if (mainTask == null) { + mainTask = new GetTemporaryKeyTask(scope, cryptoHelper, lock, callbackDispatcher, httpClient, this); + publicKeyInfo.task = mainTask; + publicKeyInfo.timeSynchronizationTask = timeService.startTimeSynchronizationTask(); + } + return mainTask.createChildTask(new ITaskCompletion<>() { + @Override + public void onSuccess(@NonNull GetTemporaryKeyResponse response) { + listener.onCreateKeySucceeded(); + } + + @Override + public void onFailure(@NonNull Throwable failure) { + listener.onCreateKeyFailed(failure); + } + }); + } finally { + lock.unlock(); + } + } + + @Override + public void onGetTemporaryKeyTaskCompletion(@NonNull GetTemporaryKeyTask task, @Nullable GetTemporaryKeyResponse response) { + final int scope = task.getScope(); + final PublicKeyInfo publicKeyInfo = getPublicKeyInfoForScope(scope); + publicKeyInfo.task = null; + if (response != null) { + final int errorCode = session.setPublicKeyForEciesScope(scope, response.getPublicKey(), response.getKeyId()); + if (errorCode == ErrorCode.OK) { + publicKeyInfo.expiration = response.getExpiration(); + timeService.completeTimeSynchronizationTask(publicKeyInfo.timeSynchronizationTask, response.getServerTime()); + PowerAuthLog.d("Saving public key for ECIES encryptor " + scope); + } else { + PowerAuthLog.e("Failed to update public key for ECIES encryption. Code = " + errorCode); + } + } + publicKeyInfo.timeSynchronizationTask = null; + } + + /** + * Get instance of {@link PublicKeyInfo} class depending on the scope. + * @param scope Scope of encryption. + * @return Instance of {@link PublicKeyInfo} class depending on the scope. + */ + @NonNull + private PublicKeyInfo getPublicKeyInfoForScope(@EciesEncryptorScope int scope) { + return scope == EciesEncryptorScope.APPLICATION ? applicationScopePublicKeyInfo : activationScopePublicKeyInfo; + } + + /** + * Internal class containing additional information about retrieved public key. + */ + private static class PublicKeyInfo { + /** + * Scope of the key. + */ + final @EciesEncryptorScope int scope; + /** + * If positive number, then contain timestamp when the key expires on the server. + */ + long expiration; + /** + * If not null, then service is currently retrieving the key from the server. + */ + GetTemporaryKeyTask task; + /** + * Time synchronization task. + */ + Object timeSynchronizationTask; + + PublicKeyInfo(@EciesEncryptorScope int scope) { + this.scope = scope; + this.expiration = -1; + this.task = null; + this.timeSynchronizationTask = null; + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/GetTemporaryKeyTask.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/GetTemporaryKeyTask.java new file mode 100644 index 00000000..393b5265 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/GetTemporaryKeyTask.java @@ -0,0 +1,242 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.sdk.impl; + +import android.text.TextUtils; +import android.util.Base64; +import android.util.Pair; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.google.gson.reflect.TypeToken; +import io.getlime.security.powerauth.core.*; +import io.getlime.security.powerauth.exception.PowerAuthErrorCodes; +import io.getlime.security.powerauth.exception.PowerAuthErrorException; +import io.getlime.security.powerauth.networking.client.HttpClient; +import io.getlime.security.powerauth.networking.client.JsonSerialization; +import io.getlime.security.powerauth.networking.endpoints.GetTemporaryKeyEndpoint; +import io.getlime.security.powerauth.networking.interfaces.ICancelable; +import io.getlime.security.powerauth.networking.interfaces.INetworkResponseListener; +import io.getlime.security.powerauth.networking.model.entity.JwtHeader; +import io.getlime.security.powerauth.networking.model.entity.JwtObject; +import io.getlime.security.powerauth.networking.model.request.GetTemporaryKeyRequest; +import io.getlime.security.powerauth.networking.model.response.GetTemporaryKeyResponse; + +import java.nio.charset.StandardCharsets; +import java.util.concurrent.locks.ReentrantLock; + +/** + * The {@code GetTemporaryKeyTask} class implements getting temporary encryption key from the server. + */ +public class GetTemporaryKeyTask extends GroupedTask { + + /** + * The task completion callback. + */ + public interface TaskCompletion { + /** + * Function is called once the {@code GetTemporaryKeyTask} finishes its job. + * @param task The completed task. + * @param response Response received from the server. If null, then task failed to get the response. + */ + void onGetTemporaryKeyTaskCompletion(@NonNull GetTemporaryKeyTask task, @Nullable GetTemporaryKeyResponse response); + } + + private final IPrivateCryptoHelper cryptoHelper; + private final HttpClient httpClient; + private final TaskCompletion taskCompletion; + private final @EciesEncryptorScope int scope; + private final JsonSerialization serialization; + + /** + * Construct task with required parameters. + * @param scope Scope of key to obtain from the server. + * @param cryptoHelper Instance of {@link IPrivateCryptoHelper} interface. + * @param sharedLock Reentrant lock shared across multiple SDK objects. + * @param dispatcher Callback dispatcher. + * @param httpClient HTTP client. + * @param completion Listener to call once the task is completed. + */ + public GetTemporaryKeyTask( + @EciesEncryptorScope int scope, + @NonNull IPrivateCryptoHelper cryptoHelper, + @NonNull ReentrantLock sharedLock, + @NonNull ICallbackDispatcher dispatcher, + @NonNull HttpClient httpClient, + @NonNull TaskCompletion completion) { + super("GetTemporaryKey", sharedLock, dispatcher); + this.scope = scope; + this.cryptoHelper = cryptoHelper; + this.httpClient = httpClient; + this.taskCompletion = completion; + this.serialization = new JsonSerialization(); + } + + /** + * Return the scope of the temporary key. + * @return Scope of the temporary key. + */ + public @EciesEncryptorScope int getScope() { + return scope; + } + + /** + * @return {@code true} if this task is configured to get the temporary key in application scope. + */ + private boolean isApplicationScope() { + return scope == EciesEncryptorScope.APPLICATION; + } + + @Override + public void onGroupedTaskStart() { + super.onGroupedTaskStart(); + try { + final Pair requestPair = prepareRequestJwt(); + if (requestPair == null) { + return; + } + ICancelable cancelable = httpClient.post(requestPair.second, new GetTemporaryKeyEndpoint(), cryptoHelper, new INetworkResponseListener<>() { + @Override + public void onNetworkResponse(@NonNull JwtObject jwtObject) { + try { + final GetTemporaryKeyResponse response = processResponseJwt(jwtObject); + validateResponse(requestPair.first, response); + complete(response); + } catch (Throwable t) { + complete(t); + } + } + + @Override + public void onNetworkError(@NonNull Throwable throwable) { + complete(throwable); + } + + @Override + public void onCancel() { + + } + }); + addCancelableOperation(cancelable); + } catch (Throwable t) { + complete(t); + } + } + + @Override + public void onGroupedTaskComplete(@Nullable GetTemporaryKeyResponse response, @Nullable Throwable failure) { + super.onGroupedTaskComplete(response, failure); + taskCompletion.onGetTemporaryKeyTaskCompletion(this, response); + } + + /** + * Prepare a pair of objects. The first object contains information before its encoded into JWT request. The second + * object is the same object, but encoded and signed as JWT. + * @return A pair of objects. The first object contains information before its encoded into JWT request. The second + * object is the same object, but encoded and signed as JWT. + */ + private Pair prepareRequestJwt() { + final Session session = cryptoHelper.getCoreSession(); + final SignatureUnlockKeys unlockKeys; + final String activationId; + final int signingKey; + if (isApplicationScope()) { + signingKey = SigningDataKey.HMAC_APPLICATION; + activationId = null; + unlockKeys = null; + } else { + signingKey = SigningDataKey.HMAC_ACTIVATION; + activationId = session.getActivationIdentifier(); + if (activationId == null) { + this.complete(new PowerAuthErrorException(PowerAuthErrorCodes.MISSING_ACTIVATION)); + return null; + } + unlockKeys = new SignatureUnlockKeys(cryptoHelper.getDeviceRelatedKey(), null, null); + } + // Prepare request data + final String applicationKey = session.getApplicationKey(); + final String challenge = Base64.encodeToString(CryptoUtils.randomBytes(18), Base64.NO_WRAP); + final GetTemporaryKeyRequest request = new GetTemporaryKeyRequest(applicationKey, activationId, challenge); + // Prepare JWT string + final String jwtHeader = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."; // {"alg":"HS256","typ":"JWT"} with dot separator + final String jwtPayload = serialization.serializeJwtObject(request); + final String jwtHeaderPlusPayload = jwtHeader + jwtPayload; + final SignedData dataToSign = new SignedData(jwtHeaderPlusPayload.getBytes(StandardCharsets.US_ASCII), null, signingKey, SignatureFormat.DEFAULT); + if (ErrorCode.OK != session.signDataWithHmacKey(dataToSign, unlockKeys)) { + this.complete(new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR, "Failed to calculate JWT signature")); + return null; + } + final String jwtSignature = Base64.encodeToString(dataToSign.signature, Base64.NO_WRAP | Base64.NO_PADDING | Base64.URL_SAFE); + final JwtObject jwtObject = new JwtObject(jwtHeaderPlusPayload + "." + jwtSignature); + return Pair.create(request, jwtObject); + } + + /** + * Process JWT response received from the server. + * @param response JWT response. + * @return Decoded payload extracted from the JWT response. + * @throws PowerAuthErrorException In case of failure. + */ + private GetTemporaryKeyResponse processResponseJwt(@NonNull JwtObject response) throws PowerAuthErrorException { + final String jwtString = response.jwt; + if (jwtString == null) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.NETWORK_ERROR, "Empty JWT response"); + } + final String[] jwtComponents = TextUtils.split(jwtString, "\\."); + if (jwtComponents.length != 3) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.NETWORK_ERROR, "Invalid JWT response"); + } + final String jwtHeader = jwtComponents[0]; + final String jwtPayload = jwtComponents[1]; + final String jwtSignature = jwtComponents[2]; + if (jwtHeader.isEmpty() || jwtPayload.isEmpty() || jwtSignature.isEmpty()) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.NETWORK_ERROR, "Invalid JWT response"); + } + final JwtHeader jwtHeaderObject = serialization.deserializeJwtObject(jwtHeader, TypeToken.get(JwtHeader.class)); + if (!"JWT".equals(jwtHeaderObject.typ)) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.NETWORK_ERROR, "Unsupported JWT type in response"); + } + if (!"ES256".equals(jwtHeaderObject.alg)) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.NETWORK_ERROR, "Unsupported JWT algorithm in response"); + } + final SignedData signedData = new SignedData( + (jwtHeader + "." + jwtPayload).getBytes(StandardCharsets.US_ASCII), + Base64.decode(jwtSignature, Base64.NO_WRAP| Base64.URL_SAFE | Base64.NO_PADDING), + isApplicationScope() ? SigningDataKey.ECDSA_MASTER_SERVER_KEY : SigningDataKey.ECDSA_PERSONALIZED_KEY, + SignatureFormat.ECDSA_JOSE); + if (ErrorCode.OK != cryptoHelper.getCoreSession().verifyServerSignedData(signedData)) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR, "Invalid signature in JWT response"); + } + return serialization.deserializeJwtObject(jwtPayload, TypeToken.get(GetTemporaryKeyResponse.class)); + } + + /** + * Validate whether values in response object match the important values form the request. + * @param request Request object. + * @param response Response object. + * @throws PowerAuthErrorException In case that important value doesn't match. + */ + private void validateResponse(@NonNull GetTemporaryKeyRequest request, @NonNull GetTemporaryKeyResponse response) throws PowerAuthErrorException { + boolean match = request.getChallenge().equals(response.getChallenge()); + match = match && request.getApplicationKey().equals(response.getApplicationKey()); + if (!isApplicationScope()) { + match = match && request.getActivationId() != null && request.getActivationId().equals(response.getActivationId()); + } + if (!match) { + throw new PowerAuthErrorException(PowerAuthErrorCodes.ENCRYPTION_ERROR, "JWT response doesn't match request"); + } + } +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/ICreateKeyListener.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/ICreateKeyListener.java new file mode 100644 index 00000000..35eef0ae --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/ICreateKeyListener.java @@ -0,0 +1,40 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.sdk.impl; + +import androidx.annotation.MainThread; +import androidx.annotation.NonNull; + +/** + * Listener for getting temporary encryption key from the server. The actual information about the key is stored in + * the low level {@code Session} object. + */ +public interface ICreateKeyListener { + + /** + * The temporary encryption key has been successfully acquired from the server. + */ + @MainThread + void onCreateKeySucceeded(); + + /** + * Failed to acquire the temporary encryption key from the server. + * @param throwable Failure to report. + */ + @MainThread + void onCreateKeyFailed(@NonNull Throwable throwable); +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IKeystoreService.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IKeystoreService.java new file mode 100644 index 00000000..c0c83b90 --- /dev/null +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IKeystoreService.java @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getlime.security.powerauth.sdk.impl; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.getlime.security.powerauth.core.EciesEncryptorScope; +import io.getlime.security.powerauth.networking.interfaces.ICancelable; + +/** + * The {@code IKeystoreService} is interface for getting temporary encryption keys for ECIES encryption from the server. + * The key itself is stored in {@link io.getlime.security.powerauth.core.Session} instance and is available for further + * encryption operations. + */ +public interface IKeystoreService { + /** + * Determine whether the service contains key for the requested encryption scope. + * @param scope The scope of the key. + * @return {@code true} if service contains a valid key for the requested encryption scope. + */ + boolean containsKeyForEncryptor(@EciesEncryptorScope int scope); + + /** + * Create a key for the requested encryptor scope. If the already exist and is valid, then does nothing. + * @param scope The scope of the key. + * @param cryptoHelper Implementation of {@link IPrivateCryptoHelper} interface. + * @param listener The listener where the result of the operation will be notified. + * @return Cancelable operation if communication with the server is required, or {@code null} if the result of + * the call has been determined immediately. + */ + @Nullable + ICancelable createKeyForEncryptor(@EciesEncryptorScope int scope, @NonNull IPrivateCryptoHelper cryptoHelper, @NonNull ICreateKeyListener listener); +} diff --git a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IPrivateCryptoHelper.java b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IPrivateCryptoHelper.java index b11a359a..8e7271ee 100644 --- a/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IPrivateCryptoHelper.java +++ b/proj-android/PowerAuthLibrary/src/main/java/io/getlime/security/powerauth/sdk/impl/IPrivateCryptoHelper.java @@ -20,6 +20,7 @@ import androidx.annotation.Nullable; import io.getlime.security.powerauth.core.EciesEncryptor; +import io.getlime.security.powerauth.core.Session; import io.getlime.security.powerauth.ecies.EciesEncryptorId; import io.getlime.security.powerauth.exception.PowerAuthErrorException; import io.getlime.security.powerauth.sdk.PowerAuthAuthentication; @@ -66,4 +67,15 @@ public interface IPrivateCryptoHelper { * {@link android.content.Context} object is not available for the crypto helper. */ @Nullable byte[] getDeviceRelatedKey(); + + /** + * @return Object implementing {@link IKeystoreService} interface. + */ + @NonNull IKeystoreService getKeystoreService(); + + /** + * @return Core {@link Session} object associated with this helper. + */ + @NonNull Session getCoreSession(); + } diff --git a/proj-android/configs/default-instrumentation-tests.properties b/proj-android/configs/default-instrumentation-tests.properties index 759a030a..f58da028 100644 --- a/proj-android/configs/default-instrumentation-tests.properties +++ b/proj-android/configs/default-instrumentation-tests.properties @@ -11,7 +11,7 @@ test.powerauth.restApiUrl=http://localhost:8080/enrollment-server test.powerauth.serverApiUrl=http://localhost:8080/powerauth-java-server test.powerauth.serverAuthUser= test.powerauth.serverAuthPass= -test.powerauth.serverVersion=1.5 +test.powerauth.serverVersion=1.9 test.powerauth.serverAutoCommit= test.powerauth.appName=AutomaticTest-Android test.powerauth.appVersion=default diff --git a/proj-xcode/PowerAuth2.xcodeproj/project.pbxproj b/proj-xcode/PowerAuth2.xcodeproj/project.pbxproj index d453d5f3..26c4563c 100644 --- a/proj-xcode/PowerAuth2.xcodeproj/project.pbxproj +++ b/proj-xcode/PowerAuth2.xcodeproj/project.pbxproj @@ -35,6 +35,8 @@ BF0825AB2632E64600B34E24 /* PowerAuthActivationCode.m in Sources */ = {isa = PBXBuildFile; fileRef = BF0825A72632E64600B34E24 /* PowerAuthActivationCode.m */; }; BF0899E22A97408500AF29E5 /* PowerAuthTimeSynchronizationService.h in Headers */ = {isa = PBXBuildFile; fileRef = BF0899E02A97408500AF29E5 /* PowerAuthTimeSynchronizationService.h */; settings = {ATTRIBUTES = (Public, ); }; }; BF0899E32A97408500AF29E5 /* PowerAuthTimeSynchronizationService.h in Headers */ = {isa = PBXBuildFile; fileRef = BF0899E02A97408500AF29E5 /* PowerAuthTimeSynchronizationService.h */; settings = {ATTRIBUTES = (Public, ); }; }; + BF15E1E82C6B990A003CCABB /* PA2ObjectSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BF15E1E12C6B990A003CCABB /* PA2ObjectSerializationTests.m */; }; + BF15E1E92C6B990A003CCABB /* PA2ObjectSerializationTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BF15E1E12C6B990A003CCABB /* PA2ObjectSerializationTests.m */; }; BF1EC6CF223BD3BB00883236 /* PA2CreateActivationRecoveryData.h in Headers */ = {isa = PBXBuildFile; fileRef = BF1EC6CD223BD3BB00883236 /* PA2CreateActivationRecoveryData.h */; }; BF1EC6D0223BD3BB00883236 /* PA2CreateActivationRecoveryData.m in Sources */ = {isa = PBXBuildFile; fileRef = BF1EC6CE223BD3BB00883236 /* PA2CreateActivationRecoveryData.m */; }; BF1EC6D7223BDF5500883236 /* PA2ConfirmRecoveryCodeRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = BF1EC6D5223BDF5500883236 /* PA2ConfirmRecoveryCodeRequest.h */; }; @@ -385,6 +387,14 @@ BF9D9C412174E3C7004FAE9C /* PA2UpgradeStartV3Response.h in Headers */ = {isa = PBXBuildFile; fileRef = BF9D9C3F2174E3C7004FAE9C /* PA2UpgradeStartV3Response.h */; }; BF9D9C422174E3C7004FAE9C /* PA2UpgradeStartV3Response.m in Sources */ = {isa = PBXBuildFile; fileRef = BF9D9C402174E3C7004FAE9C /* PA2UpgradeStartV3Response.m */; }; BFADA9C921B6C0CE001C6EC2 /* PowerAuthCustomHeaderRequestInterceptor.m in Sources */ = {isa = PBXBuildFile; fileRef = BFEC963D21B6990400FB5165 /* PowerAuthCustomHeaderRequestInterceptor.m */; }; + BFBD685C2C5A457C007AE16F /* PA2KeystoreService.m in Sources */ = {isa = PBXBuildFile; fileRef = BFBD685B2C5A457C007AE16F /* PA2KeystoreService.m */; }; + BFBD685D2C5A457C007AE16F /* PA2KeystoreService.h in Headers */ = {isa = PBXBuildFile; fileRef = BFBD685A2C5A457C007AE16F /* PA2KeystoreService.h */; }; + BFBD685E2C5A457C007AE16F /* PA2KeystoreService.h in Headers */ = {isa = PBXBuildFile; fileRef = BFBD685A2C5A457C007AE16F /* PA2KeystoreService.h */; }; + BFBD685F2C5A457C007AE16F /* PA2KeystoreService.m in Sources */ = {isa = PBXBuildFile; fileRef = BFBD685B2C5A457C007AE16F /* PA2KeystoreService.m */; }; + BFBD68742C5A838D007AE16F /* PA2JwtObject.m in Sources */ = {isa = PBXBuildFile; fileRef = BFBD68732C5A838D007AE16F /* PA2JwtObject.m */; }; + BFBD68752C5A838D007AE16F /* PA2JwtObject.h in Headers */ = {isa = PBXBuildFile; fileRef = BFBD68722C5A838D007AE16F /* PA2JwtObject.h */; }; + BFBD68762C5A838D007AE16F /* PA2JwtObject.h in Headers */ = {isa = PBXBuildFile; fileRef = BFBD68722C5A838D007AE16F /* PA2JwtObject.h */; }; + BFBD68772C5A838D007AE16F /* PA2JwtObject.m in Sources */ = {isa = PBXBuildFile; fileRef = BFBD68732C5A838D007AE16F /* PA2JwtObject.m */; }; BFC1925827FAF28F001455C1 /* TestHostAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1925727FAF28F001455C1 /* TestHostAppApp.swift */; }; BFC1925A27FAF28F001455C1 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC1925927FAF28F001455C1 /* ContentView.swift */; }; BFC1925C27FAF292001455C1 /* Assets-iOS.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFC1925B27FAF292001455C1 /* Assets-iOS.xcassets */; }; @@ -403,6 +413,18 @@ BFCEE09C216E280600B41201 /* PA2GetActivationStatusTask.h in Headers */ = {isa = PBXBuildFile; fileRef = BFCEE09A216E280600B41201 /* PA2GetActivationStatusTask.h */; }; BFCEE09D216E280600B41201 /* PA2GetActivationStatusTask.m in Sources */ = {isa = PBXBuildFile; fileRef = BFCEE09B216E280600B41201 /* PA2GetActivationStatusTask.m */; }; BFD386621F2B528600F74FF9 /* TestConfig in Resources */ = {isa = PBXBuildFile; fileRef = BFD386611F2B528600F74FF9 /* TestConfig */; }; + BFD3896E2C62640F0087F96D /* PA2GetTemporaryKeyRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = BFD3896D2C62640F0087F96D /* PA2GetTemporaryKeyRequest.m */; }; + BFD3896F2C62640F0087F96D /* PA2GetTemporaryKeyRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = BFD3896C2C62640F0087F96D /* PA2GetTemporaryKeyRequest.h */; }; + BFD389702C62640F0087F96D /* PA2GetTemporaryKeyRequest.m in Sources */ = {isa = PBXBuildFile; fileRef = BFD3896D2C62640F0087F96D /* PA2GetTemporaryKeyRequest.m */; }; + BFD389712C62640F0087F96D /* PA2GetTemporaryKeyRequest.h in Headers */ = {isa = PBXBuildFile; fileRef = BFD3896C2C62640F0087F96D /* PA2GetTemporaryKeyRequest.h */; }; + BFD389742C62647C0087F96D /* PA2GetTemporaryKeyResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = BFD389732C62647C0087F96D /* PA2GetTemporaryKeyResponse.m */; }; + BFD389752C62647C0087F96D /* PA2GetTemporaryKeyResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = BFD389722C62647C0087F96D /* PA2GetTemporaryKeyResponse.h */; }; + BFD389762C62647C0087F96D /* PA2GetTemporaryKeyResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = BFD389732C62647C0087F96D /* PA2GetTemporaryKeyResponse.m */; }; + BFD389772C62647C0087F96D /* PA2GetTemporaryKeyResponse.h in Headers */ = {isa = PBXBuildFile; fileRef = BFD389722C62647C0087F96D /* PA2GetTemporaryKeyResponse.h */; }; + BFD3897A2C626F7B0087F96D /* PA2GetTemporaryKeyTask.m in Sources */ = {isa = PBXBuildFile; fileRef = BFD389792C626F7B0087F96D /* PA2GetTemporaryKeyTask.m */; }; + BFD3897B2C626F7B0087F96D /* PA2GetTemporaryKeyTask.h in Headers */ = {isa = PBXBuildFile; fileRef = BFD389782C626F7B0087F96D /* PA2GetTemporaryKeyTask.h */; }; + BFD3897C2C626F7B0087F96D /* PA2GetTemporaryKeyTask.m in Sources */ = {isa = PBXBuildFile; fileRef = BFD389792C626F7B0087F96D /* PA2GetTemporaryKeyTask.m */; }; + BFD3897D2C626F7B0087F96D /* PA2GetTemporaryKeyTask.h in Headers */ = {isa = PBXBuildFile; fileRef = BFD389782C626F7B0087F96D /* PA2GetTemporaryKeyTask.h */; }; BFDA50362A9799BD0091A2E2 /* PowerAuthServerStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = BFDA50342A9799BD0091A2E2 /* PowerAuthServerStatus.h */; settings = {ATTRIBUTES = (Public, ); }; }; BFDA50372A9799BD0091A2E2 /* PowerAuthServerStatus.h in Headers */ = {isa = PBXBuildFile; fileRef = BFDA50342A9799BD0091A2E2 /* PowerAuthServerStatus.h */; settings = {ATTRIBUTES = (Public, ); }; }; BFDA50382A9799BD0091A2E2 /* PowerAuthServerStatus.m in Sources */ = {isa = PBXBuildFile; fileRef = BFDA50352A9799BD0091A2E2 /* PowerAuthServerStatus.m */; }; @@ -616,6 +638,7 @@ BF0825A62632E64600B34E24 /* PowerAuthActivationCode.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PowerAuthActivationCode.h; sourceTree = ""; }; BF0825A72632E64600B34E24 /* PowerAuthActivationCode.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PowerAuthActivationCode.m; sourceTree = ""; }; BF0899E02A97408500AF29E5 /* PowerAuthTimeSynchronizationService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PowerAuthTimeSynchronizationService.h; sourceTree = ""; }; + BF15E1E12C6B990A003CCABB /* PA2ObjectSerializationTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PA2ObjectSerializationTests.m; sourceTree = ""; }; BF1EC6CD223BD3BB00883236 /* PA2CreateActivationRecoveryData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PA2CreateActivationRecoveryData.h; sourceTree = ""; }; BF1EC6CE223BD3BB00883236 /* PA2CreateActivationRecoveryData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PA2CreateActivationRecoveryData.m; sourceTree = ""; }; BF1EC6D5223BDF5500883236 /* PA2ConfirmRecoveryCodeRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PA2ConfirmRecoveryCodeRequest.h; sourceTree = ""; }; @@ -759,6 +782,10 @@ BFAF730E1EAA84CA005E7572 /* PowerAuthTestServerModel.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PowerAuthTestServerModel.h; sourceTree = ""; }; BFAF730F1EAA84CA005E7572 /* PowerAuthTestServerModel.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PowerAuthTestServerModel.m; sourceTree = ""; }; BFB47D5B20753640008A6A52 /* PowerAuthCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; path = PowerAuthCore.xcodeproj; sourceTree = ""; }; + BFBD685A2C5A457C007AE16F /* PA2KeystoreService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PA2KeystoreService.h; sourceTree = ""; }; + BFBD685B2C5A457C007AE16F /* PA2KeystoreService.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PA2KeystoreService.m; sourceTree = ""; }; + BFBD68722C5A838D007AE16F /* PA2JwtObject.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PA2JwtObject.h; sourceTree = ""; }; + BFBD68732C5A838D007AE16F /* PA2JwtObject.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PA2JwtObject.m; sourceTree = ""; }; BFC1925527FAF28F001455C1 /* PowerAuth2TestsHostApp-ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "PowerAuth2TestsHostApp-ios.app"; sourceTree = BUILT_PRODUCTS_DIR; }; BFC1925727FAF28F001455C1 /* TestHostAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestHostAppApp.swift; sourceTree = ""; }; BFC1925927FAF28F001455C1 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -776,6 +803,12 @@ BFD386611F2B528600F74FF9 /* TestConfig */ = {isa = PBXFileReference; lastKnownFileType = folder; name = TestConfig; path = PowerAuth2IntegrationTests/TestConfig; sourceTree = SOURCE_ROOT; }; BFD386651F2B6BE700F74FF9 /* PowerAuthTestServerConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PowerAuthTestServerConfig.h; sourceTree = ""; }; BFD386661F2B6BE700F74FF9 /* PowerAuthTestServerConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PowerAuthTestServerConfig.m; sourceTree = ""; }; + BFD3896C2C62640F0087F96D /* PA2GetTemporaryKeyRequest.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PA2GetTemporaryKeyRequest.h; sourceTree = ""; }; + BFD3896D2C62640F0087F96D /* PA2GetTemporaryKeyRequest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PA2GetTemporaryKeyRequest.m; sourceTree = ""; }; + BFD389722C62647C0087F96D /* PA2GetTemporaryKeyResponse.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PA2GetTemporaryKeyResponse.h; sourceTree = ""; }; + BFD389732C62647C0087F96D /* PA2GetTemporaryKeyResponse.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PA2GetTemporaryKeyResponse.m; sourceTree = ""; }; + BFD389782C626F7B0087F96D /* PA2GetTemporaryKeyTask.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PA2GetTemporaryKeyTask.h; sourceTree = ""; }; + BFD389792C626F7B0087F96D /* PA2GetTemporaryKeyTask.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PA2GetTemporaryKeyTask.m; sourceTree = ""; }; BFDA50342A9799BD0091A2E2 /* PowerAuthServerStatus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PowerAuthServerStatus.h; sourceTree = ""; }; BFDA50352A9799BD0091A2E2 /* PowerAuthServerStatus.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PowerAuthServerStatus.m; sourceTree = ""; }; BFDA50512A98B34E0091A2E2 /* PA2TimeSynchronizationServiceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PA2TimeSynchronizationServiceTests.m; sourceTree = ""; }; @@ -887,6 +920,8 @@ BFFB0B9C2167A541004A06E2 /* PA2CreateActivationRequestData.m */, 1B2EEF5C1D662C960039D92A /* PA2GetActivationStatusRequest.h */, 1B2EEF5D1D662C960039D92A /* PA2GetActivationStatusRequest.m */, + BFD3896C2C62640F0087F96D /* PA2GetTemporaryKeyRequest.h */, + BFD3896D2C62640F0087F96D /* PA2GetTemporaryKeyRequest.m */, BFC581022316C4BC004FFFF7 /* PA2ValidateSignatureRequest.h */, BFC581032316C4BC004FFFF7 /* PA2ValidateSignatureRequest.m */, BF30DB5A206CEAE900430C12 /* PA2VaultUnlockRequest.h */, @@ -916,6 +951,8 @@ BF1EC6CE223BD3BB00883236 /* PA2CreateActivationRecoveryData.m */, 1B2EEF611D662C960039D92A /* PA2GetActivationStatusResponse.h */, 1B2EEF621D662C960039D92A /* PA2GetActivationStatusResponse.m */, + BFD389722C62647C0087F96D /* PA2GetTemporaryKeyResponse.h */, + BFD389732C62647C0087F96D /* PA2GetTemporaryKeyResponse.m */, 1B2EEF651D662C960039D92A /* PA2VaultUnlockResponse.h */, 1B2EEF661D662C960039D92A /* PA2VaultUnlockResponse.m */, BF7751101FC487BC008455A6 /* PA2GetTokenResponse.h */, @@ -938,6 +975,8 @@ 1B2EEF561D662C960039D92A /* PA2Codable.h */, BF585FC3215BC12D00DE49C3 /* PA2ObjectSerialization.h */, BF585FC4215BC12D00DE49C3 /* PA2ObjectSerialization.m */, + BFBD68722C5A838D007AE16F /* PA2JwtObject.h */, + BFBD68732C5A838D007AE16F /* PA2JwtObject.m */, ); name = serialization; sourceTree = ""; @@ -1092,15 +1131,14 @@ BF4BED282A9F7F1E00A8A2D0 /* PowerAuthServerStatus+Private.h */, BF6B39EF27F4777D00BDF579 /* PowerAuthExternalPendingOperation+Private.h */, BF28C1B2298168C100E2CD8E /* PowerAuthUserInfo+Private.h */, - BF585FD2215D328B00DE49C3 /* PA2PrivateCryptoHelper.h */, - BF2007522152B44F001F3614 /* PA2PrivateEncryptorFactory.h */, - BF2007532152B44F001F3614 /* PA2PrivateEncryptorFactory.m */, BF3890372A97861100D7A18E /* PA2TimeSynchronizationService.h */, BF3890382A97861100D7A18E /* PA2TimeSynchronizationService.m */, BF3890312A9783D300D7A18E /* PA2GetSystemStatusTask.h */, BF3890322A9783D300D7A18E /* PA2GetSystemStatusTask.m */, BFCEE09A216E280600B41201 /* PA2GetActivationStatusTask.h */, BFCEE09B216E280600B41201 /* PA2GetActivationStatusTask.m */, + BFD389782C626F7B0087F96D /* PA2GetTemporaryKeyTask.h */, + BFD389792C626F7B0087F96D /* PA2GetTemporaryKeyTask.m */, BF28AA1527EB1EDE00FBFCEB /* PA2SessionDataProvider.h */, BF28AA1627EB1EDE00FBFCEB /* PA2SessionDataProvider.m */, BFC192A027FC8C3F001455C1 /* PA2SessionInterface.h */, @@ -1108,6 +1146,11 @@ BF28AA1027EA0A1900FBFCEB /* PA2DefaultSessionInterface.m */, BF28AA0927EA09F000FBFCEB /* PA2SharedSessionInterface.h */, BF28AA0A27EA09F000FBFCEB /* PA2SharedSessionInterface.m */, + BFBD685A2C5A457C007AE16F /* PA2KeystoreService.h */, + BFBD685B2C5A457C007AE16F /* PA2KeystoreService.m */, + BF585FD2215D328B00DE49C3 /* PA2PrivateCryptoHelper.h */, + BF2007522152B44F001F3614 /* PA2PrivateEncryptorFactory.h */, + BF2007532152B44F001F3614 /* PA2PrivateEncryptorFactory.m */, ); name = sdk; sourceTree = ""; @@ -1190,6 +1233,7 @@ BF667EC12835A0B2006E97F8 /* PowerAuth2Tests */ = { isa = PBXGroup; children = ( + BF15E1E12C6B990A003CCABB /* PA2ObjectSerializationTests.m */, BFDA50512A98B34E0091A2E2 /* PA2TimeSynchronizationServiceTests.m */, BF667EC42835A0B2006E97F8 /* PA2AsyncOperationTests.m */, BF369FC9285370DD0004C454 /* PA2GroupedTaskTests.m */, @@ -1370,12 +1414,14 @@ BF5EB4E924C85FE200F9DDB2 /* PowerAuthToken.h in Headers */, BF0825952632E2BD00B34E24 /* PowerAuthActivationStatus+Private.h in Headers */, BF28AA1D27EB242400FBFCEB /* PowerAuthCoreSessionProvider.h in Headers */, + BFBD68762C5A838D007AE16F /* PA2JwtObject.h in Headers */, BF5EB4EA24C85FE200F9DDB2 /* PA2GetActivationStatusRequest.h in Headers */, BF5EB4EB24C85FE200F9DDB2 /* PA2CreateActivationResponseData.h in Headers */, BF5EB4EE24C85FE200F9DDB2 /* PA2GetActivationStatusResponse.h in Headers */, BF5EB4F024C85FE200F9DDB2 /* PA2RestApiObjects.h in Headers */, BFDA50572A98B6980091A2E2 /* PA2GetServerStatusResponse.h in Headers */, BF5EB4F124C85FE200F9DDB2 /* PA2PrivateTokenKeychainStore.h in Headers */, + BFD3897D2C626F7B0087F96D /* PA2GetTemporaryKeyTask.h in Headers */, BF5EB4F224C85FE200F9DDB2 /* PA2ObjectSerialization.h in Headers */, BF5EB4F324C85FE200F9DDB2 /* PA2Response.h in Headers */, BF4BED342AA08D2D00A8A2D0 /* PA2CompositeTask.h in Headers */, @@ -1404,6 +1450,7 @@ BF5EB50E24C85FE200F9DDB2 /* PA2PrivateHttpTokenProvider.h in Headers */, BF5EB50F24C85FE200F9DDB2 /* PA2ValidateSignatureRequest.h in Headers */, BF5EB51124C85FE200F9DDB2 /* PowerAuthKeychain.h in Headers */, + BFD389712C62640F0087F96D /* PA2GetTemporaryKeyRequest.h in Headers */, BF5EB51224C85FE200F9DDB2 /* PowerAuthConfiguration.h in Headers */, BFFECF2A26385BC9001DA7A9 /* PowerAuthActivationCode+Private.h in Headers */, BF4BED302A9F7F2400A8A2D0 /* PowerAuthServerStatus+Private.h in Headers */, @@ -1435,6 +1482,7 @@ BF5EB52724C85FE200F9DDB2 /* PA2Error+Decodable.h in Headers */, BF08255F263164F100B34E24 /* PowerAuthActivationRecoveryData.h in Headers */, BF5EB52824C85FE200F9DDB2 /* PowerAuthOperationTask.h in Headers */, + BFD389772C62647C0087F96D /* PA2GetTemporaryKeyResponse.h in Headers */, BF5EB52924C85FE200F9DDB2 /* PA2HttpRequest.h in Headers */, BF5EB52A24C85FE200F9DDB2 /* PA2PrivateRemoteTokenProvider.h in Headers */, BF5EB52B24C85FE200F9DDB2 /* PowerAuthClientConfiguration.h in Headers */, @@ -1444,6 +1492,7 @@ BF5EB52E24C85FE200F9DDB2 /* PA2GetActivationStatusTask.h in Headers */, BF1ED06F283CEB2700D6B380 /* PowerAuthKeychainAuthentication.h in Headers */, BF5EB52F24C85FE200F9DDB2 /* PA2CreateActivationResponse.h in Headers */, + BFBD685E2C5A457C007AE16F /* PA2KeystoreService.h in Headers */, BF08254B2631607C00B34E24 /* PowerAuthActivationStatus.h in Headers */, BFC192A227FC8C3F001455C1 /* PA2SessionInterface.h in Headers */, BF5EB53024C85FE200F9DDB2 /* PowerAuthRestApiErrorResponse.h in Headers */, @@ -1478,6 +1527,7 @@ BF585FCD215D239200DE49C3 /* PA2RestApiEndpoint.h in Headers */, BF3890392A97861100D7A18E /* PA2TimeSynchronizationService.h in Headers */, BF8CF4D82032EB41002A6B6E /* PowerAuthToken.h in Headers */, + BFBD685D2C5A457C007AE16F /* PA2KeystoreService.h in Headers */, BF8CF4D92032EB41002A6B6E /* PA2GetActivationStatusRequest.h in Headers */, BFFB0BA12167A9B4004A06E2 /* PA2CreateActivationResponseData.h in Headers */, BF0899E22A97408500AF29E5 /* PowerAuthTimeSynchronizationService.h in Headers */, @@ -1501,6 +1551,7 @@ BF8CF4E32032EB41002A6B6E /* PA2Codable.h in Headers */, BF4BED2F2A9F7F2300A8A2D0 /* PowerAuthServerStatus+Private.h in Headers */, BF8CF4E72032EB41002A6B6E /* PA2WCSessionPacket_TokenData.h in Headers */, + BFBD68752C5A838D007AE16F /* PA2JwtObject.h in Headers */, BF8CF4E82032EB41002A6B6E /* PA2PrivateTokenData.h in Headers */, BF8CF4E92032EB41002A6B6E /* PowerAuthClientSslNoValidationStrategy.h in Headers */, BF8CF4EA2032EB41002A6B6E /* PA2WCSessionPacket_ActivationStatus.h in Headers */, @@ -1557,8 +1608,10 @@ BF8CF5072032EB41002A6B6E /* PA2WCSessionPacket_Constants.h in Headers */, BF30DB5C206CEAE900430C12 /* PA2VaultUnlockRequest.h in Headers */, BFCEE09C216E280600B41201 /* PA2GetActivationStatusTask.h in Headers */, + BFD389752C62647C0087F96D /* PA2GetTemporaryKeyResponse.h in Headers */, BFDA50562A98B6980091A2E2 /* PA2GetServerStatusResponse.h in Headers */, BF8CF5082032EB41002A6B6E /* PA2CreateActivationResponse.h in Headers */, + BFD3897B2C626F7B0087F96D /* PA2GetTemporaryKeyTask.h in Headers */, BF28AA1127EA0A1900FBFCEB /* PA2DefaultSessionInterface.h in Headers */, BF8CF5092032EB41002A6B6E /* PowerAuthRestApiErrorResponse.h in Headers */, BF08256C2631670D00B34E24 /* PowerAuthActivationResult.h in Headers */, @@ -1566,6 +1619,7 @@ BF8CF50A2032EB41002A6B6E /* PowerAuthSDK.h in Headers */, BF8CF50B2032EB41002A6B6E /* PowerAuthRestApiError.h in Headers */, BF02103F2164C67F009745A2 /* PA2HttpClient.h in Headers */, + BFD3896F2C62640F0087F96D /* PA2GetTemporaryKeyRequest.h in Headers */, BF8CF50D2032EB41002A6B6E /* PA2PrivateMacros.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1928,6 +1982,7 @@ BF667ED52835A13F006E97F8 /* AsyncHelper.m in Sources */, BF369FCA285370DD0004C454 /* PA2GroupedTaskTests.m in Sources */, BF28C1C42982A05100E2CD8E /* PowerAuthUserInfoTests.m in Sources */, + BF15E1E82C6B990A003CCABB /* PA2ObjectSerializationTests.m in Sources */, BF667ED02835A0B2006E97F8 /* PowerAuthBasicHttpAuthenticationRequestInterceptorTests.m in Sources */, BF215F02287D8C9300EC3F3E /* PowerAuthAuthenticationTests.m in Sources */, BF97A72A28A3E594002F3ACE /* PowerAuthCorePasswordHelper.m in Sources */, @@ -1952,6 +2007,7 @@ BF5EB4A624C85FE200F9DDB2 /* PowerAuthClientSslNoValidationStrategy.m in Sources */, BF5EB4A724C85FE200F9DDB2 /* PA2GetActivationStatusResponse.m in Sources */, BF31607827E895DD00EDA287 /* PA2SharedLock.m in Sources */, + BFBD685F2C5A457C007AE16F /* PA2KeystoreService.m in Sources */, BF5EB4A824C85FE200F9DDB2 /* PowerAuthSDK+Private.m in Sources */, BF0825AB2632E64600B34E24 /* PowerAuthActivationCode.m in Sources */, BF5EB4A924C85FE200F9DDB2 /* PowerAuthSystem.m in Sources */, @@ -1963,6 +2019,7 @@ BF5EB4AD24C85FE200F9DDB2 /* PA2EncryptedResponse.m in Sources */, BF5EB4AE24C85FE200F9DDB2 /* PowerAuthClientConfiguration.m in Sources */, BFDA50592A98B6980091A2E2 /* PA2GetServerStatusResponse.m in Sources */, + BFD3897C2C626F7B0087F96D /* PA2GetTemporaryKeyTask.m in Sources */, BF5EB4AF24C85FE200F9DDB2 /* PA2RemoveTokenRequest.m in Sources */, BF5EB4B224C85FE200F9DDB2 /* PowerAuthSDK.m in Sources */, BF5EB4B324C85FE200F9DDB2 /* PowerAuthRestApiError.m in Sources */, @@ -1970,11 +2027,13 @@ BF28C1AB29815D6900E2CD8E /* PowerAuthUserInfo.m in Sources */, BF5EB4B524C85FE200F9DDB2 /* PA2CreateActivationRequestData.m in Sources */, BF08254D2631607C00B34E24 /* PowerAuthActivationStatus.m in Sources */, + BFD389702C62640F0087F96D /* PA2GetTemporaryKeyRequest.m in Sources */, BF1ED071283CEB2700D6B380 /* PowerAuthKeychainAuthentication.m in Sources */, BF5EB4B624C85FE200F9DDB2 /* PA2Request.m in Sources */, BF5EB4B724C85FE200F9DDB2 /* PA2VaultUnlockResponse.m in Sources */, BF5EB4B824C85FE200F9DDB2 /* PA2PrivateTokenData.m in Sources */, BFF711E6265D505E00DB696A /* PowerAuthToken+WatchSupport.m in Sources */, + BFD389762C62647C0087F96D /* PA2GetTemporaryKeyResponse.m in Sources */, BF5EB4BA24C85FE200F9DDB2 /* PA2CreateActivationRequest.m in Sources */, BF5EB4BC24C85FE200F9DDB2 /* PA2ValidateSignatureRequest.m in Sources */, BF3890362A9783D300D7A18E /* PA2GetSystemStatusTask.m in Sources */, @@ -2011,6 +2070,7 @@ BF28A9FF27E9D54100FBFCEB /* PA2AppGroupContainer.m in Sources */, BF38903C2A97861100D7A18E /* PA2TimeSynchronizationService.m in Sources */, BF5EB4D924C85FE200F9DDB2 /* PowerAuthActivation.m in Sources */, + BFBD68772C5A838D007AE16F /* PA2JwtObject.m in Sources */, BF5EB4DA24C85FE200F9DDB2 /* PA2CreateActivationRecoveryData.m in Sources */, BF1ED0BA2844D3D200D6B380 /* PA2GroupedTask.m in Sources */, BF5EB4DB24C85FE200F9DDB2 /* PowerAuthAuthorizationHttpHeader.m in Sources */, @@ -2028,6 +2088,7 @@ BF667ED42835A13E006E97F8 /* AsyncHelper.m in Sources */, BF369FCB285370DD0004C454 /* PA2GroupedTaskTests.m in Sources */, BF28C1C52982A05100E2CD8E /* PowerAuthUserInfoTests.m in Sources */, + BF15E1E92C6B990A003CCABB /* PA2ObjectSerializationTests.m in Sources */, BF667ED12835A0B2006E97F8 /* PowerAuthBasicHttpAuthenticationRequestInterceptorTests.m in Sources */, BF215F03287D8C9300EC3F3E /* PowerAuthAuthenticationTests.m in Sources */, BF97A72B28A3E594002F3ACE /* PowerAuthCorePasswordHelper.m in Sources */, @@ -2087,6 +2148,7 @@ files = ( BF28A9FE27E9D54100FBFCEB /* PA2AppGroupContainer.m in Sources */, BF8BFBE5215B8760001D6852 /* PA2HttpRequest.m in Sources */, + BFBD68742C5A838D007AE16F /* PA2JwtObject.m in Sources */, BF8CF4812032EB41002A6B6E /* PA2PrivateTokenKeychainStore.m in Sources */, BF1ED0C52846274700D6B380 /* PA2CreateTokenTask.m in Sources */, BF8CF4822032EB41002A6B6E /* PowerAuthClientSslNoValidationStrategy.m in Sources */, @@ -2100,6 +2162,7 @@ BFDA50382A9799BD0091A2E2 /* PowerAuthServerStatus.m in Sources */, BF8CF4862032EB41002A6B6E /* PowerAuthToken.m in Sources */, BF8CF4892032EB41002A6B6E /* PA2Response.m in Sources */, + BFBD685C2C5A457C007AE16F /* PA2KeystoreService.m in Sources */, BF28AA2E27ECA34900FBFCEB /* PowerAuthExternalPendingOperation.m in Sources */, BF8CF48B2032EB41002A6B6E /* PA2EncryptedResponse.m in Sources */, BF8BFBEF215BBA7B001D6852 /* PowerAuthClientConfiguration.m in Sources */, @@ -2114,6 +2177,7 @@ BF08256E2631670D00B34E24 /* PowerAuthActivationResult.m in Sources */, BF1ED070283CEB2700D6B380 /* PowerAuthKeychainAuthentication.m in Sources */, BF38903B2A97861100D7A18E /* PA2TimeSynchronizationService.m in Sources */, + BFD389742C62647C0087F96D /* PA2GetTemporaryKeyResponse.m in Sources */, BFFB0B9E2167A541004A06E2 /* PA2CreateActivationRequestData.m in Sources */, BF8CF4962032EB41002A6B6E /* PA2Request.m in Sources */, BF8CF4982032EB41002A6B6E /* PA2VaultUnlockResponse.m in Sources */, @@ -2141,6 +2205,8 @@ BF8CF4B72032EB41002A6B6E /* PowerAuthConfiguration.m in Sources */, BF2007552152B44F001F3614 /* PA2PrivateEncryptorFactory.m in Sources */, BF28AA2227EB4E8A00FBFCEB /* PA2Result.m in Sources */, + BFD3897A2C626F7B0087F96D /* PA2GetTemporaryKeyTask.m in Sources */, + BFD3896E2C62640F0087F96D /* PA2GetTemporaryKeyRequest.m in Sources */, BF8CF4B92032EB41002A6B6E /* PA2WCSessionPacket.m in Sources */, BFFB0BA22167A9B4004A06E2 /* PA2CreateActivationResponseData.m in Sources */, BF8CF4BC2032EB41002A6B6E /* PA2GetActivationStatusRequest.m in Sources */, diff --git a/proj-xcode/PowerAuth2/PowerAuthSDK.h b/proj-xcode/PowerAuth2/PowerAuthSDK.h index fc9d317b..f83f8d5c 100644 --- a/proj-xcode/PowerAuth2/PowerAuthSDK.h +++ b/proj-xcode/PowerAuth2/PowerAuthSDK.h @@ -593,25 +593,53 @@ /** Creates a new instance of ECIES encryptor suited for application's general end-to-end encryption purposes. The returned encryptor is - cryptographically bounded to the PowerAuth configuration, so it can be used with or without a valid activation. The encryptor also contains + cryptographically bound to the PowerAuth configuration, so it can be used with or without a valid activation. The encryptor also contains an associated `PowerAuthCoreEciesMetaData` object, allowing you to properly setup HTTP header for the request. + @return PowerAuthOperationTask associated with the running request or nil if the result of the function is available immediately. + */ +- (nullable id) eciesEncryptorForApplicationScopeWithCallback:(nonnull void(^)(PowerAuthCoreEciesEncryptor * _Nullable encryptor, NSError * _Nullable error))callback; + +/** + Creates a new instance of ECIES encryptor suited for application's general end-to-end encryption purposes. The returned encryptor is + cryptographically bound to a device's activation, so it can be used only when this instance has a valid activation. The encryptor also contains + an associated `PowerAuthCoreEciesMetaData` object, allowing you to properly setup HTTP header for the request. + + Note that the created encryptor has no reference to this instance of `PowerAuthSDK`. This means that if the `PowerAuthSDK` will loose its + activation in future, then the encryptor will still be capable to encrypt, or decrypt the data. This is an expected behavior, so if you + plan to keep the encryptor for multiple requests, then it's up to you to release its instance after you change the state of PowerAuthSDK. + + @return PowerAuthOperationTask associated with the running request or nil if the result of the function is available immediately. + */ +- (nullable id) eciesEncryptorForActivationScopeWithCallback:(nonnull void(^)(PowerAuthCoreEciesEncryptor * _Nullable encryptor, NSError * _Nullable error))callback; + +/** + Creates a new instance of ECIES encryptor suited for application's general end-to-end encryption purposes. The returned encryptor is + cryptographically bound to the PowerAuth configuration, so it can be used with or without a valid activation. The encryptor also contains + an associated `PowerAuthCoreEciesMetaData` object, allowing you to properly setup HTTP header for the request. + + Be aware that this method is deprecated because doesn't guarantee that encryptor is provided, or the temporary encryption + key is still valid. You should use the new asynchronous function as a replacement. + @return New instance of `PowerAuthCoreEciesEncryptor` object or nil if `PowerAuthConfiguration` contains an invalid data. */ -- (nullable PowerAuthCoreEciesEncryptor*) eciesEncryptorForApplicationScope; +- (nullable PowerAuthCoreEciesEncryptor*) eciesEncryptorForApplicationScope PA2_DEPRECATED(1.9.0); /** Creates a new instance of ECIES encryptor suited for application's general end-to-end encryption purposes. The returned encryptor is - cryptographically bounded to a device's activation, so it can be used only when this instance has a valid activation. The encryptor also contains + cryptographically bound to a device's activation, so it can be used only when this instance has a valid activation. The encryptor also contains an associated `PowerAuthCoreEciesMetaData` object, allowing you to properly setup HTTP header for the request. Note that the created encryptor has no reference to this instance of `PowerAuthSDK`. This means that if the `PowerAuthSDK` will loose its activation in future, then the encryptor will still be capable to encrypt, or decrypt the data. This is an expected behavior, so if you plan to keep the encryptor for multiple requests, then it's up to you to release its instance after you change the state of PowerAuthSDK. + Be aware that this method is deprecated because doesn't guarantee that encryptor is provided, or the temporary encryption + key is still valid. You should use the new asynchronous function as a replacement. + @return New instance of `PowerAuthCoreEciesEncryptor` object or nil if there's no valid activation. */ -- (nullable PowerAuthCoreEciesEncryptor*) eciesEncryptorForActivationScope; +- (nullable PowerAuthCoreEciesEncryptor*) eciesEncryptorForActivationScope PA2_DEPRECATED(1.9.0); @end diff --git a/proj-xcode/PowerAuth2/PowerAuthSDK.m b/proj-xcode/PowerAuth2/PowerAuthSDK.m index 85d6a83a..801dcf77 100644 --- a/proj-xcode/PowerAuth2/PowerAuthSDK.m +++ b/proj-xcode/PowerAuth2/PowerAuthSDK.m @@ -26,6 +26,7 @@ #import "PA2AsyncOperation.h" #import "PA2ObjectSerialization.h" +#import "PA2KeystoreService.h" #import "PA2TimeSynchronizationService.h" #import "PA2PrivateTokenKeychainStore.h" #import "PA2PrivateHttpTokenProvider.h" @@ -61,6 +62,7 @@ @implementation PowerAuthSDK PowerAuthKeychainConfiguration * _keychainConfiguration; PowerAuthClientConfiguration * _clientConfiguration; + PA2KeystoreService * _keystoreService; PA2TimeSynchronizationService * _timeSynchronizationService; id _tokenStore; PA2HttpClient *_client; @@ -269,10 +271,16 @@ - (NSString*) privateInstanceId return _configuration.instanceId; } -/** - This private method checks for valid PowerAuthCoreSessionSetup and throws a PowerAuthExceptionMissingConfig exception when the provided configuration - is not correct or is missing. - */ +- (id) sessionProvider +{ + return _sessionInterface; +} + +- (id) sessionInterface +{ + return _sessionInterface; // same as "sessionProvider" but exposes private interfaces +} + - (void) checkForValidSetup { // This is OK to directly access _coreSession without a proper locking. Setup depends on runtime configuration, @@ -284,6 +292,21 @@ - (void) checkForValidSetup #pragma mark - Key management +- (PA2KeystoreService*) keystoreService +{ + [_lock lock]; + if (!_keystoreService) { + // Create keystore service + _keystoreService = [[PA2KeystoreService alloc] initWithHttpClient:_client + timeService:_timeSynchronizationService + deviceRelatedKey:[self deviceRelatedKey] + sessionSetup:_coreSession.sessionSetup + sharedLock:_lock]; + } + [_lock unlock]; + return _keystoreService; +} + - (NSData*) deviceRelatedKey { // Cache the possession key in the keychain @@ -540,11 +563,6 @@ - (BOOL) hasPendingProtocolUpgrade return _sessionInterface.hasPendingProtocolUpgrade; } -- (id) sessionProvider -{ - return _sessionInterface; -} - - (void) cancelAllPendingTasks { [_getActivationStatusTask cancel]; @@ -588,27 +606,33 @@ - (void) cancelAllPendingTasks requestData.platform = [PowerAuthSystem platform]; requestData.deviceInfo = [PowerAuthSystem deviceInfo]; - PowerAuthCoreEciesEncryptor * decryptor = [[_sessionInterface writeTaskWithSession:^id _Nullable(PowerAuthCoreSession * _Nonnull session) { + // Start an activation + error = [_sessionInterface writeTaskWithSession:^NSError*(PowerAuthCoreSession * session) { return [self prepareActivation:activation forRequest:request requestData:requestData session:session]; - }] extractResult:&error]; - - if (!decryptor) { + }]; + if (error) { callback(nil, error); return nil; } - + // The create activation endpoint needs a custom object processing where we encrypt the inner data // with a different encryptor. We have to do this in the HTTP client's queue to guarantee that time - // service is already synchronized. - PA2RestApiEndpoint * endpoint = [PA2RestApiEndpoint createActivationWithCustomStep:^NSError *{ + // service is already synchronized and tempoerary key is acquired. + PA2RestApiEndpoint * endpoint = [PA2RestApiEndpoint createActivationWithCustomStep:^NSError*(PA2RestApiEndpoint * endpoint) { // Encrypt payload and put it directly to the request object. NSError * localError = nil; - request.activationData = [PA2ObjectSerialization encryptObject:requestData - encryptor:decryptor - error:&localError]; + PowerAuthCoreEciesEncryptor * decryptor = [self encryptorWithId:PA2EncryptorId_ActivationPayload error:&localError];; + if (decryptor && !localError) { + request.activationData = [PA2ObjectSerialization encryptObject:requestData + encryptor:decryptor + error:&localError]; + } + if (!localError) { + endpoint.customData = decryptor; + } return localError; }]; @@ -622,10 +646,10 @@ - (void) cancelAllPendingTasks if (status == PowerAuthRestApiResponseStatus_OK) { // Validate response from the server return [self validateActivationResponse:response - decryptor:decryptor + decryptor:endpoint.customData session:session]; } - [session resetSession]; + [session resetSession:NO]; return [PA2Result failure:error ? error : PA2MakeError(PowerAuthErrorCode_InvalidActivationData, nil)]; }] extractResult:&error]; @@ -801,10 +825,10 @@ - (NSString*) activationFingerprint The method requires request & request data and if everything's right, then request.activationData is prepared and metods returns a new decryptor, required for response decryption. */ -- (PA2Result*) prepareActivation:(PowerAuthActivation*)activation - forRequest:(PA2CreateActivationRequest*)request - requestData:(PA2CreateActivationRequestData*)requestData - session:(PowerAuthCoreSession*)session +- (NSError*) prepareActivation:(PowerAuthActivation*)activation + forRequest:(PA2CreateActivationRequest*)request + requestData:(PA2CreateActivationRequestData*)requestData + session:(PowerAuthCoreSession*)session { BOOL resetState = YES; NSError * localError = nil; @@ -819,16 +843,8 @@ - (NSString*) activationFingerprint if (resultStep1) { // Keep device's public key in requestData requestData.devicePublicKey = resultStep1.devicePublicKey; - - // Now we need to ecrypt request data with the Layer2 encryptor. - PowerAuthCoreEciesEncryptor * privateEncryptor = [self encryptorWithId:PA2EncryptorId_ActivationPayload error:&localError]; - if (!localError) { - // Everything looks OS, so finally, try notify other apps that this instance started the activation. - localError = [_sessionInterface startExternalPendingOperation:PowerAuthExternalPendingOperationType_Activation]; - if (!localError) { - return [PA2Result success:privateEncryptor]; - } - } + // Everything looks OS, so finally, try notify other apps that this instance started the activation. + localError = [_sessionInterface startExternalPendingOperation:PowerAuthExternalPendingOperationType_Activation]; } else { localError = PA2MakeError(PowerAuthErrorCode_InvalidActivationData, nil); } @@ -836,10 +852,10 @@ - (NSString*) activationFingerprint resetState = NO; // Don't reset state, there's already existing or pendign activation localError = PA2MakeError(PowerAuthErrorCode_InvalidActivationState, nil); } - if (resetState) { - [session resetSession]; + if (localError && resetState) { + [session resetSession:NO]; } - return [PA2Result failure:localError]; + return localError; } /** @@ -884,7 +900,7 @@ - (NSString*) activationFingerprint } } // If failure, then reset session and report error. - [session resetSession]; + [session resetSession:NO]; return [PA2Result failure:localError]; } @@ -987,7 +1003,7 @@ - (void) removeActivationLocal PowerAuthLog(@"Removing activaton data from keychain failed. We can't recover from this error."); } [_tokenStore removeAllLocalTokens]; - [session resetSession]; + [session resetSession:NO]; }]; } @@ -1114,7 +1130,6 @@ - (PowerAuthCoreHTTPRequestDataSignature*) signHttpRequestData:(PowerAuthCoreHTT }] extractResult:error]; } - - (BOOL) verifyServerSignedData:(nonnull NSData*)data signature:(nonnull NSString*)signature masterKey:(BOOL)masterKey @@ -1125,6 +1140,7 @@ - (BOOL) verifyServerSignedData:(nonnull NSData*)data signedData.signingDataKey = masterKey ? PowerAuthCoreSigningDataKey_ECDSA_MasterServerKey : PowerAuthCoreSigningDataKey_ECDSA_PersonalizedKey; signedData.data = data; signedData.signatureBase64 = signature; + signedData.signatureFormat = PowerAuthCoreSignatureFormat_ECDSA_DER; return [session verifyServerSignedData: signedData]; }]; } @@ -1464,6 +1480,7 @@ - (NSData*) generateInvalidBiometricKey - (id) signDataWithDevicePrivateKey:(PowerAuthAuthentication*)authentication data:(NSData*)data + format:(PowerAuthCoreSignatureFormat)format callback:(void(^)(NSData *signature, NSError *error))callback { return [self fetchEncryptedVaultUnlockKey:authentication reason:PA2VaultUnlockReason_SIGN_WITH_DEVICE_PRIVATE_KEY callback:^(NSString *encryptedEncryptionKey, NSError *error) { @@ -1475,7 +1492,8 @@ - (NSData*) generateInvalidBiometricKey signature = [_sessionInterface readTaskWithSession:^id (PowerAuthCoreSession * session) { return [session signDataWithDevicePrivateKey:encryptedEncryptionKey keys:keys - data:data]; + data:data + format:format]; }]; // Propagate error if (!signature) { @@ -1485,37 +1503,44 @@ - (NSData*) generateInvalidBiometricKey // Call back to application callback(signature, error); }]; + +} + +- (id) signDataWithDevicePrivateKey:(PowerAuthAuthentication*)authentication + data:(NSData*)data + callback:(void(^)(NSData *signature, NSError *error))callback +{ + return [self signDataWithDevicePrivateKey:authentication + data:data + format:PowerAuthCoreSignatureFormat_ECDSA_DER + callback:callback]; } - (id) signJwtWithDevicePrivateKey:(PowerAuthAuthentication*)authentication claims:(NSDictionary*)claims callback:(void(^)(NSString *jwt, NSError *error))callback { + // Prepare JWT Header + NSString * jwtHeader = @"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9."; // {"alg":"ES256","typ":"JWT"} // Prepare claims data - NSData *claimsData = [NSJSONSerialization dataWithJSONObject:claims options:0 error:nil]; + NSData * claimsData = [NSJSONSerialization dataWithJSONObject:claims options:0 error:nil]; + // Prepare data for signing + NSString * signedData = [jwtHeader stringByAppendingString:[claimsData jwtEncodedString]]; + // Calculate signature return [self signDataWithDevicePrivateKey:authentication - data:claimsData - callback:^(NSData * _Nullable signature, NSError * _Nullable error) { + data:[signedData dataUsingEncoding:NSASCIIStringEncoding] + format:PowerAuthCoreSignatureFormat_ECDSA_JOSE + callback:^(NSData * signature, NSError * error) { // Handle error if (error) { callback(nil, error); return; } - - // Prepare JWT Header - NSDictionary *header = @{ @"alg": @"ES256", @"typ": @"JWT" }; - NSData *headerData = [NSJSONSerialization dataWithJSONObject:header options:0 error:nil]; - NSString *headerBase64Encoded = [headerData base64EncodedStringWithOptions:0]; - - // Base64 Encode Claims Data - NSString *claimsBase64Encoded = [claimsData base64EncodedStringWithOptions:0]; - // Base64 Encode Signature - NSString *signatureBase64Encoded = [signature base64EncodedStringWithOptions:0]; - + NSString *jwtSignature = [signature jwtEncodedString]; // Construct JWT - NSString *jwt = [NSString stringWithFormat:@"%@.%@.%@", headerBase64Encoded, claimsBase64Encoded, signatureBase64Encoded]; - + NSString *jwt = [[signedData stringByAppendingString:@"."] stringByAppendingString:jwtSignature]; + // Call back to application callback(jwt, nil); }]; } @@ -1526,25 +1551,63 @@ - (NSData*) generateInvalidBiometricKey @implementation PowerAuthSDK (E2EE) -- (PowerAuthCoreEciesEncryptor*) eciesEncryptorForApplicationScope +- (id) eciesEncryptorForApplicationScopeWithCallback:(void (^)(PowerAuthCoreEciesEncryptor *, NSError *))callback { - PA2PrivateEncryptorFactory * factory = [[PA2PrivateEncryptorFactory alloc] initWithSessionProvider:_sessionInterface deviceRelatedKey:nil]; - return [factory encryptorWithId:PA2EncryptorId_GenericApplicationScope error:nil]; + return [self eciesEncryptorWithScope:PowerAuthCoreEciesEncryptorScope_Application callback:callback]; } -- (PowerAuthCoreEciesEncryptor*) eciesEncryptorForActivationScope +- (id) eciesEncryptorForActivationScopeWithCallback:(void (^)(PowerAuthCoreEciesEncryptor *, NSError *))callback { - return [_sessionInterface readTaskWithSession:^id (PowerAuthCoreSession * session) { - if (!session.hasValidActivation) { - PowerAuthLog(@"eciesEncryptorForActivation: There's no activation."); - return nil; + return [self eciesEncryptorWithScope:PowerAuthCoreEciesEncryptorScope_Activation callback:callback]; +} + +// Private + +- (id) eciesEncryptorWithScope:(PowerAuthCoreEciesEncryptorScope)scope + callback:(void (^)(PowerAuthCoreEciesEncryptor *, NSError *))callback +{ + return [_keystoreService createKeyForEncryptorScope:scope callback:^(NSError * error) { + PowerAuthCoreEciesEncryptor * encryptor; + if (!error) { + encryptor = [self eciesEncryptorWithScope:scope error:&error]; + } else { + encryptor = nil; } - NSData * deviceKey = [self deviceRelatedKey]; - PA2PrivateEncryptorFactory * factory = [[PA2PrivateEncryptorFactory alloc] initWithSessionProvider:_sessionInterface deviceRelatedKey:deviceKey]; - return [factory encryptorWithId:PA2EncryptorId_GenericActivationScope error:nil]; + callback(encryptor, error); }]; } +- (PowerAuthCoreEciesEncryptor*) eciesEncryptorWithScope:(PowerAuthCoreEciesEncryptorScope)scope error:(NSError**)error +{ + if (scope == PowerAuthCoreEciesEncryptorScope_Activation) { + return [[_sessionInterface readTaskWithSession:^PA2Result*(PowerAuthCoreSession * session) { + if (!session.hasValidActivation) { + return [PA2Result failure:PA2MakeError(PowerAuthErrorCode_MissingActivation, nil)]; + } + NSError * error = nil; + NSData * deviceKey = [self deviceRelatedKey]; + PA2PrivateEncryptorFactory * factory = [[PA2PrivateEncryptorFactory alloc] initWithSessionProvider:_sessionInterface deviceRelatedKey:deviceKey]; + PowerAuthCoreEciesEncryptor * encryptor = [factory encryptorWithId:PA2EncryptorId_GenericActivationScope error:&error]; + return [PA2Result success:encryptor orFailure:error]; + }] extractResult:error]; + } else { + PA2PrivateEncryptorFactory * factory = [[PA2PrivateEncryptorFactory alloc] initWithSessionProvider:_sessionInterface deviceRelatedKey:nil]; + return [factory encryptorWithId:PA2EncryptorId_GenericApplicationScope error:error]; + } +} + +// PA2_DEPRECATED(1.9.0) +- (PowerAuthCoreEciesEncryptor*) eciesEncryptorForApplicationScope +{ + return [self eciesEncryptorWithScope:PowerAuthCoreEciesEncryptorScope_Application error:nil]; +} + +// PA2_DEPRECATED(1.9.0) +- (PowerAuthCoreEciesEncryptor*) eciesEncryptorForActivationScope +{ + return [self eciesEncryptorWithScope:PowerAuthCoreEciesEncryptorScope_Activation error:nil]; +} + @end diff --git a/proj-xcode/PowerAuth2/PowerAuthToken.m b/proj-xcode/PowerAuth2/PowerAuthToken.m index 3fd7b50a..e6c439f1 100644 --- a/proj-xcode/PowerAuth2/PowerAuthToken.m +++ b/proj-xcode/PowerAuth2/PowerAuthToken.m @@ -99,7 +99,7 @@ - (PowerAuthAuthorizationHttpHeader*) generateHeader tokenIdentifier = _tokenData.identifier; // Prepare data for HMAC - NSString * protocolVersion = @"3.2"; + NSString * protocolVersion = @"3.3"; NSNumber * currentTimeMs = @((int64_t)([timeService currentTime] * 1000.0)); NSString * currentTimeString = [currentTimeMs stringValue]; NSData * currentTimeData = [currentTimeString dataUsingEncoding:NSASCIIStringEncoding]; diff --git a/proj-xcode/PowerAuth2/private/PA2DefaultSessionInterface.m b/proj-xcode/PowerAuth2/private/PA2DefaultSessionInterface.m index f863da2e..b33e7b53 100644 --- a/proj-xcode/PowerAuth2/private/PA2DefaultSessionInterface.m +++ b/proj-xcode/PowerAuth2/private/PA2DefaultSessionInterface.m @@ -68,7 +68,7 @@ - (void) loadState if (statusData) { [_session deserializeState:statusData]; } else { - [_session resetSession]; + [_session resetSession:NO]; } _stateBefore = [_session serializedState]; @@ -159,7 +159,7 @@ - (void) writeVoidTaskWithSession:(void (NS_NOESCAPE ^)(PowerAuthCoreSession *)) - (void) resetSession { WRITE_ACCESS_LOCK(); - [_session resetSession]; + [_session resetSession:NO]; WRITE_ACCESS_UNLOCK(); } diff --git a/proj-xcode/PowerAuth2/private/PA2EncryptedRequest.h b/proj-xcode/PowerAuth2/private/PA2EncryptedRequest.h index db127c4e..b03d4700 100644 --- a/proj-xcode/PowerAuth2/private/PA2EncryptedRequest.h +++ b/proj-xcode/PowerAuth2/private/PA2EncryptedRequest.h @@ -22,10 +22,6 @@ - (id) initWithCryptogram:(PowerAuthCoreEciesCryptogram*)cryptogram; -@property (nonatomic, strong) NSString * ephemeralPublicKey; -@property (nonatomic, strong) NSString * encryptedData; -@property (nonatomic, strong) NSString * mac; -@property (nonatomic, strong) NSString * nonce; -@property (nonatomic, assign) UInt64 timestamp; +@property (nonatomic, readonly, strong) PowerAuthCoreEciesCryptogram * cryptogram; @end diff --git a/proj-xcode/PowerAuth2/private/PA2EncryptedRequest.m b/proj-xcode/PowerAuth2/private/PA2EncryptedRequest.m index c7218acf..7a1cebf0 100644 --- a/proj-xcode/PowerAuth2/private/PA2EncryptedRequest.m +++ b/proj-xcode/PowerAuth2/private/PA2EncryptedRequest.m @@ -25,32 +25,14 @@ - (id) initWithCryptogram:(PowerAuthCoreEciesCryptogram*)cryptogram { self = [super init]; if (self) { - _ephemeralPublicKey = cryptogram.keyBase64; - _encryptedData = cryptogram.bodyBase64; - _mac = cryptogram.macBase64; - _nonce = cryptogram.nonceBase64; - _timestamp = cryptogram.timestamp; + _cryptogram = cryptogram; } return self; } - (NSDictionary*) toDictionary { - NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithCapacity:3]; - if (_ephemeralPublicKey) { - dict[@"ephemeralPublicKey"] = _ephemeralPublicKey; - } - if (_encryptedData) { - dict[@"encryptedData"] = _encryptedData; - } - if (_mac) { - dict[@"mac"] = _mac; - } - if (_nonce) { - dict[@"nonce"] = _nonce; - } - dict[@"timestamp"] = @(_timestamp); - return dict; + return [_cryptogram requestPayload]; } @end diff --git a/proj-xcode/PowerAuth2/private/PA2EncryptedResponse.h b/proj-xcode/PowerAuth2/private/PA2EncryptedResponse.h index caf343a2..f5af5821 100644 --- a/proj-xcode/PowerAuth2/private/PA2EncryptedResponse.h +++ b/proj-xcode/PowerAuth2/private/PA2EncryptedResponse.h @@ -20,11 +20,6 @@ @interface PA2EncryptedResponse: NSObject -@property (nonatomic, strong) NSString * encryptedData; -@property (nonatomic, strong) NSString * mac; -@property (nonatomic, strong) NSString * nonce; -@property (nonatomic, assign) UInt64 timestamp; - -- (PowerAuthCoreEciesCryptogram*) cryptogram; +@property (nonatomic, readonly, strong) PowerAuthCoreEciesCryptogram * cryptogram; @end diff --git a/proj-xcode/PowerAuth2/private/PA2EncryptedResponse.m b/proj-xcode/PowerAuth2/private/PA2EncryptedResponse.m index 4b027c74..8de04d68 100644 --- a/proj-xcode/PowerAuth2/private/PA2EncryptedResponse.m +++ b/proj-xcode/PowerAuth2/private/PA2EncryptedResponse.m @@ -25,22 +25,9 @@ - (instancetype) initWithDictionary:(NSDictionary *)dict { self = [super init]; if (self) { - _encryptedData = PA2ObjectAs(dict[@"encryptedData"], NSString); - _nonce = PA2ObjectAs(dict[@"nonce"], NSString); - _mac = PA2ObjectAs(dict[@"mac"], NSString); - _timestamp = [PA2ObjectAs(dict[@"timestamp"], NSNumber) unsignedLongLongValue]; + _cryptogram = [[PowerAuthCoreEciesCryptogram alloc] initWithResponsePayload:dict]; } return self; } -- (PowerAuthCoreEciesCryptogram*) cryptogram -{ - PowerAuthCoreEciesCryptogram * cryptogram = [[PowerAuthCoreEciesCryptogram alloc] init]; - cryptogram.bodyBase64 = _encryptedData; - cryptogram.nonceBase64 = _nonce; - cryptogram.macBase64 = _mac; - cryptogram.timestamp = _timestamp; - return cryptogram; -} - @end diff --git a/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyRequest.h b/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyRequest.h new file mode 100644 index 00000000..e2e2785b --- /dev/null +++ b/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyRequest.h @@ -0,0 +1,25 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "PA2Codable.h" + +@interface PA2GetTemporaryKeyRequest : NSObject + +@property (nonatomic, strong) NSString * applicationKey; +@property (nonatomic, strong) NSString * activationId; +@property (nonatomic, strong) NSString * challenge; + +@end diff --git a/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyRequest.m b/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyRequest.m new file mode 100644 index 00000000..5f775ade --- /dev/null +++ b/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyRequest.m @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "PA2GetTemporaryKeyRequest.h" + +@implementation PA2GetTemporaryKeyRequest + +- (NSDictionary *)toDictionary +{ + if (_activationId) { + return @{ @"applicationKey" : _applicationKey, @"activationId": _activationId, @"challenge" : _challenge }; + } else { + return @{ @"applicationKey" : _applicationKey, @"challenge" : _challenge }; + } +} + +@end diff --git a/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyResponse.h b/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyResponse.h new file mode 100644 index 00000000..593e630d --- /dev/null +++ b/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyResponse.h @@ -0,0 +1,29 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "PA2Codable.h" + +@interface PA2GetTemporaryKeyResponse : NSObject + +@property (nonatomic, strong) NSString * applicationKey; +@property (nonatomic, strong) NSString * activationId; +@property (nonatomic, strong) NSString * challenge; +@property (nonatomic, strong) NSString * keyId; +@property (nonatomic, strong) NSString * publicKey; +@property (nonatomic, assign) UInt64 expiration; +@property (nonatomic, assign) UInt64 serverTime; + +@end diff --git a/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyResponse.m b/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyResponse.m new file mode 100644 index 00000000..6112933e --- /dev/null +++ b/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyResponse.m @@ -0,0 +1,37 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "PA2GetTemporaryKeyResponse.h" +#import "PA2PrivateMacros.h" + +@implementation PA2GetTemporaryKeyResponse + +- (instancetype) initWithDictionary:(NSDictionary *)dictionary +{ + self = [super init]; + if (self) { + _applicationKey = PA2ObjectAs(dictionary[@"applicationKey"], NSString); + _activationId = PA2ObjectAs(dictionary[@"activationId"], NSString); + _challenge = PA2ObjectAs(dictionary[@"challenge"], NSString); + _publicKey = PA2ObjectAs(dictionary[@"publicKey"], NSString); + _keyId = PA2ObjectAs(dictionary[@"sub"], NSString); + _expiration = [PA2ObjectAs(dictionary[@"exp_ms"], NSNumber) unsignedLongLongValue]; + _serverTime = [PA2ObjectAs(dictionary[@"iat_ms"], NSNumber) unsignedLongLongValue]; + } + return self; +} + +@end diff --git a/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyTask.h b/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyTask.h new file mode 100644 index 00000000..60918eb0 --- /dev/null +++ b/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyTask.h @@ -0,0 +1,48 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "PA2GroupedTask.h" + +@import PowerAuthCore; + +@class PA2HttpClient, PA2GetTemporaryKeyTask, PA2GetTemporaryKeyResponse; +@protocol PowerAuthCoreSessionProvider; + +/// The `PA2GetTemporaryKeyTaskDelegate` protocol allows class that create `PA2GetTemporaryKeyTask` object +/// monitor the task completion. +@protocol PA2GetTemporaryKeyTaskDelegate +@required +/// Called when the get activation task complete its execution. +- (void) getTemporaryKeyTask:(nonnull PA2GetTemporaryKeyTask*)task + didFinishWithResponse:(nullable PA2GetTemporaryKeyResponse*)response + error:(nullable NSError*)error; +@end + +/// The `PA2GetTemporaryKeyTask` implements grouped task that gets temporary encryption key from the server. +@interface PA2GetTemporaryKeyTask : PA2GroupedTask + +@property (nonatomic, readonly) PowerAuthCoreEciesEncryptorScope encryptorScope; +@property (nonatomic, strong, nullable, readonly) NSString * applicationKey; + +- (nonnull instancetype) initWithHttpClient:(nonnull PA2HttpClient*)httpClient + sessionProvider:(nonnull id)sessionProvider + sharedLock:(nonnull id)sharedLock + applicationKey:(nonnull NSString*)applicationKey + deviceRelatedKey:(nullable NSData*)deviceRelatedKey + encryptorScope:(PowerAuthCoreEciesEncryptorScope)encryptorScope + delegate:(nonnull id)delegate; + +@end diff --git a/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyTask.m b/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyTask.m new file mode 100644 index 00000000..fef3629e --- /dev/null +++ b/proj-xcode/PowerAuth2/private/PA2GetTemporaryKeyTask.m @@ -0,0 +1,197 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "PA2GetTemporaryKeyTask.h" +#import "PA2HttpClient.h" +#import "PA2RestApiEndpoint.h" +#import "PA2JwtObject.h" +#import "PA2GetTemporaryKeyRequest.h" +#import "PA2GetTemporaryKeyResponse.h" +#import "PA2ObjectSerialization.h" +#import "PA2PrivateMacros.h" + +@implementation PA2GetTemporaryKeyTask +{ + PA2HttpClient * _client; + id _sessionProvider; + __weak id _delegate; + NSData * _deviceRelatedKey; + BOOL _isApplicationScope; +} + +- (instancetype) initWithHttpClient:(PA2HttpClient*)httpClient + sessionProvider:(id)sessionProvider + sharedLock:(id)sharedLock + applicationKey:(NSString*)applicationKey + deviceRelatedKey:(NSData*)deviceRelatedKey + encryptorScope:(PowerAuthCoreEciesEncryptorScope)encryptorScope + delegate:(id)delegate +{ + BOOL isAppScope = encryptorScope == PowerAuthCoreEciesEncryptorScope_Application; + self = [super initWithSharedLock:sharedLock + taskName:isAppScope ? @"GetTempKey-App" : @"GetTempKey-Act"]; + if (self) { + _client = httpClient; + _sessionProvider = sessionProvider; + _applicationKey = applicationKey; + _deviceRelatedKey = deviceRelatedKey; + _encryptorScope = encryptorScope; + _delegate = delegate; + _isApplicationScope = isAppScope; + } + return self; +} + +- (void) onTaskStart +{ + [super onTaskStart]; + + PA2GetTemporaryKeyRequest * request = [[PA2GetTemporaryKeyRequest alloc] init]; + PA2JwtObject * requestJwt = [self prepareRequestJwt:request]; + if (!requestJwt) { + return; + } + PA2RestApiEndpoint * endpoint = [PA2RestApiEndpoint getTemporaryKey]; + id cancelable = [_client postObject:requestJwt to:endpoint completion:^(PowerAuthRestApiResponseStatus status, id response, NSError * error) { + PA2GetTemporaryKeyResponse * objectResponse = nil; + if (status == PowerAuthRestApiResponseStatus_OK && response) { + // Note that response is already PA2JwtObject. We're using the casting because the completion closure cannot use such type, due to a bug in objc compiler. + // If objective-c class implements more than one protocol (in this case PA2Decodable and PA2Encodable), then "magic" casting to a right closure type doesn't work, + // even if the requested protocol is implemented by the class. + objectResponse = [self processResponseJwt:(PA2JwtObject*)response error:&error]; + if (objectResponse && ![self validateResponse:objectResponse withRequest:request]) { + error = PA2MakeError(PowerAuthErrorCode_Encryption, @"JWT response doesn't match request"); + objectResponse = nil; + } + } + [self complete:objectResponse error:error]; + }]; + [self replaceCancelableOperation:cancelable]; +} + +- (void) onTaskCompleteWithResult:(id)result error:(NSError*)error +{ + [super onTaskCompleteWithResult:result error:error]; + [_delegate getTemporaryKeyTask:self didFinishWithResponse:result error:error]; +} + + + +#pragma mark - Request + +- (PA2JwtObject*) prepareRequestJwt:(PA2GetTemporaryKeyRequest*)request +{ + return [_sessionProvider readTaskWithSession:^PA2JwtObject*(PowerAuthCoreSession * session) { + NSString * activationId; + if (_isApplicationScope) { + activationId = nil; + } else { + activationId = _sessionProvider.activationIdentifier; + if (!activationId) { + [self complete:nil error:PA2MakeError(PowerAuthErrorCode_MissingActivation, nil)]; + return nil; + } + } + // Update input request object + request.applicationKey = _applicationKey; + request.activationId = activationId; + request.challenge = [[PowerAuthCoreCryptoUtils randomBytes:18] base64EncodedStringWithOptions:0]; + // Prepare JWT string + NSString * jwtHeader = @"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."; // {"alg":"HS256","typ":"JWT"} with dot separator + NSString * jwtPayload = [PA2ObjectSerialization serializeJwtObject:request]; + NSString * jwtSignedData = [jwtHeader stringByAppendingString:jwtPayload]; + PowerAuthCoreSignedData * dataToSign = [[PowerAuthCoreSignedData alloc] init]; + dataToSign.data = [jwtSignedData dataUsingEncoding:NSASCIIStringEncoding]; + dataToSign.signingDataKey = _isApplicationScope ? PowerAuthCoreSigningDataKey_HMAC_Application : PowerAuthCoreSigningDataKey_HMAC_Activation; + if (![session signDataWithHmacKey:dataToSign keys:[self signatureUnlockKeys]]) { + [self complete:nil error:PA2MakeError(PowerAuthErrorCode_Encryption, @"Failed to calculate JWT signature")]; + return nil; + } + NSString * jwtString = [[jwtSignedData stringByAppendingString:@"."] stringByAppendingString:[dataToSign.signature jwtEncodedString]]; + return [[PA2JwtObject alloc] initWithJwt:jwtString]; + }]; +} + +- (PowerAuthCoreSignatureUnlockKeys*) signatureUnlockKeys +{ + if (_isApplicationScope) { + return nil; + } + PowerAuthCoreSignatureUnlockKeys * keys = [[PowerAuthCoreSignatureUnlockKeys alloc] init]; + keys.possessionUnlockKey = _deviceRelatedKey; + return keys; +} + + +#pragma mark - Response + +- (PA2GetTemporaryKeyResponse*) processResponseJwt:(PA2JwtObject*)responseJwt error:(NSError**)error +{ + NSString * jwtString = responseJwt.jwt; + if (!jwtString) { + *error = PA2MakeError(PowerAuthErrorCode_NetworkError, @"Empty JWT response"); + return nil; + } + NSArray * jwtComponents = [jwtString componentsSeparatedByString:@"."]; + if (jwtComponents.count != 3) { + *error = PA2MakeError(PowerAuthErrorCode_NetworkError, @"Invalid JWT response"); + return nil; + } + NSString * jwtHeader = jwtComponents[0]; + NSString * jwtPayload = jwtComponents[1]; + NSString * jwtSignature = jwtComponents[2]; + if (jwtHeader.length == 0 || jwtPayload.length == 0 || jwtSignature.length == 0) { + *error = PA2MakeError(PowerAuthErrorCode_NetworkError, @"Invalid JWT response"); + return nil; + } + PA2JwtHeader * jwtHeaderObj = (PA2JwtHeader*)[PA2ObjectSerialization deserializeJwtObject:jwtHeader forClass:[PA2JwtHeader class] error:nil]; + if (!jwtHeaderObj) { + *error = PA2MakeError(PowerAuthErrorCode_NetworkError, @"Invalid JWT header in response"); + return nil; + } + if (![jwtHeaderObj.typ isEqualToString:@"JWT"]) { + *error = PA2MakeError(PowerAuthErrorCode_NetworkError, @"Unsupported JWT type in response"); + } + if (![jwtHeaderObj.alg isEqualToString:@"ES256"]) { + *error = PA2MakeError(PowerAuthErrorCode_NetworkError, @"Unsupported JWT algorithm in response"); + return nil; + } + PowerAuthCoreSignedData * signedData = [[PowerAuthCoreSignedData alloc] init]; + signedData.signingDataKey = _isApplicationScope ? PowerAuthCoreSigningDataKey_ECDSA_MasterServerKey : PowerAuthCoreSigningDataKey_ECDSA_PersonalizedKey; + signedData.signatureFormat = PowerAuthCoreSignatureFormat_ECDSA_JOSE; + signedData.data = [[NSString stringWithFormat:@"%@.%@", jwtHeader, jwtPayload] dataUsingEncoding:NSUTF8StringEncoding]; + signedData.signature = [[NSData alloc] initWithJwtEncodedString:jwtSignature]; + BOOL valid = [_sessionProvider readBoolTaskWithSession:^BOOL(PowerAuthCoreSession * session) { + return [session verifyServerSignedData:signedData]; + }]; + if (!valid) { + *error = PA2MakeError(PowerAuthErrorCode_Encryption, @"Invalid signature in JWT response"); + return nil; + } + return [PA2ObjectSerialization deserializeJwtObject:jwtPayload forClass:[PA2GetTemporaryKeyResponse class] error:error]; +} + +- (BOOL) validateResponse:(PA2GetTemporaryKeyResponse*)response withRequest:(PA2GetTemporaryKeyRequest*)request +{ + BOOL match = [response.challenge isEqualToString:request.challenge]; + match = match && [response.applicationKey isEqualToString:request.applicationKey]; + if (!_isApplicationScope) { + match = match && [response.activationId isEqualToString:request.activationId]; + } + return match; +} + +@end diff --git a/proj-xcode/PowerAuth2/private/PA2HttpClient.m b/proj-xcode/PowerAuth2/private/PA2HttpClient.m index 9ad4ad27..6154c7a3 100644 --- a/proj-xcode/PowerAuth2/private/PA2HttpClient.m +++ b/proj-xcode/PowerAuth2/private/PA2HttpClient.m @@ -18,6 +18,7 @@ #import "PA2AsyncOperation.h" #import "PA2CompositeTask.h" #import "PA2PrivateMacros.h" +#import "PA2KeystoreService.h" #import "PowerAuthLog.h" #import "PA2Result.h" @@ -174,9 +175,15 @@ static void _LogHttpResponse(PA2RestApiEndpoint * endpoint, NSHTTPURLResponse * completion:(void(^)(PowerAuthRestApiResponseStatus status, id response, NSError * error))completion cancel:(void(^)(void))customCancelBlock { - if (endpoint.requireSynchronizedTime && !_timeService.isTimeSynchronized) { - // Endpoint require encryption and time is not synchronized yet. We have to create a composite task that cover both - // time synchronization and actual request execution. + PA2KeystoreService * keystoreService = _cryptoHelper.keystoreService; + + PowerAuthCoreEciesEncryptorScope encryptorScope = endpoint.isEncryptedWithApplicationScope ? PowerAuthCoreEciesEncryptorScope_Application : PowerAuthCoreEciesEncryptorScope_Activation; + BOOL requireTimeSynchronization = endpoint.requireSynchronizedTime && !_timeService.isTimeSynchronized; + BOOL requireEncryptionKey = endpoint.isEncrypted && ![keystoreService hasKeyForEncryptorScope:encryptorScope]; + + if (requireTimeSynchronization || requireEncryptionKey) { + // Endpoint require encryption key or time is not synchronized yet. We have to create a composite task that handle multiple + // requests before an actual request is executed. PA2CompositeTask * compositeTask = [[PA2CompositeTask alloc] initWithCancelBlock:customCancelBlock]; // Prepare common completion block with the composite task. void (^compositeCompletion)(PowerAuthRestApiResponseStatus, id, NSError *) = ^(PowerAuthRestApiResponseStatus status, id response, NSError *error) { @@ -189,18 +196,34 @@ static void _LogHttpResponse(PA2RestApiEndpoint * endpoint, NSHTTPURLResponse * } }); }; - // Start the time synchronization - id synchronizationTask = [_timeService synchronizeTimeWithCallback:^(NSError * error) { - if (!error) { - // The time has been successfully synchronized, we can continue with the actual request. - NSOperation* actualOperation = [self postOperationWithObject:object to:endpoint auth:authentication completion:compositeCompletion cancel:nil]; - [compositeTask replaceOperationTask:actualOperation]; - } else { - // Report error to composite completion. - compositeCompletion(PowerAuthRestApiResponseStatus_ERROR, nil, error); - } - } callbackQueue:_completionQueue]; - [compositeTask replaceOperationTask:synchronizationTask]; + // Now determine what type of task should be executed before an actual task. + if (requireEncryptionKey) { + // Acquire temporary encryption key. This also synchronizes time as a side effect. + id getKeyTask = [keystoreService createKeyForEncryptorScope:encryptorScope callback:^(NSError * error) { + if (!error) { + // The temporary encryption key has been successfully obtained, we can continue with the actual request. + NSOperation* actualOperation = [self postOperationWithObject:object to:endpoint auth:authentication completion:compositeCompletion cancel:nil]; + [compositeTask replaceOperationTask:actualOperation]; + } else { + // Report error to composite completion. + compositeCompletion(PowerAuthRestApiResponseStatus_ERROR, nil, error); + } + }]; + [compositeTask replaceOperationTask:getKeyTask]; + } else { + // Start the time synchronization + id synchronizationTask = [_timeService synchronizeTimeWithCallback:^(NSError * error) { + if (!error) { + // The time has been successfully synchronized, we can continue with the actual request. + NSOperation* actualOperation = [self postOperationWithObject:object to:endpoint auth:authentication completion:compositeCompletion cancel:nil]; + [compositeTask replaceOperationTask:actualOperation]; + } else { + // Report error to composite completion. + compositeCompletion(PowerAuthRestApiResponseStatus_ERROR, nil, error); + } + } callbackQueue:_completionQueue]; + [compositeTask replaceOperationTask:synchronizationTask]; + } return compositeTask; } // Endpoint doesn't require time synchronization, or time is already synchronized. diff --git a/proj-xcode/PowerAuth2/private/PA2HttpRequest.m b/proj-xcode/PowerAuth2/private/PA2HttpRequest.m index c071effd..64cc1277 100644 --- a/proj-xcode/PowerAuth2/private/PA2HttpRequest.m +++ b/proj-xcode/PowerAuth2/private/PA2HttpRequest.m @@ -74,9 +74,9 @@ - (NSMutableURLRequest*) buildRequestWithHelper:(id)help } // Execute custom step before the request is serialized. - NSError * (^beforeSerialization)(void) = _endpoint.beforeRequestSerialization; + NSError * (^beforeSerialization)(PA2RestApiEndpoint*) = _endpoint.beforeRequestSerialization; if (beforeSerialization) { - NSError * customStepError = beforeSerialization(); + NSError * customStepError = beforeSerialization(_endpoint); if (customStepError) { if (error) *error = customStepError; return nil; diff --git a/proj-xcode/PowerAuth2/private/PA2JwtObject.h b/proj-xcode/PowerAuth2/private/PA2JwtObject.h new file mode 100644 index 00000000..20ac6834 --- /dev/null +++ b/proj-xcode/PowerAuth2/private/PA2JwtObject.h @@ -0,0 +1,38 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "PA2Codable.h" + +@interface PA2JwtHeader : NSObject + +- (instancetype) initJwtWithAlg:(NSString*)alg; + +- (instancetype) initWithTyp:(NSString*)typ + withAlg:(NSString*)alg; + +@property (nonatomic, strong) NSString * typ; +@property (nonatomic, strong) NSString * alg; + +@end + + +@interface PA2JwtObject : NSObject + +- (instancetype) initWithJwt:(NSString*)jwt; + +@property (nonatomic, strong) NSString * jwt; + +@end diff --git a/proj-xcode/PowerAuth2/private/PA2JwtObject.m b/proj-xcode/PowerAuth2/private/PA2JwtObject.m new file mode 100644 index 00000000..978993d3 --- /dev/null +++ b/proj-xcode/PowerAuth2/private/PA2JwtObject.m @@ -0,0 +1,79 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "PA2JwtObject.h" +#import "PA2PrivateMacros.h" + +#pragma mark - JWT Header + +@implementation PA2JwtHeader + +- (instancetype) initWithTyp:(NSString*)typ + withAlg:(NSString*)alg +{ + self = [super init]; + if (self) { + _typ = typ; + _alg = alg; + } + return self; +} + +- (instancetype) initJwtWithAlg:(NSString *)alg +{ + return [self initWithTyp:@"JWT" withAlg:alg]; +} + +- (instancetype) initWithDictionary:(NSDictionary *)dictionary +{ + return [self initWithTyp:PA2ObjectAs(dictionary[@"typ"], NSString) + withAlg:PA2ObjectAs(dictionary[@"alg"], NSString)]; +} + +- (NSDictionary *)toDictionary +{ + return @{ + @"typ": _typ, + @"alg": _alg + }; +} + +@end + +#pragma mark - JWT Object + +@implementation PA2JwtObject + +- (instancetype) initWithJwt:(NSString*)jwt +{ + self = [super init]; + if (self) { + _jwt = jwt; + } + return self; +} + +- (instancetype) initWithDictionary:(NSDictionary *)dictionary +{ + return [self initWithJwt:PA2ObjectAs(dictionary[@"jwt"], NSString)]; +} + +- (NSDictionary *) toDictionary +{ + return @{ @"jwt": _jwt }; +} + +@end diff --git a/proj-xcode/PowerAuth2/private/PA2KeystoreService.h b/proj-xcode/PowerAuth2/private/PA2KeystoreService.h new file mode 100644 index 00000000..bbd9c862 --- /dev/null +++ b/proj-xcode/PowerAuth2/private/PA2KeystoreService.h @@ -0,0 +1,44 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import + +#import "PA2SessionInterface.h" +#import "PA2TimeSynchronizationService.h" +#import "PA2GetTemporaryKeyTask.h" + +/// The `PA2KeystoreService` manages temporary encryption keys for PowerAuthSDK instance. +@interface PA2KeystoreService : NSObject + +- (nonnull instancetype) initWithHttpClient:(nonnull PA2HttpClient*)httpClient + timeService:(nonnull id)timeService + deviceRelatedKey:(nonnull NSData*)deviceRelatedKey + sessionSetup:(nonnull PowerAuthCoreSessionSetup*)sessionSetup + sharedLock:(nonnull id)sharedLock; +/** + Determine whether instance of this service contains a temporary encryption key for the requested encryption scope. + */ +- (BOOL) hasKeyForEncryptorScope:(PowerAuthCoreEciesEncryptorScope)encryptorScope; + +/** + Create a temporary encryption key for the requested scope. If such key already exists and is still valid, then function does nothing + and returns nil. If the key is not available, or is already expired, then the function returns asynchronous task with an underlying + HTTP request. + */ +- (nullable id) createKeyForEncryptorScope:(PowerAuthCoreEciesEncryptorScope)encryptorScope + callback:(nonnull void(^)(NSError * _Nullable error))callback; + +@end diff --git a/proj-xcode/PowerAuth2/private/PA2KeystoreService.m b/proj-xcode/PowerAuth2/private/PA2KeystoreService.m new file mode 100644 index 00000000..b91842fb --- /dev/null +++ b/proj-xcode/PowerAuth2/private/PA2KeystoreService.m @@ -0,0 +1,211 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import "PA2KeystoreService.h" +#import "PA2PrivateMacros.h" +#import "PA2HttpClient.h" +#import "PA2GetTemporaryKeyResponse.h" +#import + +// We don't want to use the key that's close to its expiration on the server. This constant specifies for how much +// we move the expiration time to backward. +#define PUBLIC_KEY_EXPIRATION_THRESHOLD 10.0 + +#pragma mark - Service data + +@interface PA2PublicKeyInfo : NSObject + +- (instancetype) initWithScope:(PowerAuthCoreEciesEncryptorScope)scope; + +@property (nonatomic, readonly) PowerAuthCoreEciesEncryptorScope scope; +@property (nonatomic, strong) PA2GetTemporaryKeyTask * task; +@property (nonatomic, assign) NSTimeInterval expiration; +@property (nonatomic, strong) id timeSynchronizationTask; + +- (void) clearTask; + +@end + +#pragma mark - Service implementation + +@implementation PA2KeystoreService +{ + id _sessionInterface; + id _timeService; + id _lock; + PA2HttpClient * _httpClient; + NSString * _applicationKey; + NSData * _deviceRelatedKey; + + PA2PublicKeyInfo * _pkiAppScope; + PA2PublicKeyInfo * _pkiActScope; +} + +- (instancetype) initWithHttpClient:(PA2HttpClient*)httpClient + timeService:(id)timeService + deviceRelatedKey:(NSData*)deviceRelatedKey + sessionSetup:(PowerAuthCoreSessionSetup*)sessionSetup + sharedLock:(id)sharedLock +{ + self = [super init]; + if (self) { + _sessionInterface = httpClient.sessionInterface; + _timeService = timeService; + _httpClient = httpClient; + _lock = sharedLock; + _applicationKey = sessionSetup.applicationKey; + _deviceRelatedKey = deviceRelatedKey; + _pkiAppScope = [[PA2PublicKeyInfo alloc] initWithScope:PowerAuthCoreEciesEncryptorScope_Application]; + _pkiActScope = [[PA2PublicKeyInfo alloc] initWithScope:PowerAuthCoreEciesEncryptorScope_Activation]; + } + return self; +} + +- (id) createKeyForEncryptorScope:(PowerAuthCoreEciesEncryptorScope)encryptorScope callback:(void (^)(NSError *))callback +{ + if (encryptorScope == PowerAuthCoreEciesEncryptorScope_Activation && ![self hasValidActivation]) { + callback(PA2MakeError(PowerAuthErrorCode_MissingActivation, nil)); + return nil; + } + + [_lock lock]; + id task = nil; + if ([self hasKeyForEncryptorScope:encryptorScope]) { + // Key already exist + callback(nil); + } else { + // Key must be received from the server + PA2PublicKeyInfo * pki = [self pkiForScope:encryptorScope]; + PA2GetTemporaryKeyTask * mainTask = pki.task; + if (!mainTask) { + mainTask = [[PA2GetTemporaryKeyTask alloc] initWithHttpClient:_httpClient + sessionProvider:_sessionInterface + sharedLock:_lock + applicationKey:_applicationKey + deviceRelatedKey:_deviceRelatedKey + encryptorScope:encryptorScope + delegate:self]; + pki.task = mainTask; + pki.timeSynchronizationTask = [_timeService startTimeSynchronizationTask]; + } + task = [mainTask createChildTask:^(PA2GetTemporaryKeyResponse * _Nullable result, NSError * _Nullable error) { + callback(error); + }]; + } + [_lock unlock]; + return task; +} + +- (PA2PublicKeyInfo*) pkiForScope:(PowerAuthCoreEciesEncryptorScope)encryptorScope +{ + return encryptorScope == PowerAuthCoreEciesEncryptorScope_Application ? _pkiAppScope : _pkiActScope; +} + +- (BOOL) hasKeyForEncryptorScope:(PowerAuthCoreEciesEncryptorScope)encryptorScope +{ + // This function is using access to two separately locked sections. The goal is to do not + // overlap the critical sections. So, we have to query information in two separate steps. + BOOL keyIsExpired; + BOOL keyIsSet; + + [_lock lock]; + PA2PublicKeyInfo * pki = [self pkiForScope:encryptorScope]; + NSTimeInterval expiration = pki.expiration; + keyIsSet = expiration >= 0.0; + keyIsExpired = expiration - PUBLIC_KEY_EXPIRATION_THRESHOLD < [_timeService currentTime]; + if (keyIsExpired) { + pki.expiration = -1; + } + [_lock unlock]; + + return [_sessionInterface readBoolTaskWithSession:^BOOL(PowerAuthCoreSession * session) { + BOOL hasKey = [session hasPublicKeyForEciesScope:encryptorScope]; + if (hasKey && keyIsExpired && keyIsSet) { + PowerAuthLog(@"Removing expired public key for ECIES encryptor %d", encryptorScope); + [session removePublicKeyForEciesScope:encryptorScope]; + hasKey = NO; + } + return hasKey; + }]; +} + +#pragma mark - PA2GetTemporaryKeyTaskDelegate + +- (void) getTemporaryKeyTask:(PA2GetTemporaryKeyTask *)task didFinishWithResponse:(PA2GetTemporaryKeyResponse *)response error:(NSError *)error +{ + // [_lock lock] is guaranteed, because this method is called from task's completion while locked with shared lock. + // So, we can freely mutate objects in this instance. + PowerAuthCoreEciesEncryptorScope scope = task.encryptorScope; + PA2PublicKeyInfo * pki = [self pkiForScope:scope]; + if (pki.task == task) { + if (response) { + NSTimeInterval receivedServerTime = 0.001 * (NSTimeInterval)response.serverTime; + [_timeService completeTimeSynchronizationTask:pki.timeSynchronizationTask withServerTime:receivedServerTime]; + [self updatePublicKeyForEncryptorScope:scope withResponse:response]; + } + [pki clearTask]; + } +} + +- (BOOL) updatePublicKeyForEncryptorScope:(PowerAuthCoreEciesEncryptorScope)encryptorScope withResponse:(PA2GetTemporaryKeyResponse*)response +{ + BOOL success = [_sessionInterface readBoolTaskWithSession:^BOOL(PowerAuthCoreSession * session) { + PowerAuthCoreErrorCode ec = [session setPublicKeyForEciesScope:encryptorScope publicKey:response.publicKey publicKeyId:response.keyId]; + if (ec != PowerAuthCoreErrorCode_Ok) { + PowerAuthLog(@"Failed to update public key for ECIES encryption. Code = %d", ec); + return NO; + } + return YES; + }]; + if (success) { + PA2PublicKeyInfo * pki = [self pkiForScope:encryptorScope]; + pki.expiration = 0.001 * response.expiration; + PowerAuthLog(@"Saving public key for ECIES encryptor %d", encryptorScope); + } + return success; +} + + +#pragma mark - Support functions + +- (BOOL) hasValidActivation +{ + return [[_sessionInterface readTaskWithSession:^id _Nullable(PowerAuthCoreSession * session) { + return @([session hasValidActivation]); + }] boolValue]; +} + +@end + + +@implementation PA2PublicKeyInfo + +- (instancetype) initWithScope:(PowerAuthCoreEciesEncryptorScope)scope +{ + self = [super init]; + if (self) { + _scope = scope; + } + return self; +} + +- (void) clearTask +{ + _task = nil; + _timeSynchronizationTask = nil; +} + +@end diff --git a/proj-xcode/PowerAuth2/private/PA2ObjectSerialization.h b/proj-xcode/PowerAuth2/private/PA2ObjectSerialization.h index 3feb9144..47be2c68 100644 --- a/proj-xcode/PowerAuth2/private/PA2ObjectSerialization.h +++ b/proj-xcode/PowerAuth2/private/PA2ObjectSerialization.h @@ -83,3 +83,33 @@ error:(NSError**)error; @end + + +@interface PA2ObjectSerialization (JWT) + +/** + Serialize object into Base64Url encoded string. + */ ++ (NSString*) serializeJwtObject:(id)object; + +/** + Deserialize object from Base64Url encoded string. + */ ++ (id) deserializeJwtObject:(NSString*)data forClass:(Class)aClass error:(NSError**)error; + +@end + + +@interface NSData (JWTEncoded) + +/** + Init data with Base64Url encoded string. The "JWT Encoded" naming is used to avoid conflicts with another libraries. + */ +- (instancetype) initWithJwtEncodedString:(NSString*)jwtEncodedString; + +/** + Return bytes represented as Base64Url encoded string. + */ +- (NSString*) jwtEncodedString; + +@end diff --git a/proj-xcode/PowerAuth2/private/PA2ObjectSerialization.m b/proj-xcode/PowerAuth2/private/PA2ObjectSerialization.m index 7a08ef70..c85048e7 100644 --- a/proj-xcode/PowerAuth2/private/PA2ObjectSerialization.m +++ b/proj-xcode/PowerAuth2/private/PA2ObjectSerialization.m @@ -178,3 +178,71 @@ + (NSData*) decryptData:(NSData*)data @end +#pragma mark - Base64Url + +static NSString * ConvertToBase64Url(NSString * base64) +{ + // Remove padding and replace Base64 characters with Base64Url equivalent. + return [[[base64 stringByReplacingOccurrencesOfString:@"=" withString:@""] + stringByReplacingOccurrencesOfString:@"+" withString:@"-"] + stringByReplacingOccurrencesOfString:@"/" withString:@"_"]; +} + +static NSString * ConvertFromBase64Url(NSString * base64Url) +{ + // At first, translate special Base64Url characters into regular Base64 characters + NSString * base64 = [[base64Url stringByReplacingOccurrencesOfString:@"_" withString:@"/"] + stringByReplacingOccurrencesOfString:@"-" withString:@"+"]; + // Padding is optional, so append '=' if no padding is found + if (![base64 hasSuffix:@"="]) { + // Append suffix if not present + NSUInteger padCount = 4 - (base64.length & 3); + if (padCount < 4) { + base64 = [base64 stringByAppendingString:[@"===" substringToIndex:padCount]]; + } + } + return base64; +} + +@implementation NSData (JWTEncoded) + +- (instancetype) initWithJwtEncodedString:(NSString *)jwtEncodedString +{ + NSString * base64 = ConvertFromBase64Url(jwtEncodedString); + if (base64) { + return [self initWithBase64EncodedString:base64 options:0]; + } + return nil; +} + +- (NSString*) jwtEncodedString +{ + return ConvertToBase64Url([self base64EncodedStringWithOptions:0]); +} + +@end + +#pragma mark - JWT + +@implementation PA2ObjectSerialization (JWT) + ++ (NSString*) serializeJwtObject:(id)object +{ + NSString * base64 = [[self serializeObject:object] base64EncodedStringWithOptions:0]; + return ConvertToBase64Url(base64); +} + ++ (id) deserializeJwtObject:(NSString*)data forClass:(Class)aClass error:(NSError**)error +{ + NSString * base64 = ConvertFromBase64Url(data); + NSData * objectData = [[NSData alloc] initWithBase64EncodedString:base64 options:0]; + if (!objectData) { + if (error) { + *error = PA2MakeError(PowerAuthErrorCode_NetworkError, @"Failed to deserialize JWT object."); + } + return nil; + } + return [self deserializeObject:objectData forClass:aClass error:error]; +} + +@end diff --git a/proj-xcode/PowerAuth2/private/PA2PrivateCryptoHelper.h b/proj-xcode/PowerAuth2/private/PA2PrivateCryptoHelper.h index e18dcb15..8971c366 100644 --- a/proj-xcode/PowerAuth2/private/PA2PrivateCryptoHelper.h +++ b/proj-xcode/PowerAuth2/private/PA2PrivateCryptoHelper.h @@ -20,7 +20,7 @@ @class PowerAuthAuthorizationHttpHeader; @class PowerAuthCoreEciesEncryptor; @class PowerAuthAuthentication; - +@class PA2KeystoreService; /** The `PA2PrivateCryptoHelper` protocol provides a minimal interface for a several cryptographic tasks required internally in the SDK, but provided @@ -43,4 +43,9 @@ authentication:(PowerAuthAuthentication*)authentication error:(NSError**)error; +/** + Returns instance of keystore service. + */ +- (PA2KeystoreService*) keystoreService; + @end diff --git a/proj-xcode/PowerAuth2/private/PA2PrivateEncryptorFactory.m b/proj-xcode/PowerAuth2/private/PA2PrivateEncryptorFactory.m index a4394602..25d96623 100644 --- a/proj-xcode/PowerAuth2/private/PA2PrivateEncryptorFactory.m +++ b/proj-xcode/PowerAuth2/private/PA2PrivateEncryptorFactory.m @@ -73,6 +73,10 @@ - (PowerAuthCoreEciesEncryptor*) encryptorForScope:(PowerAuthCoreEciesEncryptorS return [[_sessionProvider readTaskWithSession:^PA2Result* _Nullable(PowerAuthCoreSession * _Nonnull session) { // Prepare data required for encryptor construction NSString * activationId = nil; + NSString * temporaryKeyId = [session publicKeyIdForEciesScope:scope]; + if (!temporaryKeyId) { + return [PA2Result failure:PA2MakeError(PowerAuthErrorCode_Encryption, @"Temporary key for ECIES is not set")]; + } PowerAuthCoreSignatureUnlockKeys * unlockKeys = nil; if (scope == PowerAuthCoreEciesEncryptorScope_Activation) { // For activation scope, also prepare activation ID and possession unlock key. @@ -84,7 +88,7 @@ - (PowerAuthCoreEciesEncryptor*) encryptorForScope:(PowerAuthCoreEciesEncryptorS unlockKeys = [[PowerAuthCoreSignatureUnlockKeys alloc] init]; unlockKeys.possessionUnlockKey = _deviceRelatedKey; } - // Prepare the rest of information required for o + // Prepare the rest of information required for encryptor creation NSData * sharedInfo1Data = [sharedInfo1 dataUsingEncoding:NSUTF8StringEncoding]; NSString * applicationKey = session.applicationKey; // Now create the encryptor @@ -96,6 +100,7 @@ - (PowerAuthCoreEciesEncryptor*) encryptorForScope:(PowerAuthCoreEciesEncryptorS } // And assign the associated metadata encryptor.associatedMetaData = [[PowerAuthCoreEciesMetaData alloc] initWithApplicationKey:applicationKey + temporaryKeyId:temporaryKeyId activationIdentifier:activationId]; return [PA2Result success:encryptor]; }] extractResult:error]; diff --git a/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.h b/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.h index 2e700ddb..bd8b96ca 100644 --- a/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.h +++ b/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.h @@ -49,6 +49,10 @@ /// Returns YES, if request requires encryption @property (nonatomic, assign, readonly) BOOL isEncrypted; +/// Returns YES, if request requires application scopeed encryption +@property (nonatomic, assign, readonly) BOOL isEncryptedWithApplicationScope; + + /// Returns YES, if request needs to be signed with PA signature @property (nonatomic, assign, readonly) BOOL isSigned; @@ -59,11 +63,14 @@ @property (nonatomic, assign, readonly) BOOL requireSynchronizedTime; /// Contains block that will be executed on networking queue, before the request is serialized. -@property (nonatomic, strong, readonly) NSError *(^beforeRequestSerialization)(void); +@property (nonatomic, strong, readonly) NSError *(^beforeRequestSerialization)(PA2RestApiEndpoint * endpoint); + +/// Arbitrary data produced in custom serialization steps. +@property (nonatomic, strong) id customData; #pragma mark - Endpoint construction -+ (instancetype) createActivationWithCustomStep:(NSError*(^)(void))customStep; ++ (instancetype) createActivationWithCustomStep:(NSError*(^)(PA2RestApiEndpoint * endpoint))customStep; + (instancetype) getActivationStatus; + (instancetype) removeActivation; @@ -82,4 +89,6 @@ + (instancetype) getSystemStatus; ++ (instancetype) getTemporaryKey; + @end diff --git a/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.m b/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.m index f9cf315b..4bf93b08 100644 --- a/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.m +++ b/proj-xcode/PowerAuth2/private/PA2RestApiEndpoint.m @@ -29,7 +29,7 @@ @implementation PA2RestApiEndpoint #pragma mark - Activation -+ (instancetype) createActivationWithCustomStep:(NSError*(^)(void))customStep ++ (instancetype) createActivationWithCustomStep:(NSError*(^)(PA2RestApiEndpoint * endpoint))customStep { PA2RestApiEndpoint * endpoint = [[PA2RestApiEndpoint alloc] initWithPath:@"/pa/v3/activation/create" request:[PA2CreateActivationRequest class] @@ -159,6 +159,19 @@ + (instancetype) getSystemStatus flags:FL_ALLOWED_IN_UPGRADE]; } ++ (instancetype) getTemporaryKey +{ + // FL_ALLOWED_IN_UPGRADE is probably ignored because endoint is not signed. + // We keep it only to return semantically correct information in `isAvailableInProtocolUpgrade` + return [[PA2RestApiEndpoint alloc] initWithPath:@"/pa/v3/keystore/create" + request:[PA2JwtObject class] + response:[PA2JwtObject class] + encryptor:PA2EncryptorId_None + authUriId:nil + flags:FL_ALLOWED_IN_UPGRADE]; + +} + #pragma mark - Public getters - (BOOL) isEncrypted @@ -166,6 +179,13 @@ - (BOOL) isEncrypted return _encryptor != PA2EncryptorId_None; } +- (BOOL) isEncryptedWithApplicationScope +{ + return _encryptor == PA2EncryptorId_ActivationPayload || + _encryptor == PA2EncryptorId_ActivationRequest || + _encryptor == PA2EncryptorId_GenericApplicationScope; +} + - (BOOL) isSigned { return _authUriId != nil; diff --git a/proj-xcode/PowerAuth2/private/PA2RestApiObjects.h b/proj-xcode/PowerAuth2/private/PA2RestApiObjects.h index 17aa3971..bd6714a7 100644 --- a/proj-xcode/PowerAuth2/private/PA2RestApiObjects.h +++ b/proj-xcode/PowerAuth2/private/PA2RestApiObjects.h @@ -23,6 +23,8 @@ #import "PA2RemoveTokenRequest.h" #import "PA2ConfirmRecoveryCodeRequest.h" #import "PA2EncryptedRequest.h" +#import "PA2GetTemporaryKeyRequest.h" +#import "PA2JwtObject.h" // Response objects #import "PA2GetServerStatusResponse.h" @@ -35,4 +37,5 @@ #import "PA2EncryptedResponse.h" #import "PA2UpgradeStartV3Response.h" #import "PA2ConfirmRecoveryCodeResponse.h" +#import "PA2GetTemporaryKeyResponse.h" #import diff --git a/proj-xcode/PowerAuth2/private/PA2Result.h b/proj-xcode/PowerAuth2/private/PA2Result.h index d160619c..20e8d0dd 100644 --- a/proj-xcode/PowerAuth2/private/PA2Result.h +++ b/proj-xcode/PowerAuth2/private/PA2Result.h @@ -56,6 +56,12 @@ + (nonnull PA2Result*) failure:(nonnull ResultType)result withData:(nonnull id)data; +/** + Create result object with failure or success. If both + */ ++ (nonnull PA2Result*) success:(nullable ResultType)result + orFailure:(nullable NSError*)failure; + /** Return result and set error to provided NSError pointer in case result is failure. */ diff --git a/proj-xcode/PowerAuth2/private/PA2Result.m b/proj-xcode/PowerAuth2/private/PA2Result.m index 0afb656d..83875c13 100644 --- a/proj-xcode/PowerAuth2/private/PA2Result.m +++ b/proj-xcode/PowerAuth2/private/PA2Result.m @@ -49,6 +49,11 @@ + (id) failure:(NSError*)failure withData:(id)data return [[PA2Result alloc] initWithResult:nil error:failure data:data]; } ++ (id)success:(id)result orFailure:(NSError*)failure +{ + return [[PA2Result alloc] initWithResult:result error:failure data:nil]; +} + - (id) extractResult:(NSError**)error { if (error) { diff --git a/proj-xcode/PowerAuth2/private/PA2SharedSessionInterface.m b/proj-xcode/PowerAuth2/private/PA2SharedSessionInterface.m index 8c4fb193..a8c29d49 100644 --- a/proj-xcode/PowerAuth2/private/PA2SharedSessionInterface.m +++ b/proj-xcode/PowerAuth2/private/PA2SharedSessionInterface.m @@ -273,7 +273,7 @@ - (BOOL) readBoolTaskWithSession:(BOOL (NS_NOESCAPE^)(PowerAuthCoreSession *))ta - (void) resetSession { - WRITE_BLOCK([_session resetSession]) + WRITE_BLOCK([_session resetSession:NO]) } - (NSString*) activationIdentifier @@ -446,7 +446,7 @@ - (void) loadState:(BOOL)force if (statusData) { [_session deserializeState:statusData]; } else { - [_session resetSession]; + [_session resetSession:NO]; } _stateBefore = [_session serializedState]; diff --git a/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.h b/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.h index 949c4251..5beb8097 100644 --- a/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.h +++ b/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.h @@ -34,6 +34,10 @@ Contains instance identifier */ @property (nonatomic, strong, readonly) NSString * privateInstanceId; +/** + Contains instnace of keystore service. + */ +@property (nonatomic, strong, readonly) PA2KeystoreService * keystoreService; /** Returns key required for unlok the possesion factor. diff --git a/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.m b/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.m index d7d196a3..ecb35279 100644 --- a/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.m +++ b/proj-xcode/PowerAuth2/private/PowerAuthSDK+Private.m @@ -31,9 +31,9 @@ - (PowerAuthCoreEciesEncryptor*) encryptorWithId:(PA2EncryptorId)encryptorId err } - (PowerAuthAuthorizationHttpHeader*) authorizationHeaderForData:(NSData*)data - endpoint:(PA2RestApiEndpoint*)endpoint - authentication:(PowerAuthAuthentication*)authentication - error:(NSError**)error + endpoint:(PA2RestApiEndpoint*)endpoint + authentication:(PowerAuthAuthentication*)authentication + error:(NSError**)error { return [[self.sessionProvider writeTaskWithSession:^PA2Result* _Nullable(PowerAuthCoreSession * _Nonnull session) { if (self.hasPendingProtocolUpgrade || self.hasProtocolUpgradeAvailable) { diff --git a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKDefaultTests.m b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKDefaultTests.m index b57d9eb4..f0deb3b0 100644 --- a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKDefaultTests.m +++ b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKDefaultTests.m @@ -15,6 +15,7 @@ */ #import "PowerAuthSDKDefaultTests.h" +#import "PA2ObjectSerialization.h" @implementation PowerAuthSDKDefaultTests @@ -1336,14 +1337,14 @@ - (void) testJwtSignature NSString * jwtClaims = jwtComponents[1]; NSString * jwtSignature = jwtComponents[2]; // Validate header - NSData * jwtHeaderData = [[NSData alloc] initWithBase64EncodedString:jwtHeader options:0]; + NSData * jwtHeaderData = [[NSData alloc] initWithJwtEncodedString:jwtHeader]; XCTAssertNotNil(jwtHeaderData); NSDictionary * headerObject = [NSJSONSerialization JSONObjectWithData:jwtHeaderData options:0 error:NULL]; XCTAssertNotNil(headerObject); XCTAssertEqualObjects(@"JWT", headerObject[@"typ"]); XCTAssertEqualObjects(@"ES256", headerObject[@"alg"]); // Validate claims - NSData * jwtClaimsData = [[NSData alloc] initWithBase64EncodedString:jwtClaims options:0]; + NSData * jwtClaimsData = [[NSData alloc] initWithJwtEncodedString:jwtClaims]; XCTAssertNotNil(jwtClaimsData); NSDictionary * claims = [NSJSONSerialization JSONObjectWithData:jwtClaimsData options:0 error:NULL]; XCTAssertEqual(originalClaims.count, claims.count); @@ -1352,9 +1353,13 @@ - (void) testJwtSignature XCTAssertEqualObjects(originalObj, obj); }]; // Validate signature - NSData * jwtSignatureData = [[NSData alloc] initWithBase64EncodedString:jwtSignature options:0]; + NSData * jwtSignedData = [[NSString stringWithFormat:@"%@.%@", jwtHeader, jwtClaims] dataUsingEncoding:NSASCIIStringEncoding]; + NSData * jwtSignatureData = [[NSData alloc] initWithJwtEncodedString:jwtSignature]; XCTAssertNotNil(jwtSignatureData); - BOOL result = [_helper.testServerApi verifyECDSASignature:_sdk.activationIdentifier data:jwtClaimsData signature:jwtSignatureData]; + BOOL result = [_helper.testServerApi verifyECDSASignature:_sdk.activationIdentifier + data:jwtSignedData + signature:jwtSignatureData + signatureFormat:@"JOSE"]; XCTAssertTrue(result); } diff --git a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKProtocolUpgradeTests.m b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKProtocolUpgradeTests.m index 12c433b0..12f621e7 100644 --- a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKProtocolUpgradeTests.m +++ b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSDKProtocolUpgradeTests.m @@ -62,7 +62,7 @@ @implementation PowerAuthSDKProtocolUpgradeTests #define HAS_PENDING_COMMIT_STATE 1 #define UPGRADE_OLD_PROTOCOL @"2.1" -#define UPGRADE_NEW_PROTOCOL @"3.2" +#define UPGRADE_NEW_PROTOCOL @"3.3" // Adjust SDK specific objects #if HAS_ASYNC_TASK_PROTOCOL == 1 diff --git a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSdkTestHelper.m b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSdkTestHelper.m index 7d116378..4d11f208 100644 --- a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSdkTestHelper.m +++ b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthSdkTestHelper.m @@ -48,7 +48,7 @@ - (NSString*) activationId @end -static NSString * PA_Ver = @"3.2"; +static NSString * PA_Ver = @"3.3"; @implementation PowerAuthSdkTestHelper diff --git a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerAPI.h b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerAPI.h index 97e9b361..2cc67555 100644 --- a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerAPI.h +++ b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerAPI.h @@ -202,10 +202,19 @@ allowBiometry:(BOOL)allowBiometry; /** - Request for the asymmetric signature (ECDSA) validation procedure. + Request for the asymmetric signature (ECDSA) validation procedure. The signature format is DER. */ -- (BOOL) verifyECDSASignature:(NSString*)activationId data:(NSData*)data signature:(NSData*)signature; - +- (BOOL) verifyECDSASignature:(NSString*)activationId + data:(NSData*)data + signature:(NSData*)signature; +/** + Request for the asymmetric signature (ECDSA) validation procedure. Use nil (fallback to "DER") or "JOSE" as + signature format. + */ +- (BOOL) verifyECDSASignature:(NSString*)activationId + data:(NSData*)data + signature:(NSData*)signature + signatureFormat:(NSString*)signatureFormat; #pragma mark - Tokens diff --git a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerAPI.m b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerAPI.m index 76d5e8a2..2f2ca424 100644 --- a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerAPI.m +++ b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerAPI.m @@ -94,7 +94,11 @@ - (BOOL) validateConnection } if (!_appVersion.supported) { NSLog(@"Application version '%@' is not supported", _appVersion.applicationVersionName); - return NO; + if (![self supportApplicationVersion:_appVersion.applicationVersionId]) { + NSLog(@"Failed to set application version '%@' supported", _appVersion.applicationVersionName); + return NO; + } + _appVersion.supported = YES; } _hasValidConnection = YES; @@ -334,21 +338,34 @@ - (PATSVerifySignatureResponse*) verifyOfflineSignature:(NSString*)activationId } - (BOOL) verifyECDSASignature:(NSString*)activationId data:(NSData*)data signature:(NSData*)signature +{ + return [self verifyECDSASignature:activationId data:data signature:signature signatureFormat:nil]; +} + +- (BOOL) verifyECDSASignature:(NSString*)activationId + data:(NSData*)data + signature:(NSData*)signature + signatureFormat:(NSString*)signatureFormat { NSString * dataB64 = [data base64EncodedStringWithOptions:0]; NSString * signatureB64 = [signature base64EncodedStringWithOptions:0]; - NSDictionary * response = [_rest request:@"VerifyECDSASignature" params:@[activationId, dataB64, signatureB64]]; + NSArray * params; + if (signatureFormat) { + params = @[activationId, dataB64, signatureB64, signatureFormat]; + } else { + params = @[activationId, dataB64, signatureB64]; + } + NSDictionary * response = [_rest request:@"VerifyECDSASignature" params:params]; return [response[@"signatureValid"] boolValue]; } - #pragma mark - Tokens - (PATSTokenValidationResponse*) validateTokenRequest:(PATSTokenValidationRequest*)request { [self checkForValidConnection]; NSArray * params; - if (_testServerConfig.serverMaxProtovolVersion == PATS_P32) { + if (_testServerConfig.serverMaxProtovolVersion >= PATS_P32) { params = @[ request.tokenIdentifier, request.tokenDigest, request.nonce, request.timestamp, request.protocolVersion]; } else { params = @[ request.tokenIdentifier, request.tokenDigest, request.nonce, request.timestamp]; diff --git a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerConfig.h b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerConfig.h index 0187d25d..1e2d990e 100644 --- a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerConfig.h +++ b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerConfig.h @@ -31,6 +31,7 @@ typedef NS_ENUM(int, PowerAuthTestServerVersion) { PATS_V1_6 = 10600, // V3.2 crypto + Activation OTP, applicationId as String, userInfo PATS_V1_7 = 10700, // V3.2 crypto + Activation OTP, applicationId as String, userInfo PATS_V1_8 = 10800, // V3.2 crypto + Activation OTP, applicationId as String, userInfo + PATS_V1_9 = 10900, // V3.3 crypto + Activation OTP, applicationId as String, userInfo, temporary keys }; /** @@ -41,6 +42,7 @@ typedef NS_ENUM(int, PowerAuthProtocolVersion) { PATS_P3, // V3 crypto PATS_P31, // V3.1 crypto PATS_P32, // V3.2 crypto + PATS_P33, // V3.3 crypto }; /** diff --git a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerConfig.m b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerConfig.m index 17352019..87bcaa6c 100644 --- a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerConfig.m +++ b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/PowerAuthTestServerConfig.m @@ -53,6 +53,9 @@ + (instancetype) loadFromJsonFile:(NSString *)path PowerAuthProtocolVersion PATSProtoVer(PowerAuthTestServerVersion serverVer) { + if (serverVer >= PATS_V1_9) { + return PATS_P33; + } if (serverVer >= PATS_V1_5) { return PATS_P32; } @@ -60,7 +63,7 @@ PowerAuthProtocolVersion PATSProtoVer(PowerAuthTestServerVersion serverVer) } static int s_KnownVersions[] = { - PATS_V1_0, PATS_V1_1, PATS_V1_2, PATS_V1_2_5, PATS_V1_3, PATS_V1_4, PATS_V1_5, + PATS_V1_0, PATS_V1_1, PATS_V1_2, PATS_V1_2_5, PATS_V1_3, PATS_V1_4, PATS_V1_5, PATS_V1_6, PATS_V1_7, PATS_V1_8, PATS_V1_9, 0 }; diff --git a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/RestEndpoints.bundle/VerifyECDSASignature.json b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/RestEndpoints.bundle/VerifyECDSASignature_v10.json similarity index 98% rename from proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/RestEndpoints.bundle/VerifyECDSASignature.json rename to proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/RestEndpoints.bundle/VerifyECDSASignature_v10.json index b26b007e..d284ddd2 100644 --- a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/RestEndpoints.bundle/VerifyECDSASignature.json +++ b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/RestEndpoints.bundle/VerifyECDSASignature_v10.json @@ -8,4 +8,4 @@ "response": { "class": "dictionary" } -} \ No newline at end of file +} diff --git a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/RestEndpoints.bundle/VerifyECDSASignature_v19.json b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/RestEndpoints.bundle/VerifyECDSASignature_v19.json new file mode 100644 index 00000000..108ef548 --- /dev/null +++ b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/RestEndpoints.bundle/VerifyECDSASignature_v19.json @@ -0,0 +1,12 @@ +{ + "path": "/rest/v3/signature/ecdsa/verify", + "parameters": [ + "activationId", + "data", + "signature", + "signatureFormat" + ], + "response": { + "class": "dictionary" + } +} diff --git a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/RestEndpoints.bundle/mappings.json b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/RestEndpoints.bundle/mappings.json index ce3f4458..058aacc7 100644 --- a/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/RestEndpoints.bundle/mappings.json +++ b/proj-xcode/PowerAuth2IntegrationTests/PowerAuthServer/RestEndpoints.bundle/mappings.json @@ -9,8 +9,9 @@ "1.6" : "v15", "1.7" : "v15", "1.8" : "v15", + "1.9" : "v19", - "*" : "v15" + "*" : "v19" }, "mappings": { "v10": { @@ -20,7 +21,8 @@ "CreateApplicationVersion": "CreateApplicationVersion_v10", "SupportApplicationVersion": "SupportApplicationVersion_v10", "UnsupportApplicationVersion": "UnsupportApplicationVersion_v10", - "TokenValidate": "TokenValidate_v10" + "TokenValidate": "TokenValidate_v10", + "VerifyECDSASignature": "VerifyECDSASignature_v10" }, "v13": { "GetApplicationList": "GetApplicationList_v13", @@ -29,12 +31,19 @@ "CreateApplicationVersion": "CreateApplicationVersion_v13", "SupportApplicationVersion": "SupportApplicationVersion_v13", "UnsupportApplicationVersion": "UnsupportApplicationVersion_v13", - "TokenValidate": "TokenValidate_v10" + "TokenValidate": "TokenValidate_v10", + "VerifyECDSASignature": "VerifyECDSASignature_v10" }, "v15": { "#base": "v13", "GetApplicationDetail": "GetApplicationDetail_v15", "CreateApplicationVersion": "CreateApplicationVersion_v15", "TokenValidate": "TokenValidate_v15" + }, + "v19": { "#base": "v15", + "GetApplicationDetail": "GetApplicationDetail_v15", + "CreateApplicationVersion": "CreateApplicationVersion_v15", + "TokenValidate": "TokenValidate_v15", + "VerifyECDSASignature": "VerifyECDSASignature_v19" } - } + } } diff --git a/proj-xcode/PowerAuth2Tests/PA2ObjectSerializationTests.m b/proj-xcode/PowerAuth2Tests/PA2ObjectSerializationTests.m new file mode 100644 index 00000000..05e2e901 --- /dev/null +++ b/proj-xcode/PowerAuth2Tests/PA2ObjectSerializationTests.m @@ -0,0 +1,71 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#import +#import +#import + +#import "PA2ObjectSerialization.h" +#import "PA2PrivateMacros.h" + +@interface TestJwtObject : NSObject +@property (nonatomic, strong) NSString * text; +@end + +@implementation TestJwtObject +- (NSDictionary *)toDictionary +{ + return _text ? @{@"text": _text} : @{}; +} +- (instancetype)initWithDictionary:(NSDictionary *)dictionary +{ + self = [super init]; + if (self) { + _text = PA2ObjectAs(dictionary[@"text"], NSString); + } + return self; +} +@end + +@interface PA2ObjectSerializationTests : XCTestCase +@end + +@implementation PA2ObjectSerializationTests + +- (void) testJwtSerialization +{ + TestJwtObject * data = (TestJwtObject*)[PA2ObjectSerialization deserializeJwtObject:@"eyJ0ZXh0Ijoixb7DtMW-w6QifQ" forClass:[TestJwtObject class] error:nil]; + XCTAssertTrue([data.text isEqualToString:@"žôžä"]); + NSString * serializedData = [PA2ObjectSerialization serializeJwtObject:data]; + XCTAssertTrue([@"eyJ0ZXh0Ijoixb7DtMW-w6QifQ" isEqualToString:serializedData]); + data = (TestJwtObject*)[PA2ObjectSerialization deserializeJwtObject:@"eyJ0ZXh0Ijoi8J-SqT8_In0" forClass:[TestJwtObject class] error:nil]; + XCTAssertTrue([data.text isEqualToString:@"💩??"]); + serializedData = [PA2ObjectSerialization serializeJwtObject:data]; + XCTAssertTrue([@"eyJ0ZXh0Ijoi8J-SqT8_In0" isEqualToString:serializedData]); +} + +- (void) testJwtDataConversion +{ + for (NSUInteger i = 0; i < 1000; i++) { + NSUInteger len = arc4random_uniform(129); + NSData * input = [PowerAuthCoreCryptoUtils randomBytes:len]; + NSString * inputB64url = [input jwtEncodedString]; + NSData * output = [[NSData alloc] initWithJwtEncodedString:inputB64url]; + XCTAssertEqualObjects(input, output); + } +} + +@end diff --git a/proj-xcode/PowerAuthCore/PowerAuthCoreEciesEncryptor.h b/proj-xcode/PowerAuthCore/PowerAuthCoreEciesEncryptor.h index 571b20a7..7c07acf1 100644 --- a/proj-xcode/PowerAuthCore/PowerAuthCoreEciesEncryptor.h +++ b/proj-xcode/PowerAuthCore/PowerAuthCoreEciesEncryptor.h @@ -146,6 +146,20 @@ */ @interface PowerAuthCoreEciesCryptogram : NSObject +/** + Initialize cryptogram with JSON dictionary containing encrypted response payload. + */ +- (nullable instancetype) initWithResponsePayload:(nonnull id)responsePayload; + +/** + Get encrypted request payload in form of dictionary serializable to JSON. + */ +- (nonnull NSDictionary*) requestPayload; + +/** + Temporary key identifier + */ +@property (nonatomic, strong, nullable) NSString * temporaryKeyId; /** Encrypted data */ @@ -205,6 +219,7 @@ Initializes object with required `applicationKey` and with optional `activationIdentifier` */ - (nonnull instancetype) initWithApplicationKey:(nonnull NSString*)applicationKey + temporaryKeyId:(nonnull NSString*)temporaryKeyId activationIdentifier:(nullable NSString*)activationIdentifier; /** @@ -212,6 +227,11 @@ */ @property (nonatomic, strong, readonly, nonnull) NSString * applicationKey; +/** + Contains required temporary key identifier required for the HTTP header construction. + */ +@property (nonatomic, strong, readonly, nonnull) NSString * temporaryKeyId; + /** Contains optional activation identifier. */ diff --git a/proj-xcode/PowerAuthCore/PowerAuthCoreEciesEncryptor.mm b/proj-xcode/PowerAuthCore/PowerAuthCoreEciesEncryptor.mm index 076762bc..20a72f90 100644 --- a/proj-xcode/PowerAuthCore/PowerAuthCoreEciesEncryptor.mm +++ b/proj-xcode/PowerAuthCore/PowerAuthCoreEciesEncryptor.mm @@ -117,6 +117,7 @@ - (nullable PowerAuthCoreEciesCryptogram *) encryptRequest:(NSData *)data } PowerAuthCoreEciesCryptogram * cryptogram = [[PowerAuthCoreEciesCryptogram alloc] init]; cryptogram.timestamp = [timeService currentTime] * 1000.0; + cryptogram.temporaryKeyId = _associatedMetaData.temporaryKeyId; // Prepare ECIES parameters ECIESParameters params; params.timestamp = cryptogram.timestamp; @@ -176,6 +177,48 @@ @implementation PowerAuthCoreEciesCryptogram ECIESCryptogram _c; } +static inline void _SafeDictionarySet(NSMutableDictionary * dict, NSString * key, id value) +{ + if (value) [dict setObject:value forKey:key]; +} + +static inline id _SafeDictionaryGet(NSDictionary * dict, NSString * key, Class aClass) +{ + id value = dict[key]; + if ([value isKindOfClass:aClass]) { + return value; + } + return nil; +} + +- (instancetype) initWithResponsePayload:(id)responsePayload +{ + if (![responsePayload isKindOfClass:[NSDictionary class]]) { + return nil; + } + NSDictionary * dict = (NSDictionary*)responsePayload; + self = [super init]; + if (self) { + self.bodyBase64 = _SafeDictionaryGet(dict, @"encryptedData", [NSString class]); + self.nonceBase64 = _SafeDictionaryGet(dict, @"nonce", [NSString class]); + self.macBase64 = _SafeDictionaryGet(dict, @"mac", [NSString class]); + self.timestamp = [_SafeDictionaryGet(dict, @"timestamp", [NSNumber class]) unsignedLongLongValue]; + } + return self; +} + +- (NSDictionary*) requestPayload +{ + NSMutableDictionary * payload = [NSMutableDictionary dictionary]; + _SafeDictionarySet(payload, @"temporaryKeyId", _temporaryKeyId); + _SafeDictionarySet(payload, @"ephemeralPublicKey", self.keyBase64); + _SafeDictionarySet(payload, @"encryptedData", self.bodyBase64); + _SafeDictionarySet(payload, @"nonce", self.nonceBase64); + _SafeDictionarySet(payload, @"mac", self.macBase64); + payload[@"timestamp"] = @(_timestamp); + return payload; +} + - (ECIESCryptogram &) cryptogramRef { return _c; @@ -266,11 +309,13 @@ - (NSString*) nonceBase64 @implementation PowerAuthCoreEciesMetaData - (instancetype) initWithApplicationKey:(NSString*)applicationKey + temporaryKeyId:(NSString*)temporaryKeyId activationIdentifier:(NSString*)activationIdentifier { self = [super init]; if (self) { _applicationKey = applicationKey; + _temporaryKeyId = temporaryKeyId; _activationIdentifier = activationIdentifier; } return self; @@ -285,10 +330,10 @@ - (NSString*) httpHeaderValue { NSString * protocolVersion = [PowerAuthCoreSession maxSupportedHttpProtocolVersion:PowerAuthCoreProtocolVersion_NA]; NSString * value = [[[[@"PowerAuth version=\"" - stringByAppendingString:protocolVersion] - stringByAppendingString:@"\", application_key=\""] - stringByAppendingString:_applicationKey] - stringByAppendingString:@"\""]; + stringByAppendingString:protocolVersion] + stringByAppendingString:@"\", application_key=\""] + stringByAppendingString:_applicationKey] + stringByAppendingString:@"\""]; if (_activationIdentifier) { return [[[value stringByAppendingString:@", activation_id=\""] stringByAppendingString:_activationIdentifier] @@ -300,8 +345,9 @@ - (NSString*) httpHeaderValue - (cc7::ByteArray) associatedData { auto appKey = cc7::objc::CopyFromNSString(_applicationKey); + auto keyId = cc7::objc::CopyFromNSString(_temporaryKeyId); auto activationId = cc7::objc::CopyFromNSString(_activationIdentifier); - return ECIESUtils::buildAssociatedData(appKey, activationId); + return ECIESUtils::buildAssociatedData(appKey, keyId, activationId); } @end diff --git a/proj-xcode/PowerAuthCore/PowerAuthCoreSession.h b/proj-xcode/PowerAuthCore/PowerAuthCoreSession.h index dbd4e13e..00a9d102 100644 --- a/proj-xcode/PowerAuthCore/PowerAuthCoreSession.h +++ b/proj-xcode/PowerAuthCore/PowerAuthCoreSession.h @@ -36,11 +36,12 @@ /** Resets session into its initial state. The existing session's setup and EEK is preserved - after the call. + after the call. If `fullReset` parameter is YES, then also resets data not relevant + to the activation state. For example, ECIES public key for application scope. This function changes the session's state, so write access must be guaranteed. */ -- (void) resetSession; +- (void) resetSession:(BOOL)fullReset; /** Returns YES if PowerAuthCore library was compiled with a debug features. It is highly recommended @@ -184,9 +185,9 @@ returned value is nil, then the error occured. You can determine the failure reason from DEBUG log: - EC_Encryption, if you provided invalid Base64 strings or if signature is invalid - EC_WrongState, if called in wrong session's state - EC_WrongParam, if some required parameter is missing + PowerAuthCoreErrorCode_Encryption, if you provided invalid Base64 strings or if signature is invalid + PowerAuthCoreErrorCode_WrongState, if called in wrong session's state + PowerAuthCoreErrorCode_WrongParam, if some required parameter is missing This function changes the session's state, so write access must be guaranteed. */ @@ -212,10 +213,10 @@ If the returned value is nil, then the error occured. You can determine the failure reason from DEBUG log: - EC_Encryption, if provided data, signature or keys are invalid. - If this error occurs then the session resets its state. - EC_WrongState, if called in wrong session's state - EC_WrongParam, if required parameter is missing + PowerAuthCoreErrorCode_Encryption, if provided data, signature or keys are invalid. + If this error occurs then the session resets its state. + PowerAuthCoreErrorCode_WrongState, if called in wrong session's state + PowerAuthCoreErrorCode_WrongParam, if required parameter is missing This function changes the session's state, so write access must be guaranteed. */ @@ -234,10 +235,10 @@ Returns YES if operation succeeds. In case of faulure, you can determine the failure reason from DEBUG log: - EC_Encryption, if some internal encryption failed - if this error occurs, then the session resets its state - EC_WrongState, if called in wrong session's state - EC_WrongParam, if required parameter is missing + PowerAuthCoreErrorCode_Encryption, if some internal encryption failed + if this error occurs, then the session resets its state + PowerAuthCoreErrorCode_WrongState, if called in wrong session's state + PowerAuthCoreErrorCode_WrongParam, if required parameter is missing This function changes the session's state, so write access must be guaranteed. */ @@ -294,9 +295,9 @@ Returns string with autorization header or nil if opeartion failed. You can determine the failure reason from DEBUG log: - EC_Encryption, if some cryptographic operation failed - EC_WrongState, if the session has no valid activation - EC_WrongParam, if some required parameter is missing + PowerAuthCoreErrorCode_Encryption, if some cryptographic operation failed + PowerAuthCoreErrorCode_WrongState, if the session has no valid activation + PowerAuthCoreErrorCode_WrongParam, if some required parameter is missing This function changes the session's state, so write access must be guaranteed. */ @@ -315,14 +316,31 @@ Validates whether the data has been signed with master server private key. Returns YES if signature is valid. In case of error, you can determine the failure reason from DEBUG log: - EC_Encryption if signature is not valid or some cryptographic operation failed - EC_WrongState if session contains invalid setup - EC_WrongParam if signedData object doesn't contain signature + PowerAuthCoreErrorCode_Encryption if signature is not valid or some cryptographic operation failed + PowerAuthCoreErrorCode_WrongState if session contains invalid setup + PowerAuthCoreErrorCode_WrongParam if signedData object doesn't contain signature This function access the session's state, so read access must be guaranteed. */ - (BOOL) verifyServerSignedData:(nonnull PowerAuthCoreSignedData*)signedData; +/** + Calculates HMAC-SHA256 signature with using key specified in |dataToSign|. The output signature is + also stored to provided data object. If `HMAC_Activation` key is requested, then |unlockKeys| must + contain possession factor unlock key and the session must have valid activation. + + Returns YES if signature is calculated. In case of error, you can determine the failure reason + from DEBUG log: + PowerAuthCoreErrorCode_Ok if operation succeeded and signature is computed. + PowerAuthCoreErrorCode_Encryption if cryptographic operation failed. + PowerAuthCoreErrorCode_WrongState if session contains invalid setup, or valid activation is required + for the requested key. + PowerAuthCoreErrorCode_WrongParam if keys structure doesn't contain possession factor unlock key + and the key is required. + */ +- (BOOL) signDataWithHmacKey:(nonnull PowerAuthCoreSignedData*)dataToSign + keys:(nullable PowerAuthCoreSignatureUnlockKeys*)unlockKeys; + #pragma mark - Signature keys management /** @@ -349,9 +367,9 @@ Returns YES if operation succeeds or NO in case of failure. You can determine the failure reason from DEBUG log: - EC_Encryption, if underlying cryptograhic operation did fail or - if you provided too short passwords. - EC_WrongState, if the session has no valid activation + PowerAuthCoreErrorCode_Encryption, if underlying cryptograhic operation did fail or + if you provided too short passwords. + PowerAuthCoreErrorCode_WrongState, if the session has no valid activation This function changes the session's state, so write access must be guaranteed. */ @@ -365,9 +383,9 @@ Returns YES if operation succeeds or NO in case of failure. You can determine the failure reason from DEBUG log: - EC_Encryption, if general encryption error occurs - EC_WrongState, if the session has no valid activation - EC_WrongParam, if some required parameter is missing + PowerAuthCoreErrorCode_Encryption, if general encryption error occurs + PowerAuthCoreErrorCode_WrongState, if the session has no valid activation + PowerAuthCoreErrorCode_WrongParam, if some required parameter is missing This function changes the session's state, so write access must be guaranteed. */ @@ -386,7 +404,7 @@ Removes existing key for biometric signatures from the session. You have to save state of the session after the operation. Returns YES if operation succeeds or NO in case of failure. You can determine the failure reason from DEBUG log: - EC_WrongState, if the session has no valid activation + PowerAuthCoreErrorCode_WrongState, if the session has no valid activation This function changes the session's state, so write access must be guaranteed. */ @@ -411,9 +429,9 @@ Retuns NSData object with a derived cryptographic key or nil in case of failure. You can determine the failure reason from DEBUG log: - EC_Encryption, if general encryption error occurs - EC_WrongState, if the session has no valid activation - EC_WrongParam, if some required parameter is missing + PowerAuthCoreErrorCode_Encryption, if general encryption error occurs + PowerAuthCoreErrorCode_WrongState, if the session has no valid activation + PowerAuthCoreErrorCode_WrongParam, if some required parameter is missing This function access the session's state, so read access must be guaranteed. */ @@ -431,15 +449,16 @@ Retuns NSData object with calculated signature or nil in case of failure. You can determine the failure reason from DEBUG log: - EC_Encryption, if general encryption error occurs - EC_WrongState, if the session has no valid activation - EC_WrongParam, if some required parameter is missing + PowerAuthCoreErrorCode_Encryption, if general encryption error occurs + PowerAuthCoreErrorCode_WrongState, if the session has no valid activation + PowerAuthCoreErrorCode_WrongParam, if some required parameter is missing This function access the session's state, so read access must be guaranteed. */ - (nullable NSData*) signDataWithDevicePrivateKey:(nonnull NSString*)cVaultKey keys:(nonnull PowerAuthCoreSignatureUnlockKeys*)unlockKeys - data:(nonnull NSData*)data; + data:(nonnull NSData*)data + format:(PowerAuthCoreSignatureFormat)format; #pragma mark - External Encryption Key @@ -457,11 +476,10 @@ decode. The data signing will also work correctly, but only for a possession factor, which is by design not protected with EEK. - Returns YES if operation succeeded or NO in case of failure. You can determine the failure - reason from DEBUG log: - EC_WrongParam if key is already set and new EEK is different, or - if provided key has invalid length. - EC_WrongState if you're setting key to activated session which doesn't use EEK + Returns:: + PowerAuthCoreErrorCode_WrongParam if key is already set and new EEK is different, or + if provided key has invalid length. + PowerAuthCoreErrorCode_WrongState if you're setting key to activated session which doesn't use EEK This function access the session's state, so read access must be guaranteed. */ @@ -474,12 +492,11 @@ You have to save state of the session after the operation. - Returns YES if operation succeeded or NO in case of failure. You can determine the failure - reason from DEBUG log: - EC_WrongParam if the EEK has wrong size - EC_WrongState if session has no valid activation, or - if the EEK is already set. - EC_Encryption if internal cryptographic operation failed + Returns: + PowerAuthCoreErrorCode_WrongParam if the EEK has wrong size + PowerAuthCoreErrorCode_WrongState if session has no valid activation, or + if the EEK is already set. + PowerAuthCoreErrorCode_Encryption if internal cryptographic operation failed This function changes the session's state, so write access must be guaranteed. */ @@ -492,11 +509,10 @@ You have to save state of the session after the operation. - Returns YES if operation succeeded or NO in case of failure. You can determine the failure - reason from DEBUG log: - EC_WrongState if session has no valid activation, or - if session has no EEK set - EC_Encryption if internal cryptographic operation failed + Returns: + PowerAuthCoreErrorCode_WrongState if session has no valid activation, or + if session has no EEK set + PowerAuthCoreErrorCode_Encryption if internal cryptographic operation failed This function changes the session's state, so write access must be guaranteed. */ @@ -514,6 +530,38 @@ - (nullable PowerAuthCoreEciesEncryptor*) eciesEncryptorForScope:(PowerAuthCoreEciesEncryptorScope)scope keys:(nullable PowerAuthCoreSignatureUnlockKeys*)unlockKeys sharedInfo1:(nullable NSData*)sharedInfo1; +/** + Sets a server's public key and its identifier for ECIES encryption. The scope of the encryption is + determined by |scope| parameter. + + Returns: + PowerAuthCoreErrorCode_Ok if operation succeeded. + PowerAuthCoreErrorCode_WrongState if activation scope is used and the session has no valid activation, or + if session object has no valid setup. + PowerAuthCoreErrorCode_WrongParam if public key is empty, or doesn't contain Base64 encoded data, or + if key identifier is empty. + + */ +- (PowerAuthCoreErrorCode) setPublicKeyForEciesScope:(PowerAuthCoreEciesEncryptorScope)scope + publicKey:(nonnull NSString*)publicKey + publicKeyId:(nonnull NSString*)publicKeyId; + +/** + Removes a server's public key and its identifier store for the given scope. It's safe to call this + function if key for given scope is not set. + */ +- (void) removePublicKeyForEciesScope:(PowerAuthCoreEciesEncryptorScope)scope; + +/** + Determines whether session contains stored server's public key for ECIES scope. + */ +- (BOOL) hasPublicKeyForEciesScope:(PowerAuthCoreEciesEncryptorScope)scope; + +/** + Returns identifier of server's public key for given scope. If no key is set, or if function is called in + wrong state, then returns `nil`. + */ +- (nullable NSString*) publicKeyIdForEciesScope:(PowerAuthCoreEciesEncryptorScope)scope; #pragma mark - Utilities for generic keys @@ -602,7 +650,7 @@ /** Returns textual representation for given protocol version. For example, for `PowerAuthCoreProtocolVersion_V3` - returns "3.2". You can use `PowerAuthCoreProtocolVersion_NA` to get the lastest supported version. + returns "3.3". You can use `PowerAuthCoreProtocolVersion_NA` to get the lastest supported version. */ + (nonnull NSString*) maxSupportedHttpProtocolVersion:(PowerAuthCoreProtocolVersion)protocolVersion; diff --git a/proj-xcode/PowerAuthCore/PowerAuthCoreSession.mm b/proj-xcode/PowerAuthCore/PowerAuthCoreSession.mm index 2b69d4f8..a83097c2 100644 --- a/proj-xcode/PowerAuthCore/PowerAuthCoreSession.mm +++ b/proj-xcode/PowerAuthCore/PowerAuthCoreSession.mm @@ -72,10 +72,10 @@ - (void) dealloc delete _session; } -- (void) resetSession +- (void) resetSession:(BOOL)fullReset { REQUIRE_WRITE_ACCESS(); - _session->resetSession(); + _session->resetSession(fullReset); } @@ -295,6 +295,22 @@ - (BOOL) verifyServerSignedData:(nonnull PowerAuthCoreSignedData*)signedData return error == EC_Ok; } +- (BOOL) signDataWithHmacKey:(nonnull PowerAuthCoreSignedData*)dataToSign + keys:(nullable PowerAuthCoreSignatureUnlockKeys*)unlockKeys +{ + REQUIRE_READ_ACCESS(); + ErrorCode error; + if (dataToSign != nil) { + SignatureUnlockKeys cpp_keys; + PowerAuthCoreSignatureUnlockKeysToStruct(unlockKeys, cpp_keys); + error = _session->signDataWithHmacKey(dataToSign.signedDataRef, cpp_keys); + } else { + error = EC_WrongParam; + } + REPORT_ERROR_CODE(@"signDataWithHmacKey", error); + return error == EC_Ok; +} + #pragma mark - Signature keys management @@ -364,15 +380,17 @@ - (nullable NSData*) deriveCryptographicKeyFromVaultKey:(nonnull NSString*)cVaul - (nullable NSData*) signDataWithDevicePrivateKey:(nonnull NSString*)cVaultKey keys:(nonnull PowerAuthCoreSignatureUnlockKeys*)unlockKeys data:(nonnull NSData*)data + format:(PowerAuthCoreSignatureFormat)format { REQUIRE_READ_ACCESS(); std::string cpp_c_vault_key = cc7::objc::CopyFromNSString(cVaultKey); cc7::ByteArray cpp_data = cc7::objc::CopyFromNSData(data); + auto cpp_format = static_cast(format); SignatureUnlockKeys cpp_keys; PowerAuthCoreSignatureUnlockKeysToStruct(unlockKeys, cpp_keys); cc7::ByteArray cpp_signature; - auto error = _session->signDataWithDevicePrivateKey(cpp_c_vault_key, cpp_keys, cpp_data, cpp_signature); + auto error = _session->signDataWithDevicePrivateKey(cpp_c_vault_key, cpp_keys, cpp_data, cpp_format, cpp_signature); if (error == EC_Ok) { return cc7::objc::CopyToNSData(cpp_signature); } @@ -435,6 +453,37 @@ - (nullable PowerAuthCoreEciesEncryptor*) eciesEncryptorForScope:(PowerAuthCoreE return [[PowerAuthCoreEciesEncryptor alloc] initWithObject:cpp_encryptor timeService:_timeService]; } +- (PowerAuthCoreErrorCode) setPublicKeyForEciesScope:(PowerAuthCoreEciesEncryptorScope)scope + publicKey:(NSString*)publicKey + publicKeyId:(NSString*)publicKeyId +{ + REQUIRE_READ_ACCESS(); // we don't nodify shared data, so read access is OK + auto cpp_scope = static_cast(scope); + auto cpp_public_key = cc7::objc::CopyFromNSString(publicKey); + auto cpp_public_key_id = cc7::objc::CopyFromNSString(publicKeyId); + auto error = _session->setPublicKeyForEciesScope(cpp_scope, cpp_public_key, cpp_public_key_id); + REPORT_ERROR_CODE(@"SetPublicKeyForEciesScope", error); + return static_cast(error); +} + +- (void) removePublicKeyForEciesScope:(PowerAuthCoreEciesEncryptorScope)scope +{ + REQUIRE_READ_ACCESS(); // we don't nodify shared data, so read access is OK + _session->removePublicKeyForEciesScope(static_cast(scope)); +} + +- (BOOL) hasPublicKeyForEciesScope:(PowerAuthCoreEciesEncryptorScope)scope +{ + REQUIRE_READ_ACCESS(); + return _session->hasPublicKeyForEciesScope(static_cast(scope)); +} + +- (NSString*) publicKeyIdForEciesScope:(PowerAuthCoreEciesEncryptorScope)scope +{ + REQUIRE_READ_ACCESS(); + return cc7::objc::CopyToNullableNSString(_session->getPublicKeyIdForEciesScope(static_cast(scope))); +} + #pragma mark - Utilities for generic keys + (nonnull NSData*) normalizeSignatureUnlockKeyFromData:(nonnull NSData*)data diff --git a/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h b/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h index 5be16f36..943a526e 100644 --- a/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h +++ b/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.h @@ -49,6 +49,11 @@ String with complete cryptographic configuration. */ @property (nonatomic, strong, readonly, nonnull) NSString * configuration; +/** + Application key extracted from the cryptographic configuration. + */ +@property (nonatomic, strong, readonly, nonnull) NSString * applicationKey; + /** Optional external encryption key. If the data object size is equal to 16 bytes, then the key is considered as valid and will be used during the cryptographic operations. @@ -303,14 +308,55 @@ typedef NS_ENUM(int, PowerAuthCoreSigningDataKey) { `KEY_SERVER_PRIVATE` key was used for signature calculation */ PowerAuthCoreSigningDataKey_ECDSA_PersonalizedKey = 1, + /** + `APP_SECRET` key is used for HMAC-SHA256 signature calculation. + */ + PowerAuthCoreSigningDataKey_HMAC_Application = 2, + /** + `KEY_TRANSPORT` key is used for HMAC-SHA256 signature calculation. + */ + PowerAuthCoreSigningDataKey_HMAC_Activation = 3 +}; + +/** + The `PowerAuthCoreSignatureFormat` enumeration defines signature type expected at input, or produced + at output. + */ +typedef NS_ENUM(int, PowerAuthCoreSignatureFormat) { + /** + If used, then `PowerAuthCoreSignatureFormat_ECDSA_DER` is used for ECDSA signature. + For the HMAC signature, the raw bytes is always used. + */ + PowerAuthCoreSignatureFormat_Default = 0, + /** + ECDSA signature in DER format is expected at input, or produced at output: + ``` + // ASN.1 notation: + ECDSASignature ::= SEQUENCE { + r INTEGER, + s INTEGER + } + ``` + */ + PowerAuthCoreSignatureFormat_ECDSA_DER = 1, + /** + ECDSA signature in JOSE format is epxpected at input, or produced at output. + */ + PowerAuthCoreSignatureFormat_ECDSA_JOSE = 2 }; /** The PowerAuthCoreSignedData object contains data and signature calculated from data. */ @interface PowerAuthCoreSignedData : NSObject - +/** + A signing key to use. + */ @property (nonatomic, assign) PowerAuthCoreSigningDataKey signingDataKey; +/** + A format of signature expected at input or produced at output. + */ +@property (nonatomic, assign) PowerAuthCoreSignatureFormat signatureFormat; /** A data protected with signature */ diff --git a/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.mm b/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.mm index 78a034a5..e613cc1f 100644 --- a/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.mm +++ b/proj-xcode/PowerAuthCore/PowerAuthCoreTypes.mm @@ -60,6 +60,12 @@ - (id) initWithConfiguration:(NSString *)configuration return _setup; } +- (NSString*) applicationKey +{ + return cc7::objc::CopyToNSString(_setup.applicationKey); +} + + - (void) setExternalEncryptionKey:(NSData *)externalEncryptionKey { _setup.externalEncryptionKey = cc7::objc::CopyFromNSData(externalEncryptionKey); @@ -68,7 +74,6 @@ - (NSData*) externalEncryptionKey { return cc7::objc::CopyToNSData(_setup.externalEncryptionKey); } - @end @implementation PowerAuthCoreHTTPRequestData @@ -151,6 +156,17 @@ - (void) setSigningDataKey:(PowerAuthCoreSigningDataKey)signingDataKey _signedData.signingKey = static_cast(signingDataKey); } +// Signature type + +- (PowerAuthCoreSignatureFormat) signatureFormat +{ + return static_cast(_signedData.signatureFormat); +} + +- (void) setSignatureFormat:(PowerAuthCoreSignatureFormat)signatureType +{ + _signedData.signatureFormat = static_cast(signatureType); +} // Bytes setters and getters @@ -199,7 +215,7 @@ - (void) setSignatureBase64:(NSString *)signatureBase64 #ifdef DEBUG - (NSString*) description { - return [NSString stringWithFormat:@"", self.dataBase64, self.signatureBase64]; + return [NSString stringWithFormat:@"", _signedData.signingKey, self.dataBase64, self.signatureBase64]; } #endif diff --git a/proj-xcode/PowerAuthCoreTests/PowerAuthCoreEciesEncryptorTest.mm b/proj-xcode/PowerAuthCoreTests/PowerAuthCoreEciesEncryptorTest.mm index 497b57ee..cf80f35e 100644 --- a/proj-xcode/PowerAuthCoreTests/PowerAuthCoreEciesEncryptorTest.mm +++ b/proj-xcode/PowerAuthCoreTests/PowerAuthCoreEciesEncryptorTest.mm @@ -63,6 +63,8 @@ @implementation PowerAuthCoreEciesEncryptorTest NSString * _activationId; NSArray * _sharedInfo1Values; NSUInteger _sharedInfo1Index; + NSString * _tempKeyIdActivation; + NSString * _tempKeyIdApplication; io::getlime::powerAuth::ECIESDecryptor _decryptor; @@ -89,6 +91,8 @@ - (void) setUp @[@"/pa/recovery/confirm", @"CONFIRM_RECOVERY_CODE"] ]; _timeService = [[TestTimeService alloc] init]; + _tempKeyIdActivation = [NSUUID UUID].UUIDString; + _tempKeyIdApplication = [NSUUID UUID].UUIDString; } - (void) testGenerateEciesTestVectors @@ -101,7 +105,6 @@ - (void) testGenerateEciesTestVectors NSMutableArray * arrSharedInfo1s = [NSMutableArray array]; NSMutableArray * arrEncryptedRequests = [NSMutableArray array]; NSMutableArray * arrEncryptedResponses = [NSMutableArray array]; - td[@"configuration"] = @{ @"keyMasterPrivate": [_masterKeyPair.privateKey.privateKeyBytes base64EncodedStringWithOptions:0], @"keyMasterPublic": [_masterKeyPair.publicKey.publicKeyBytes base64EncodedStringWithOptions:0], @@ -110,7 +113,9 @@ - (void) testGenerateEciesTestVectors @"applicationKey": _appKey, @"applicationSecret": _appSecret, @"transportKey": [_transportKey base64EncodedStringWithOptions:0], - @"activationId": _activationId + @"activationId": _activationId, + @"tempKeyIdApplication": _tempKeyIdApplication, + @"tempKeyIdActivation": _tempKeyIdActivation }; td[@"testData"] = tda; for (int i = 0; i < 16; i++) { @@ -152,6 +157,7 @@ - (void) testGenerateEciesTestVectors [arrScopes addObject:@(appScope)]; NSDictionary * req = @{ + @"temporaryKeyId": appScope ? _tempKeyIdApplication : _tempKeyIdActivation, @"ephemeralPublicKey": request.keyBase64, @"encryptedData": request.bodyBase64, @"mac": request.macBase64, @@ -189,6 +195,8 @@ - (void) testGenerateEciesTestVectors TestGen(@"final PublicKey masterServerPublicKey = keyConvertor.convertBytesToPublicKey(Base64.getDecoder().decode(\"%@\"));", td[@"configuration"][@"keyMasterPublic"]); TestGen(@"final PrivateKey serverPrivateKey = keyConvertor.convertBytesToPrivateKey(ByteUtils.concat(new byte[1], Base64.getDecoder().decode(\"%@\")));", td[@"configuration"][@"keyServerPrivate"]); TestGen(@"final PublicKey serverPublicKey = keyConvertor.convertBytesToPublicKey(Base64.getDecoder().decode(\"%@\"));", td[@"configuration"][@"keyServerPublic"]); + TestGen(@"final String tempKeyIdApplication = \"%@\";", _tempKeyIdApplication); + TestGen(@"final String tempKeyIdActivation = \"%@\";", _tempKeyIdActivation); TestGen(@"final String activationId = \"%@\";", _activationId); TestGen(@"final String applicationKey = \"%@\";", _appKey); TestGen(@"final String applicationSecret = \"%@\";", _appSecret); @@ -227,6 +235,7 @@ - (void) testGenerateEciesTestVectors TestGen(@"final EncryptedRequest[] encryptedRequest = {"); [arrEncryptedRequests enumerateObjectsUsingBlock:^(NSDictionary * obj, NSUInteger idx, BOOL * _Nonnull stop) { TestGen(@" new EncryptedRequest("); + TestGen(@" \"%@\",", obj[@"temporaryKeyId"]); TestGen(@" \"%@\",", obj[@"ephemeralPublicKey"]); TestGen(@" \"%@\",", obj[@"encryptedData"]); TestGen(@" \"%@\",", obj[@"mac"]); @@ -279,6 +288,7 @@ - (NSData*) sh2ForScope:(BOOL)appScope - (PowerAuthCoreEciesEncryptor*) createEncryptor:(BOOL)appScope sh1:(NSString*)sh1 { NSData * publicKeyBytes = appScope ? _masterKeyPair.publicKey.publicKeyBytes : _serverKeyPair.publicKey.publicKeyBytes; + NSString * tempKeyId = appScope ? _tempKeyIdApplication : _tempKeyIdActivation; NSData * sharedInfo1 = [sh1 dataUsingEncoding:NSUTF8StringEncoding]; NSData * sharedInfo2 = [self sh2ForScope:appScope]; NSString * activationId = appScope ? nil : _activationId; @@ -286,7 +296,9 @@ - (PowerAuthCoreEciesEncryptor*) createEncryptor:(BOOL)appScope sh1:(NSString*)s publicKey:publicKeyBytes sharedInfo1:sharedInfo1 sharedInfo2:sharedInfo2]; - encryptor.associatedMetaData = [[PowerAuthCoreEciesMetaData alloc] initWithApplicationKey:_appKey activationIdentifier:activationId]; + encryptor.associatedMetaData = [[PowerAuthCoreEciesMetaData alloc] initWithApplicationKey:_appKey + temporaryKeyId:tempKeyId + activationIdentifier:activationId]; return encryptor; } diff --git a/src/PowerAuth/ECIES.cpp b/src/PowerAuth/ECIES.cpp index c2bf938e..57c216c8 100644 --- a/src/PowerAuth/ECIES.cpp +++ b/src/PowerAuth/ECIES.cpp @@ -405,15 +405,15 @@ namespace powerAuth // MARK: - Utilities - // - cc7::ByteArray ECIESUtils::buildAssociatedData(const std::string &applicationKey, const std::string &activationId) { + cc7::ByteArray ECIESUtils::buildAssociatedData(const std::string &applicationKey, const std::string & temporaryKeyId, const std::string &activationId) { auto version = Version_GetMaxSupportedHttpProtocolVersion(Version_Latest); cc7::ByteArray ad; if (activationId.empty()) { // Application scope - ad = utils::ByteUtils_Join({ cc7::MakeRange(version), cc7::MakeRange(applicationKey) }); + ad = utils::ByteUtils_Join({ cc7::MakeRange(version), cc7::MakeRange(applicationKey), cc7::MakeRange(temporaryKeyId) }); } else { // Activation scope - ad = utils::ByteUtils_Join({ cc7::MakeRange(version), cc7::MakeRange(applicationKey), cc7::MakeRange(activationId) }); + ad = utils::ByteUtils_Join({ cc7::MakeRange(version), cc7::MakeRange(applicationKey), cc7::MakeRange(activationId), cc7::MakeRange(temporaryKeyId) }); } return ad; } diff --git a/src/PowerAuth/Session.cpp b/src/PowerAuth/Session.cpp index 68fbbc51..6ae250fe 100644 --- a/src/PowerAuth/Session.cpp +++ b/src/PowerAuth/Session.cpp @@ -44,7 +44,8 @@ namespace powerAuth _state(SS_Empty), _setup(setup), _pd(nullptr), - _ad(nullptr) + _ad(nullptr), + _sd(new protocol::SessionData()) { if (protocol::ValidateSessionSetup(_setup, false)) { CC7_LOG("Session %p: Object created.", this); @@ -58,13 +59,17 @@ namespace powerAuth { delete _pd; delete _ad; + delete _sd; CC7_LOG("Session %p: Object destroyed.", this); } - void Session::resetSession() + void Session::resetSession(bool full_reset) { LOCK_GUARD(); + if (full_reset) { + _sd->reset(); + } commitNewPersistentState(nullptr, SS_Empty); } @@ -675,6 +680,10 @@ namespace powerAuth CC7_LOG("Session %p: ServerSig: Session has no valid setup.", this); return EC_WrongState; } + if (!data.isEcdsaSignature()) { + CC7_LOG("Session %p: ServerSig: Unsupported key.", this); + return EC_WrongParam; + } bool use_master_server_key = data.signingKey == SignedData::ECDSA_MasterServerKey; if (!use_master_server_key && !hasValidActivation()) { CC7_LOG("Session %p: ServerSig: There's no valid activation.", this); @@ -697,7 +706,13 @@ namespace powerAuth } if (nullptr != ec_public_key) { // validate signature - success = crypto::ECDSA_ValidateSignature(data.data, data.signature, ec_public_key); + if (data.signatureFormat == SignedData::ECDSA_JOSE) { + // Convert signature from JOSE to DER first. + success = crypto::ECDSA_ValidateSignature(data.data, crypto::ECDSA_JOSEtoDER(data.signature), ec_public_key); + } else { + // No signature conversion required. + success = crypto::ECDSA_ValidateSignature(data.data, data.signature, ec_public_key); + } // } else { CC7_LOG("Session %p: ServerSig: %s public key is invalid.", this, use_master_server_key ? "Master server" : "Server"); @@ -707,6 +722,39 @@ namespace powerAuth return success ? EC_Ok : EC_Encryption; } + + ErrorCode Session::signDataWithHmacKey(SignedData &data, const SignatureUnlockKeys & keys) const + { + LOCK_GUARD(); + if (!hasValidSetup()) { + CC7_LOG("Session %p: HmacSign: Session has no valid setup.", this); + return EC_WrongState; + } + if (!data.isHmacSignature()) { + CC7_LOG("Session %p: HmacSign: Unsupported key.", this); + return EC_WrongParam; + } + bool app_scope = data.signingKey == SignedData::HMAC_Application; + if (!app_scope && !hasValidActivation()) { + CC7_LOG("Session %p: ServerSig: There's no valid activation.", this); + return EC_WrongState; + } + cc7::ByteArray signing_key; + if (app_scope) { + signing_key.readFromBase64String(_setup.applicationSecret); + } else { + // Unlock transport key + protocol::SignatureKeys plain; + protocol::SignatureUnlockKeysReq unlock_request(protocol::SF_Transport, &keys, eek(), nullptr, 0); + if (false == protocol::UnlockSignatureKeys(plain, _pd->sk, unlock_request)) { + CC7_LOG("Session %p: HmacSign: You have to provide possession key.", this); + return EC_WrongParam; + } + signing_key = protocol::DeriveSecretKeyFromIndex(plain.transportKey, cc7::FromBase64String(_setup.applicationSecret)); + } + data.signature = crypto::HMAC_SHA256(data.data, signing_key); + return data.signature.empty() ? EC_Encryption : EC_Ok; + } // MARK: - Signature keys management - @@ -862,7 +910,8 @@ namespace powerAuth } ErrorCode Session::signDataWithDevicePrivateKey(const std::string & c_vault_key, const SignatureUnlockKeys & keys, - const cc7::ByteRange & in_data, cc7::ByteArray & out_signature) + const cc7::ByteRange & in_data, SignedData::SignatureFormat out_format, + cc7::ByteArray & out_signature) { LOCK_GUARD(); cc7::ByteArray vault_key; @@ -890,6 +939,13 @@ namespace powerAuth // Signature calculation failed. break; } + if (out_format == SignedData::ECDSA_JOSE) { + out_signature = crypto::ECDSA_DERtoJOSE(out_signature); + if (out_signature.empty()) { + // Conversion to JOSE format failed. + break; + } + } code = EC_Ok; } while (false); @@ -1053,8 +1109,8 @@ namespace powerAuth ErrorCode Session::getEciesEncryptor(ECIESEncryptorScope scope, const SignatureUnlockKeys & keys, const cc7::ByteRange & sharedInfo1, ECIESEncryptor & out_encryptor) const { LOCK_GUARD(); - if (!hasValidSetup()) { - CC7_LOG("Session %p: ECIES: Session has no valid setup.", this); + if (!hasPublicKeyForEciesScope(scope)) { + CC7_LOG("Session %p: ECIES: Session has no public key for scope.", this); return EC_WrongState; } // Other parameters for ECIES encryptor @@ -1066,7 +1122,7 @@ namespace powerAuth // We have to just compute hash from APP_SECRET (as is) and use // the master server public key. sharedInfo2 = crypto::SHA256(cc7::MakeRange(_setup.applicationSecret)); - ecPublicKey = cc7::FromBase64String(_setup.masterServerPublicKey); + ecPublicKey = _sd->ecies_application_public_key.key_data; // } else if (scope == ECIES_ActivationScope) { // For the "activation" scope, we need to at first validate whether there's @@ -1085,7 +1141,7 @@ namespace powerAuth // The sharedInfo2 is defined as HMAC_SHA256(key: KEY_TRANSPORT, data: APP_SECRET) // We need to also use the server's public key as EC public key. sharedInfo2 = crypto::HMAC_SHA256(cc7::MakeRange(_setup.applicationSecret), plain_keys.transportKey); - ecPublicKey = _pd->serverPublicKey; + ecPublicKey = _sd->ecies_activation_public_key.key_data; // } else { // Scope is not known @@ -1097,6 +1153,75 @@ namespace powerAuth return EC_Ok; } + + ErrorCode Session::setPublicKeyForEciesScope(ECIESEncryptorScope scope, const std::string & public_key, const std::string & key_id) + { + LOCK_GUARD(); + if (!hasValidSetup()) { + CC7_LOG("Session %p: ECIES: Session has no valid setup.", this); + return EC_WrongState; + } + bool result; + if (scope == ECIES_ApplicationScope) { + // For "application" scope, just update the key. + result = _sd->ecies_application_public_key.setKey(public_key, key_id); + } else { + // For the "activation" scope, we need to at first validate whether there's + // some activation. + if (!hasValidActivation()) { + CC7_LOG("Session %p: ECIES: Session has no valid activation.", this); + return EC_WrongState; + } + // Now update the key. + result = _sd->ecies_activation_public_key.setKey(public_key, key_id); + } + return result ? EC_Ok : EC_WrongParam; + } + + + void Session::removePublicKeyForEciesScope(ECIESEncryptorScope scope) + { + LOCK_GUARD(); + if (scope == ECIES_ApplicationScope) { + _sd->ecies_application_public_key.clear(); + } else { + _sd->ecies_activation_public_key.clear(); + } + } + + + bool Session::hasPublicKeyForEciesScope(ECIESEncryptorScope scope) const + { + LOCK_GUARD(); + if (!hasValidSetup()) { + CC7_LOG("Session %p: ECIES: Session has no valid setup.", this); + return false; + } + if (scope == ECIES_ApplicationScope) { + return _sd->ecies_application_public_key.isValid(); + } else { + if (!hasValidActivation()) { + CC7_LOG("Session %p: ECIES: Session has no valid activation.", this); + return false; + } + return _sd->ecies_activation_public_key.isValid(); + } + } + + std::string Session::getPublicKeyIdForEciesScope(ECIESEncryptorScope scope) const + { + LOCK_GUARD(); + if (hasPublicKeyForEciesScope(scope)) { + if (scope == ECIES_ApplicationScope) { + return _sd->ecies_application_public_key.identifier; + } else { + return _sd->ecies_activation_public_key.identifier; + } + } + return std::string(); + } + + // MARK: - Protocol upgrade - ErrorCode Session::startProtocolUpgrade() @@ -1254,6 +1379,11 @@ namespace powerAuth new_state = SS_Empty; } + // Additionally, clear other internal structures. + if (new_state == SS_Empty) { + _sd->resetActivationData(); + } + // Finally, change internal state of the session changeState(new_state); } diff --git a/src/PowerAuth/crypto/ECC.cpp b/src/PowerAuth/crypto/ECC.cpp index 28fb8276..cbd450d1 100644 --- a/src/PowerAuth/crypto/ECC.cpp +++ b/src/PowerAuth/crypto/ECC.cpp @@ -23,6 +23,9 @@ #include +#include "../utils/DataReader.h" +#include "../utils/DataWriter.h" + namespace io { namespace getlime @@ -31,7 +34,8 @@ namespace powerAuth { namespace crypto { - + using namespace io::getlime::powerAuth; + // ------------------------------------------------------------------------------------------- // MARK: - ECC routines - // @@ -207,6 +211,9 @@ namespace crypto signedDataHash.data(), (int)signedDataHash.size(), signature.data(), (int)signature.size(), publicKey); + if (result != 1) { + ERR_print_errors_fp(stdout); + } return result == 1; } @@ -236,7 +243,135 @@ namespace crypto signature.resize(signatureSize); return true; } + + // ------------------------------------------------------------------------------------------- + // MARK: - ECDSA Format - + // + static bool _DecodeAsn1ByteSequence(utils::DataReader & reader, cc7::ByteRange & out_data, size_t & out_size) + { + cc7::byte tmp; + if (!reader.readByte(tmp) || tmp != 0x02) { + // Invalid sequence header + return false; + } + if (!reader.readAsn1Count(out_size)) { + // Invalid size + return false; + } + if (out_size > 33) { + // Too big + return false; + } + return reader.readMemoryRange(out_data, out_size); + } + + cc7::ByteArray ECDSA_DERtoJOSE(const cc7::ByteRange & der_signature) + { + cc7::ByteArray out; + auto reader = utils::DataReader(der_signature); + + cc7::byte tmp; + // Read first byte (sequence) + if (!reader.readByte(tmp) || tmp != 0x30) { + return out; + } + size_t sign_length, r_length, s_length; + cc7::ByteRange R, S; + if (!reader.readAsn1Count(sign_length)) { + return out; + } + // Overall length should match DER length - offset + if (sign_length != der_signature.size() - reader.currentOffset()) { + return out; + } + // Read R. + if (!_DecodeAsn1ByteSequence(reader, R, r_length)) { + return out; + } + // Read S. + if (!_DecodeAsn1ByteSequence(reader, S, s_length)) { + return out; + } + + // Everything looks fine. Now construct JOSE signature. + out.reserve(64); + + // Append R + if (r_length > 32) { + out.append(R.subRangeFrom(r_length - 32)); + } else { + if (r_length < 32) { + out.append(32 - r_length, 0); + } + out.append(R); + } + // Append S + if (s_length > 32) { + out.append(S.subRangeFrom(s_length - 32)); + } else { + if (s_length < 32) { + out.append(32 - s_length, 0); + } + out.append(S); + } + return out; + } + + static cc7::ByteArray _SkipPaddingBytes(const cc7::ByteRange & r) + { + cc7::ByteArray out; + size_t offset = 0, size = r.size(); + while (offset != size) { + if (r[offset] != 0) { + break; + } + ++offset; + } + // If the encoded number is negative, then keep zero byte as prefix. + if (r[offset] > 0x7F) { + if (offset == 0) { + // We're already at the beginning of range, so prepend zero before the sequence + out.push_back(0); + } else { + // Offset is greater than 0, so we can copy zero from the padding + offset--; + } + } + out.append(r.subRangeFrom(offset)); + return out; + } + + static cc7::ByteArray _EncodeAsn1ByteSequence(utils::DataWriter & writer, const cc7::ByteRange & bytes) + { + writer.reset(); + writer.writeByte(0x02); + writer.writeAsn1Count(bytes.size()); + writer.writeMemory(bytes); + return writer.serializedData(); + } + + cc7::ByteArray ECDSA_JOSEtoDER(const cc7::ByteRange & jose_signature) + { + if (jose_signature.size() != 64) { + return cc7::ByteArray(); + } + // Split input data into half and skip zero leading bytes for each parameter. + auto R = _SkipPaddingBytes(jose_signature.subRangeTo(32)); + auto S = _SkipPaddingBytes(jose_signature.subRangeFrom(32)); + + auto writer = utils::DataWriter(); + auto encoded_R = _EncodeAsn1ByteSequence(writer, R); + auto encoded_S = _EncodeAsn1ByteSequence(writer, S); + // Encode the whole sequence + writer.reset(); + writer.writeByte(0x30); + writer.writeAsn1Count(encoded_R.size() + encoded_S.size()); + writer.writeMemory(encoded_R); + writer.writeMemory(encoded_S); + return writer.serializedData(); + } + // ------------------------------------------------------------------------------------------- // MARK: - ECDH - // diff --git a/src/PowerAuth/crypto/ECC.h b/src/PowerAuth/crypto/ECC.h index 4e8382fc..726e4e1d 100644 --- a/src/PowerAuth/crypto/ECC.h +++ b/src/PowerAuth/crypto/ECC.h @@ -90,6 +90,15 @@ namespace crypto Computes signature for data with given private key. */ bool ECDSA_ComputeSignature(const cc7::ByteRange & data, EC_KEY * privateKey, cc7::ByteArray & signature); + + /** + Convert ECDSA signature from DER format to JOSE. If operation fails, then returned array is empty. + */ + cc7::ByteArray ECDSA_DERtoJOSE(const cc7::ByteRange & der_signature); + /** + Convert ECDSA signature from JOSE to DER format. If operation fails, then returned array is empty. + */ + cc7::ByteArray ECDSA_JOSEtoDER(const cc7::ByteRange & jose_signature); // ------------------------------------------------------------------------------------------- // MARK: - ECDH - diff --git a/src/PowerAuth/jni/ECIESEncryptorJNI.cpp b/src/PowerAuth/jni/ECIESEncryptorJNI.cpp index 2e2850b3..11d593c7 100644 --- a/src/PowerAuth/jni/ECIESEncryptorJNI.cpp +++ b/src/PowerAuth/jni/ECIESEncryptorJNI.cpp @@ -34,7 +34,7 @@ CC7_JNI_MODULE_CLASS_BEGIN() // Helper functions // ---------------------------------------------------------------------------- -jobject CreateJavaCryptogramFromCppObject(JNIEnv * env, const ECIESCryptogram & cryptogram, const ECIESParameters & parameters) +jobject CreateJavaCryptogramFromCppObject(JNIEnv * env, const ECIESCryptogram & cryptogram, const ECIESParameters & parameters, const std::string & temporaryKeyId) { if (!env) { CC7_ASSERT(false, "Missing required parameter or java environment is not valid."); @@ -51,6 +51,7 @@ jobject CreateJavaCryptogramFromCppObject(JNIEnv * env, const ECIESCryptogram & CC7_JNI_SET_FIELD_BYTEARRAY(resultObject, resultClazz, "mac", cc7::jni::CopyToNullableJavaByteArray(env, cryptogram.mac)); CC7_JNI_SET_FIELD_BYTEARRAY(resultObject, resultClazz, "key", cc7::jni::CopyToNullableJavaByteArray(env, cryptogram.key)); CC7_JNI_SET_FIELD_BYTEARRAY(resultObject, resultClazz, "nonce", cc7::jni::CopyToNullableJavaByteArray(env, cryptogram.nonce)); + CC7_JNI_SET_FIELD_STRING(resultObject, resultClazz, "temporaryKeyId", cc7::jni::CopyToNullableJavaString(env, temporaryKeyId)); CC7_JNI_SET_FIELD_LONG(resultObject, resultClazz, "timestamp", (jlong) parameters.timestamp); return resultObject; } @@ -82,7 +83,7 @@ jobject CreateJavaEncryptorFromCppObject(JNIEnv * env, const ECIESEncryptor & en return resultObject; } -static cc7::ByteArray LoadAssociatedData(JNIEnv * env, jobject encryptor) +static cc7::ByteArray LoadAssociatedData(JNIEnv * env, jobject encryptor, std::string & out_temporary_key_id) { if (!env) { CC7_ASSERT(false, "Missing required parameter or java environment is not valid."); @@ -101,9 +102,10 @@ static cc7::ByteArray LoadAssociatedData(JNIEnv * env, jobject encryptor) return cc7::ByteArray(); } // Extract parameters from EciesMetaData object + out_temporary_key_id = cc7::jni::CopyFromJavaString(env, CC7_JNI_GET_FIELD_STRING(metaData, metaDataClazz, "temporaryKeyId")); auto applicationKey = cc7::jni::CopyFromJavaString(env, CC7_JNI_GET_FIELD_STRING(metaData, metaDataClazz, "applicationKey")); auto activationId = cc7::jni::CopyFromJavaString(env, CC7_JNI_GET_FIELD_STRING(metaData, metaDataClazz, "activationIdentifier")); - return ECIESUtils::buildAssociatedData(applicationKey, activationId); + return ECIESUtils::buildAssociatedData(applicationKey, out_temporary_key_id, activationId); } // ---------------------------------------------------------------------------- @@ -244,8 +246,9 @@ CC7_JNI_METHOD_PARAMS(jobject, encryptRequestImpl, jbyteArray requestData, jlong // Encrypt request ECIESCryptogram cppCryptogram; ECIESParameters cppParameters; + std::string temporaryKeyId; cppParameters.timestamp = (cc7::U64)timestamp; - cppParameters.associatedData = LoadAssociatedData(env, thiz); + cppParameters.associatedData = LoadAssociatedData(env, thiz, temporaryKeyId); if (cppParameters.associatedData.empty()) { return nullptr; } @@ -254,7 +257,7 @@ CC7_JNI_METHOD_PARAMS(jobject, encryptRequestImpl, jbyteArray requestData, jlong CC7_ASSERT(false, "ECIESCryptogram.encryptRequest: failed with error code %d", ec); return nullptr; } - return CreateJavaCryptogramFromCppObject(env, cppCryptogram, cppParameters); + return CreateJavaCryptogramFromCppObject(env, cppCryptogram, cppParameters, temporaryKeyId); } // @@ -270,7 +273,8 @@ CC7_JNI_METHOD_PARAMS(jbyteArray, decryptResponseImpl, jobject cryptogram) // Copy parameters to CPP objects ECIESCryptogram cppCryptogram; ECIESParameters cppParameters; - cppParameters.associatedData = LoadAssociatedData(env, thiz); + std::string foo; + cppParameters.associatedData = LoadAssociatedData(env, thiz, foo); if (cppParameters.associatedData.empty()) { return nullptr; } diff --git a/src/PowerAuth/jni/ECIESEncryptorJNI.h b/src/PowerAuth/jni/ECIESEncryptorJNI.h index f50137b7..540cc5e1 100644 --- a/src/PowerAuth/jni/ECIESEncryptorJNI.h +++ b/src/PowerAuth/jni/ECIESEncryptorJNI.h @@ -22,7 +22,7 @@ /** Creates a new ECIESCryptogram java object from given C++ cryptogram structure. */ -CC7_EXTERN_C jobject CreateJavaCryptogramFromCppObject(JNIEnv * env, const io::getlime::powerAuth::ECIESCryptogram & cryptogram, const io::getlime::powerAuth::ECIESParameters & parameters); +CC7_EXTERN_C jobject CreateJavaCryptogramFromCppObject(JNIEnv * env, const io::getlime::powerAuth::ECIESCryptogram & cryptogram, const io::getlime::powerAuth::ECIESParameters & parameters, const std::string & temporaryKeyId); /** Loads a content from ECIESCryptogram java object into cryptogram C++ structure. diff --git a/src/PowerAuth/jni/SessionJNI.cpp b/src/PowerAuth/jni/SessionJNI.cpp index 4f8af7c3..407e1620 100644 --- a/src/PowerAuth/jni/SessionJNI.cpp +++ b/src/PowerAuth/jni/SessionJNI.cpp @@ -98,13 +98,13 @@ CC7_JNI_METHOD_PARAMS(void, destroy, jlong handle) } // -// public native void resetSession() +// public native void resetSession(boolean fullReset) // -CC7_JNI_METHOD(void, resetSession) +CC7_JNI_METHOD_PARAMS(void, resetSession, jboolean fullReset) { auto session = CC7_THIS_OBJ(); if (session) { - session->resetSession(); + session->resetSession(fullReset); } } @@ -461,16 +461,53 @@ CC7_JNI_METHOD_PARAMS(jint, verifyServerSignedData, jobject signedData) } // Load parameters into C++ objects jclass requestClazz = CC7_JNI_MODULE_FIND_CLASS("SignedData"); - // Get type of key - bool useMasterKey = CC7_JNI_GET_FIELD_BOOL(signedData, requestClazz, "useMasterKey"); + // Get enum types + jint signingKey = CC7_JNI_GET_FIELD_INT(signedData, requestClazz, "signingKey"); + jint signatureFormat = CC7_JNI_GET_FIELD_INT(signedData, requestClazz, "signatureFormat"); // Prepare cpp structure SignedData cppSignedData; - cppSignedData.signingKey = useMasterKey ? SignedData::ECDSA_MasterServerKey : SignedData::ECDSA_PersonalizedKey; - cppSignedData.data = cc7::jni::CopyFromJavaByteArray(env, CC7_JNI_GET_FIELD_BYTEARRAY(signedData, requestClazz, "data")); - cppSignedData.signature = cc7::jni::CopyFromJavaByteArray(env, CC7_JNI_GET_FIELD_BYTEARRAY(signedData, requestClazz, "signature")); + cppSignedData.signingKey = static_cast(signingKey); + cppSignedData.signatureFormat = static_cast(signatureFormat); + cppSignedData.data = cc7::jni::CopyFromJavaByteArray(env, CC7_JNI_GET_FIELD_BYTEARRAY(signedData, requestClazz, "data")); + cppSignedData.signature = cc7::jni::CopyFromJavaByteArray(env, CC7_JNI_GET_FIELD_BYTEARRAY(signedData, requestClazz, "signature")); return (jint) session->verifyServerSignedData(cppSignedData); } +// +// public native int signDataWithHmacKey(SignedData dataToSign, SignatureUnlockKeys unlockKeys) +// +CC7_JNI_METHOD_PARAMS(jint, signDataWithHmacKey, jobject dataToSign, jobject unlockKeys) +{ + auto session = CC7_THIS_OBJ(); + if (!session || !dataToSign) { + CC7_ASSERT(false, "Missing dataToSign or internal handle."); + return EC_WrongParam; + } + // Load parameters into C++ objects + SignatureUnlockKeys cppUnlockKeys; + if (unlockKeys != nullptr) { + if (false == LoadSignatureUnlockKeys(cppUnlockKeys, env, unlockKeys)) { + return EC_WrongParam; + } + } + // Prepare signing data + jclass requestClazz = CC7_JNI_MODULE_FIND_CLASS("SignedData"); + // Get type of key + jint signingKey = CC7_JNI_GET_FIELD_INT(dataToSign, requestClazz, "signingKey"); + jint signatureFormat = CC7_JNI_GET_FIELD_INT(dataToSign, requestClazz, "signatureFormat"); + // Prepare cpp structure + SignedData cppDataToSign; + cppDataToSign.signingKey = static_cast(signingKey); + cppDataToSign.signatureFormat = static_cast(signatureFormat); + cppDataToSign.data = cc7::jni::CopyFromJavaByteArray(env, CC7_JNI_GET_FIELD_BYTEARRAY(dataToSign, requestClazz, "data")); + // Call session + auto ec = session->signDataWithHmacKey(cppDataToSign, cppUnlockKeys); + if (ec == EC_Ok) { + CC7_JNI_SET_FIELD_BYTEARRAY(dataToSign, requestClazz, "signature", cc7::jni::CopyToJavaByteArray(env, cppDataToSign.signature)); + } + return (jint) ec; +} + // ---------------------------------------------------------------------------- // Signature keys management // ---------------------------------------------------------------------------- @@ -575,9 +612,9 @@ CC7_JNI_METHOD_PARAMS(jbyteArray, deriveCryptographicKeyFromVaultKey, jstring cV } // -// public native byte[] signDataWithDevicePrivateKey(String cVaultKey, SignatureUnlockKeys unlockKeys, byte[] data); +// public native byte[] signDataWithDevicePrivateKey(String cVaultKey, SignatureUnlockKeys unlockKeys, byte[] data, int signatureFormat); // -CC7_JNI_METHOD_PARAMS(jbyteArray, signDataWithDevicePrivateKey, jstring cVaultKey, jobject unlockKeys, jbyteArray data) +CC7_JNI_METHOD_PARAMS(jbyteArray, signDataWithDevicePrivateKey, jstring cVaultKey, jobject unlockKeys, jbyteArray data, jint signatureFormat) { auto session = CC7_THIS_OBJ(); if (!session || !cVaultKey || !unlockKeys || !data) { @@ -587,12 +624,13 @@ CC7_JNI_METHOD_PARAMS(jbyteArray, signDataWithDevicePrivateKey, jstring cVaultKe // Load parameters into C++ objects std::string cppCVaultKey = cc7::jni::CopyFromJavaString(env, cVaultKey); cc7::ByteArray cppData = cc7::jni::CopyFromJavaByteArray(env, data); + auto cppSignatureFormat = static_cast(signatureFormat); SignatureUnlockKeys cppUnlockKeys; if (false == LoadSignatureUnlockKeys(cppUnlockKeys, env, unlockKeys)) { return NULL; } cc7::ByteArray signature; - ErrorCode code = session->signDataWithDevicePrivateKey(cppCVaultKey, cppUnlockKeys, cppData, signature); + ErrorCode code = session->signDataWithDevicePrivateKey(cppCVaultKey, cppUnlockKeys, cppData, cppSignatureFormat, signature); if (code != EC_Ok) { return NULL; } @@ -696,6 +734,72 @@ CC7_JNI_METHOD_PARAMS(jobject, getEciesEncryptorImpl, jint scope, jobject unlock return CreateJavaEncryptorFromCppObject(env, cppEncryptor, timeService); } +// +// public native int setPublicKeyForEciesScope(int scope, String publicKey, String publicKeyId) +// +CC7_JNI_METHOD_PARAMS(jint, setPublicKeyForEciesScope, jint scope, jstring publicKey, jstring publicKeyId) +{ + auto session = CC7_THIS_OBJ(); + if (!session) { + CC7_ASSERT(false, "Missing internal handle."); + return EC_WrongState; + } + // Load parameters into C++ objects + auto cppScope = (ECIESEncryptorScope) scope; + auto cppPublicKey = cc7::jni::CopyFromJavaString(env, publicKey); + auto cppPublicKeyId = cc7::jni::CopyFromJavaString(env, publicKeyId); + // Call Session + return session->setPublicKeyForEciesScope(cppScope, cppPublicKey, cppPublicKeyId); +} + +// +// public native int removePublicKeyForEciesScope(int scope) +// +CC7_JNI_METHOD_PARAMS(void, removePublicKeyForEciesScope, jint scope) +{ + auto session = CC7_THIS_OBJ(); + if (!session) { + CC7_ASSERT(false, "Missing internal handle."); + return; + } + // Load parameters into C++ objects + auto cppScope = (ECIESEncryptorScope) scope; + // Call Session + session->removePublicKeyForEciesScope(cppScope); +} + +// +// public native boolean hasPublicKeyForEciesScope(int scope) +// +CC7_JNI_METHOD_PARAMS(jboolean, hasPublicKeyForEciesScope, jint scope) +{ + auto session = CC7_THIS_OBJ(); + if (!session) { + CC7_ASSERT(false, "Missing internal handle."); + return false; + } + // Load parameters into C++ objects + auto cppScope = (ECIESEncryptorScope) scope; + // Call Session + return session->hasPublicKeyForEciesScope(cppScope); +} + +// +// public native String getPublicKeyIdForEciesScope(int scope) +// +CC7_JNI_METHOD_PARAMS(jstring, getPublicKeyIdForEciesScope, jint scope) +{ + auto session = CC7_THIS_OBJ(); + if (!session) { + CC7_ASSERT(false, "Missing internal handle."); + return nullptr; + } + // Load parameters into C++ objects + auto cppScope = (ECIESEncryptorScope) scope; + // Call Session + auto cppPublicKeyId = session->getPublicKeyIdForEciesScope(cppScope); + return cc7::jni::CopyToNullableJavaString(env, cppPublicKeyId); +} // ---------------------------------------------------------------------------- // Utilities diff --git a/src/PowerAuth/protocol/Constants.cpp b/src/PowerAuth/protocol/Constants.cpp index 10abe9a0..a356a8c4 100644 --- a/src/PowerAuth/protocol/Constants.cpp +++ b/src/PowerAuth/protocol/Constants.cpp @@ -26,7 +26,7 @@ namespace protocol { // Power Auth version string const std::string PA_VERSION_V2("2.1"); - const std::string PA_VERSION_V3("3.2"); + const std::string PA_VERSION_V3("3.3"); // PA HTTP Auth header. const std::string PA_AUTH_HEADER_NAME ("X-PowerAuth-Authorization"); diff --git a/src/PowerAuth/protocol/PrivateTypes.cpp b/src/PowerAuth/protocol/PrivateTypes.cpp index b7a525b4..545282a8 100644 --- a/src/PowerAuth/protocol/PrivateTypes.cpp +++ b/src/PowerAuth/protocol/PrivateTypes.cpp @@ -326,6 +326,31 @@ namespace protocol return result; } + + // + // MARK: - Session Data - + // + + bool SessionData::PublicKeyWithId::setKey(const std::string &key_data_base64, const std::string &key_id) + { + if (key_data_base64.empty()) { + CC7_LOG("Empty key data provided for ECIES public key."); + return false; + } + if (key_id.empty()) { + CC7_LOG("Empty identifier provided for ECIES public key."); + return false; + } + cc7::ByteArray data; + if (!data.readFromBase64String(key_data_base64) || data.empty()) { + CC7_LOG("Invalid key data provided for ECIES public key."); + return false; + } + key_data = data; + identifier = key_id; + return true; + } + } // io::getlime::powerAuth::detail } // io::getlime::powerAuth } // io::getlime diff --git a/src/PowerAuth/protocol/PrivateTypes.h b/src/PowerAuth/protocol/PrivateTypes.h index 43e6af13..51229892 100644 --- a/src/PowerAuth/protocol/PrivateTypes.h +++ b/src/PowerAuth/protocol/PrivateTypes.h @@ -344,7 +344,75 @@ namespace protocol */ bool DeserializeRecoveryData(const cc7::ByteRange & serialized, const cc7::ByteRange vault_key, RecoveryData & out_data); + // + // MARK: - Session Data - + // + + /** + The SessionData is internal structure that keeps various information used by Session class. + */ + struct SessionData + { + /** + The structure contains information about EC public key used in ECIES encryption scheme. + */ + struct PublicKeyWithId + { + /** + Contains EC public key data for ECIES encryption scheme. + */ + cc7::ByteArray key_data; + /** + Contains identifier of this key. + */ + std::string identifier; + + /** + Determine whether this public key appears to be valid. This is true only if `key_data` and `identifier` + members are not empty. + */ + inline bool isValid() const { + return !key_data.empty() && !identifier.empty(); + } + + /** + Clear key data and the identifier. + */ + inline void clear() { + key_data.clear(); + identifier.clear(); + } + + /** + Set new |key_data_base64| and |key_id| to the structure. + */ + bool setKey(const std::string & key_data_base64, const std::string & key_id); + }; + /** + Contains public key for application scoped ECIES. + */ + PublicKeyWithId ecies_application_public_key; + /** + Contains public key for activation scoped ECIES. + */ + PublicKeyWithId ecies_activation_public_key; + + /** + Resets the content of session data structure. + */ + inline void reset() { + ecies_application_public_key.clear(); + resetActivationData(); + } + /** + Resets only the data related to activation. + */ + inline void resetActivationData() { + ecies_activation_public_key.clear(); + } + }; + } // io::getlime::powerAuth::detail } // io::getlime::powerAuth } // io::getlime diff --git a/src/PowerAuth/utils/DataReader.cpp b/src/PowerAuth/utils/DataReader.cpp index 02d7b877..bf051658 100644 --- a/src/PowerAuth/utils/DataReader.cpp +++ b/src/PowerAuth/utils/DataReader.cpp @@ -240,6 +240,38 @@ namespace utils } return true; } + + bool DataReader::readAsn1Count(size_t &out_value) + { + byte tmp[4] = { 0, 0, 0, 0 }; + if (!readByte(tmp[0])) { + return false; + } + if (!(tmp[0] & 0x80)) { + // One byte with length lesser than 0x80 + out_value = tmp[0]; + // + } else { + // Length is encoded in multiple bytes. The first byte determines the length + // of encoded length. Bit 7 is set to 1. + size_t llength = tmp[0] & 0x7F; + tmp[0] = 0; + if (llength > 4 || !llength) { + // Too long, we support up to 4 bytes length. Other invalid + // value is zero. + return false; + } + size_t offset = 4 - llength; + if (!readRawMemory(tmp + offset, llength)) { + return false; + } + out_value = (size_t(tmp[0]) << 24) | + (size_t(tmp[1]) << 16) | + (size_t(tmp[2]) << 8 ) | + size_t(tmp[3]); + } + return true; + } // Data versioning diff --git a/src/PowerAuth/utils/DataReader.h b/src/PowerAuth/utils/DataReader.h index ebc1ec9b..40ef8e26 100644 --- a/src/PowerAuth/utils/DataReader.h +++ b/src/PowerAuth/utils/DataReader.h @@ -140,10 +140,15 @@ namespace utils bool readU64(cc7::U64 & out_value); /** - Returns count from data stream. This is complementary method + Reads count from data stream. This is complementary method to DataWriter::writeCount(). */ bool readCount(size_t & out_value); + /** + Reads count in ASN.1 format from the data stream. This is complementary method + to DataWriter::writeAsn1Count(). + */ + bool readAsn1Count(size_t & out_value); // Data versioning diff --git a/src/PowerAuth/utils/DataWriter.cpp b/src/PowerAuth/utils/DataWriter.cpp index 8ae68182..7399815d 100644 --- a/src/PowerAuth/utils/DataWriter.cpp +++ b/src/PowerAuth/utils/DataWriter.cpp @@ -135,6 +135,35 @@ namespace utils } return true; } + + bool DataWriter::writeAsn1Count(size_t n) + { + if (n <= 0x7F) { + // + writeByte(n); + // + } else if (n <= 0xFF) { + // + writeByte(0x81); + writeByte(n); + // + } else if (n <= 0xFFFF) { + // + writeByte(0x82); + writeU16(n); + // + } else if (n <= 0x3FFFFFFF) { + // + writeByte(0x84); + writeU32((cc7::U32)n); + // + } else { + // ASN.1 supports even bigger numbers, but it's overkill for our purpose + CC7_ASSERT(false, "Count is too big."); + return false; + } + return true; + } size_t DataWriter::maxCount() { diff --git a/src/PowerAuth/utils/DataWriter.h b/src/PowerAuth/utils/DataWriter.h index c996b6b2..2c0f8f62 100644 --- a/src/PowerAuth/utils/DataWriter.h +++ b/src/PowerAuth/utils/DataWriter.h @@ -105,6 +105,11 @@ namespace utils */ bool writeCount(size_t count); + /** + Write a count to the stream in ASN.1 binary format. + */ + bool writeAsn1Count(size_t count); + /** Returns maximum supported value which can be serialized as a counter. The returned value is the same for all supported diff --git a/src/PowerAuthTests/pa2CryptoECDSATests.cpp b/src/PowerAuthTests/pa2CryptoECDSATests.cpp index cb932e4e..21df91a5 100644 --- a/src/PowerAuthTests/pa2CryptoECDSATests.cpp +++ b/src/PowerAuthTests/pa2CryptoECDSATests.cpp @@ -65,6 +65,12 @@ namespace powerAuthTests ccstAssertTrue(success); ccstAssertFalse(signature.empty()); + // convert to JOSE and back to DER + auto jose_signature = crypto::ECDSA_DERtoJOSE(signature); + ccstAssertFalse(signature.empty()); + auto der_signature = crypto::ECDSA_JOSEtoDER(jose_signature); + ccstAssertEqual(signature, der_signature); + // Validate signature auto result = crypto::ECDSA_ValidateSignature(message, signature, public_key); ccstAssertTrue(result); diff --git a/src/PowerAuthTests/pa2DataWriterReaderTests.cpp b/src/PowerAuthTests/pa2DataWriterReaderTests.cpp index 3affeb6d..1db0e43c 100644 --- a/src/PowerAuthTests/pa2DataWriterReaderTests.cpp +++ b/src/PowerAuthTests/pa2DataWriterReaderTests.cpp @@ -36,6 +36,7 @@ namespace powerAuthTests pa2DataWriterReaderTests() { CC7_REGISTER_TEST_METHOD(testReadWriteCount) + CC7_REGISTER_TEST_METHOD(testReadWriteAsn1Count) CC7_REGISTER_TEST_METHOD(testReadWriteMethods) CC7_REGISTER_TEST_METHOD(testNotEnoughData) CC7_REGISTER_TEST_METHOD(testVersions) @@ -81,6 +82,57 @@ namespace powerAuthTests ccstAssertTrue(simulated_failure_passed, "Max value not tested properly."); } + /* + The test validates writeAsn1Count() / readAsn1Count() functionality + */ + void testReadWriteAsn1Count() + { + size_t test_value = 1; + size_t restored_value; + bool simulated_failure_passed = false; + while (test_value <= ((size_t)-1)/2) { + + DataWriter writer; + bool write_result = writer.writeAsn1Count(test_value); + DataReader reader(writer.serializedData()); + bool read_result = reader.readAsn1Count(restored_value); + + if (test_value <= DataWriter::maxCount()) { + // Values should be correct + ccstAssertTrue(write_result); + ccstAssertTrue(read_result); + ccstAssertEqual(reader.remainingSize(), 0); + ccstAssertEqual(test_value, restored_value, "Restored: 0x%x, Expected 0x%x", restored_value, test_value); + } else { + ccstAssertFalse(write_result); + // read result is not important + simulated_failure_passed = true; + } + + // Calculate next test value + if (test_value & 1) { + test_value += 1; + } else { + test_value = (test_value << 1) - 1; + } + } + // There must be at least one pass when write_result is false. + ccstAssertTrue(simulated_failure_passed, "Max value not tested properly."); + // Now test a special cases + { + // Length encoded in 3 bytes + DataWriter writer; + writer.writeByte(0x83); + writer.writeByte(0x10); + writer.writeByte(0x3E); + writer.writeByte(0x8F); + DataReader reader(writer.serializedData()); + size_t size; + ccstAssertTrue(reader.readAsn1Count(size)); + ccstAssertEqual(0x103E8F, size); + } + } + void readWriteSequenceTest(ByteArray * arr) { if (arr) { diff --git a/src/PowerAuthTests/pa2SessionTests.cpp b/src/PowerAuthTests/pa2SessionTests.cpp index 182333bb..7af61ade 100644 --- a/src/PowerAuthTests/pa2SessionTests.cpp +++ b/src/PowerAuthTests/pa2SessionTests.cpp @@ -44,6 +44,22 @@ namespace powerAuthTests { public: + struct ECKeyPair + { + EC_KEY * private_key; + std::string public_key_str; + + ECKeyPair() { + private_key = crypto::ECC_GenerateKeyPair(); + public_key_str = crypto::ECC_ExportPublicKeyToB64(private_key); + } + + ~ECKeyPair() { + EC_KEY_free(private_key); + private_key = nullptr; + } + }; + pa2SessionTests() { CC7_REGISTER_TEST_METHOD(testKeyValueMapNormalization); @@ -58,9 +74,10 @@ namespace powerAuthTests CC7_REGISTER_TEST_METHOD(testPersistentDataUpgradeFromV4ToV5); } - EC_KEY * _masterServerPrivateKey; + ECKeyPair _masterServerPrivateKey; + ECKeyPair _eciesApplicationScopedPrivateKey; + ECKeyPair _eciesActivationScopedPrivateKey; - std::string _masterServerPublicKeyStr; SessionSetup _setup; std::string _activation_code; @@ -69,17 +86,13 @@ namespace powerAuthTests std::string _recovery_code; std::string _recovery_puk; - const std::string PA_VER = "3.2"; + const std::string PA_VER = "3.3"; void setUp() override { - _masterServerPrivateKey = crypto::ECC_GenerateKeyPair(); - ccstAssertNotNull(_masterServerPrivateKey); - _masterServerPublicKeyStr = crypto::ECC_ExportPublicKeyToB64(_masterServerPrivateKey); - _setup.applicationKey = "MDEyMzQ1Njc4OUFCQ0RFRg=="; _setup.applicationSecret = "QUJDREVGMDEyMzQ1Njc4OQ=="; - _setup.masterServerPublicKey = _masterServerPublicKeyStr; + _setup.masterServerPublicKey = _masterServerPrivateKey.public_key_str; // prepare some other constants _activation_code = "VVVVV-VVVVV-VVVVV-VTFVA"; @@ -91,8 +104,6 @@ namespace powerAuthTests void tearDown() override { - EC_KEY_free(_masterServerPrivateKey); - _masterServerPrivateKey = nullptr; } // unit tests @@ -236,6 +247,7 @@ namespace powerAuthTests } const bool USE_RECOVERY_CODE = break_in_step == 4; + const bool TEST_ECIES_RESET = break_in_step == 4; EC_KEY * serverPrivateKey = nullptr; EC_KEY * devicePublicKey = nullptr; @@ -822,7 +834,7 @@ namespace powerAuthTests keys.possessionUnlockKey = possessionUnlock; cc7::ByteArray signature; - ec = s1.signDataWithDevicePrivateKey(cVaultKey, keys, cc7::MakeRange("Hello World!"), signature); + ec = s1.signDataWithDevicePrivateKey(cVaultKey, keys, cc7::MakeRange("Hello World!"), SignedData::ECDSA_DER, signature); ccstAssertEqual(ec, EC_Ok); ccstAssertTrue(!signature.empty()); // Validate signature... @@ -881,13 +893,19 @@ namespace powerAuthTests } // ECIES "application" scope { + ccstAssertFalse(s1.hasPublicKeyForEciesScope(ECIES_ApplicationScope)); + ccstAssertFalse(s1.hasPublicKeyForEciesScope(ECIES_ActivationScope)); + ec = s1.setPublicKeyForEciesScope(ECIES_ApplicationScope, _eciesApplicationScopedPrivateKey.public_key_str, "ecies-app-scope"); + ccstAssertEqual(ec, EC_Ok); + ccstAssertTrue(s1.hasPublicKeyForEciesScope(ECIES_ApplicationScope)); + ccstAssertFalse(s1.hasPublicKeyForEciesScope(ECIES_ActivationScope)); SignatureUnlockKeys foo; ECIESEncryptor encryptor; ec = s1.getEciesEncryptor(ECIES_ApplicationScope, foo, cc7::MakeRange("/pa/test"), encryptor); ccstAssertEqual(ec, EC_Ok); ccstAssertEqual(encryptor.sharedInfo1(), cc7::MakeRange("/pa/test")); ccstAssertEqual(encryptor.sharedInfo2(), crypto::SHA256(cc7::MakeRange(_setup.applicationSecret))); - ccstAssertEqual(encryptor.publicKey(), cc7::FromBase64String(_setup.masterServerPublicKey)); + ccstAssertEqual(encryptor.publicKey(), cc7::FromBase64String(_eciesApplicationScopedPrivateKey.public_key_str)); // Now try to encrypt data ECIESCryptogram request_enc; @@ -896,7 +914,7 @@ namespace powerAuthTests // ...and decrypt on "server" side ECIESCryptogram request_dec; - ECIESDecryptor decryptor(crypto::ECC_ExportPrivateKey(_masterServerPrivateKey), + ECIESDecryptor decryptor(crypto::ECC_ExportPrivateKey(_eciesApplicationScopedPrivateKey.private_key), cc7::MakeRange("/pa/test"), crypto::SHA256(cc7::MakeRange(_setup.applicationSecret))); cc7::ByteArray request_data; @@ -905,7 +923,15 @@ namespace powerAuthTests ccstAssertEqual(request_data, cc7::MakeRange("Hello!")); } // ECIES "activation" scope - { + { + ccstAssertTrue(s1.hasPublicKeyForEciesScope(ECIES_ApplicationScope)); + ccstAssertFalse(s1.hasPublicKeyForEciesScope(ECIES_ActivationScope)); + ec = s1.setPublicKeyForEciesScope(ECIES_ActivationScope, _eciesActivationScopedPrivateKey.public_key_str, "ecies-act-scope"); + ccstAssertEqual(ec, EC_Ok); + ccstAssertTrue(s1.hasPublicKeyForEciesScope(ECIES_ApplicationScope)); + ccstAssertTrue(s1.hasPublicKeyForEciesScope(ECIES_ActivationScope)); + + SignatureUnlockKeys keys; keys.possessionUnlockKey = possessionUnlock; ECIESEncryptor encryptor; @@ -913,7 +939,7 @@ namespace powerAuthTests ccstAssertEqual(ec, EC_Ok); ccstAssertEqual(encryptor.sharedInfo1(), cc7::MakeRange("/pa/activation/test")); ccstAssertEqual(encryptor.sharedInfo2(), crypto::HMAC_SHA256(cc7::MakeRange(_setup.applicationSecret), protocol::DeriveSecretKey(MASTER_SHARED_SECRET, 1000))); - ccstAssertEqual(encryptor.publicKey(), crypto::ECC_ExportPublicKey(serverPrivateKey)); + ccstAssertEqual(encryptor.publicKey(), cc7::FromBase64String(_eciesActivationScopedPrivateKey.public_key_str)); // Now try to encrypt data ECIESCryptogram request_enc; @@ -922,7 +948,7 @@ namespace powerAuthTests // ...and decrypt on "server" side ECIESCryptogram request_dec; - ECIESDecryptor decryptor(crypto::ECC_ExportPrivateKey(serverPrivateKey), + ECIESDecryptor decryptor(crypto::ECC_ExportPrivateKey(_eciesActivationScopedPrivateKey.private_key), cc7::MakeRange("/pa/activation/test"), crypto::HMAC_SHA256(cc7::MakeRange(_setup.applicationSecret), protocol::DeriveSecretKey(MASTER_SHARED_SECRET, 1000))); cc7::ByteArray request_data; @@ -950,6 +976,46 @@ namespace powerAuthTests ccstAssertEqual(ec, EC_WrongState); ccstAssertTrue(recovery_data.isEmpty()); } + if (TEST_ECIES_RESET) { + // Test Reset session + ccstAssertTrue(s1.hasPublicKeyForEciesScope(ECIES_ApplicationScope)); + ccstAssertTrue(s1.hasPublicKeyForEciesScope(ECIES_ActivationScope)); + ccstAssertEqual("ecies-app-scope", s1.getPublicKeyIdForEciesScope(ECIES_ApplicationScope)); + ccstAssertEqual("ecies-act-scope", s1.getPublicKeyIdForEciesScope(ECIES_ActivationScope)); + s1.resetSession(false); + ccstAssertTrue(s1.hasPublicKeyForEciesScope(ECIES_ApplicationScope)); + ccstAssertFalse(s1.hasPublicKeyForEciesScope(ECIES_ActivationScope)); + ccstAssertEqual("ecies-app-scope", s1.getPublicKeyIdForEciesScope(ECIES_ApplicationScope)); + ccstAssertEqual("", s1.getPublicKeyIdForEciesScope(ECIES_ActivationScope)); + s1.resetSession(true); + ccstAssertFalse(s1.hasPublicKeyForEciesScope(ECIES_ApplicationScope)); + ccstAssertFalse(s1.hasPublicKeyForEciesScope(ECIES_ActivationScope)); + ccstAssertEqual("", s1.getPublicKeyIdForEciesScope(ECIES_ApplicationScope)); + ccstAssertEqual("", s1.getPublicKeyIdForEciesScope(ECIES_ActivationScope)); + // + ccstAssertEqual(EC_WrongState, s1.setPublicKeyForEciesScope(ECIES_ActivationScope, _eciesActivationScopedPrivateKey.public_key_str, "another-key-id")); + } else { + // Test remove key + ccstAssertTrue(s1.hasPublicKeyForEciesScope(ECIES_ApplicationScope)); + ccstAssertTrue(s1.hasPublicKeyForEciesScope(ECIES_ActivationScope)); + ccstAssertEqual("ecies-app-scope", s1.getPublicKeyIdForEciesScope(ECIES_ApplicationScope)); + ccstAssertEqual("ecies-act-scope", s1.getPublicKeyIdForEciesScope(ECIES_ActivationScope)); + // + s1.removePublicKeyForEciesScope(ECIES_ActivationScope); + ccstAssertTrue(s1.hasPublicKeyForEciesScope(ECIES_ApplicationScope)); + ccstAssertFalse(s1.hasPublicKeyForEciesScope(ECIES_ActivationScope)); + ccstAssertEqual("ecies-app-scope", s1.getPublicKeyIdForEciesScope(ECIES_ApplicationScope)); + ccstAssertEqual("", s1.getPublicKeyIdForEciesScope(ECIES_ActivationScope)); + // + s1.removePublicKeyForEciesScope(ECIES_ApplicationScope); + ccstAssertFalse(s1.hasPublicKeyForEciesScope(ECIES_ApplicationScope)); + ccstAssertFalse(s1.hasPublicKeyForEciesScope(ECIES_ActivationScope)); + ccstAssertEqual("", s1.getPublicKeyIdForEciesScope(ECIES_ApplicationScope)); + ccstAssertEqual("", s1.getPublicKeyIdForEciesScope(ECIES_ActivationScope)); + } + // Remove ECIES key should always work + s1.removePublicKeyForEciesScope(ECIES_ActivationScope); + s1.removePublicKeyForEciesScope(ECIES_ApplicationScope); // release keys, just for sure EC_KEY_free(serverPrivateKey); @@ -1210,7 +1276,7 @@ namespace powerAuthTests std::string T_calculateActivationSignature(const std::string & code) { cc7::ByteArray signature; - bool result = crypto::ECDSA_ComputeSignature(cc7::MakeRange(code), _masterServerPrivateKey, signature); + bool result = crypto::ECDSA_ComputeSignature(cc7::MakeRange(code), _masterServerPrivateKey.private_key, signature); if (!result) { ccstFailure("Activation signature calculation failed"); return std::string(); @@ -1222,7 +1288,7 @@ namespace powerAuthTests { cc7::ByteArray signature; if (private_key == nullptr) { - private_key = _masterServerPrivateKey; + private_key = _masterServerPrivateKey.private_key; } bool result = crypto::ECDSA_ComputeSignature(data, private_key, signature); if (!result) {