diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..95ea85c --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,107 @@ +name: Build and check + +on: + push: + branches: + - develop + - master + - release/* + pull_request: + +jobs: + buildJob: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'zulu' + + - name: Assemble Library + run: ./gradlew library:assemble + + lintJob: + name: Lint + runs-on: ubuntu-latest + needs: [buildJob] + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'zulu' + + - name: Lint + run: ./gradlew library:lint + + - name: Upload lint results artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: lint-results-debug + path: library/build/reports/lint-results-debug.html + retention-days: 5 + + unitTestJob: + name: Unit Test + runs-on: ubuntu-latest + needs: [buildJob] + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'zulu' + + - name: Unit Test + run: ./gradlew library:test + + - name: Upload unit test results artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: unit-test-results-debug + path: library/build/reports/tests/testDebugUnitTest/ + retention-days: 5 + + intrTestJob: + name: Instrumentation Test + runs-on: macos-latest + needs: [buildJob] + steps: + - name: Checkout the repo + uses: actions/checkout@v4 + + - name: Set up JDK 11 + uses: actions/setup-java@v4 + with: + java-version: '11' + distribution: 'zulu' + + - name: Run instrumentation tests on emulator + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 29 + script: > + ./gradlew library:connectedAndroidTest + -Ptest.sslPinning.baseUrl=${{ secrets.SSL_PINNING_TEST_BASE_URL }} + -Ptest.sslPinning.appName=${{ secrets.SSL_PINNING_TEST_APP_NAME }} + + - name: Upload instrumentation test results artifact + if: always() + uses: actions/upload-artifact@v4 + with: + name: instrumentation-test-results-debug + path: library/build/reports/androidTests/connected/ + retention-days: 5 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8fd3302..0b8dc06 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ fastlane/Preview.html fastlane/screenshots fastlane/test_output fastlane/readme.md + +# jEnv +.java-version \ No newline at end of file diff --git a/README.md b/README.md index 374827a..0e07b9f 100644 --- a/README.md +++ b/README.md @@ -3,21 +3,20 @@ `WultraSSLPinning` is an Android library implementing dynamic SSL pinning, written in Kotlin. -- [Introduction](#introduction) - [Installation](#installation) - [Requirements](#requirements) - [Gradle](#gradle) - [Usage](#usage) - [Configuration](#configuration) - - [Predefined Fingerprints](#predefined-fingerprints) - - [Update Fingerprints](#updating-fingerprints) - - [Fingerprint Validation](#fingerprint-validation) - - [Global Validation Observers](#global-validation-observers) - - [Integration](#integration) - - [PowerAuth Integration](#powerauth-integration) - - [PowerAuth Integration from Java](#powerauth-integration-from-java) - - [Integration with HttpsUrlConnection](#integration-with-httpsurlconnection) - - [Integration with OkHttp](#integration-with-okhttp) + - [Predefined Fingerprints](#predefined-fingerprints) +- [Updating Fingerprints](#updating-fingerprints) +- [Fingerprint Validation](#fingerprint-validation) + - [Global Validation Observers](#global-validation-observers) +- [Integration](#integration) + - [PowerAuth Integration](#powerauth-integration) + - [PowerAuth Integration from Java](#powerauth-integration-from-java) + - [Integration with HttpsUrlConnection](#integration-with-httpsurlconnection) + - [Integration with OkHttp](#integration-with-okhttp) - [Switching Server Certificate](#switching-server-certificate) - [FAQ](#faq) - [License](#license) @@ -25,64 +24,56 @@ - [Security Disclosure](#security-disclosure) -## Introduction - The SSL pinning (or [public key, or certificate pinning](https://en.wikipedia.org/wiki/Transport_Layer_Security#Certificate_pinning)) is a technique mitigating [Man-in-the-middle attacks](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) against the secure HTTPS communication. -The typical Android solution is to bundle the hash of the certificate, -or the exact data of the certificate into the application. -The connection is then validated via `X509TrustManager`. -Popular `OkHttp` library has built in `CertificatePinner` class that simplifies the integration. +The typical Android solution is to bundle the hash of the certificate, or the exact data of the certificate into the application. The connection is then validated via `X509TrustManager`. + +The popular `OkHttp` library has a built-in `CertificatePinner` class that simplifies the integration. -In general, this works well, but it has, unfortunately, one major drawback in the certificate's expiration date. -The certificate expiration forces you to update your application regularly before the certificate expires. -Unfortunatelly, some percentage of users don't update their apps automatically. -In effect, users on older versions, will not be able to contact the application servers. +In general, this works well, but it has, unfortunately, one major drawback, the certificate's expiration date. The certificate expiration forces you to update your application regularly before the certificate expires. Unfortunately, some percentage of users don't update their apps automatically. In effect, users on older versions, will not be able to contact the application servers. -A solution to this problem is the **dynamic SSL pinning**, -where the list of certificate fingerprints is securely downloaded from the remote server. +A solution to this problem is the **dynamic SSL pinning**, where a list of certificate fingerprints is securely downloaded from the remote server. -`WultraSSLPinning` library does precisely this: +The `WultraSSLPinning` library does precisely this: - Manages the dynamic list of certificates, downloaded from the remote server. -- All entries in the list are signed with your private key and validated in the library using the public key (we're using ECDSA-SHA-256 algorithm) -- Provides easy to use fingerprint validation on the TLS handshake. +- All entries in the list are signed with your private key and validated in the library using the public key (we're using the ECDSA-SHA-256 algorithm) +- Provides easy-to-use fingerprint validation on the TLS handshake. Before you start using the library, you should also check our other related projects: - [Mobile Utility Server](https://github.com/wultra/mobile-utility-server) - the server component that provides dynamic JSON data consumed by this library. -- [Dynamic SSL Pinning Tool](https://github.com/wultra/ssl-pinning-tool) - the command line tool written in Java, for generating static JSON data consumed by this library. - [iOS version](https://github.com/wultra/ssl-pinning-ios) of the library ## Installation ### Requirements -- minSdkVersion 16 (Android 4.1 Jelly Bean) +- `minSdkVersion 19` (Android 4.4 KitKat) ### Gradle -To use **WultraSSLPinning** in you Android app add this dependency: +To use **WultraSSLPinning** in your Android app add this dependency: ```gradle implementation 'com.wultra.android.sslpinning:wultra-ssl-pinning:1.x.y' ``` -Note that this documentation is using version `1.x.y` as an example. You can find the latest version at [github's release](https://github.com/wultra/ssl-pinning-android/releases#docucheck-keep-link) page. The Android Studio IDE can also find and offer updates for your application’s dependencies. +Note that this documentation is using version `1.x.y` as an example. You can find the latest version on [github's release](https://github.com/wultra/ssl-pinning-android/releases#docucheck-keep-link) page. The Android Studio IDE can also find and offer updates for your application’s dependencies. -Also make sure you have `mavenCentral()` repository among the project repositories. +Also, make sure you have the `mavenCentral()` repository among the project repositories. ## Usage - `CertStore` - the main class which provides all the library features -- `CertStoreConfiguration` - the configuration class for `CertStore` class +- `CertStoreConfiguration` - the configuration class for the `CertStore` class -The next chapters of this document will explain how to configure and use `CertStore` for the SSL pinning purposes. +The next chapters of this document will explain how to configure and use `CertStore` for SSL pinning purposes. ### Configuration -An example for `CertStore` configuration in Kotlin: +An example of `CertStore` configuration in Kotlin: ```kotlin val publicKey: ByteArray = Base64.decode("BMne....kdh2ak=", Base64.NO_WRAP) @@ -100,27 +91,23 @@ val certStore = CertStore.powerAuthCertStore(configuration = configuration, appC The configuration has the following properties: - `serviceUrl` - parameter defining URL with a remote list of certificates (JSON). -- `publicKey` - byte array containing the public key counterpart to the private key, used for fingerprint signing. -- `useChallenge` - parameter that defines whether remote server requires challenge request header: +- `publicKey` - a byte array containing the public key counterpart to the private key, used for fingerprint signing. +- `useChallenge` - parameter that defines whether the remote server requires a challenge request header: - use `true` in case you're connecting to [Mobile Utility Server](https://github.com/wultra/mobile-utility-server) or similar service. - - use `false` in case the remote server provides a static data, generated by [SSL Pinning Tool](https://github.com/wultra/ssl-pinning-tool). + - use `false` in case the remote server provides static data, generated by [SSL Pinning Tool](https://github.com/wultra/ssl-pinning-tool). - `expectedCommonNames` - an optional array of strings, defining which domains you expect in certificate validation. - `identifier` - optional string identifier for scenarios, where multiple `CertStore` instances are used in the application. -- `fallbackCertificates` - optional hardcoded data for a fallback fingerprints. See the next chapter of this document for details. +- `fallbackCertificates` - optional hardcoded data for fallback fingerprints. See the next chapter of this document for details. - `periodicUpdateIntervalMillis` - defines interval for default updates. The default value is 1 week. -- `expirationUpdateTreshold` - defines time window before the next certificate will expire. In this time window `CertStore` will try to update the list of fingerprints more often than usual. Default value is 2 weeks before the next expiration. +- `expirationUpdateThreshold` - defines the time window before the next certificate will expire. In this time window `CertStore` will try to update the list of fingerprints more often than usual. The default value is 2 weeks before the next expiration. - `executorService` - defines `java.util.concurrent.ExecutorService` for running updates. If not defined updates run on a dedicated thread (not pooled). - `sslValidationStrategy` - defines the validation strategy for HTTPS connections initiated from the library itself. If not set, then the standard certificate chain validation provided by the operating system is used. Be aware that altering this option may put your application at risk. You should not ship your application to production with SSL validation turned off. See [FAQ](#download-fingerprints-from-test-server) for more details. ### Predefined Fingerprints -The `CertStoreConfiguration` may contain an optional data with predefined certificate fingerprints. -This technique can speed up the first application's startup when the database of fingerprints is empty. -You still need to [update](#updating-fingerprints) your application, once the fallback fingerprints expire. +The `CertStoreConfiguration` may contain optional data with predefined certificate fingerprints. This technique can speed up the first application's startup when the database of fingerprints is empty. You still need to [update](#updating-fingerprints) your application, once the fallback fingerprints expire. -To configure the property, you need to provide `GetFingerprintResponse` with a fallback certificate fingerprints. -The data should contain the same data as are usually received from the server, -except that `signature` property is not validated (but must be provided). For example: +To configure the property, you need to provide `GetFingerprintResponse` with a fallback certificate fingerprints. The data should contain the same data as are usually received from the server, except that the `signature` property is not validated (but must be provided). For example: ```kotlin val fallbackEntry = GetFingerprintResponse.Entry( @@ -142,13 +129,13 @@ val certStore = CertStore.powerAuthCertStore(configuration = configuration, appC To update the list of fingerprints from the remote server, use the following code: ```kotlin -certStore.update(UpdateMode.DEFAULT, UpdateMode.DEFAULT, object : DefaultUpdateObserver() { +certStore.update(UpdateMode.DEFAULT, object: DefaultUpdateObserver() { override fun continueExecution() { - // Certstore is likely up-to-date, you can resume execution of your code. + // CertStore is likely up-to-date, you can resume execution of your code. } override fun handleFailedUpdate(type: UpdateType, result: UpdateResult) { - // There was an error during the update, present an error to the user. + // There was an error during the update. Present an error to the user. } }) @@ -156,7 +143,7 @@ certStore.update(UpdateMode.DEFAULT, UpdateMode.DEFAULT, object : DefaultUpdateO The method is asynchronous. `DefaultUpdateObserver` has two callbacks: -- `continueExecution()` tells you that the certstore likely contains up-to-date data and your application should continue with the flow. +- `continueExecution()` tells you that the `CertStore` likely contains up-to-date data and your application should continue with the flow. - `handleFailedUpdate(UpdateType, UpdateResult)` tells you that there was an error during the update execution you should handle. Both callbacks are notified on the main thread. @@ -164,13 +151,13 @@ Both callbacks are notified on the main thread. `DefaultUpdateObserver` is the default implementation of `UpdateObserver`. In case you need more control over the flow, you can use the interface directly: ```kotlin -certStore.update(UpdateMode.DEFAULT, UpdateMode.DEFAULT, object : UpdateObserver() { +certStore.update(UpdateMode.DEFAULT, object: UpdateObserver() { override fun onUpdateStarted(type: UpdateType) { - // Certstore update started, either in DIRECT, SILENT or NO_UPDATE mode + // CertStore update started, either in DIRECT, SILENT or NO_UPDATE mode } override fun onUpdateFinished(type: UpdateType, result: UpdateResult) { - // Certstore update of a given type finished asynchronously with some result. + // CertStore update of a given type finished asynchronously with some result. } }) @@ -185,41 +172,26 @@ Both callbacks are notified on the main thread. There are three update types: -- `UpdateType.DIRECT` - The update is either **forced** or the -library is missing essential data (fingerprints). The app is not advised to continue -until the update is finished because there's a high risk of failing network requests -due to server certificates evaluated as untrusted. -- `UpdateType.SILENT` - The update is not critical but will be performed. -The library has data but the data are going to expire soon. There's low risk -of failing network requests due to server certificates evaluated as untrusted. -- `UpdateType.NO_UPDATE` - No update will be performed. The library -has data and they are not going to expire soon. There's low risk -of failing network requests due to server certificates evaluated as untrusted. +- `UpdateType.DIRECT` - The update is either **forced** or the library is missing essential data (fingerprints). The app is not advised to continue until the update is finished because there's a high risk of failing network requests due to server certificates being evaluated as untrusted. +- `UpdateType.SILENT` - The update is not critical but will be performed. The library has data but the data are going to expire soon. There's a low risk of failing network requests due to server certificates being evaluated as untrusted. +- `UpdateType.NO_UPDATE` - No update will be performed. The library has data and they are not going to expire soon. There's a low risk of failing network requests due to server certificates being evaluated as untrusted. The update function works in two basic modes: - **Forced mode**, this happens when the mode is forced (`UpdateMode.FORCED`). -- **Default mode**, this mode does internal evaluation of the stored data and configuration -and tries to avoid unnecessary downloads when the data are ok. +- **Default mode**, this mode does internal evaluation of the stored data and configuration and tries to avoid unnecessary downloads when the data are ok. -_Note: In any update type, there's still a risk of failing network requests -due to server certificates evaluated as untrusted. This is due to the fact -that server certificate might be replaced at any time and the library might -not be aware of it yet. To mitigate this cases it's recommended to implement -a [global validation observer](#global-validation-observers)._ + +Note: In any update type, there's still a risk of failing network requests due to server certificates being evaluated as untrusted. This is because the server certificate might be replaced at any time and the library might not be aware of it yet. To mitigate these cases it's recommended to implement a [global validation observer](#global-validation-observers). + -Updates are performed on an `ExecutorService` defined in the configuration, -if not defined, the update run on a dedicated thread. +Updates are performed on an `ExecutorService` defined in the configuration, if not defined, the update runs on a dedicated thread. -Note that your app is responsible for invoking the update method. -The app typically has to call the update during the application's startup, -before the first secure HTTPS request is initiated to a server that's supposed -to be validated with the pinning. +Note that your app is responsible for invoking the update method. The app typically has to call the update during the application's startup, before the first secure HTTPS request is initiated to a server that's supposed to be validated with the pinning. ## Fingerprint Validation -The `CertStore` provides several methods for certificate fingerprint validation. -You can choose the one which suits best your scenario: +The `CertStore` provides several methods for certificate fingerprint validation. You can choose the one which suits best your scenario: ```kotlin // [ 1 ] If you already have the common name (e.g. domain) and certificate fingerprint @@ -238,27 +210,13 @@ val certificate: java.security.cert.X509Certificate = connection.getServerCertif val validationResult = certStore.validateCertificate(certificate) ``` -Each `validate...` method returns `ValidationResult` enum with following options: - -- `ValidationResult.TRUSTED` - the server certificate is trusted. You can continue with the connection - - The right response in this situation is to continue with the ongoing communication. - -- `ValidationResult.UNTRUSTED` - the server certificate is not trusted. You should cancel the ongoing connection. - - The untrusted result means that `CertStore` has some fingerprints stored in its - database, but none matches the value you requested for validation. The right - response on this situation is always to cancel the ongoing connection. +Each `validate...` method returns the `ValidationResult` enum with the following options: -- `ValidationResult.EMPTY` - the fingerprint database is empty, or there's no fingerprint for the validated common name. - - The "empty" validation result typically means that the `CertStore` should update - the list of certificates immediately. Before you do this, you should check whether - the requested common name is what you're expecting. To simplify this step, you can set - the list of expected common names in the `CertStoreConfiguration` and treat all others as untrusted. +- `ValidationResult.TRUSTED` - the server certificate is trusted. You can continue with the connection. The right response in this situation is to continue with the ongoing communication. +- `ValidationResult.UNTRUSTED` - the server certificate is not trusted. You should cancel the ongoing connection. The untrusted result means that `CertStore` has some fingerprints stored in its database, but none matches the value you requested for validation. The right response to this situation is always to cancel the ongoing connection. +- `ValidationResult.EMPTY` - the fingerprint database is empty, or there's no fingerprint for the validated common name. The "empty" validation result typically means that the `CertStore` should update the list of certificates immediately. Before you do this, you should check whether the requested common name is what you're expecting. To simplify this step, you can set the list of expected common names in the `CertStoreConfiguration` and treat all others as untrusted. - For all situations, the right response in this situation is always to cancel the ongoing - connection. +For all situations, the right response in this situation is always to cancel the ongoing connection. The full challenge handling in your app may look like this: @@ -277,24 +235,15 @@ if (validationResult != ValidationResult.TRUSTED) { ### Global Validation Observers -In order to be notified about all validation failures there is `ValidationObserver` -interface and methods on `CertStore` for adding/removing global validation observers. +In order to be notified about all validation failures there is the `ValidationObserver` interface and methods on `CertStore` for adding/removing global validation observers. -Motivation for these global validation observers is that some validation failures -(e.g. those happening in `SSLSocketFactory` instances created by `SSLSocketIntegration.createSSLPinningSocketFactory(CertStore)`) -are out of reach of the app integrating the pinning library. -These global validation observers are notified about all validation failures. -The app can then react with force updating the fingerprints. +The motivation for these global validation observers is that some validation failures (e.g. those happening in `SSLSocketFactory` instances created by `SSLSocketIntegration.createSSLPinningSocketFactory(CertStore)`) are out of reach of the app integrating the pinning library. These global validation observers are notified about all validation failures. The app can then react with force updating the fingerprints. ## Integration ### PowerAuth Integration -The **WultraSSLPinning** library contains classes for integration with the PowerAuth SDK. -The most important one is the `PowerAuthSslPinningValidationStrategy` class, -which implements `PA2ClientValidationStrategy` with SSL pinning. -You can simply instantiate in with an existing `CertStore` and set it in `PA2ClientConfiguration`. -Then the class will provide SSL pinning for all communication initiated within the PowerAuth SDK. +The **WultraSSLPinning** library contains classes for integration with the PowerAuth SDK. The most important one is the `PowerAuthSslPinningValidationStrategy` class, which implements `PA2ClientValidationStrategy` with SSL pinning. You can simply instantiate in with an existing `CertStore` and set it in `PA2ClientConfiguration`. Then the class will provide SSL pinning for all communication initiated within the PowerAuth SDK. For example, this is how the configuration sequence may look like if you want to use both, `PowerAuthSDK` and `CertStore`: @@ -322,27 +271,25 @@ val powerAuthClientConfiguration = PowerAuthClientConfiguration.Builder() .build() val powerAuth = PowerAuthSDK.Builder(powerAuthConfiguration) - .clientContifuration(powerAuthClientConfiguration) + .clientConfiguration(powerAuthClientConfiguration) .build(appContext) ``` ### PowerAuth Integration From Java -Some of the Kotlin's PowerAuthSDK integration APIs are inconvenient in Java. -A `CertStore` integrating PowerAuthSDK can be created with: +Some of Kotlin's PowerAuthSDK integration APIs are inconvenient in Java. A `CertStore` integrating PowerAuthSDK can be created with: ```java CertStore store = PowerAuthCertStore.createInstance(configuration, context); ``` -or +Or: ```java CertStore store = PowerAuthCertStore.createInstance(configuration, context, "my-keychain-identifier"); ``` -Note that Kotlin's way of construction `CertStore.powerAuthCertStore` is not available in Java. -Calling this in Java would be way too cumbersome, but will work: +Note that Kotlin's way of construction `CertStore.powerAuthCertStore` is not available in Java. Calling this in Java would be way too cumbersome, but will work: ```java PowerAuthIntegrationKt.powerAuthCertStore(CertStore.Companion, configuration, context, null);` @@ -365,7 +312,7 @@ connection.connect() ### Integration With `OkHttp` - To integrate with OkHttp, use following code: +To integrate with OkHttp, use the following code: ```kotlin val sslSocketFactory = SSLPinningIntegration.createSSLPinningSocketFactory(certStore); @@ -376,23 +323,15 @@ val okhttpClient = OkHttpClient.Builder() .build() ``` -In the code above, use `SSLSocketFactory` provided by `SSLPinningIntegration.createSSLPinningSocketFactory(...)` -and an instance of `SSLPinningX509TrustManager`. +In the code above, use `SSLSocketFactory` provided by `SSLPinningIntegration.createSSLPinningSocketFactory(...)` and an instance of `SSLPinningX509TrustManager`. ## Switching Server Certificate -Certificate pinning is great for your app's security but at the same time, it requires -care when deploying it to your customers. -Be careful with the update parameters in `CertStoreConfiguration` serving for the default -updates, namely with setting the frequencies of update. +Certificate pinning is great for your app's security but at the same time, it requires care when deploying it to your customers. Be careful with the update parameters in `CertStoreConfiguration` serving for the default updates, namely with setting the frequencies of updates. -Sudden change of a certificate on a pinned domain is best resolved by utilizing -a [global validation observer](#global-validation-observers). The observer -is notified about validation failures. The app can then -force updating the fingerprints to resolve the failing TLS handshakes. +A sudden change of a certificate on a pinned domain is best resolved by utilizing a [global validation observer](#global-validation-observers). The observer is notified about validation failures. The app can then force updating the fingerprints to resolve the failing TLS handshakes. -Note that failed validation itself doesn't affect the stored fingerprints, -update is necessary to make a change. +Note that failed validation itself doesn't affect the stored fingerprints, an update is necessary to make a change. ## FAQ @@ -408,11 +347,11 @@ WultraDebug.loggingLevel = WultraDebug.WultraLoggingLevel.DEBUG There's an optional dependency on [PowerAuthSDK](https://github.com/wultra/powerauth-mobile-sdk). -However, the library requires several cryptographic primitives (see `CryptoProvider`) that are provided by **PowerAuthSDK**. Also most of our clients are already using PowerAuthSDK in their applications. Therefore it's a non-brainer to use **PowerAuthSDK** for the cryptography in **WultraSSLPinning**. +However, the library requires several cryptographic primitives (see `CryptoProvider`) that are provided by **PowerAuthSDK**. Also, most of our clients are already using PowerAuthSDK in their applications. Therefore it's a no-brainer to use **PowerAuthSDK** for the cryptography in **WultraSSLPinning**. > Be aware that library version 1.1.x+ requires at least PowerAuth mobile SDK 1.4.2 and newer. This requirement is due to improvements in the secure data storage we have implemented in that version of the SDK. -If needed the library can be used without PowerAuthSDK. In this case you can't use any class from `com.wultra.android.sslpinning.integration.powerauth` package since they expect PowerAuthSDK to be present. Also you have to provide you own implementation of `CryptoProvider` and `SecureDataStore`. +If needed the library can be used without PowerAuthSDK. In this case, you can't use any class from the `com.wultra.android.sslpinning.integration.powerauth` package since they expect PowerAuthSDK to be present. Also, you have to provide your implementation of `CryptoProvider` and `SecureDataStore`. ### What is pinned? @@ -421,58 +360,48 @@ In SSL pinning there are [two options](https://www.owasp.org/index.php/Certifica 1. **Pin the certificate** (DER encoding) 2. **Pin the public key** -**WultraSSLpinning** tooling (e.g. this Android library, -[iOS version](https://github.com/wultra/ssl-pinning-ios) and -[Dynamic SSL Pinning Tool](https://github.com/wultra/ssl-pinning-tool)) use *option 1: they pin the certificate*. +**WultraSSLPinning** tooling (e.g. this Android library, [iOS version](https://github.com/wultra/ssl-pinning-ios) and [Dynamic SSL Pinning Tool](https://github.com/wultra/ssl-pinning-tool)) use the first option: they pin the certificate. -In Java (Android) world this means that the library computes the fingerprint -from: +In Java (Android) world this means that the library computes the fingerprint from: ```java Certificate certificate = ...; -byte[] bytesToComputeFringerprintFrom = certificate.getEncoded(); +byte[] bytesToComputeFingerprintFrom = certificate.getEncoded(); ``` -Note: Many blog posts and tools for certificate pinning on Android instead mention/use option 2 - public key pinning. -An example is [CertificatePinner](https://square.github.io/okhttp/3.x/okhttp/okhttp3/CertificatePinner.html) -from popular [OkHttp](http://square.github.io/okhttp/) library. + +Note: Many blog posts and tools for certificate pinning on Android instead mention/use the second option - public key pinning. An example is [CertificatePinner](https://square.github.io/okhttp/3.x/okhttp/okhttp3/CertificatePinner.html) from popular [OkHttp](http://square.github.io/okhttp/) library. + -In case of public key pinning the fingerprint is computed from: +In the case of public key pinning, the fingerprint is computed from: ```java Certificate certificate = ...; byte[] bytesToComputeFringerprintFrom = certificate.getPublicKey().getEncoded(); ``` -This means that `CertificatePinner` cannot be readily used with **WultraSSLPinning** library. +This means that `CertificatePinner` cannot be readily used with the **WultraSSLPinning** library. **PowerAuthSDK** already provides these functions. -If you do not desire to integrate PowerAuthSDK you can implement necessary interfaces yourself. -The core of the library is using `CryptoProvider` and `SecureDataStore` interfaces and therefore is implementation independent. - +If you do not desire to integrate PowerAuthSDK you can implement necessary interfaces yourself. The core of the library uses `CryptoProvider` and `SecureDataStore` interfaces and therefore is implementation-independent. ### How to use public key pinning instead of certificate pinning? -If you really want to use public key pinning instead of certificate pinning -(e.g. because you are fond of OkHttp's `CertificatePinner`). -You have to do couple of things: +If you really want to use public key pinning instead of certificate pinning (e.g. because you are fond of OkHttp's `CertificatePinner`). You have to do a couple of things: -* You need different fingerprints in the update json. -[Dynamic SSL Pinning Tool](https://github.com/wultra/ssl-pinning-tool) -computes only certificate pinning. Therefore you need to generate those -fingerprints yourself. -* Don't use these classes/methods (they are bound to certificate pinning): - * `CertStore.validateCertificate(X509Certificate)` - * `SSLPinningX509TrustManager` - * `SSLPinningIntegration.createSSLPinningSocketFactory(CertStore)` - * `PowerAuthSslPinningValidationStrategy` +- You need different fingerprints in the update JSON. [Dynamic SSL Pinning Tool](https://github.com/wultra/ssl-pinning-tool) computes only certificate pinning. Therefore you need to generate those fingerprints yourself. +- Don't use these classes/methods (they are bound to certificate pinning): + - `CertStore.validateCertificate(X509Certificate)` + - `SSLPinningX509TrustManager` + - `SSLPinningIntegration.createSSLPinningSocketFactory(CertStore)` + - `PowerAuthSslPinningValidationStrategy` -You can use `CertStore.validateCertficateData(commonName, byteArray)` -only if you pass public key bytes as `byteArray`. +You can use `CertStore.validateCertficateData(commonName, byteArray)` only if you pass public key bytes as `byteArray`. For validating certificates, utilize `CertStore.validateFingerprint()` this way: + ```kotlin fun validateCertWithPublicKeyPinning(certificate: X509Certificate): ValidationResult { val key = certificate.publicKey.encoded @@ -482,35 +411,32 @@ fun validateCertWithPublicKeyPinning(certificate: X509Certificate): ValidationRe } ``` -If you need `SSLSocketFactory`, reimplement `X509TrustManager` -using the above `validateCertWithPublicKeyPinning()` method. +If you need `SSLSocketFactory`, reimplement `X509TrustManager` using the above `validateCertWithPublicKeyPinning()` method. ### How can I use `OkHttp` to pin only some domains? -If your app connects to both pinned and not pinned domains, then -create two instances of OkHttp client. +If your app connects to both pinned and not pinned domains, then create two instances of OkHttp client. Use one instance to communicate with the pinned domains. Set it up according to [Integration with OkHttp](#integration-with-okhttp). -Use the second instance to communicate with the domains that are not pinned. -Use normal setup for this one, don't use `SSLSocketFactory` and `TrustManager` provided by this library. +Use the second instance to communicate with the domains that are not pinned. Use normal setup for this one, don't use `SSLSocketFactory` and `TrustManager` provided by this library. ### TLS 1.2 Support for older Android versions -This library supports TLS 1.2 for older Android version (API < 21), but in some cases, -your app will need to call `ProviderInstaller.installIfNeeded` (part of the Play Services), to install system support. +This library supports TLS 1.2 for older Android versions (API < 21), but in some cases, your app will need to call `ProviderInstaller.installIfNeeded` (part of the Play Services), to install system support. + +### Download fingerprints from a test server -### Download fingerprints from test server +If your app connects to the development server with a self-signed certificate, then you can set `SslValidationStrategy.noValidation()` to `sslValidationStrategy` configuration to turn off the certificate chain validation. -If your app connects to development server with self-signed certificate, then you can set `SslValidationStrategy.noValidation()` to `sslValidationStrategy` configuration to turn-off the certificate chain validation. +Be aware, that using this option will lead to the use of an unsafe implementation of `HostnameVerifier` and `X509TrustManager` SSL client validation. This is useful for debug/testing purposes only, e.g. when an untrusted self-signed SSL certificate is used on the server side. -Be aware, that using this option will lead to use an unsafe implementation of `HostnameVerifier` and `X509TrustManager` SSL client validation. This is useful for debug/testing purposes only, e.g. when untrusted self-signed SSL certificate is used on server side. +It's strictly recommended to use this option only in debug flavors of your application. Deploying to production may cause a "Security alert" in the Google Developer Console. Please see [this](https://support.google.com/faqs/answer/7188426) and [this](https://support.google.com/faqs/answer/6346016) Google Help Center articles for more details. Beginning 1 March 2017, Google Play will block the publishing of any new apps or updates that use such unsafe implementation of `HostnameVerifier`. -It's strictly recommended to use this option only in debug flavours of your application. Deploying to production may cause "Security alert" in Google Developer Console. Please see [this](https://support.google.com/faqs/answer/7188426) and [this](https://support.google.com/faqs/answer/6346016) Google Help Center articles for more details. Beginning 1 March 2017, Google Play will block publishing of any new apps or updates that use such unsafe implementation of `HostnameVerifier`. +How to solve this problem for debug/production flavors in Gradle build script: -How to solve this problem for debug/production flavours in gradle build script: +1. Define a boolean type `buildConfigField` in flavor configuration. -1. Define boolean type `buildConfigField` in flavour configuration. ``` productFlavors { production { @@ -523,6 +449,7 @@ How to solve this problem for debug/production flavours in gradle build script: ``` 2. In code use this conditional initialization for [CertStoreConfiguration.Builder]: + ```kotlin val publicKey = Base64.decode("BMne....kdh2ak=", Base64.NO_WRAP) val builder = CertStoreConfiguration.Builder( @@ -539,15 +466,12 @@ How to solve this problem for debug/production flavours in gradle build script: ## License -All sources are licensed using Apache 2.0 license. You can use them with no restriction. -If you are using this library, please let us know. We will be happy to share and promote your project. +All sources are licensed using Apache 2.0 license. You can use them with no restrictions. If you are using this library, please let us know. We will be happy to share and promote your project. ## Contact -If you need any assistance, do not hesitate to drop us a line at [hello@wultra.com](mailto:hello@wultra.com) -or our official [gitter.im/wultra](https://gitter.im/wultra) channel. +If you need any assistance, do not hesitate to drop us a line at [hello@wultra.com](mailto:hello@wultra.com) or our official [wultra.com/discord](https://wultra.com/discord) channel. ### Security Disclosure -If you believe you have identified a security vulnerability with WultraSSLPinning, -you should report it as soon as possible via email to [support@wultra.com](mailto:support@wultra.com). Please do not post it to a public issue tracker. +If you believe you have identified a security vulnerability with WultraSSLPinning, you should report it as soon as possible via email to [support@wultra.com](mailto:support@wultra.com). Please do not post it to a public issue tracker. diff --git a/build.gradle b/build.gradle deleted file mode 100644 index a48556a..0000000 --- a/build.gradle +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2018 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. - */ - -buildscript { - ext.kotlinVersion = '1.5.31' - ext.dokkaVersion = '1.5.30' - ext.jacocoVersion = "0.8.7" - repositories { - mavenCentral() - google() - } - dependencies { - classpath "com.android.tools.build:gradle:4.2.2" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion" - classpath "org.jetbrains.dokka:dokka-gradle-plugin:$dokkaVersion" - // releasing - classpath "org.jacoco:org.jacoco.core:${jacocoVersion}" - } -} - -allprojects { - repositories { - mavenCentral() - google() - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} - -ext { - compileSdkVersion = 28 - targetSdkVersion = 28 - minSdkVersion = 19 - - powerAuthSdkVersion = "1.6.1" - gsonVersion = "2.8.6" - - mockitoVersion = "3.4.6" - powermockVersion = "2.0.7" -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..9c5df9e --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,31 @@ +/* + * Copyright 2018 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. + */ + +buildscript { + repositories { + mavenCentral() + google() + } + dependencies { + classpath("com.android.tools.build:gradle:${Constants.BuildScript.androidPluginVersion}") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${Constants.BuildScript.kotlinVersion}") + classpath("org.jetbrains.dokka:dokka-gradle-plugin:${Constants.BuildScript.dokkaVersion}") + } +} + +tasks.register("clean", Delete::class) { + delete(rootProject.buildDir) +} \ No newline at end of file diff --git a/buildSrc/.gitignore b/buildSrc/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/buildSrc/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..edb8a9b --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,32 @@ +/* + * Copyright 2023 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions + * and limitations under the License. + */ + +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() + google() +} + +val androidPluginVersion: String by System.getProperties() +val kotlinVersion: String by System.getProperties() + +dependencies { + implementation("com.android.tools.build", "gradle", androidPluginVersion) + implementation(kotlin("gradle-plugin", kotlinVersion)) +} \ No newline at end of file diff --git a/buildSrc/gradle.properties b/buildSrc/gradle.properties new file mode 100644 index 0000000..f2daf0a --- /dev/null +++ b/buildSrc/gradle.properties @@ -0,0 +1,19 @@ +# +# Copyright 2023 Wultra s.r.o. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions +# and limitations under the License. +# + +systemProp.kotlinVersion=1.8.20 +systemProp.androidPluginVersion=7.4.2 +systemProp.dokkaVersion=1.8.10 \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt new file mode 100644 index 0000000..0b433a5 --- /dev/null +++ b/buildSrc/src/main/kotlin/Constants.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2023 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions + * and limitations under the License. + */ + +import org.gradle.api.JavaVersion + +object Constants { + object BuildScript { + // These have to be defined in buildSrc/gradle.properties + // It's the only way to make them available in buildSrc/build.gradle.kts.kts + val androidPluginVersion: String by System.getProperties() + val kotlinVersion: String by System.getProperties() + val dokkaVersion: String by System.getProperties() + } + + object Java { + val sourceCompatibility = JavaVersion.VERSION_1_8 + val targetCompatibility = JavaVersion.VERSION_1_8 + const val kotlinJvmTarget = "1.8" + } + + object Android { + const val compileSdkVersion = 33 + const val targetSdkVersion = 33 + const val minSdkVersion = 19 + const val buildToolsVersion = "33.0.2" + } + + object Dependencies { + const val powerAuthSdkVersion = "1.8.0" + } +} diff --git a/buildSrc/src/main/kotlin/com/wultra/gradle/sslpinning/WultraSslPinningTestPlugin.kt b/buildSrc/src/main/kotlin/com/wultra/gradle/sslpinning/WultraSslPinningTestPlugin.kt new file mode 100644 index 0000000..3b44ed7 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/wultra/gradle/sslpinning/WultraSslPinningTestPlugin.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions + * and limitations under the License. + */ + +package com.wultra.gradle.sslpinning + +import com.android.build.gradle.BaseExtension +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.kotlin.dsl.getByType + +/** + * Plugin for handling server configurations for instrumentation tests. + */ +class WultraSslPinningTestPlugin : Plugin { + + override fun apply(target: Project) { + target.loadPropertiesFromGradleProps() + target.addInstrumentationArgumentsToDefaultConfig() + } + + private val instrumentationArgumentKeys = setOf("test.sslPinning.baseUrl", "test.sslPinning.appName") + private val instrumentationArguments = mutableMapOf() + + private fun Project.loadPropertiesFromGradleProps() { + for (key in instrumentationArgumentKeys) { + project.properties[key]?.let { + instrumentationArguments[key] = it.toString() + } + } + } + + private fun Project.addInstrumentationArgumentsToDefaultConfig() { + project.extensions.getByType().let { + it.defaultConfig { + for (entry in instrumentationArguments) { + this.testInstrumentationRunnerArguments[entry.key] = entry.value + } + } + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/com.wultra.android.sslpinning.test.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/com.wultra.android.sslpinning.test.properties new file mode 100644 index 0000000..5cde6a3 --- /dev/null +++ b/buildSrc/src/main/resources/META-INF/gradle-plugins/com.wultra.android.sslpinning.test.properties @@ -0,0 +1 @@ +implementation-class=com.wultra.gradle.sslpinning.WultraSslPinningTestPlugin diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 28861d2..943f0cb 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6f2308e..81b8f92 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Oct 07 12:28:27 CEST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index cccdd3d..65dcd68 100755 --- a/gradlew +++ b/gradlew @@ -1,78 +1,129 @@ -#!/usr/bin/env sh +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# 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 +# +# https://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. +# ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -89,84 +140,105 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index f955316..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,20 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,19 +25,23 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,38 +65,26 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/library/.gitignore b/library/.gitignore index 796b96d..96c674e 100644 --- a/library/.gitignore +++ b/library/.gitignore @@ -1 +1,2 @@ /build +/jacoco.exec diff --git a/library/android-coverage.gradle b/library/android-coverage.gradle deleted file mode 100644 index 3783998..0000000 --- a/library/android-coverage.gradle +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2018 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. - */ -apply plugin: 'jacoco' - - -jacoco { - toolVersion = rootProject.ext.jacocoVersion -} - -/** - * Create coverage report for unit tests ('src/test' source set). - */ -task unitTestCoverageReport(type: JacocoReport, - group: 'verification', - description: 'Creates unit test coverage report (for tests under \'test\' source set).', - dependsOn: ['testDebugUnitTest']) { - - reports { - html.enabled = true - } - - doFirst { - def mainSrc = "${project.projectDir}/src/main/java" - def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*'] - def debugKotlinTree = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/debug", excludes: fileFilter) - def debugJavaTree = fileTree(dir: "$project.buildDir/intermediates/javac/debug", excludes: fileFilter) - - sourceDirectories = files([mainSrc]) - classDirectories = files([debugKotlinTree, debugJavaTree]) - executionData = fileTree(dir: "$buildDir", includes: [ - "jacoco/testDebugUnitTest.exec" - ]) - } -} - -/** - * Create unified coverage report for unit tests and instrumented tests ('src/test' and 'src/androidTest' source sets). - */ -task unifiedCoverageReport(type: JacocoReport, - group: 'verification', - description: 'Creates unified test coverage report for unit tests (under \'test\' source set)' + - ' and instrumentation tests (under \'androidTest\' source set).', - dependsOn: ['testDebugUnitTest', 'createDebugCoverageReport', 'unitTestCoverageReport']) { - - reports { - html.enabled = true - } - - doFirst { - def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', 'android/**/*.*'] - def debugKotlinTree = fileTree(dir: "$project.buildDir/tmp/kotlin-classes/debug", excludes: fileFilter) - def debugJavaTree = fileTree(dir: "$project.buildDir/intermediates/javac/debug", excludes: fileFilter) - def mainSrc = "${project.projectDir}/src/main/java" - - sourceDirectories = files([mainSrc]) - classDirectories = files([debugKotlinTree, debugJavaTree]) - executionData = fileTree(dir: "$buildDir", includes: [ - "jacoco/testDebugUnitTest.exec", - "intermediates/jacoco_coverage_dir/debugAndroidTest/connectedDebugAndroidTest/code-coverage/*coverage.ec" - ]) - } -} - diff --git a/library/build.gradle b/library/build.gradle deleted file mode 100644 index 0aef840..0000000 --- a/library/build.gradle +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2018 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. - */ - -plugins { - id 'com.android.library' - id 'kotlin-android' - id 'org.jetbrains.dokka' - id 'maven-publish' - id 'signing' -} - -android { - compileSdkVersion rootProject.ext.compileSdkVersion - - defaultConfig { - minSdkVersion rootProject.ext.minSdkVersion - targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 1 - versionName VERSION_NAME - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - testInstrumentationRunnerArguments clearPackageData: 'true' - } - - buildTypes { - debug { - testCoverageEnabled true - } - release { - minifyEnabled false - consumerProguardFiles 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' - } -} - -dependencies { - implementation fileTree(dir: 'libs', include: ['*.jar']) - - compileOnly "com.wultra.android.powerauth:powerauth-sdk:${powerAuthSdkVersion}" - - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" - implementation "com.google.code.gson:gson:${gsonVersion}" - implementation 'androidx.annotation:annotation:1.2.0' - - testImplementation "com.wultra.android.powerauth:powerauth-sdk:${powerAuthSdkVersion}" - testImplementation 'junit:junit:4.13.2' - testImplementation "org.mockito:mockito-core:${mockitoVersion}" - testImplementation "org.powermock:powermock-module-junit4:${powermockVersion}" - testImplementation "org.powermock:powermock-api-mockito2:${powermockVersion}" - testImplementation 'org.bouncycastle:bcprov-jdk15on:1.60' - testImplementation "io.getlime.security:powerauth-java-crypto:0.19.0" - - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'androidx.test:rules:1.4.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation "com.wultra.android.powerauth:powerauth-sdk:${powerAuthSdkVersion}" - androidTestImplementation 'com.squareup.okhttp3:okhttp:3.12.12' -} - -apply from: 'android-release-aar.gradle' -apply from: 'android-coverage.gradle' \ No newline at end of file diff --git a/library/build.gradle.kts b/library/build.gradle.kts new file mode 100644 index 0000000..9684c9e --- /dev/null +++ b/library/build.gradle.kts @@ -0,0 +1,101 @@ +/* + * Copyright 2018 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. + */ + +plugins { + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.dokka") + id("maven-publish") + id("signing") + id("com.wultra.android.sslpinning.test") +} + +android { + namespace = "com.wultra.android.sslpinning" + testNamespace = "com.wultra.android.sslpinning.test" + compileSdk = Constants.Android.compileSdkVersion + + defaultConfig { + minSdk = Constants.Android.minSdkVersion + @Suppress("DEPRECATION") + targetSdk = Constants.Android.targetSdkVersion + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments["clearPackageData"] = "true" + } + + buildTypes { + debug { + enableUnitTestCoverage = true + enableAndroidTestCoverage = true + } + release { + isMinifyEnabled = false + consumerProguardFiles("proguard-rules.pro") + } + } + + compileOptions { + sourceCompatibility = Constants.Java.sourceCompatibility + targetCompatibility = Constants.Java.targetCompatibility + } + + kotlinOptions { + jvmTarget = Constants.Java.kotlinJvmTarget + } + + // avoids a gradle warning, otherwise unused due to custom config in android-release-aar.gradle + publishing { + singleVariant("release") { + withSourcesJar() + withJavadocJar() + } + } + + lint { + // to handle warning coming from a transitive dependency + // - obsolete 'androidx.fragment' through 'powerauth-sdk' + disable.add("ObsoleteLintCustomCheck") + } +} + +dependencies { + compileOnly("com.wultra.android.powerauth:powerauth-sdk:${Constants.Dependencies.powerAuthSdkVersion}") + + implementation("org.jetbrains.kotlin:kotlin-stdlib:${Constants.BuildScript.kotlinVersion}") + implementation("com.google.code.gson:gson:2.10.1") + implementation("androidx.annotation:annotation:1.7.1") + + testImplementation("com.wultra.android.powerauth:powerauth-sdk:${Constants.Dependencies.powerAuthSdkVersion}") + testImplementation("junit:junit:4.13.2") + testImplementation("io.mockk:mockk:1.13.5") + testImplementation("org.bouncycastle:bcprov-jdk15on:1.70") + testImplementation("io.getlime.security:powerauth-java-crypto:1.4.0") + + androidTestImplementation("androidx.test:runner:1.5.2") + androidTestImplementation("androidx.test:rules:1.5.0") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("com.wultra.android.powerauth:powerauth-sdk:${Constants.Dependencies.powerAuthSdkVersion}") + androidTestImplementation("com.squareup.okhttp3:okhttp:4.10.0") + + constraints { + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${Constants.BuildScript.kotlinVersion}") { + because("Avoids conflicts with 'kotlin-stdlib'") + } + } +} + +apply("android-release-aar.gradle") \ No newline at end of file diff --git a/library/gradle.properties b/library/gradle.properties index e5741b2..880ab1d 100644 --- a/library/gradle.properties +++ b/library/gradle.properties @@ -14,6 +14,6 @@ # and limitations under the License. # -VERSION_NAME=1.3.0 +VERSION_NAME=1.4.0 GROUP_ID=com.wultra.android.sslpinning ARTIFACT_ID=wultra-ssl-pinning diff --git a/library/proguard-rules.pro b/library/proguard-rules.pro index 1069196..6b8799b 100644 --- a/library/proguard-rules.pro +++ b/library/proguard-rules.pro @@ -3,4 +3,6 @@ # classes that will be serialized/deserialized over Gson -keepclassmembers class com.wultra.android.sslpinning.model.** { ; -} \ No newline at end of file +} +# necessary for R8 fullMode +-keep,allowobfuscation class com.wultra.android.sslpinning.model.** \ No newline at end of file diff --git a/library/src/androidTest/java/com/wultra/android/sslpinning/CertStoreChallengeTest.kt b/library/src/androidTest/java/com/wultra/android/sslpinning/CertStoreChallengeTest.kt index ad441e2..7f67562 100644 --- a/library/src/androidTest/java/com/wultra/android/sslpinning/CertStoreChallengeTest.kt +++ b/library/src/androidTest/java/com/wultra/android/sslpinning/CertStoreChallengeTest.kt @@ -18,7 +18,10 @@ package com.wultra.android.sslpinning import androidx.test.ext.junit.runners.AndroidJUnit4 import android.util.Base64 +import androidx.test.platform.app.InstrumentationRegistry import com.wultra.android.sslpinning.integration.powerauth.powerAuthCertStore +import org.junit.Assume +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import java.net.URL @@ -26,11 +29,23 @@ import java.net.URL @RunWith(AndroidJUnit4::class) class CertStoreChallengeTest: CommonTest() { - private val baseUrl = "https://mobile-utility-server.herokuapp.com/app" - private val appName = "rb-ekonto" + private lateinit var baseUrl: String + private lateinit var appName: String + + @Before + override fun setUp() { + super.setUp() + InstrumentationRegistry.getArguments().getString("test.sslPinning.baseUrl")?.let { + baseUrl = it + } + InstrumentationRegistry.getArguments().getString("test.sslPinning.appName")?.let { + appName = it + } + } @Test fun validateUpdateWithChallenge() { + Assume.assumeTrue(::baseUrl.isInitialized && ::appName.isInitialized) val config = CertStoreConfiguration.Builder(getServiceUrl("/init?appName=$appName"), getPublicKeyFromServer()) .useChallenge(true) .build() diff --git a/library/src/androidTest/java/com/wultra/android/sslpinning/CertStoreLoadSaveTest.kt b/library/src/androidTest/java/com/wultra/android/sslpinning/CertStoreLoadSaveTest.kt index 5e96fdd..87a32fc 100644 --- a/library/src/androidTest/java/com/wultra/android/sslpinning/CertStoreLoadSaveTest.kt +++ b/library/src/androidTest/java/com/wultra/android/sslpinning/CertStoreLoadSaveTest.kt @@ -78,8 +78,8 @@ class CertStoreLoadSaveTest : CommonTest() { val loadedData = store.loadCachedData() Assert.assertNotNull(loadedData) Assert.assertEquals((nextUpdate.time/1000)*1000, loadedData!!.nextUpdate.time) - Assert.assertEquals(2, loadedData!!.certificates.size) - val ci = loadedData!!.certificates[0] + Assert.assertEquals(2, loadedData.certificates.size) + val ci = loadedData.certificates[0] Assert.assertEquals("github.com", ci.commonName) Assert.assertEquals("aaa", String(ci.fingerprint)) Assert.assertEquals((date.time/1000)*1000, ci.expires.time) diff --git a/library/src/androidTest/java/com/wultra/android/sslpinning/CommonTest.kt b/library/src/androidTest/java/com/wultra/android/sslpinning/CommonTest.kt index df478f7..76685e0 100644 --- a/library/src/androidTest/java/com/wultra/android/sslpinning/CommonTest.kt +++ b/library/src/androidTest/java/com/wultra/android/sslpinning/CommonTest.kt @@ -30,7 +30,7 @@ abstract class CommonTest { val appContext = InstrumentationRegistry.getInstrumentation().targetContext @Before - fun setUp() { + open fun setUp() { WultraDebug.loggingLevel = WultraDebug.WultraLoggingLevel.DEBUG clearStorage() } diff --git a/library/src/androidTest/java/com/wultra/android/sslpinning/TestUtils.kt b/library/src/androidTest/java/com/wultra/android/sslpinning/TestUtils.kt index 67dfa9b..85ca843 100644 --- a/library/src/androidTest/java/com/wultra/android/sslpinning/TestUtils.kt +++ b/library/src/androidTest/java/com/wultra/android/sslpinning/TestUtils.kt @@ -55,9 +55,9 @@ const val jsonData = """ "fingerprints": [ { "name" : "github.com", - "fingerprint" : "CuOEv9Td6dE+UMWFfAWkQsk/jgFEXuSzRUDSK9Hjfxs=", - "expires" : 1648684799, - "signature" : "MEQCIBOa9pLAazjrgk1xUH2ZiZgIwayHHptqtCtFdtddvReRAiB6gyTnd4rOsIzIquXcZPYAnz3Rr76gz6zNrcZ4Uuw/og==" + "fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=", + "expires" : 1710460799, + "signature" : "MEUCICB69UpMPOdtrsR6XcJqHEh2L2RO4oSJ3SZ7BYnTBJbGAiEAnZ7rEWdMVGwa59Wx5QbAorEFxXH89Iu0CnqWa96Eda0=" } ] } @@ -67,7 +67,7 @@ const val jsonDataFingerprintsEmpty = "{\"fingerprints\":[]}" const val jsonDataAllEmpty = "{}" -val url = URL("https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/d2ea7150639e1269b9fd54a746e876fa35ed239c/ssl-pinning-signatures.json") +val url = URL("https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/07eb5b4b67e63d37d224912bc5951c7b589b35e6/ssl-pinning-signatures.json") const val publicKey = "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE=" fun getPublicKeyBytes(): ByteArray { return Base64.decode(publicKey, android.util.Base64.NO_WRAP) @@ -97,7 +97,7 @@ fun updateAndCheck(store: CertStore, updateMode: UpdateMode, expectedUpdateResul val initLatch = CountDownLatch(1) val latch = CountDownLatch(1) val updateResultWrapper = UpdateWrapperInstr() - val updateStarted = store.update(updateMode, object : UpdateObserver { + store.update(updateMode, object : UpdateObserver { override fun onUpdateStarted(type: UpdateType) { updateResultWrapper.updateType = type initLatch.countDown() diff --git a/library/src/main/AndroidManifest.xml b/library/src/main/AndroidManifest.xml index 590ff1f..671ee4d 100644 --- a/library/src/main/AndroidManifest.xml +++ b/library/src/main/AndroidManifest.xml @@ -14,4 +14,4 @@ ~ and limitations under the License. --> - + diff --git a/library/src/main/java/com/wultra/android/sslpinning/CertStore.kt b/library/src/main/java/com/wultra/android/sslpinning/CertStore.kt index 271948f..b647501 100644 --- a/library/src/main/java/com/wultra/android/sslpinning/CertStore.kt +++ b/library/src/main/java/com/wultra/android/sslpinning/CertStore.kt @@ -312,7 +312,7 @@ class CertStore internal constructor(private val configuration: CertStoreConfigu var signedBytes = challenge.toByteArray(Charsets.UTF_8) signedBytes += '&'.code.toByte() signedBytes += data - if (!cryptoProvider.ecdsaValidateSignatures(SignedData(signedBytes, signature), publicKey)) { + if (!cryptoProvider.ecdsaValidateSignature(SignedData(signedBytes, signature), publicKey)) { WultraDebug.error("Invalid signature in $RESPONSE_SIGNATURE_HEADER header") return UpdateResult.INVALID_SIGNATURE } @@ -361,7 +361,7 @@ class CertStore internal constructor(private val configuration: CertStoreConfigu break } - if (!cryptoProvider.ecdsaValidateSignatures(signedData, publicKey)) { + if (!cryptoProvider.ecdsaValidateSignature(signedData, publicKey)) { // detected invalid signature WultraDebug.error("CertStore: Invalid signature detected. CN = '${entry.name}'") result = UpdateResult.INVALID_SIGNATURE diff --git a/library/src/main/java/com/wultra/android/sslpinning/SslValidationStrategy.kt b/library/src/main/java/com/wultra/android/sslpinning/SslValidationStrategy.kt index ecccf72..899adc1 100644 --- a/library/src/main/java/com/wultra/android/sslpinning/SslValidationStrategy.kt +++ b/library/src/main/java/com/wultra/android/sslpinning/SslValidationStrategy.kt @@ -82,6 +82,7 @@ abstract class SslValidationStrategy { * Implements SSL validation strategy that trust any server certificate. * See [SslValidationStrategy.noValidation] for more details. */ +@Suppress("CustomX509TrustManager") internal class NoSslValidationStrategy: SslValidationStrategy() { override fun sslSocketFactory(): SSLSocketFactory? { val trustAllCerts = Array(1) { object : X509TrustManager { diff --git a/library/src/main/java/com/wultra/android/sslpinning/integration/SSLPinningIntegration.kt b/library/src/main/java/com/wultra/android/sslpinning/integration/SSLPinningIntegration.kt index 0109217..ef5b72a 100644 --- a/library/src/main/java/com/wultra/android/sslpinning/integration/SSLPinningIntegration.kt +++ b/library/src/main/java/com/wultra/android/sslpinning/integration/SSLPinningIntegration.kt @@ -81,7 +81,7 @@ class SSLPinningIntegration { sc.init(null, trustSslPinningCerts, null) return Tls12SocketFactory(sc.socketFactory) } catch (e: Exception) { - Log.e("TLS12Factory", e.message) + Log.e("TLS12Factory", e.message ?: "") } } diff --git a/library/src/main/java/com/wultra/android/sslpinning/integration/SSLPinningX509TrustManager.kt b/library/src/main/java/com/wultra/android/sslpinning/integration/SSLPinningX509TrustManager.kt index c59686c..4055b95 100644 --- a/library/src/main/java/com/wultra/android/sslpinning/integration/SSLPinningX509TrustManager.kt +++ b/library/src/main/java/com/wultra/android/sslpinning/integration/SSLPinningX509TrustManager.kt @@ -28,6 +28,7 @@ import javax.net.ssl.X509TrustManager * * @author Tomas Kypta, tomas.kypta@wultra.com */ +@Suppress("CustomX509TrustManager") class SSLPinningX509TrustManager(private val certStore: CertStore) : X509TrustManager { @SuppressLint("TrustAllX509TrustManager") diff --git a/library/src/main/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthCryptoProvider.kt b/library/src/main/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthCryptoProvider.kt index 6b8fcac..ab32958 100644 --- a/library/src/main/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthCryptoProvider.kt +++ b/library/src/main/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthCryptoProvider.kt @@ -20,6 +20,7 @@ import com.wultra.android.sslpinning.interfaces.CryptoProvider import com.wultra.android.sslpinning.interfaces.ECPublicKey import com.wultra.android.sslpinning.interfaces.SignedData import io.getlime.security.powerauth.core.CryptoUtils +import io.getlime.security.powerauth.core.EcPublicKey import java.lang.IllegalArgumentException import java.security.SecureRandom @@ -33,15 +34,14 @@ class PowerAuthCryptoProvider : CryptoProvider { private val randomGenerator = SecureRandom() - override fun ecdsaValidateSignatures(signedData: SignedData, publicKey: ECPublicKey): Boolean { + override fun ecdsaValidateSignature(signedData: SignedData, publicKey: ECPublicKey): Boolean { val ecKey = publicKey as? PA2ECPublicKey ?: throw IllegalArgumentException("Invalid ECPublicKey object.") - - return CryptoUtils.ecdsaValidateSignature(signedData.data, signedData.signature, ecKey.data) + return CryptoUtils.ecdsaValidateSignature(signedData.data, signedData.signature, ecKey.ecPublicKey) } override fun importECPublicKey(publicKey: ByteArray): ECPublicKey? { // TODO consider validation of the data - return PA2ECPublicKey(data = publicKey) + return PA2ECPublicKey(EcPublicKey(publicKey)) } override fun hashSha256(data: ByteArray): ByteArray { @@ -57,6 +57,8 @@ class PowerAuthCryptoProvider : CryptoProvider { /** * An implementation `ECPublicKey` protocol of a public key in EC based cryptography - * done with PowerAuth. + * done with PowerAuth. PowerAuth is using NIST P-256 curve under the hood. + * + * @param ecPublicKey PowerAuth representation of public key for elliptic curve based cryptography routines. */ -data class PA2ECPublicKey(val data: ByteArray) : ECPublicKey \ No newline at end of file +data class PA2ECPublicKey(val ecPublicKey: EcPublicKey) : ECPublicKey \ No newline at end of file diff --git a/library/src/main/java/com/wultra/android/sslpinning/interfaces/CryptoProvider.kt b/library/src/main/java/com/wultra/android/sslpinning/interfaces/CryptoProvider.kt index bacde36..d7901ee 100644 --- a/library/src/main/java/com/wultra/android/sslpinning/interfaces/CryptoProvider.kt +++ b/library/src/main/java/com/wultra/android/sslpinning/interfaces/CryptoProvider.kt @@ -32,7 +32,7 @@ interface CryptoProvider { * @param publicKey EC public key * @return True if all signatures are correct */ - fun ecdsaValidateSignatures(signedData: SignedData, publicKey: ECPublicKey): Boolean + fun ecdsaValidateSignature(signedData: SignedData, publicKey: ECPublicKey): Boolean /** * Constructs a new ECPublicKey object from given ASN.1 formatted data blob. diff --git a/library/src/main/java/com/wultra/android/sslpinning/model/CachedData.kt b/library/src/main/java/com/wultra/android/sslpinning/model/CachedData.kt index d7be941..d1a20b7 100644 --- a/library/src/main/java/com/wultra/android/sslpinning/model/CachedData.kt +++ b/library/src/main/java/com/wultra/android/sslpinning/model/CachedData.kt @@ -45,5 +45,23 @@ internal data class CachedData(var certificates: Array, internal fun sort() { certificates.sort() } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CachedData + + if (!certificates.contentEquals(other.certificates)) return false + if (nextUpdate != other.nextUpdate) return false + + return true + } + + override fun hashCode(): Int { + var result = certificates.contentHashCode() + result = 31 * result + nextUpdate.hashCode() + return result + } } diff --git a/library/src/main/java/com/wultra/android/sslpinning/model/CertificateInfo.kt b/library/src/main/java/com/wultra/android/sslpinning/model/CertificateInfo.kt index 8535b16..e4c00f5 100644 --- a/library/src/main/java/com/wultra/android/sslpinning/model/CertificateInfo.kt +++ b/library/src/main/java/com/wultra/android/sslpinning/model/CertificateInfo.kt @@ -17,7 +17,7 @@ package com.wultra.android.sslpinning.model import java.io.Serializable -import java.util.* +import java.util.Date /** * Data class for holding certificate info necessary for certificate validation. @@ -45,4 +45,24 @@ data class CertificateInfo(val commonName: String, } return this.commonName.compareTo(other.commonName) } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CertificateInfo + + if (commonName != other.commonName) return false + if (!fingerprint.contentEquals(other.fingerprint)) return false + if (expires != other.expires) return false + + return true + } + + override fun hashCode(): Int { + var result = commonName.hashCode() + result = 31 * result + fingerprint.contentHashCode() + result = 31 * result + expires.hashCode() + return result + } } \ No newline at end of file diff --git a/library/src/main/java/com/wultra/android/sslpinning/model/GetFingerprintResponse.kt b/library/src/main/java/com/wultra/android/sslpinning/model/GetFingerprintResponse.kt index 0f657a5..dac2757 100644 --- a/library/src/main/java/com/wultra/android/sslpinning/model/GetFingerprintResponse.kt +++ b/library/src/main/java/com/wultra/android/sslpinning/model/GetFingerprintResponse.kt @@ -55,6 +55,45 @@ data class GetFingerprintResponse(val fingerprints: Array) { val signedString = "${name}&${fingerprintPart}&${expirationTimestampInSeconds}" return SignedData(data = signedString.toByteArray(Charsets.UTF_8), signature = signature) } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as Entry + + if (name != other.name) return false + if (!fingerprint.contentEquals(other.fingerprint)) return false + if (expires != other.expires) return false + if (signature != null) { + if (other.signature == null) return false + if (!signature.contentEquals(other.signature)) return false + } else if (other.signature != null) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + fingerprint.contentHashCode() + result = 31 * result + expires.hashCode() + result = 31 * result + (signature?.contentHashCode() ?: 0) + return result + } } + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as GetFingerprintResponse + + if (!fingerprints.contentEquals(other.fingerprints)) return false + + return true + } + + override fun hashCode(): Int { + return fingerprints.contentHashCode() + } } \ No newline at end of file diff --git a/library/src/test/java/com/wultra/android/sslpinning/CertStoreConfigurationTest.java b/library/src/test/java/com/wultra/android/sslpinning/CertStoreConfigurationTest.java index 2d72067..eea8c64 100644 --- a/library/src/test/java/com/wultra/android/sslpinning/CertStoreConfigurationTest.java +++ b/library/src/test/java/com/wultra/android/sslpinning/CertStoreConfigurationTest.java @@ -16,28 +16,24 @@ package com.wultra.android.sslpinning; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + import com.wultra.android.sslpinning.model.GetFingerprintResponse; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.modules.junit4.PowerMockRunner; import java.net.MalformedURLException; import java.net.URL; -import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.concurrent.TimeUnit; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; - /** * @author Tomas Kypta, tomas.kypta@wultra.com */ -@RunWith(PowerMockRunner.class) -public class CertStoreConfigurationTest extends CommonJavaTest { +public class CertStoreConfigurationTest extends CommonKotlinTest { @Test public void testBasicConfiguration() throws Exception { diff --git a/library/src/test/java/com/wultra/android/sslpinning/CertStoreUpdateTest.java b/library/src/test/java/com/wultra/android/sslpinning/CertStoreUpdateTest.java deleted file mode 100644 index bf72c00..0000000 --- a/library/src/test/java/com/wultra/android/sslpinning/CertStoreUpdateTest.java +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright 2018 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 com.wultra.android.sslpinning; - -import androidx.annotation.NonNull; - -import com.wultra.android.sslpinning.integration.DefaultUpdateObserver; -import com.wultra.android.sslpinning.interfaces.ECPublicKey; -import com.wultra.android.sslpinning.interfaces.SignedData; -import com.wultra.android.sslpinning.service.RemoteDataProvider; -import com.wultra.android.sslpinning.service.RemoteDataRequest; -import com.wultra.android.sslpinning.service.RemoteDataResponse; - -import org.jetbrains.annotations.NotNull; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.modules.junit4.PowerMockRunner; - -import java.net.URL; -import java.util.Base64; -import java.util.Date; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import static java.util.Collections.emptyMap; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * Unit tests for {@link CertStore} updates. - * - * @author Tomas Kypta, tomas.kypta@wultra.com - */ -@RunWith(PowerMockRunner.class) -public class CertStoreUpdateTest extends CommonJavaTest { - - @Test - public void testCorrectUpdate() throws Exception { - when(cryptoProvider.ecdsaValidateSignatures(any(SignedData.class), any(ECPublicKey.class))) - .thenAnswer(invocation -> true); - - String pinningJsonUrl = "https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/34866234bbaa3350dc0ddc5680a65a6f4e7c549e/ssl-pinning-signatures.json"; - UpdateResult updateResult = performForcedUpdate(pinningJsonUrl); - assertEquals(UpdateResult.OK, updateResult); - } - - @Test - public void testInvalidSignatureUpdate() throws Exception { - when(cryptoProvider.ecdsaValidateSignatures(any(SignedData.class), any(ECPublicKey.class))) - .thenAnswer(invocation -> false); - - String pinningJsonUrl = "https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/34866234bbaa3350dc0ddc5680a65a6f4e7c549e/ssl-pinning-signatures.json"; - UpdateResult updateResult = performForcedUpdate(pinningJsonUrl); - assertEquals(UpdateResult.INVALID_SIGNATURE, updateResult); - } - - @Test - public void testExpiredUpdate() throws Exception { - when(cryptoProvider.ecdsaValidateSignatures(any(SignedData.class), any(ECPublicKey.class))) - .thenAnswer(invocation -> false); - - String pinningJsonUrl = "https://gist.githubusercontent.com/TomasKypta/5a6d99fe441a8c0d201b673d88e223a6/raw/0d12746cad1247ebf9a5b1706afabf8486a7a62e/ssl-pinning-signatures_expired.json"; - UpdateResult updateResult = performForcedUpdate(pinningJsonUrl); - assertEquals(UpdateResult.STORE_IS_EMPTY, updateResult); - } - - @Test - public void testUpdateSignatureGithub() throws Exception { - String publicKey = "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE="; - byte[] publicKeyBytes = java.util.Base64.getDecoder().decode(publicKey); - - CertStoreConfiguration config = TestUtils.getCertStoreConfiguration( - new Date(), - new String[]{"github.com"}, - new URL("https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/34866234bbaa3350dc0ddc5680a65a6f4e7c549e/ssl-pinning-signatures.json"), - publicKeyBytes, - null); - RemoteDataProvider remoteDataProvider = mock(RemoteDataProvider.class); - String jsonData = - "{\n" + - " \"fingerprints\": [\n" + - " {\n" + - " \"name\" : \"github.com\",\n" + - " \"fingerprint\" : \"trmmrz6GbL4OajB+fdoXOzcrLTrD8GrxX5dxh3OEgAg=\",\n" + - " \"expires\" : 1652184000,\n" + - " \"signature\" : \"MEUCIQCs1y/nyrKh4+2DIuX/PufUYiaVUdt2FBZQg6rBeZ/r4QIgNlT4owBwJ1ThrDsE0SwGipTNI74vP1vNyLNEwuXY4lE=\"\n" + - " }\n" + - " ]\n" + - "}"; - when(remoteDataProvider.getFingerprints(new RemoteDataRequest(emptyMap()))).thenReturn(new RemoteDataResponse(200, emptyMap(), jsonData.getBytes())); - - CertStore store = new CertStore(config, cryptoProvider, secureDataStore, remoteDataProvider); - TestUtils.assignHandler(store, handler); - TestUtils.updateAndCheck(store, UpdateMode.FORCED, UpdateResult.OK); - } - - @Test - public void testUpdateWithNoUpdateObserver() throws Exception { - String publicKey = "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE="; - byte[] publicKeyBytes = java.util.Base64.getDecoder().decode(publicKey); - - CertStoreConfiguration config = TestUtils.getCertStoreConfiguration( - new Date(), - new String[]{"github.com"}, - new URL("https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/c5b021db0fcd40b1262ab513bf375e4641834925/ssl-pinning-signatures.json"), - publicKeyBytes, - null); - RemoteDataProvider remoteDataProvider = mock(RemoteDataProvider.class); - String jsonData = - "{\n" + - " \"fingerprints\": [\n" + - " {\n" + - " \"name\" : \"github.com\",\n" + - " \"fingerprint\" : \"trmmrz6GbL4OajB+fdoXOzcrLTrD8GrxX5dxh3OEgAg=\",\n" + - " \"expires\" : 1652184000,\n" + - " \"signature\" : \"MEUCIQCs1y/nyrKh4+2DIuX/PufUYiaVUdt2FBZQg6rBeZ/r4QIgNlT4owBwJ1ThrDsE0SwGipTNI74vP1vNyLNEwuXY4lE=\"\n" + - " }\n" + - " ]\n" + - "}"; - - CountDownLatch latch = new CountDownLatch(1); - when(remoteDataProvider.getFingerprints(new RemoteDataRequest(emptyMap()))).thenAnswer( - invocation -> { - byte[] bytes = jsonData.getBytes(); - latch.countDown(); - return new RemoteDataResponse(200, emptyMap(), bytes); - }); - - CertStore store = new CertStore(config, cryptoProvider, secureDataStore, remoteDataProvider); - TestUtils.assignHandler(store, handler); - - - store.update(UpdateMode.FORCED, new DefaultUpdateObserver() { - @Override - public void onUpdateStarted(@NotNull UpdateType type) { - assertEquals(UpdateType.DIRECT, type); - super.onUpdateStarted(type); - } - - @Override - public void onUpdateFinished(@NotNull UpdateType type, @NotNull UpdateResult result) { - assertEquals(UpdateType.DIRECT, type); - super.onUpdateFinished(type, result); - } - - @Override - public void handleFailedUpdate(@NotNull UpdateType type, @NotNull UpdateResult result) { - fail(); - } - - @Override - public void continueExecution() { - - } - }); - assertTrue(latch.await(2, TimeUnit.SECONDS)); - } - - @NonNull - private UpdateResult performForcedUpdate(String pinningJsonUrl) throws Exception { - String publicKey = "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE="; - byte[] publicKeyBytes = Base64.getDecoder().decode(publicKey); - - CertStoreConfiguration config = TestUtils.getCertStoreConfiguration( - new Date(), - new String[]{"github.com"}, - new URL(pinningJsonUrl), - publicKeyBytes, - null); - CertStore store = new CertStore(config, cryptoProvider, secureDataStore); - TestUtils.assignHandler(store, handler); - - return TestUtils.updateAndCheck(store, UpdateMode.FORCED, null); - } -} diff --git a/library/src/test/java/com/wultra/android/sslpinning/CertStoreUpdateTest.kt b/library/src/test/java/com/wultra/android/sslpinning/CertStoreUpdateTest.kt new file mode 100644 index 0000000..23d8a09 --- /dev/null +++ b/library/src/test/java/com/wultra/android/sslpinning/CertStoreUpdateTest.kt @@ -0,0 +1,175 @@ +/* + * Copyright 2018 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 com.wultra.android.sslpinning + +import com.wultra.android.sslpinning.integration.DefaultUpdateObserver +import com.wultra.android.sslpinning.service.RemoteDataProvider +import com.wultra.android.sslpinning.service.RemoteDataResponse +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert +import org.junit.Test +import java.net.URL +import java.util.Base64 +import java.util.Date +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit + +/** + * Unit tests for [CertStore] updates. + * + * @author Tomas Kypta, tomas.kypta@wultra.com + */ +class CertStoreUpdateTest : CommonKotlinTest() { + + @Test + @Throws(Exception::class) + fun testCorrectUpdate() { + every { cryptoProvider.ecdsaValidateSignature(any(), any()) } returns true + + val publicKey = + "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE=" + val pinningJsonUrl = + "https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/07eb5b4b67e63d37d224912bc5951c7b589b35e6/ssl-pinning-signatures.json" + val updateResult = performForcedUpdate(publicKey, pinningJsonUrl) + Assert.assertEquals(UpdateResult.OK, updateResult) + } + + @Test + @Throws(Exception::class) + fun testInvalidSignatureUpdate() { + every { cryptoProvider.ecdsaValidateSignature(any(), any()) } returns false + + val publicKey = + "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE=" + val pinningJsonUrl = + "https://gist.githubusercontent.com/TomasKypta/40be50cc63d2f4c00abcbbf4554f0e32/raw/9cc9029d9e8248b0cd9a36b98382040114dd1d4a/ssl-pinning-signatures_Mar2023.json" + val updateResult = performForcedUpdate(publicKey, pinningJsonUrl) + Assert.assertEquals(UpdateResult.INVALID_SIGNATURE, updateResult) + } + + @Test + @Throws(Exception::class) + fun testExpiredUpdate() { + every { cryptoProvider.ecdsaValidateSignature(any(), any()) } returns false + + val publicKey = + "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE=" + val pinningJsonUrl = + "https://gist.githubusercontent.com/TomasKypta/5a6d99fe441a8c0d201b673d88e223a6/raw/0d12746cad1247ebf9a5b1706afabf8486a7a62e/ssl-pinning-signatures_expired.json" + val updateResult = performForcedUpdate(publicKey, pinningJsonUrl) + Assert.assertEquals(UpdateResult.STORE_IS_EMPTY, updateResult) + } + + @Test + @Throws(Exception::class) + fun testUpdateSignatureGithub() { + val publicKey = + "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE=" + val publicKeyBytes = Base64.getDecoder().decode(publicKey) + val config = TestUtils.getCertStoreConfiguration( + Date(), arrayOf("github.com"), + URL("https://gist.githubusercontent.com/"), + publicKeyBytes, + null + ) + val remoteDataProvider: RemoteDataProvider = mockk() + val jsonData = """{ + "fingerprints": [ + { + "name" : "github.com", + "fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=", + "expires" : 1710460799, + "signature" : "MEUCICB69UpMPOdtrsR6XcJqHEh2L2RO4oSJ3SZ7BYnTBJbGAiEAnZ7rEWdMVGwa59Wx5QbAorEFxXH89Iu0CnqWa96Eda0=" + } + ] +}""" + every { remoteDataProvider.getFingerprints(any()) } answers { + RemoteDataResponse(200, emptyMap(), jsonData.toByteArray()) + } + val store = CertStore(config, cryptoProvider, secureDataStore, remoteDataProvider) + TestUtils.assignHandler(store, handler) + TestUtils.updateAndCheck(store, UpdateMode.FORCED, UpdateResult.OK) + } + + @Test + @Throws(Exception::class) + fun testUpdateWithNoUpdateObserver() { + val publicKey = + "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE=" + val publicKeyBytes = Base64.getDecoder().decode(publicKey) + val config = TestUtils.getCertStoreConfiguration( + Date(), arrayOf("github.com"), + URL("https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/c5b021db0fcd40b1262ab513bf375e4641834925/ssl-pinning-signatures.json"), + publicKeyBytes, + null + ) + val remoteDataProvider: RemoteDataProvider = mockk() + val jsonData = """{ + "fingerprints": [ + { + "name" : "github.com", + "fingerprint" : "trmmrz6GbL4OajB+fdoXOzcrLTrD8GrxX5dxh3OEgAg=", + "expires" : 1652184000, + "signature" : "MEUCIQCs1y/nyrKh4+2DIuX/PufUYiaVUdt2FBZQg6rBeZ/r4QIgNlT4owBwJ1ThrDsE0SwGipTNI74vP1vNyLNEwuXY4lE=" + } + ] +}""" + val latch = CountDownLatch(1) + every { remoteDataProvider.getFingerprints(any()) } answers { + val bytes = jsonData.toByteArray() + latch.countDown() + RemoteDataResponse(200, emptyMap(), bytes) + } + val store = CertStore(config, cryptoProvider, secureDataStore, remoteDataProvider) + TestUtils.assignHandler(store, handler) + store.update(UpdateMode.FORCED, object : DefaultUpdateObserver() { + override fun onUpdateStarted(type: UpdateType) { + Assert.assertEquals(UpdateType.DIRECT, type) + super.onUpdateStarted(type) + } + + override fun onUpdateFinished(type: UpdateType, result: UpdateResult) { + Assert.assertEquals(UpdateType.DIRECT, type) + super.onUpdateFinished(type, result) + } + + override fun handleFailedUpdate(type: UpdateType, result: UpdateResult) { + Assert.fail() + } + + override fun continueExecution() {} + }) + Assert.assertTrue(latch.await(2, TimeUnit.SECONDS)) + } + + @Throws(Exception::class) + private fun performForcedUpdate( + publicKey: String, + pinningJsonUrl: String + ): UpdateResult { + val publicKeyBytes = Base64.getDecoder().decode(publicKey) + val config = TestUtils.getCertStoreConfiguration( + Date(), arrayOf("github.com"), + URL(pinningJsonUrl), + publicKeyBytes, + null + ) + val store = CertStore(config, cryptoProvider, secureDataStore) + TestUtils.assignHandler(store, handler) + return TestUtils.updateAndCheck(store, UpdateMode.FORCED, null) + } +} \ No newline at end of file diff --git a/library/src/test/java/com/wultra/android/sslpinning/CertStoreValidationTest.java b/library/src/test/java/com/wultra/android/sslpinning/CertStoreValidationTest.java index 666ecd6..620d743 100644 --- a/library/src/test/java/com/wultra/android/sslpinning/CertStoreValidationTest.java +++ b/library/src/test/java/com/wultra/android/sslpinning/CertStoreValidationTest.java @@ -19,8 +19,6 @@ import com.wultra.android.sslpinning.model.GetFingerprintResponse; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.modules.junit4.PowerMockRunner; import java.net.URL; import java.security.cert.X509Certificate; @@ -35,19 +33,30 @@ * * @author Tomas Kypta, tomas.kypta@wultra.com */ -@RunWith(PowerMockRunner.class) -public class CertStoreValidationTest extends CommonJavaTest { +public class CertStoreValidationTest extends CommonKotlinTest { @Test - public void testValidationGithubFallback() throws Exception { + public void testValidationGithubFallbackValid() throws Exception { + // necessary to update the fingerprint from time to time + // Mar 2023 - "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=" + validateGithubWithFallbackOnly("kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=", ValidationResult.TRUSTED); + } + + @Test + public void testValidationGithubFallbackInvalid() throws Exception { + // outdated fingerprint + validateGithubWithFallbackOnly("trmmrz6GbL4OajB+fdoXOzcrLTrD8GrxX5dxh3OEgAg=", ValidationResult.UNTRUSTED); + } + + private void validateGithubWithFallbackOnly(String fingerprintBase64, + ValidationResult expectedResult) throws Exception { X509Certificate cert = TestUtils.getCertificateFromUrl("https://github.com"); - String publicKey = "BEG6g28LNWRcmdFzexSNTKPBYZnDtKrCyiExFKbktttfKAF7wG4Cx1Nycr5PwCoICG1dRseLyuDxUilAmppPxAo="; - byte[] publicKeyBytes = java.util.Base64.getDecoder().decode(publicKey); + String publicKey = "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE="; + byte[] publicKeyBytes = Base64.getDecoder().decode(publicKey); - String signatureBase64 = "MEUCIQCs1y/nyrKh4+2DIuX/PufUYiaVUdt2FBZQg6rBeZ/r4QIgNlT4owBwJ1ThrDsE0SwGipTNI74vP1vNyLNEwuXY4lE="; + String signatureBase64 = "MEUCICB69UpMPOdtrsR6XcJqHEh2L2RO4oSJ3SZ7BYnTBJbGAiEAnZ7rEWdMVGwa59Wx5QbAorEFxXH89Iu0CnqWa96Eda0="; byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64); - String fingerprintBase64 = "trmmrz6GbL4OajB+fdoXOzcrLTrD8GrxX5dxh3OEgAg="; byte[] fingerprintBytes = Base64.getDecoder().decode(fingerprintBase64); GetFingerprintResponse.Entry fallbackEntry = new GetFingerprintResponse.Entry( @@ -66,27 +75,51 @@ public void testValidationGithubFallback() throws Exception { CertStore store = new CertStore(config, cryptoProvider, secureDataStore); TestUtils.assignHandler(store, handler); ValidationResult result = store.validateCertificate(cert); - assertEquals(ValidationResult.TRUSTED, result); + assertEquals(expectedResult, result); + } + + @Test + public void testValidationGithubUpdateWithOutdatedData() throws Exception { + // json with outdated data, correct public key + validateGithubWithUpdateJsonOnly("BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE=", + "https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/34866234bbaa3350dc0ddc5680a65a6f4e7c549e/ssl-pinning-signatures.json", + UpdateResult.STORE_IS_EMPTY, ValidationResult.EMPTY); + } + @Test + public void testValidationGithubUpdateWithInvalidSignature() throws Exception { + // json with current data, different public key + validateGithubWithUpdateJsonOnly("BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE=", + "https://gist.githubusercontent.com/TomasKypta/40be50cc63d2f4c00abcbbf4554f0e32/raw/9cc9029d9e8248b0cd9a36b98382040114dd1d4a/ssl-pinning-signatures_Mar2023.json", + UpdateResult.INVALID_SIGNATURE, ValidationResult.EMPTY); } @Test - public void testValidationGithubUpdatedJson() throws Exception { + public void testValidationGithubUpdateWithValidData() throws Exception { + // json with current data, correct public key + validateGithubWithUpdateJsonOnly("BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE=", + "https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/07eb5b4b67e63d37d224912bc5951c7b589b35e6/ssl-pinning-signatures.json", + UpdateResult.OK, ValidationResult.TRUSTED); + } + + private void validateGithubWithUpdateJsonOnly(String publicKey, + String signaturesJsonUrl, + UpdateResult expectedUpdateResult, + ValidationResult expectedValidationResult) throws Exception { X509Certificate cert = TestUtils.getCertificateFromUrl("https://github.com"); - String publicKey = "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE="; - byte[] publicKeyBytes = java.util.Base64.getDecoder().decode(publicKey); + byte[] publicKeyBytes = Base64.getDecoder().decode(publicKey); CertStoreConfiguration config = TestUtils.getCertStoreConfiguration( new Date(), new String[]{"github.com"}, - new URL("https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/34866234bbaa3350dc0ddc5680a65a6f4e7c549e/ssl-pinning-signatures.json"), + new URL(signaturesJsonUrl), publicKeyBytes, null); CertStore store = new CertStore(config, cryptoProvider, secureDataStore); TestUtils.assignHandler(store, handler); - TestUtils.updateAndCheck(store, UpdateMode.FORCED, UpdateResult.OK); + TestUtils.updateAndCheck(store, UpdateMode.FORCED, expectedUpdateResult); ValidationResult result = store.validateCertificate(cert); - assertEquals(ValidationResult.TRUSTED, result); + assertEquals(expectedValidationResult, result); } } diff --git a/library/src/test/java/com/wultra/android/sslpinning/CommonJavaTest.java b/library/src/test/java/com/wultra/android/sslpinning/CommonJavaTest.java deleted file mode 100644 index 6018a61..0000000 --- a/library/src/test/java/com/wultra/android/sslpinning/CommonJavaTest.java +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright 2018 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 com.wultra.android.sslpinning; - -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Handler; -import android.os.Looper; -import android.util.Log; - -import com.wultra.android.sslpinning.integration.powerauth.PA2ECPublicKey; -import com.wultra.android.sslpinning.interfaces.CryptoProvider; -import com.wultra.android.sslpinning.interfaces.ECPublicKey; -import com.wultra.android.sslpinning.interfaces.SecureDataStore; -import com.wultra.android.sslpinning.interfaces.SignedData; - -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.junit.Before; -import org.junit.BeforeClass; -import org.mockito.Mock; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; - -import java.security.MessageDigest; -import java.security.Security; -import java.util.Base64; - -import io.getlime.security.powerauth.crypto.lib.config.PowerAuthConfiguration; -import io.getlime.security.powerauth.crypto.lib.util.SignatureUtils; -import io.getlime.security.powerauth.provider.CryptoProviderUtil; -import io.getlime.security.powerauth.provider.CryptoProviderUtilBouncyCastle; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; - -/** - * Common setup for java-based tests. - * - * @author Tomas Kypta, tomas.kypta@wultra.com - */ -@PowerMockIgnore({ - "javax.net.ssl.*", - "javax.security.auth.x500.*", - "org.bouncycastle.*", - "java.security.*" -}) -@PrepareForTest({ - android.util.Base64.class, - Log.class, - Looper.class -}) -public abstract class CommonJavaTest { - - @Mock - protected Context context; - - @Mock - protected SharedPreferences sharedPrefs; - - @Mock - protected CryptoProvider cryptoProvider; - - @Mock - protected SecureDataStore secureDataStore; - - @Mock - protected Handler handler; - - @BeforeClass - public static void setUpClass() { - Security.addProvider(new BouncyCastleProvider()); - PowerAuthConfiguration.INSTANCE.setKeyConvertor(new CryptoProviderUtilBouncyCastle()); - } - - @Before - public void setUp() { - PowerMockito.mockStatic(android.util.Base64.class); - when(android.util.Base64.encodeToString(any(byte[].class), anyInt())) - .thenAnswer(invocation -> - new String(java.util.Base64.getEncoder().encode((byte[]) invocation.getArgument(0))) - ); - when(android.util.Base64.encode(any(byte[].class), anyInt())) - .thenAnswer(invocation -> - java.util.Base64.getEncoder().encode((byte[]) invocation.getArgument(0)) - ); - when(android.util.Base64.decode(anyString(), anyInt())) - .thenAnswer(invocation -> - Base64.getDecoder().decode((String) invocation.getArgument(0)) - ); - - PowerMockito.mockStatic(Log.class); - when(Log.e(anyString(), anyString())) - .then(invocation -> { - System.out.println((String) invocation.getArgument(1)); - return 0; - }); - when(cryptoProvider.hashSha256(any(byte[].class))) - .thenAnswer(invocation -> { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - return digest.digest(invocation.getArgument(0)); - }); - when(cryptoProvider.importECPublicKey(any(byte[].class))) - .thenAnswer(invocation -> - new PA2ECPublicKey(invocation.getArgument(0)) - ); - when(cryptoProvider.ecdsaValidateSignatures(any(SignedData.class), any(ECPublicKey.class))) - .thenAnswer(invocation -> { - SignatureUtils utils = new SignatureUtils(); - SignedData signedData = invocation.getArgument(0); - PA2ECPublicKey pubKey = invocation.getArgument(1); - final CryptoProviderUtil keyConvertor = PowerAuthConfiguration.INSTANCE.getKeyConvertor(); - return utils.validateECDSASignature(signedData.getData(), - signedData.getSignature(), - keyConvertor.convertBytesToPublicKey(pubKey.getData())); - }); - - PowerMockito.mockStatic(Looper.class); - when(Looper.getMainLooper()) - .thenReturn(null); - - when(handler.post(any())) - .thenAnswer(invocation -> { - Runnable runnable = invocation.getArgument(0); - Thread t = new Thread(runnable); - t.start(); - return true; - }); - - when(context.getApplicationContext()) - .thenReturn(context); - when(context.getSharedPreferences(anyString(), anyInt())) - .thenReturn(sharedPrefs); - } -} diff --git a/library/src/test/java/com/wultra/android/sslpinning/CommonKotlinTest.kt b/library/src/test/java/com/wultra/android/sslpinning/CommonKotlinTest.kt index ab59d48..b4f0b87 100644 --- a/library/src/test/java/com/wultra/android/sslpinning/CommonKotlinTest.kt +++ b/library/src/test/java/com/wultra/android/sslpinning/CommonKotlinTest.kt @@ -22,26 +22,20 @@ import android.os.Handler import android.os.Looper import android.util.Base64 import android.util.Log -import com.wultra.android.sslpinning.integration.powerauth.PA2ECPublicKey import com.wultra.android.sslpinning.interfaces.CryptoProvider -import com.wultra.android.sslpinning.interfaces.ECPublicKey import com.wultra.android.sslpinning.interfaces.SecureDataStore import com.wultra.android.sslpinning.interfaces.SignedData -import io.getlime.security.powerauth.crypto.lib.config.PowerAuthConfiguration +import io.getlime.security.powerauth.crypto.lib.util.KeyConvertor import io.getlime.security.powerauth.crypto.lib.util.SignatureUtils -import io.getlime.security.powerauth.provider.CryptoProviderUtilBouncyCastle +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.impl.annotations.MockK +import io.mockk.mockkStatic +import io.mockk.unmockkAll import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.junit.After import org.junit.Before import org.junit.BeforeClass -import org.mockito.ArgumentMatchers -import org.mockito.ArgumentMatchers.anyString -import org.mockito.Mock -import org.mockito.Mockito -import org.mockito.Mockito.`when` -import org.mockito.Mockito.anyInt -import org.powermock.api.mockito.PowerMockito -import org.powermock.core.classloader.annotations.PowerMockIgnore -import org.powermock.core.classloader.annotations.PrepareForTest import java.security.MessageDigest import java.security.Security @@ -50,25 +44,21 @@ import java.security.Security * * @author Tomas Kypta, tomas.kypta@wultra.com */ -@PowerMockIgnore("javax.net.ssl.*", "javax.security.auth.x500.*", "org.bouncycastle.*", "java.security.*") -@PrepareForTest(Base64::class, - Log::class, - Looper::class) open class CommonKotlinTest { - @Mock + @MockK lateinit var cryptoProvider: CryptoProvider - @Mock + @MockK lateinit var secureDataStore: SecureDataStore - @Mock + @MockK lateinit var handler: Handler - @Mock + @MockK lateinit var context: Context - @Mock + @MockK lateinit var sharedPrefs: SharedPreferences companion object { @@ -77,63 +67,73 @@ open class CommonKotlinTest { @JvmStatic fun setUpClass() { Security.addProvider(BouncyCastleProvider()) - PowerAuthConfiguration.INSTANCE.keyConvertor = CryptoProviderUtilBouncyCastle() } } - fun any(): T = Mockito.any() - @Before fun setUp() { - PowerMockito.mockStatic(Base64::class.java) - Mockito.`when`(Base64.encodeToString(any(), ArgumentMatchers.anyInt())) - .thenAnswer { invocation -> String(java.util.Base64.getEncoder().encode(invocation.getArgument(0) as ByteArray)) } - - Mockito.`when`(Base64.encode(any(), ArgumentMatchers.anyInt())) - .thenAnswer { invocation -> java.util.Base64.getEncoder().encode(invocation.getArgument(0) as ByteArray) } - - Mockito.`when`(Base64.decode(ArgumentMatchers.anyString(), ArgumentMatchers.anyInt())) - .thenAnswer { invocation -> java.util.Base64.getDecoder().decode(invocation.getArgument(0) as String) } - - PowerMockito.mockStatic(Log::class.java) - Mockito.`when`(Log.e(ArgumentMatchers.anyString(), ArgumentMatchers.anyString())) - .then { invocation -> - println(invocation.getArgument(1) as String) - 0 - } - - Mockito.`when`(cryptoProvider.hashSha256(any())) - .thenAnswer { invocation -> - val digest = MessageDigest.getInstance("SHA-256") - digest.digest(invocation.getArgument(0)) - } - - Mockito.`when`(cryptoProvider.importECPublicKey(any())) - .thenAnswer { invocation -> PA2ECPublicKey(invocation.getArgument(0)) } - - Mockito.`when`(cryptoProvider.ecdsaValidateSignatures(any(), any())) - .thenAnswer { invocation -> - val utils = SignatureUtils() - val signedData: SignedData = invocation.getArgument(0) - val pubKey: PA2ECPublicKey = invocation.getArgument(1) - val keyConvertor = PowerAuthConfiguration.INSTANCE.keyConvertor - utils.validateECDSASignature(signedData.data, - signedData.signature, - keyConvertor.convertBytesToPublicKey(pubKey.data)) - } - - PowerMockito.mockStatic(Looper::class.java) - Mockito.`when`(Looper.getMainLooper()) - .thenReturn(null) - - Mockito.`when`(handler.post(any())) - .then { invocation -> - val runnable = invocation.getArgument(0) - runnable.run() - return@then true - } - - `when`(context.applicationContext).thenReturn(context) - `when`(context.getSharedPreferences(anyString(), anyInt())).thenReturn(sharedPrefs) + MockKAnnotations.init(this, relaxUnitFun = true) + + mockkStatic(Base64::class) + every { Base64.encodeToString(any(), any()) } answers { + String(java.util.Base64.getEncoder().encode(it.invocation.args[0] as ByteArray)) + } + every { Base64.encode(any(), any()) } answers { + java.util.Base64.getEncoder().encode(it.invocation.args[0] as ByteArray) + } + every { Base64.decode(any(), any()) } answers { + java.util.Base64.getDecoder().decode(it.invocation.args[0] as String) + } + + mockkStatic(Log::class) + every { Log.e(any(), any()) } answers { + println("error: ${it.invocation.args[1] as String}") + 0 + } + every { Log.w(any(), any()) } answers { + println("warning: ${it.invocation.args[1] as String}") + 0 + } + + every { cryptoProvider.hashSha256(any()) } answers { + val digest = MessageDigest.getInstance("SHA-256") + digest.digest(it.invocation.args[0] as ByteArray) + } + + every { cryptoProvider.importECPublicKey(any()) } answers { + TestPA2ECPublicKey(it.invocation.args[0] as ByteArray) + } + + every { cryptoProvider.ecdsaValidateSignature(any(), any()) } answers { + val utils = SignatureUtils() + val signedData: SignedData = it.invocation.args[0] as SignedData + val pubKey: TestPA2ECPublicKey = it.invocation.args[1] as TestPA2ECPublicKey + val keyConvertor = KeyConvertor() + utils.validateECDSASignature(signedData.data, + signedData.signature, + keyConvertor.convertBytesToPublicKey(pubKey.data)) + } + + every { secureDataStore.load(any()) } returns null + every { secureDataStore.save(any(), any()) } returns false + + mockkStatic(Looper::class) + every { Looper.getMainLooper() } returns null + + every { handler.post(any()) } answers { + val runnable = it.invocation.args[0] as Runnable + runnable.run() + true + } + + every { context.applicationContext } returns context + every { context.getSharedPreferences(any(), any()) } returns sharedPrefs + + every { sharedPrefs.getInt(any(), any()) } returns 0 + } + + @After + fun tearDown() { + unmockkAll() } } \ No newline at end of file diff --git a/library/src/test/java/com/wultra/android/sslpinning/TestPA2ECPublicKey.kt b/library/src/test/java/com/wultra/android/sslpinning/TestPA2ECPublicKey.kt new file mode 100644 index 0000000..74c5531 --- /dev/null +++ b/library/src/test/java/com/wultra/android/sslpinning/TestPA2ECPublicKey.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2023 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions + * and limitations under the License. + */ + +package com.wultra.android.sslpinning + +import com.wultra.android.sslpinning.interfaces.ECPublicKey + +/** + * Test PA2ECPublicKey class used to avoid loading native code in EcPublicKey. + */ +data class TestPA2ECPublicKey(val data: ByteArray) : ECPublicKey \ No newline at end of file diff --git a/library/src/test/java/com/wultra/android/sslpinning/ValidationObserverTest.kt b/library/src/test/java/com/wultra/android/sslpinning/ValidationObserverTest.kt index 39adce5..f2b4700 100644 --- a/library/src/test/java/com/wultra/android/sslpinning/ValidationObserverTest.kt +++ b/library/src/test/java/com/wultra/android/sslpinning/ValidationObserverTest.kt @@ -17,22 +17,22 @@ package com.wultra.android.sslpinning import com.wultra.android.sslpinning.TestUtils.assignHandler +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.runs +import io.mockk.verify import org.junit.Assert.assertEquals +import org.junit.Before import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyString -import org.mockito.Mockito.* -import org.powermock.modules.junit4.PowerMockRunner import java.net.URL import java.util.* -import org.mockito.Mockito.`when` as wh /** * Test global validation observers. * * @author Tomas Kypta, tomas.kypta@wultra.com */ -@RunWith(PowerMockRunner::class) class ValidationObserverTest : CommonKotlinTest() { @Test @@ -46,39 +46,59 @@ class ValidationObserverTest : CommonKotlinTest() { val config = TestUtils.getCertStoreConfiguration( Date(), arrayOf("github.com"), - URL("https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/34866234bbaa3350dc0ddc5680a65a6f4e7c549e/ssl-pinning-signatures.json"), + URL("https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/07eb5b4b67e63d37d224912bc5951c7b589b35e6/ssl-pinning-signatures.json"), publicKeyBytes, null) val store = CertStore(config, cryptoProvider, secureDataStore) assignHandler(store, handler) - var observer: ValidationObserver = mock(ValidationObserver::class.java) + var observer: ValidationObserver = mockkValidationObserver() store.addValidationObserver(observer) val result = store.validateCertificate(cert) assertEquals(ValidationResult.EMPTY, result) - verify(observer, times(0)).onValidationUntrusted(anyString()) - verify(observer, times(1)).onValidationEmpty(anyString()) - verify(observer, times(0)).onValidationTrusted(anyString()) + verify(exactly = 0) { + observer.onValidationUntrusted(any()) + observer.onValidationTrusted(any()) + } + verify(exactly = 1) { + observer.onValidationEmpty(any()) + } store.removeValidationObserver(observer) TestUtils.updateAndCheck(store, UpdateMode.FORCED, UpdateResult.OK) - observer = mock(ValidationObserver::class.java) + observer = mockkValidationObserver() store.addValidationObserver(observer) val result2 = store.validateCertificate(cert) assertEquals(ValidationResult.TRUSTED, result2) - verify(observer, times(0)).onValidationUntrusted(anyString()) - verify(observer, times(0)).onValidationEmpty(anyString()) - verify(observer, times(1)).onValidationTrusted(anyString()) + verify(exactly = 0) { + observer.onValidationUntrusted(any()) + observer.onValidationEmpty(any()) + } + verify(exactly = 1) { + observer.onValidationTrusted(any()) + } store.removeAllValidationObservers() - observer = mock(ValidationObserver::class.java) + observer = mockkValidationObserver() store.addValidationObserver(observer) val result3 = store.validateCertificate(certGoogle) assertEquals(ValidationResult.UNTRUSTED, result3) - verify(observer, times(1)).onValidationUntrusted(anyString()) - verify(observer, times(0)).onValidationEmpty(anyString()) - verify(observer, times(0)).onValidationTrusted(anyString()) + verify(exactly = 0) { + observer.onValidationEmpty(any()) + observer.onValidationTrusted(any()) + } + verify(exactly = 1) { + observer.onValidationUntrusted(any()) + } store.removeAllValidationObservers() } + + private fun mockkValidationObserver(): ValidationObserver { + val observer: ValidationObserver = mockk() + every { observer.onValidationEmpty(any()) } just runs + every { observer.onValidationTrusted(any()) } just runs + every { observer.onValidationUntrusted(any()) } just runs + return observer + } } \ No newline at end of file diff --git a/library/src/test/java/com/wultra/android/sslpinning/integration/SSLPinningIntegrationTest.java b/library/src/test/java/com/wultra/android/sslpinning/integration/SSLPinningIntegrationTest.java index 9750510..c57f0b7 100644 --- a/library/src/test/java/com/wultra/android/sslpinning/integration/SSLPinningIntegrationTest.java +++ b/library/src/test/java/com/wultra/android/sslpinning/integration/SSLPinningIntegrationTest.java @@ -18,14 +18,12 @@ import com.wultra.android.sslpinning.CertStore; import com.wultra.android.sslpinning.CertStoreConfiguration; -import com.wultra.android.sslpinning.CommonJavaTest; +import com.wultra.android.sslpinning.CommonKotlinTest; import com.wultra.android.sslpinning.TestUtils; import com.wultra.android.sslpinning.integration.powerauth.PowerAuthCertStore; import org.junit.Assert; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.modules.junit4.PowerMockRunner; import java.net.URL; @@ -36,8 +34,7 @@ * * @author Tomas Kypta, tomas.kypta@wultra.com */ -@RunWith(PowerMockRunner.class) -public class SSLPinningIntegrationTest extends CommonJavaTest { +public class SSLPinningIntegrationTest extends CommonKotlinTest { @Test public void testSSLPinningIntegrationApis() throws Exception { diff --git a/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthIntegrationTest.java b/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthIntegrationTest.java index 17b2256..a9c63ae 100644 --- a/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthIntegrationTest.java +++ b/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthIntegrationTest.java @@ -18,13 +18,11 @@ import com.wultra.android.sslpinning.CertStore; import com.wultra.android.sslpinning.CertStoreConfiguration; -import com.wultra.android.sslpinning.CommonJavaTest; +import com.wultra.android.sslpinning.CommonKotlinTest; import com.wultra.android.sslpinning.TestUtils; import org.junit.Assert; import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.modules.junit4.PowerMockRunner; import java.net.URL; @@ -33,8 +31,7 @@ * * @author Tomas Kypta, tomas.kypta@wultra.com */ -@RunWith(PowerMockRunner.class) -public class PowerAuthIntegrationTest extends CommonJavaTest { +public class PowerAuthIntegrationTest extends CommonKotlinTest { @Test public void testPowerAuthCertStoreApis() throws Exception { @@ -68,7 +65,7 @@ public void testPowerAuthSecureDataStoreApis() { Assert.assertNotNull(secureDataStore2); } - @Test(expected = IllegalArgumentException.class) + @Test(expected = NullPointerException.class) public void testPowerAuthSecureDataStoreApisCrash() { new PowerAuthSecureDataStore(context, null); } diff --git a/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthIntegrationTestKt.kt b/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthIntegrationTestKt.kt index 159786e..3ae88d4 100644 --- a/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthIntegrationTestKt.kt +++ b/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthIntegrationTestKt.kt @@ -22,8 +22,6 @@ import com.wultra.android.sslpinning.CommonKotlinTest import com.wultra.android.sslpinning.TestUtils import org.junit.Assert import org.junit.Test -import org.junit.runner.RunWith -import org.powermock.modules.junit4.PowerMockRunner import java.net.URL /** @@ -31,7 +29,6 @@ import java.net.URL * * @author Tomas Kypta, tomas.kypta@wultra.com */ -@RunWith(PowerMockRunner::class) class PowerAuthIntegrationTestKt : CommonKotlinTest() { @Test diff --git a/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthSslPinningValidationStrategyTest.java b/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthSslPinningValidationStrategyTest.java deleted file mode 100644 index 64b7d7a..0000000 --- a/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthSslPinningValidationStrategyTest.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright 2018 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 com.wultra.android.sslpinning.integration.powerauth; - -import com.wultra.android.sslpinning.CertStore; -import com.wultra.android.sslpinning.CertStoreConfiguration; -import com.wultra.android.sslpinning.CommonJavaTest; -import com.wultra.android.sslpinning.TestUtils; -import com.wultra.android.sslpinning.UpdateMode; -import com.wultra.android.sslpinning.UpdateResult; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.powermock.modules.junit4.PowerMockRunner; - -import java.net.URL; -import java.net.URLConnection; -import java.util.Date; - -import javax.net.ssl.HostnameVerifier; -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLSocketFactory; - -import io.getlime.security.powerauth.networking.ssl.PA2ClientValidationStrategy; - -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; - -/** - * Unit test for PowerAuthSslPinningValidationStrategy. - * - * @author Tomas Kypta, tomas.kypta@wultra.com - */ -@RunWith(PowerMockRunner.class) -public class PowerAuthSslPinningValidationStrategyTest extends CommonJavaTest { - - @Test - public void testPowerAuthSslPinningValidationStrategyOnGithubSuccess() throws Exception { - String publicKey = "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE="; - byte[] publicKeyBytes = java.util.Base64.getDecoder().decode(publicKey); - - CertStoreConfiguration config = TestUtils.getCertStoreConfiguration( - new Date(), - new String[]{"github.com"}, - new URL("https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/34866234bbaa3350dc0ddc5680a65a6f4e7c549e/ssl-pinning-signatures.json"), - publicKeyBytes, - null); - CertStore store = new CertStore(config, cryptoProvider, secureDataStore); - TestUtils.assignHandler(store, handler); - TestUtils.updateAndCheck(store, UpdateMode.FORCED, UpdateResult.OK); - - PA2ClientValidationStrategy strategy = new PowerAuthSslPinningValidationStrategy(store); - - URL url = new URL("https://github.com"); - URLConnection urlConnection = url.openConnection(); - final HttpsURLConnection sslConnection = (HttpsURLConnection) urlConnection; - final SSLSocketFactory sslSocketFactory = strategy.getSSLSocketFactory(); - if (sslSocketFactory != null) { - sslConnection.setSSLSocketFactory(sslSocketFactory); - } - final HostnameVerifier hostnameVerifier = strategy.getHostnameVerifier(); - if (hostnameVerifier != null) { - sslConnection.setHostnameVerifier(hostnameVerifier); - } - - verify(cryptoProvider, times(0)).hashSha256(any(byte[].class)); - - sslConnection.connect(); - int response = sslConnection.getResponseCode(); - assertEquals(2, response / 100); - sslConnection.disconnect(); - - verify(cryptoProvider, times(1)).hashSha256(any(byte[].class)); - verify(secureDataStore).load(anyString()); - } - - @Test(expected = SSLHandshakeException.class) - public void testPowerAuthSslPinningValidationStrategyOnGithubFailure() throws Exception { - String publicKey = "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE="; - byte[] publicKeyBytes = java.util.Base64.getDecoder().decode(publicKey); - - CertStoreConfiguration config = TestUtils.getCertStoreConfiguration( - new Date(), - new String[]{"github.com"}, - new URL("https://test.wultra.com"), - publicKeyBytes, - null); - CertStore store = new CertStore(config, cryptoProvider, secureDataStore); - TestUtils.assignHandler(store, handler); - - PA2ClientValidationStrategy strategy = new PowerAuthSslPinningValidationStrategy(store); - - URL url = new URL("https://github.com"); - URLConnection urlConnection = url.openConnection(); - final HttpsURLConnection sslConnection = (HttpsURLConnection) urlConnection; - final SSLSocketFactory sslSocketFactory = strategy.getSSLSocketFactory(); - if (sslSocketFactory != null) { - sslConnection.setSSLSocketFactory(sslSocketFactory); - } - final HostnameVerifier hostnameVerifier = strategy.getHostnameVerifier(); - if (hostnameVerifier != null) { - sslConnection.setHostnameVerifier(hostnameVerifier); - } - - verify(cryptoProvider, times(0)).hashSha256(any(byte[].class)); - - sslConnection.connect(); - } -} diff --git a/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthSslPinningValidationStrategyTest.kt b/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthSslPinningValidationStrategyTest.kt new file mode 100644 index 0000000..4adeb41 --- /dev/null +++ b/library/src/test/java/com/wultra/android/sslpinning/integration/powerauth/PowerAuthSslPinningValidationStrategyTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2018 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 com.wultra.android.sslpinning.integration.powerauth + +import com.wultra.android.sslpinning.CertStore +import com.wultra.android.sslpinning.CommonKotlinTest +import com.wultra.android.sslpinning.TestUtils +import com.wultra.android.sslpinning.UpdateMode +import com.wultra.android.sslpinning.UpdateResult +import io.getlime.security.powerauth.networking.ssl.HttpClientValidationStrategy +import io.mockk.verify +import org.junit.Assert +import org.junit.Test +import java.net.URL +import java.util.Base64 +import java.util.Date +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLHandshakeException + +/** + * Unit test for PowerAuthSslPinningValidationStrategy. + * + * @author Tomas Kypta, tomas.kypta@wultra.com + */ +class PowerAuthSslPinningValidationStrategyTest : CommonKotlinTest() { + @Test + @Throws(Exception::class) + fun testPowerAuthSslPinningValidationStrategyOnGithubSuccess() { + val publicKey = + "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE=" + val publicKeyBytes = Base64.getDecoder().decode(publicKey) + val config = TestUtils.getCertStoreConfiguration( + Date(), arrayOf("github.com"), + URL("https://gist.githubusercontent.com/hvge/7c5a3f9ac50332a52aa974d90ea2408c/raw/07eb5b4b67e63d37d224912bc5951c7b589b35e6/ssl-pinning-signatures.json"), + publicKeyBytes, + null + ) + val store = CertStore(config, cryptoProvider, secureDataStore) + TestUtils.assignHandler(store, handler) + TestUtils.updateAndCheck(store, UpdateMode.FORCED, UpdateResult.OK) + val strategy: HttpClientValidationStrategy = PowerAuthSslPinningValidationStrategy(store) + val url = URL("https://github.com") + val urlConnection = url.openConnection() + val sslConnection = urlConnection as HttpsURLConnection + val sslSocketFactory = strategy.sslSocketFactory + if (sslSocketFactory != null) { + sslConnection.sslSocketFactory = sslSocketFactory + } + val hostnameVerifier = strategy.hostnameVerifier + if (hostnameVerifier != null) { + sslConnection.hostnameVerifier = hostnameVerifier + } + verify(exactly = 0) { cryptoProvider.hashSha256(any()) } + sslConnection.connect() + val response = sslConnection.responseCode + Assert.assertEquals(2, (response / 100).toLong()) + sslConnection.disconnect() + verify(exactly = 1) { cryptoProvider.hashSha256(any()) } + verify { secureDataStore.load(any()) } + } + + @Test(expected = SSLHandshakeException::class) + @Throws(Exception::class) + fun testPowerAuthSslPinningValidationStrategyOnGithubFailure() { + val publicKey = + "BC3kV9OIDnMuVoCdDR9nEA/JidJLTTDLuSA2TSZsGgODSshfbZg31MS90WC/HdbU/A5WL5GmyDkE/iks6INv+XE=" + val publicKeyBytes = Base64.getDecoder().decode(publicKey) + val config = TestUtils.getCertStoreConfiguration( + Date(), arrayOf("github.com"), + URL("https://test.wultra.com"), + publicKeyBytes, + null + ) + val store = CertStore(config, cryptoProvider, secureDataStore) + TestUtils.assignHandler(store, handler) + val strategy: HttpClientValidationStrategy = PowerAuthSslPinningValidationStrategy(store) + val url = URL("https://github.com") + val urlConnection = url.openConnection() + val sslConnection = urlConnection as HttpsURLConnection + val sslSocketFactory = strategy.sslSocketFactory + if (sslSocketFactory != null) { + sslConnection.sslSocketFactory = sslSocketFactory + } + val hostnameVerifier = strategy.hostnameVerifier + if (hostnameVerifier != null) { + sslConnection.hostnameVerifier = hostnameVerifier + } + verify(exactly = 0) { cryptoProvider.hashSha256(any()) } + sslConnection.connect() + } +} \ No newline at end of file diff --git a/library/src/test/java/com/wultra/android/sslpinning/model/CachedDataTest.kt b/library/src/test/java/com/wultra/android/sslpinning/model/CachedDataTest.kt new file mode 100644 index 0000000..63c9dd2 --- /dev/null +++ b/library/src/test/java/com/wultra/android/sslpinning/model/CachedDataTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2023 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions + * and limitations under the License. + */ + +package com.wultra.android.sslpinning.model + +import android.util.Base64 +import com.wultra.android.sslpinning.CertStore +import io.mockk.every +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import java.util.Date + +/** + * + */ +internal class CachedDataTest { + @Before + fun setUp() { + mockkStatic(Base64::class) + every { Base64.decode(any(), any()) } answers { + java.util.Base64.getDecoder().decode(it.invocation.args[0] as String) + } + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun testEntries() { + // the 1st item has different signature, otherwise the data are the same + val jsonData = listOf("""{"fingerprints": [ + { + "name" : "github.com", + "fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=", + "expires" : 1710460799, + "signature" : "MEQCIElYrNRc/RnIJTFM9Or90Op+5YfEc+OA0JCOzEdewx07AiAm/xAKMkhu9k9mXNFNyUSB/A1FbnqKEegpEpsugY5Z/Q==" + } + ]}""", """{"fingerprints": [ + { + "name" : "github.com", + "fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=", + "expires" : 1710460799, + "signature" : "MEUCIQDGSbss+QVvF5juP3y7/DkUPYIWopabdHrZETGqYMctLgIgX7aKQ8+22AIlmuWczXZKze4w20ycsKzaps4reobjikA=" + } + ]}""") + + val responses = jsonData.map { + CertStore.GSON.fromJson(it, GetFingerprintResponse::class.java) + } + responses.forEach { + Assert.assertEquals(1, it.fingerprints.size) + } + + val date = Date() + val cachedDatas = responses.map { + val certInfos = it.fingerprints.map { entry -> CertificateInfo(entry) }.toTypedArray() + Assert.assertEquals(1, certInfos.size) + CachedData(certInfos, date) + } + + Assert.assertEquals(2, cachedDatas.size) + Assert.assertEquals(cachedDatas[0], cachedDatas[1]) + } + +} \ No newline at end of file diff --git a/library/src/test/java/com/wultra/android/sslpinning/model/CertificateInfoTest.kt b/library/src/test/java/com/wultra/android/sslpinning/model/CertificateInfoTest.kt new file mode 100644 index 0000000..6f0c5b3 --- /dev/null +++ b/library/src/test/java/com/wultra/android/sslpinning/model/CertificateInfoTest.kt @@ -0,0 +1,88 @@ +/* + * Copyright 2023 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions + * and limitations under the License. + */ + +package com.wultra.android.sslpinning.model + +import android.util.Base64 +import com.wultra.android.sslpinning.CertStore +import io.mockk.every +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import junit.framework.TestCase.assertTrue +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test + + +/** + * + */ +internal class CertificateInfoTest { + + @Before + fun setUp() { + mockkStatic(Base64::class) + every { Base64.decode(any(), any()) } answers { + java.util.Base64.getDecoder().decode(it.invocation.args[0] as String) + } + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun testIndexOf() { + val certList = mutableListOf() + + // different signatures of the same fingerprints + // simulates updates with different data + val jsonData = listOf("""{"fingerprints": [ + { + "name" : "github.com", + "fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=", + "expires" : 1710460799, + "signature" : "MEQCIElYrNRc/RnIJTFM9Or90Op+5YfEc+OA0JCOzEdewx07AiAm/xAKMkhu9k9mXNFNyUSB/A1FbnqKEegpEpsugY5Z/Q==" + } + ]}""", """{"fingerprints": [ + { + "name" : "github.com", + "fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=", + "expires" : 1710460799, + "signature" : "MEUCIQDGSbss+QVvF5juP3y7/DkUPYIWopabdHrZETGqYMctLgIgX7aKQ8+22AIlmuWczXZKze4w20ycsKzaps4reobjikA=" + } + ]}""") + + // we add the first one + val response = CertStore.GSON.fromJson(jsonData[0], GetFingerprintResponse::class.java) + Assert.assertEquals(1, response.fingerprints.size) + for (entry in response.fingerprints) { + certList.add(CertificateInfo(entry)) + } + + for (json in jsonData) { + val resp = CertStore.GSON.fromJson(json, GetFingerprintResponse::class.java) + Assert.assertEquals(1, resp.fingerprints.size) + val ci = CertificateInfo(resp.fingerprints[0]) + // the data should pre already present + val idx = certList.indexOf(ci) + assertTrue(idx != -1) + assertTrue(certList.contains(ci)) + } + } +} \ No newline at end of file diff --git a/library/src/test/java/com/wultra/android/sslpinning/model/GetFingerprintResponseTest.kt b/library/src/test/java/com/wultra/android/sslpinning/model/GetFingerprintResponseTest.kt new file mode 100644 index 0000000..d5bc910 --- /dev/null +++ b/library/src/test/java/com/wultra/android/sslpinning/model/GetFingerprintResponseTest.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2023 Wultra s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions + * and limitations under the License. + */ + +package com.wultra.android.sslpinning.model + +import android.util.Base64 +import com.wultra.android.sslpinning.CertStore +import io.mockk.every +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import org.junit.After +import org.junit.Assert +import org.junit.Before +import org.junit.Test + + +/** + * + */ +internal class GetFingerprintResponseTest { + @Before + fun setUp() { + mockkStatic(Base64::class) + every { Base64.decode(any(), any()) } answers { + java.util.Base64.getDecoder().decode(it.invocation.args[0] as String) + } + } + + @After + fun tearDown() { + unmockkAll() + } + + @Test + fun testEntries() { + // the 1st item has different signature, otherwise the data are the same + val jsonData = """{"fingerprints": [ + { + "name" : "github.com", + "fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=", + "expires" : 1710460799, + "signature" : "MEQCIElYrNRc/RnIJTFM9Or90Op+5YfEc+OA0JCOzEdewx07AiAm/xAKMkhu9k9mXNFNyUSB/A1FbnqKEegpEpsugY5Z/Q==" + },{ + "name" : "github.com", + "fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=", + "expires" : 1710460799, + "signature" : "MEUCIQDGSbss+QVvF5juP3y7/DkUPYIWopabdHrZETGqYMctLgIgX7aKQ8+22AIlmuWczXZKze4w20ycsKzaps4reobjikA=" + },{ + "name" : "github.com", + "fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=", + "expires" : 1710460799, + "signature" : "MEUCIQDGSbss+QVvF5juP3y7/DkUPYIWopabdHrZETGqYMctLgIgX7aKQ8+22AIlmuWczXZKze4w20ycsKzaps4reobjikA=" + } + ]}""" + + // we should parse 3 items from it + val response = CertStore.GSON.fromJson(jsonData, GetFingerprintResponse::class.java) + Assert.assertEquals(3, response.fingerprints.size) + + // in set there should be just 2 entries + val set = response.fingerprints.toSet() + Assert.assertEquals(2, set.size) + + // but the CertificateInfo is just 1 + val certSet = mutableSetOf() + for (entry in response.fingerprints) { + certSet.add(CertificateInfo(entry)) + } + // now there should be just 1 item - one fingerprint + Assert.assertEquals(1, certSet.size) + } + + @Test + fun testParse() { + // the 1st item has different signature, otherwise the data are the same + val jsonData = """{"fingerprints": [ + { + "name" : "github.com", + "fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=", + "expires" : 1710460799, + "signature" : "MEQCIElYrNRc/RnIJTFM9Or90Op+5YfEc+OA0JCOzEdewx07AiAm/xAKMkhu9k9mXNFNyUSB/A1FbnqKEegpEpsugY5Z/Q==" + },{ + "name" : "github.com", + "fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=", + "expires" : 1710460799, + "signature" : "MEUCIQDGSbss+QVvF5juP3y7/DkUPYIWopabdHrZETGqYMctLgIgX7aKQ8+22AIlmuWczXZKze4w20ycsKzaps4reobjikA=" + },{ + "name" : "github.com", + "fingerprint" : "kqN/vV4hpTqVxxbhFE9EL1grlND6/Gc+tnF6TrUaiKc=", + "expires" : 1710460799, + "signature" : "MEUCIQDGSbss+QVvF5juP3y7/DkUPYIWopabdHrZETGqYMctLgIgX7aKQ8+22AIlmuWczXZKze4w20ycsKzaps4reobjikA=" + } + ]}""" + + val response1 = CertStore.GSON.fromJson(jsonData, GetFingerprintResponse::class.java) + val response2 = CertStore.GSON.fromJson(jsonData, GetFingerprintResponse::class.java) + Assert.assertEquals(response1, response2) + } +} \ No newline at end of file diff --git a/scripts/android-publish-build.sh b/scripts/android-publish-build.sh index d89b0f1..d107f21 100755 --- a/scripts/android-publish-build.sh +++ b/scripts/android-publish-build.sh @@ -5,7 +5,8 @@ TOP=$(dirname $0) source "${TOP}/common-functions.sh" source "${TOP}/deploy.cfg.sh" -SRC_ROOT="`( cd \"$TOP/..\" && pwd )`" + +[[ -z "$SRC_ROOT" ]] && FAILURE "Missing SRC_ROOT in deploy.cfg.sh" # ----------------------------------------------------------------------------- # USAGE prints help and exits the script with error code from provided parameter @@ -64,44 +65,40 @@ function MAKE_VER VER_SUFFIX="-$VER_SUFFIX" fi local NEW_VER=${VER}${VER_SUFFIX} - local CUR_VER=$(LOAD_CURRENT_VERSION) - local PROP_PATH="$SRC_ROOT/${DEPLOY_VERSION_FILE}" VALIDATE_AND_SET_VERSION_STRING "$VER" - [[ "$CUR_VER" == "-1" ]] && FAILURE "Failed to load version from gradle.properties file." + LOG "Changing version to:" + + for PROP_FILE in "${DEPLOY_VERSION_FILES[@]}" + do + local PROP_PATH="$SRC_ROOT/${PROP_FILE}" + + local VERSION=$(GET_PROPERTY "$PROP_PATH" "VERSION_NAME") + local GROUP_ID=$(GET_PROPERTY "$PROP_PATH" "GROUP_ID") + local ARTIFACT_ID=$(GET_PROPERTY "$PROP_PATH" "ARTIFACT_ID") + + [[ -z "$VERSION" ]] && FAILURE "Failed to load VERSION_NAME from gradle.properties file." + [[ -z "$GROUP_ID" ]] && FAILURE "Failed to load GROUP_ID from gradle.properties file." + [[ -z "$ARTIFACT_ID" ]] && FAILURE "Failed to load ARTIFACT_ID from gradle.properties file." + + local CUR_VER=$VERSION - PUSH_DIR "${SRC_ROOT}" - #### - sed -e "s/$CUR_VER/$NEW_VER/g" "${PROP_PATH}" > "${PROP_PATH}.new" - $MV "${PROP_PATH}.new" "${PROP_PATH}" - git add ${DEPLOY_VERSION_FILE} - #### - POP_DIR + PUSH_DIR "${SRC_ROOT}" + #### + sed -e "s/$CUR_VER/$NEW_VER/g" "${PROP_PATH}" > "${PROP_PATH}.new" + $MV "${PROP_PATH}.new" "${PROP_PATH}" + git add ${PROP_FILE} + #### + POP_DIR - LOG_LINE - LOG "Version changed to:" - PRINT_CURRENT_VERSION $DO_REPO - LOG_LINE + LOG_LINE + LOG " - Version : ${NEW_VER}" + LOG " - Dependency : ${GROUP_ID}:${ARTIFACT_ID}:${NEW_VER}" + LOG_LINE + done } - -# ----------------------------------------------------------------------------- -# LOAD_CURRENT_VERSION loads version from gradle.properties file and prints -# it to stdout. -# ----------------------------------------------------------------------------- -function LOAD_CURRENT_VERSION -{ - local PROP_PATH="$SRC_ROOT/${DEPLOY_VERSION_FILE}" - local V="-1" - if [ -f "$PROP_PATH" ]; then - source "$PROP_PATH" - if [ ! -z "${VERSION_NAME}" ]; then - V="${VERSION_NAME}" - fi - fi - echo $V -} # ----------------------------------------------------------------------------- # PRINT_CURRENT_VERSION loads and prints rich version info from gradle.properties # file. Parameters: @@ -110,15 +107,21 @@ function LOAD_CURRENT_VERSION function PRINT_CURRENT_VERSION { local REPO=$1 - local VER=$(LOAD_CURRENT_VERSION) - local PROP_PATH="$SRC_ROOT/${DEPLOY_VERSION_FILE}" - - [[ "$VER" == "-1" ]] && FAILURE "Failed to load version from gradle.properties file." + for PROP_FILE in "${DEPLOY_VERSION_FILES[@]}" + do + local PROP_PATH="$SRC_ROOT/${PROP_FILE}" - source "${PROP_PATH}" + local VERSION=$(GET_PROPERTY "$PROP_PATH" "VERSION_NAME") + local GROUP_ID=$(GET_PROPERTY "$PROP_PATH" "GROUP_ID") + local ARTIFACT_ID=$(GET_PROPERTY "$PROP_PATH" "ARTIFACT_ID") + + [[ -z "$VERSION" ]] && FAILURE "Failed to load VERSION_NAME from gradle.properties file." + [[ -z "$GROUP_ID" ]] && FAILURE "Failed to load GROUP_ID from gradle.properties file." + [[ -z "$ARTIFACT_ID" ]] && FAILURE "Failed to load ARTIFACT_ID from gradle.properties file." - LOG " - Version : ${VER}" - LOG " - Dependency : ${GROUP_ID}:${ARTIFACT_ID}:${VER}" + LOG " - Version : ${VERSION}" + LOG " - Dependency : ${GROUP_ID}:${ARTIFACT_ID}:${VERSION}" + done } ############################################################################### @@ -129,6 +132,7 @@ DO_PUBLISH='' DO_SIGN=$DEPLOY_ALLOW_SIGN DO_REPO='' GRADLE_PARAMS='' +GRADLE_ROOT=${GRADLE_ROOT:-$SRC_ROOT} REQUIRE_COMMAND git @@ -204,7 +208,7 @@ fi LOG_LINE -PUSH_DIR "${SRC_ROOT}" +PUSH_DIR "${GRADLE_ROOT}" #### GRADLE_CMD_LINE="$GRADLE_PARAMS $DO_CLEAN assembleRelease $DO_PUBLISH" DEBUG_LOG "Gradle command line >> ./gradlew $GRADLE_CMD_LINE" diff --git a/scripts/common-functions.sh b/scripts/common-functions.sh index 39843a2..6d5370c 100644 --- a/scripts/common-functions.sh +++ b/scripts/common-functions.sh @@ -25,40 +25,40 @@ LAST_LOG_IS_LINE=0 # ----------------------------------------------------------------------------- function __COMMON_FUNCTIONS_SELF_UPDATE { - local self=$0 - local backup=$self.backup - local remote="https://raw.githubusercontent.com/wultra/library-deploy/master/common-functions.sh" - LOG_LINE - LOG "This script is going to update itself:" - LOG " source : $remote" - LOG " dest : $self" - LOG_LINE - PROMPT_YES_FOR_CONTINUE - cp $self $backup - wget $remote -O $self - LOG_LINE - LOG "Update looks good. Now you can:" - LOG " - press CTRL+C to cancel next step" - LOG " - or type 'y' to remove backup file" - LOG_LINE - PROMPT_YES_FOR_CONTINUE "Would you like to remove backup file?" - rm $backup + local self=$0 + local backup=$self.backup + local remote="https://raw.githubusercontent.com/wultra/library-deploy/master/common-functions.sh" + LOG_LINE + LOG "This script is going to update itself:" + LOG " source : $remote" + LOG " dest : $self" + LOG_LINE + PROMPT_YES_FOR_CONTINUE + cp $self $backup + wget $remote -O $self + LOG_LINE + LOG "Update looks good. Now you can:" + LOG " - press CTRL+C to cancel next step" + LOG " - or type 'y' to remove backup file" + LOG_LINE + PROMPT_YES_FOR_CONTINUE "Would you like to remove backup file?" + rm $backup } # ----------------------------------------------------------------------------- # FAILURE prints error to stderr and exits the script with error code 1 # ----------------------------------------------------------------------------- function FAILURE { - echo "$CMD: Error: $@" 1>&2 - exit 1 + echo "$CMD: Error: $@" 1>&2 + exit 1 } # ----------------------------------------------------------------------------- # WARNING prints warning to stderr # ----------------------------------------------------------------------------- function WARNING { - echo "$CMD: Warning: $@" 1>&2 - LAST_LOG_IS_LINE=0 + echo "$CMD: Warning: $@" 1>&2 + LAST_LOG_IS_LINE=0 } # ----------------------------------------------------------------------------- # LOG @@ -74,24 +74,24 @@ function WARNING # ----------------------------------------------------------------------------- function LOG { - if [ $VERBOSE -gt 0 ]; then - echo "$CMD: $@" - LAST_LOG_IS_LINE=0 - fi + if [ $VERBOSE -gt 0 ]; then + echo "$CMD: $@" + LAST_LOG_IS_LINE=0 + fi } function LOG_LINE { - if [ $LAST_LOG_IS_LINE -eq 0 ] && [ $VERBOSE -gt 0 ]; then - echo "$CMD: -----------------------------------------------------------------------------" - LAST_LOG_IS_LINE=1 - fi + if [ $LAST_LOG_IS_LINE -eq 0 ] && [ $VERBOSE -gt 0 ]; then + echo "$CMD: -----------------------------------------------------------------------------" + LAST_LOG_IS_LINE=1 + fi } function DEBUG_LOG { - if [ $VERBOSE -gt 1 ]; then - echo "$CMD: $@" - LAST_LOG_IS_LINE=0 - fi + if [ $VERBOSE -gt 1 ]; then + echo "$CMD: $@" + LAST_LOG_IS_LINE=0 + fi } function EXIT_SUCCESS { @@ -108,21 +108,21 @@ function EXIT_SUCCESS # ----------------------------------------------------------------------------- function PROMPT_YES_FOR_CONTINUE { - local prompt="$@" - local answer - if [ -z "$prompt" ]; then - prompt="Would you like to continue?" - fi - read -p "$prompt (type y or yes): " answer - case "$answer" in - y | yes | Yes | YES) - LAST_LOG_IS_LINE=0 - return - ;; - *) - FAILURE "Aborted by user." - ;; - esac + local prompt="$@" + local answer + if [ -z "$prompt" ]; then + prompt="Would you like to continue?" + fi + read -p "$prompt (type y or yes): " answer + case "$answer" in + y | yes | Yes | YES) + LAST_LOG_IS_LINE=0 + return + ;; + *) + FAILURE "Aborted by user." + ;; + esac } # ----------------------------------------------------------------------------- # REQUIRE_COMMAND uses "which" buildin command to test existence of requested @@ -133,14 +133,14 @@ function PROMPT_YES_FOR_CONTINUE # ----------------------------------------------------------------------------- function REQUIRE_COMMAND { - set +e - local tool=$1 - local path=`which $tool` - if [ -z $path ]; then - FAILURE "$tool: required command not found." - fi - set -e - DEBUG_LOG "$tool: found at $path" + set +e + local tool=$1 + local path=`which $tool` + if [ -z $path ]; then + FAILURE "$tool: required command not found." + fi + set -e + DEBUG_LOG "$tool: found at $path" } # ----------------------------------------------------------------------------- # REQUIRE_COMMAND_PATH is similar to REQUIRE_COMMAND, but on success, prints @@ -152,14 +152,14 @@ function REQUIRE_COMMAND # ----------------------------------------------------------------------------- function REQUIRE_COMMAND_PATH { - set +e - local tool=$1 - local path=`which $tool` - if [ -z $path ]; then - FAILURE "$tool: required command not found." - fi - set -e - echo $path + set +e + local tool=$1 + local path=`which $tool` + if [ -z $path ]; then + FAILURE "$tool: required command not found." + fi + set -e + echo $path } # ----------------------------------------------------------------------------- # Validates "verbose" command line switch and adjusts VERBOSE global variable @@ -167,13 +167,13 @@ function REQUIRE_COMMAND_PATH # ----------------------------------------------------------------------------- function SET_VERBOSE_LEVEL_FROM_SWITCH { - case "$1" in - -v0) VERBOSE=0 ;; - -v1) VERBOSE=1 ;; - -v2) VERBOSE=2 ;; - *) FAILURE "Invalid verbose level $1" ;; - esac - UPDATE_VERBOSE_COMMANDS + case "$1" in + -v0) VERBOSE=0 ;; + -v1) VERBOSE=1 ;; + -v2) VERBOSE=2 ;; + *) FAILURE "Invalid verbose level $1" ;; + esac + UPDATE_VERBOSE_COMMANDS } # ----------------------------------------------------------------------------- # Updates verbose switches for common commands. Function will create following @@ -185,19 +185,19 @@ function SET_VERBOSE_LEVEL_FROM_SWITCH # ----------------------------------------------------------------------------- function UPDATE_VERBOSE_COMMANDS { - if [ $VERBOSE -lt 2 ]; then - # No verbose - CP="cp" - RM="rm -f" - MD="mkdir -p" - MV="mv" - else - # verbose - CP="cp -v" - RM="rm -f -v" - MD="mkdir -p -v" - MV="mv -v" - fi + if [ $VERBOSE -lt 2 ]; then + # No verbose + CP="cp" + RM="rm -f" + MD="mkdir -p" + MV="mv" + else + # verbose + CP="cp -v" + RM="rm -f -v" + MD="mkdir -p -v" + MV="mv -v" + fi } # ----------------------------------------------------------------------------- # Validate if $1 as VERSION has valid format: x.y.z @@ -205,19 +205,19 @@ function UPDATE_VERBOSE_COMMANDS # ----------------------------------------------------------------------------- function VALIDATE_AND_SET_VERSION_STRING { - if [ -z "$1" ]; then - FAILURE "Version string is empty" - fi - local rx='^([0-9]+\.){2}(\*|[0-9]+)$' - if [[ ! "$1" =~ $rx ]]; then - FAILURE "Version string is invalid: '$1'" - fi - if [ -z "$VERSION" ]; then - VERSION=$1 - DEBUG_LOG "Changing version to $VERSION" - else - FAILURE "Version string is already set to $VERSION" - fi + if [ -z "$1" ]; then + FAILURE "Version string is empty" + fi + local rx='^([0-9]+\.){2}(\*|[0-9]+)$' + if [[ ! "$1" =~ $rx ]]; then + FAILURE "Version string is invalid: '$1'" + fi + if [ -z "$VERSION" ]; then + VERSION=$1 + DEBUG_LOG "Changing version to $VERSION" + else + FAILURE "Version string is already set to $VERSION" + fi } # ----------------------------------------------------------------------------- # Loads shared credentials, like API keys & logins. The function performs @@ -229,20 +229,20 @@ function VALIDATE_AND_SET_VERSION_STRING # ----------------------------------------------------------------------------- function LOAD_API_CREDENTIALS { - if [ x${API_CREDENTIALS} == x1 ]; then - DEBUG_LOG "Credentials are already set." - elif [ ! -z "${API_CREDENTIALS_FILE}" ]; then - source "${API_CREDENTIALS_FILE}" - elif [ -f "${HOME}/.lime/credentials" ]; then - source "${HOME}/.lime/credentials" - elif [ -f ".lime-credentials" ]; then - source ".lime-credentials" - else - FAILURE "Unable to locate credentials file." - fi - if [ x${LIME_CREDENTIALS} != x1 ]; then - FAILURE "Credentials file must set LIME_CREDENTIALS variable to 1" - fi + if [ x${API_CREDENTIALS} == x1 ]; then + DEBUG_LOG "Credentials are already set." + elif [ ! -z "${API_CREDENTIALS_FILE}" ]; then + source "${API_CREDENTIALS_FILE}" + elif [ -f "${HOME}/.lime/credentials" ]; then + source "${HOME}/.lime/credentials" + elif [ -f ".lime-credentials" ]; then + source ".lime-credentials" + else + FAILURE "Unable to locate credentials file." + fi + if [ x${LIME_CREDENTIALS} != x1 ]; then + FAILURE "Credentials file must set LIME_CREDENTIALS variable to 1" + fi } # ----------------------------------------------------------------------------- @@ -251,19 +251,19 @@ function LOAD_API_CREDENTIALS # ----------------------------------------------------------------------------- function PUSH_DIR { - if [ $VERBOSE -gt 1 ]; then - pushd "$1" - else - pushd "$1" > /dev/null - fi + if [ $VERBOSE -gt 1 ]; then + pushd "$1" + else + pushd "$1" > /dev/null + fi } function POP_DIR { - if [ $VERBOSE -gt 1 ]; then - popd - else - popd > /dev/null - fi + if [ $VERBOSE -gt 1 ]; then + popd + else + popd > /dev/null + fi } # ----------------------------------------------------------------------------- @@ -275,18 +275,18 @@ function POP_DIR # ----------------------------------------------------------------------------- function SHA256 { - local HASH=( `shasum -a 256 "$1"` ) - echo ${HASH[0]} + local HASH=( `shasum -a 256 "$1"` ) + echo ${HASH[0]} } function SHA384 { - local HASH=( `shasum -a 384 "$1"` ) - echo ${HASH[0]} + local HASH=( `shasum -a 384 "$1"` ) + echo ${HASH[0]} } function SHA512 { - local HASH=( `shasum -a 512 "$1"` ) - echo ${HASH[0]} + local HASH=( `shasum -a 512 "$1"` ) + echo ${HASH[0]} } # ----------------------------------------------------------------------------- @@ -315,6 +315,21 @@ function GET_XCODE_VERSION esac } +# ----------------------------------------------------------------------------- +# Prints value of property from Java property file into stdout. +# The format of file is: +# KEY1=VALUE1 +# KEY2=VALUE2 +# +# Parameters: +# $1 - property file +# $2 - property key to print +# ----------------------------------------------------------------------------- +function GET_PROPERTY +{ + grep "^$2=" "$1" | cut -d'=' -f2 +} + ############################################################################### # Global scope # Gets full path to current directory and exits with error when @@ -332,5 +347,5 @@ if [ -z "$TOP" ]; then fi if [ "$CMD" == "common-functions.sh" ] && [ "$1" == "selfupdate" ]; then - __COMMON_FUNCTIONS_SELF_UPDATE + __COMMON_FUNCTIONS_SELF_UPDATE fi diff --git a/scripts/deploy.cfg.sh b/scripts/deploy.cfg.sh index 3aac488..b1c8e0d 100644 --- a/scripts/deploy.cfg.sh +++ b/scripts/deploy.cfg.sh @@ -1,5 +1,5 @@ # gradle.properties file where library version is stored -DEPLOY_VERSION_FILE="library/gradle.properties" +DEPLOY_VERSION_FILES=("library/gradle.properties") # Name of remote repository. Variable is used in communication with user. DEPLOY_REMOTE_NAME="Maven Central" # Gradle task to publish library to remote repository @@ -7,6 +7,9 @@ DEPLOY_REMOTE_TASK="publishReleasePublicationToSonatypeRepository" # Set 0 / 1 whether signing is allowed DEPLOY_ALLOW_SIGN=1 +# Sources root +SRC_ROOT="`( cd \"$TOP/..\" && pwd )`" + # ----------------------------------------------------------------------------- # Function that adjusts GRADLE_PARAMS global variable with parameters required # for proper library deployment to remote repository. diff --git a/scripts/release.sh b/scripts/release.sh index 518d087..87d48ed 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -5,7 +5,8 @@ TOP=$(dirname $0) source "${TOP}/common-functions.sh" source "${TOP}/deploy.cfg.sh" -SRC_ROOT="`( cd \"$TOP/..\" && pwd )`" + +[[ -z "$SRC_ROOT" ]] && FAILURE "Missing SRC_ROOT in deploy.cfg.sh" # ----------------------------------------------------------------------------- # USAGE prints help and exits the script with error code from provided parameter diff --git a/settings.gradle b/settings.gradle.kts similarity index 70% rename from settings.gradle rename to settings.gradle.kts index 2357313..068409d 100644 --- a/settings.gradle +++ b/settings.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2018 Wultra s.r.o. + * Copyright 2023 Wultra s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,5 +13,12 @@ * See the License for the specific language governing permissions * and limitations under the License. */ - -include ':library' +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + mavenCentral() + mavenLocal() + google() + } +} +include(":library")