diff --git a/.github/actions/basic-preflight-check/action.yml b/.github/actions/basic-preflight-check/action.yml
new file mode 100644
index 0000000..847df54
--- /dev/null
+++ b/.github/actions/basic-preflight-check/action.yml
@@ -0,0 +1,31 @@
+# This action's steps must be synced with preMergeRequestCheck Gradle task's checks to ensure that
+# we check required stuff on CI and at the same time developers can run the Gradle task to verify their
+# changes before making a PR. This action can be used in more workflows than just PR, since these
+# basic pre merge request checks (Detekt, library tests, ...) are fundamental to be checked in
+# basically all situations like merging, pushing to dev, etc.
+name: Basic preflight check
+description: Action that contains basic checks like running Detekt or library tests that are common to multiple workflows
+
+runs:
+ using: "composite"
+ steps:
+ - name: Detekt
+ shell: bash
+ run: ./gradlew detekt
+
+ - name: Assemble release variant
+ shell: bash
+ # Exclude sample app module from build. It requires the library artifacts to be published.
+ run: ./gradlew assembleRelease -x :app:assembleRelease
+
+ - name: Library tests
+ shell: bash
+ run: ./gradlew testDebugUnitTest -x :app:testDebugUnitTest
+
+ - name: Binary compatibility check
+ shell: bash
+ run: ./gradlew apiCheck
+
+ - name: Build logic tests
+ shell: bash
+ run: ./gradlew build-logic:logic:test
diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
new file mode 100644
index 0000000..76dd43f
--- /dev/null
+++ b/.github/actions/setup/action.yml
@@ -0,0 +1,11 @@
+name: Setup
+description: Action that performs common setup tasks like setting up Java
+
+runs:
+ using: "composite"
+ steps:
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: '17'
+ distribution: 'corretto'
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 0000000..99559a3
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,62 @@
+name: Deploy
+
+on:
+ push:
+ tags:
+ # Gradle build logic relies on this tag format, so if you need to change it, you need to change
+ # it there as well.
+ - bom-*
+
+env:
+ GPG_KEY: ${{ secrets.ANDROID_GPG_KEY }}
+ GPG_PASSWORD: ${{ secrets.ANDROID_SIGNING_PASSWORD }}
+ MAVEN_USERNAME: ${{ secrets.ANDROID_MAVEN_CENTRAL_USERNAME }}
+ MAVEN_PASSWORD: ${{ secrets.ANDROID_MAVEN_CENTRAL_PASSWORD }}
+
+jobs:
+ # This job's steps must be synced with prePublishCheck Gradle task's checks to ensure that
+ # we check required stuff on CI and at the same time developers can run the Gradle task to verify
+ # their changes before publishing.
+ preflight_check:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+
+ - uses: ./.github/actions/setup
+ - uses: ./.github/actions/basic-preflight-check
+
+ - name: Verify publishing
+ run: ./gradlew verifyPublishing
+
+ - name: Verify BOM version
+ run: ./gradlew verifyBomVersion
+
+ - name: Artifacts tests
+ # We need to publish the latest versions to Maven local first before we can run tests
+ # on published artifacts
+ run: |
+ ./gradlew publishToMavenLocal \
+ -PsigningInMemoryKey=$GPG_KEY \
+ -PsigningInMemoryKeyPassword=$GPG_PASSWORD \
+ -PmavenCentralUsername=$MAVEN_USERNAME \
+ -PmavenCentralPassword=$MAVEN_PASSWORD
+
+ ./gradlew :app:testDebugUnitTest
+
+ publish:
+ needs: preflight_check
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+
+ - uses: ./.github/actions/setup
+
+ - name: Publish to Maven Central
+ run: |
+ ./gradlew --stacktrace publishAndReleaseToMavenCentral \
+ -PsigningInMemoryKey=$GPG_KEY \
+ -PsigningInMemoryKeyPassword=$GPG_PASSWORD \
+ -PmavenCentralUsername=$MAVEN_USERNAME \
+ -PmavenCentralPassword=$MAVEN_PASSWORD
diff --git a/.github/workflows/main_branch.yml b/.github/workflows/main_branch.yml
new file mode 100644
index 0000000..eb1c135
--- /dev/null
+++ b/.github/workflows/main_branch.yml
@@ -0,0 +1,16 @@
+name: Main branch
+
+on:
+ push:
+ branches:
+ - main
+
+jobs:
+ main:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+
+ - uses: ./.github/actions/setup
+ - uses: ./.github/actions/basic-preflight-check
diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml
new file mode 100644
index 0000000..a3e2cb4
--- /dev/null
+++ b/.github/workflows/pull_request.yml
@@ -0,0 +1,17 @@
+name: Pull request
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+
+jobs:
+ pull_request:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+
+ - uses: ./.github/actions/setup
+ - uses: ./.github/actions/basic-preflight-check
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 7edb8b7..b589d56 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -1,12 +1,6 @@
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 2cdc89a..824785d 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -1,5 +1,5 @@
-
+
\ No newline at end of file
diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml
deleted file mode 100644
index 931b96c..0000000
--- a/.idea/runConfigurations.xml
+++ /dev/null
@@ -1,13 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..9e929a6
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,30 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
+to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+## [Unreleased]
+### core
+### datastore
+### datastore-preferences
+### jetpack
+
+## BOM [1.0.0] - TBD
+
+### core
+#### Added
+- First version of the artifact 🎉
+
+### datastore
+#### Added
+- First version of the artifact 🎉
+
+### datastore-preferences
+#### Added
+- First version of the artifact 🎉
+
+### jetpack
+#### Added
+- First version of the artifact 🎉
diff --git a/NOTICE.txt b/NOTICE.txt
new file mode 100644
index 0000000..834a174
--- /dev/null
+++ b/NOTICE.txt
@@ -0,0 +1,2 @@
+This project includes code derived from the Jetpack Security Crypto library,
+developed by Google LLC, and licensed under the Apache License 2.0.
diff --git a/README.md b/README.md
index 904eaa5..e61f378 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,287 @@
-# ackee-security
+# Ackee Security
+
+[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
+[![Maven Central](https://img.shields.io/maven-central/v/io.github.ackeecz/security-bom)](https://central.sonatype.com/artifact/io.github.ackeecz/security-bom)
+
+## Overview
+
+Ackee Security is a library focusing on a security-related logic. In [Ackee](https://www.ackee.cz/)
+we mainly use it to share some common security-related implementations across our projects, but it
+contains useful logic suited for anyone's needs. More specifically, you can use it as a 100%
+compatible replacement for [Jetpack Security Crypto](https://developer.android.com/reference/kotlin/androidx/security/crypto/package-summary)
+library and there is more!
+
+## Architecture
+
+Library consists of several modules:
+- `core` contains some basic core logic like `MasterKey` class that is being used by other modules of the library
+- `datastore` provides encrypted `DataStore` implementation
+- `datastore-preferences` provides encrypted `PreferenceDataStore` implementation
+- `jetpack` is a rewritten and improved Jetpack Security library
+
+### Core
+
+Contains basic core security-related logic like `MasterKey` class (rewritten from Jetpack Security)
+that is used by other modules to encrypt the data. You don't have to depend on this module directly,
+if you use `datastore` modules or `jetpack`.
+
+### DataStore
+
+DataStore modules provide an encrypted version of `DataStore`s. They use [Tink](https://github.com/tink-crypto/tink-java)
+library for cryptographic algorithms under the hood the same as original Jetpack Security and
+`jetpack` module as well.
+
+### Jetpack
+
+This was the main reason why we decided to create this library. We have been using Jetpack Security
+library on several projects, but it had some issues. First, it was
+[silently deprecated](https://developer.android.com/privacy-and-security/cryptography#jetpack_security_crypto_library)
+without providing any alternative. The latest stable version was released in 2021, which is pretty old,
+especially considering that it is a library focused on security. There were several issues, some of them
+fixed in alpha releases, but they never made it to stable. Since we have been already using Jetpack
+Security, needed some alternative and otherwise liked the abstractions it provided, we decided to
+completely rewrite it and fix the known issues it had.
+
+`jetpack` module contains `EncryptedSharedPreferences` and `EncryptedFile` implementations. `MasterKey`
+is ported as well, but is part of `core` module, because it is reused by other modules as well. However,
+if you need original Jetpack Security functionality, it is sufficient to depend on `jetpack` only and
+`core` is included automatically.
+
+#### Compatibility with Jetpack Security
+
+`jetpack` is 100% compatible in terms of data compatibility with Jetpack Security. It means that if
+you already use Jetpack Security on your project and want to switch to `jetpack`, you can just replace
+the library, make necessary adjustments to source code and run the app. The already created encrypted data
+will work fine with the `jetpack` implementation.
+
+Regarding source code compatibility, we had to make some big necessary breaking changes to improve the
+implementation and we also did some smaller not necessary breaking changes, which are easy to adapt to, but
+we believe they improve the API. We tried to keep the API as consistent with Jetpack Security as possible
+and only broke it when it provided some benefits.
+
+The smaller changes mostly involve `MasteKey` class changes. When you use this class to get a master key,
+it actually returns its instance that needs to be passed to encrypted implementations. This provides
+a more type-safe API compared to a general `String` representation.
+
+The biggest breaking changes involve `EncryptedSharedPreferences`. The original Jetpack Security's
+implementation returned the instance of `SharedPreferences`, which was beneficial, because you could
+have used this on all places where you needed a regular `SharedPreferences` types. However, there were
+also some problems. Those problems might not be noticeable for a few key-value pairs stored to preferences,
+but becomes visible for a lot of key-value pairs or data of a bigger size. All crypto operations of
+original `EncryptedSharedPreferences` (and `EncryptedFile` as well) are executed on the caller's thread,
+possibly blocking it for more intensive operations. This is especially problematic for methods, where
+you do not expect this even for the regular `SharedPreferences` like `apply`, which is actually one of
+those most problematic methods. Since we wanted to improve this and use coroutines for that, we had
+to break this completely and we introduced a new `EncryptedSharedPreferences` interface that is basically
+a 1:1 copy of the `SharedPreferences` interface, but have all relevant methods `suspend` to not block
+caller's thread. We understand, that this big breaking change might be problematic for apps relying
+heavily on `SharedPreferences` (e.g. passing it to a third-party library), so there is also an extension
+`EncryptedSharedPreferences.adaptToSharedPreferences`, which adapts `EncryptedSharedPreferences` to
+`SharedPreferences`. However, you should not use this, unless really necessary, and you should migrate
+to `EncryptedSharedPreferences` to get all benefits it offers, as the adapter just blocks while waiting
+for the internal `EncryptedSharedPreferences` suspend functions to complete.
+
+#### Improvements over Jetpack Security
+
+During rewrite of Jetpack Security library we made following improvements:
+- Rewritten from Java to 100% Kotlin.
+- All logic is covered by tests. We followed a careful process of refactoring, when we first covered
+all the existing functionality by tests and then started to rewrite the implementations, which gave
+us a confidence to not break anything.
+- Improve some APIs like `MasterKey`, which is now more type-safe and also offers `KeyGenParameterSpec.Builder`
+configured with the same default values as the original implementation, but you can take this and apply
+additional custom configurations before building the final spec and getting a key.
+- Improve performance of the `EncryptedSharedPreferences` for various methods like getting all key-value
+pairs that made unnecessary extra encryptions/decryptions under the hood.
+- Remove all blocking calls and making heavy methods suspend instead.
+- Fix synchronization issues during master key creation and increase Tink library version from the old one,
+used in Jetpack Security, that also had some synchronization issues, that were fixed in later releases.
+- Since one of the major issues of Jetpack Security was an outdated Tink library, which makes all the
+crypto operations and Jetpack Security was basically just a thin abstraction over it, we wanted to
+try to prevent the same issues in the future and so we decided to force clients of Ackee Security library
+to depend on Tink explicitly. This allows clients to have a better control over updates, independent
+of Ackee Security updates.
+- Fix several bugs discovered in `EncryptedSharedPreferences` during covering the logic by tests:
+ - If you saved empty string `Set`, you didn't get it back by using `getStringSet`, but you got
+ default value passed in parameter instead.
+ - Storing `Set` with null threw NPE.
+ - `get*` methods didn't throw `ClassCastException` as specified in `SharedPreferences` contracts,
+ when you tried to access some key using an incorrect get method.
+ - Contract of `SharedPreferences.registerOnSharedPreferenceChangeListener` specifies that it does not
+ store strong references on the listener objects, but it actually incorrectly did.
+ - Contract of `OnSharedPreferenceChangeListener.onSharedPreferenceChanged` specifies, that it has to be
+ invoked from the main thread, but this was not ensured.
+ - `OnSharedPreferenceChangeListener.onSharedPreferenceChanged` was being called multiple times per
+ one key in one editor, if the editor did multiple changes on the same key.
+ - `OnSharedPreferenceChangeListener.onSharedPreferenceChanged` was being called even when the key
+ was added and then removed in the same editor.
+
+## Setup
+
+Add the following dependencies to your `libs.versions.toml`, depending on what you need. You should
+always use BOM to be sure to get binary compatible dependencies. If you need only `jetpack` features,
+just declare BOM and `io.github.ackeecz:security-jetpack`. If you need only particular DataStore,
+then declare BOM and particular DataStore dependency, e.g. `io.github.ackeecz:security-datastore`.
+You don't need to declare `io.github.ackeecz:security-core` dependency, unless you depend only on
+`core` without any DataStore or `jetpack` modules.
+
+```toml
+[versions]
+ackee-security-bom = "SPECIFY_VERSION"
+tink = "SPECIFY_VERSION"
+
+[libraries]
+ackee-security-bom = { module = "io.github.ackeecz:security-bom", version.ref = "ackee-security-bom" }
+ackee-security-core = { module = "io.github.ackeecz:security-core" }
+ackee-security-datastore = { module = "io.github.ackeecz:security-datastore" }
+ackee-security-datastore-preferences = { module = "io.github.ackeecz:security-datastore-preferences" }
+ackee-security-jetpack = { module = "io.github.ackeecz:security-jetpack" }
+
+tink-android = { module = "com.google.crypto.tink:tink-android", version.ref = "tink" }
+```
+
+Then specify dependencies in your `build.gradle.kts`:
+
+```kotlin
+dependencies {
+
+ // Always use BOM
+ implementation(platform(libs.ackee.security.bom))
+ // Optional core dependency. Needed to be specified only if you do not use any other artifact
+ // and want to use core in your app.
+ implementation(libs.ackee.security.core)
+ // For encrypted DataStore
+ implementation(libs.ackee.security.datastore)
+ // For encrypted preferences DataStore
+ implementation(libs.ackee.security.datastore.preferences)
+ // For Jetpack Security port
+ implementation(libs.ackee.security.jetpack)
+
+ // Dependency on Tink must be included explicitly. This allows clients of Ackee Security library
+ // to control the version of Tink themselves, being able to keep it up-to-date as much as possible
+ // and not depend on Ackee Security releases.
+ implementation(libs.tink.android)
+}
+```
+
+## Usage
+
+Basic usage of the main library functionality is described bellow. You can also take a look on tests
+to get even more detailed picture.
+
+### DataStore
+
+The usages of encrypted DataStore implementations are almost the same as the classic DataStore. Both
+classic and preferences encrypted DataStore implementations can be created using property delegates
+or factories. The main difference is the `DataStoreCryptoParams` class that contains necessary
+parameters specific to crypto operations over DataStore. Check the documentation of this class for
+more details of what you can customize. Once you create an encrypted version of DataStore, you can
+use it exactly the same as the classic unencrypted DataStore instance.
+
+Encrypted DataStore delegate:
+
+```kotlin
+val Context.myDataStore by encryptedDataStore(
+ cryptoParams = DataStoreCryptoParams(
+ encryptionScheme = DataStoreEncryptionScheme.AES256_GCM_HKDF_4KB,
+ getMasterKey = { MasterKey.getOrCreate() },
+ ),
+ fileName = "filename",
+ serializer = serializer,
+ // Other params as in dataStore delegate
+)
+```
+
+Encrypted DataStore factory:
+
+```kotlin
+DataStoreFactory.createEncrypted(
+ context = context,
+ cryptoParams = DataStoreCryptoParams(
+ encryptionScheme = DataStoreEncryptionScheme.AES256_GCM_HKDF_4KB,
+ getMasterKey = { MasterKey.getOrCreate() },
+ ),
+ serializer = serializer,
+ produceFile = { context.dataStoreFile("encrypted_data") },
+ // Other params as in DataStoreFactory.create
+)
+```
+
+Encrypted PreferenceDataStore delegate:
+
+```kotlin
+val Context.myDataStore by encryptedPreferencesDataStore(
+ cryptoParams = DataStoreCryptoParams(
+ encryptionScheme = DataStoreEncryptionScheme.AES256_GCM_HKDF_4KB,
+ getMasterKey = { MasterKey.getOrCreate() },
+ ),
+ name = "preferences_name",
+ // Other params as in preferencesDataStore delegate
+)
+```
+
+Encrypted PreferenceDataStore factory:
+
+```kotlin
+PreferenceDataStoreFactory.createEncrypted(
+ context = context,
+ cryptoParams = DataStoreCryptoParams(
+ encryptionScheme = DataStoreEncryptionScheme.AES256_GCM_HKDF_4KB,
+ getMasterKey = { MasterKey.getOrCreate() },
+ ),
+ produceFile = { context.preferencesDataStoreFile("encrypted_data") },
+ // Other params as in PreferenceDataStoreFactory.create
+)
+```
+
+### Jetpack
+
+Using classes from `jetpack` module is almost the same as using the Jetpack Security classes.
+
+`EncryptedFile`:
+
+```kotlin
+val encryptedFile = EncryptedFile.Builder(
+ file = File(context.filesDir, "secret_data"),
+ context = context,
+ encryptionScheme = EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB,
+ getMasterKey = { MasterKey.getOrCreate() },
+).build()
+// Write to the encrypted file
+val encryptedOutputStream = encryptedFile.openFileOutput()
+// Read the encrypted file
+val encryptedInputStream = encryptedFile.openFileInput()
+```
+
+`EncryptedSharedPreferences`:
+
+```kotlin
+val encryptedSharedPreferences: EncryptedSharedPreferences = EncryptedSharedPreferences.create(
+ fileName = "secret_shared_prefs",
+ getMasterKey = { MasterKey.getOrCreate() },
+ context = context,
+ prefKeyEncryptionScheme = EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
+ prefValueEncryptionScheme = EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
+)
+// Use EncryptedSharedPreferences and Editor as you would normally use SharedPreferences
+encryptedSharedPreferences.edit {
+ putString("secret_key", "secret_value")
+}
+```
+
+As discussed above in Architecture section, `EncryptedSharedPreferences.create` no longer return
+`SharedPreferences` type but a new `EncryptedSharedPreferences`. You are highly encouraged to
+use this new type, but if you really **do** need `SharedPreferences`, there is an extension, that
+can adapt `EncryptedSharedPreferences` to `SharedPreferences`.
+
+```kotlin
+val sharedPreferences: SharedPreferences = encryptedSharedPreferences.adaptToSharedPreferences()
+```
+
+## Credits
+
+Developed by [Ackee](https://www.ackee.cz) team with 💙.
+
+`MasterKey` class from `core` and `EncryptedFile` and `EncryptedSharedPreferences` classes from
+`jetpack` are based on [Jetpack Security Crypto library](https://developer.android.com/reference/kotlin/androidx/security/crypto/package-summary)
+published by Google LLC, under the Apache License 2.0.
diff --git a/RELEASING.md b/RELEASING.md
new file mode 100644
index 0000000..5e45b2b
--- /dev/null
+++ b/RELEASING.md
@@ -0,0 +1,46 @@
+# Releasing
+
+If you release for the first time or you forgot the details, long version is recommended, which
+explains the steps in a great detail. Otherwise you can use TLDR version.
+
+## TLDR version
+
+1. Increase versions of the necessary library artifacts to be released, including the BOM version.
+ You can use `checkIfUpdateNeededSinceCurrentTag` Gradle task to help you figure out what
+ artifacts have changed since the last release.
+2. Update `CHANGELOG` for the new release.
+3. Create a tag for the new BOM version in the required format.
+4. Run `prePublishCheck` Gradle task and fix all found issues if needed.
+5. If you were forced to publish more artifacts, update `CHANGELOG` and fast-forward the tag to the
+ latest commit.
+6. Push the tag. CI will perform necessary checks and publish all artifacts.
+
+## Long version
+
+Once you are ready to publish new versions of library artifacts, you can start publishing process:
+
+1. First you need to increase the versions of all artifacts that need to be published, including the BOM version.
+ You can use `checkIfUpdateNeededSinceCurrentTag` Gradle task to figure out what modules have changed since the
+ last release. It does not have to mean that everything needs to be released. For example if you
+ did changes to both `datastore` and `jetpack`, the task will report changes in both, but you might
+ want to release just `datastore` and keep the changes in `jetpack` for the future release. However,
+ there are situations, when you will be forced to publish some updates, e.g. when you update `core-internal`.
+ In this case you will need to publish new versions of all artifacts that depend on it to preserve
+ binary compatibility between modules, because `core-internal` might contain breaking changes to public
+ API. It might be difficult to ensure that proper artifacts are updated when necessary due to these
+ kind of dependencies and that's why `verifyPublishing` task exists, that fails if there might be some
+ potentially incompatible dependencies and forces you to publish relevant artifacts together in a single
+ BOM.
+2. Update `CHANGELOG` for the new release.
+3. Create a tag for the new BOM version in the required format. You can optionally run `verifyBomVersion`
+ Gradle task to verify, if the version of the BOM artifact is synced with the version in the tag.
+4. Run `prePublishCheck` Gradle task. This task performs same checks as CI during a deployment, so you
+ can fix issues faster by running this quicker local verification before pushing to the remote.
+ This task performs usual checks like building modules, running tests, etc., but it also
+ runs all custom check tasks mentioned above, so you actually do not have to run them separately and
+ you can just run `prePublishCheck`, but it is good to know that they exist and what they do. You
+ can also find more detailed information in the documentation comments in the source code of those tasks.
+5. If you were forced to publish more artifacts, update `CHANGELOG` and fast-forward the tag to the
+ latest commit.
+6. Push the tag. CI will perform necessary checks and publish all artifacts.
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index c5f133e..18fe9ca 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -1,5 +1,4 @@
import io.github.ackeecz.security.properties.LibraryProperties
-import io.github.ackeecz.security.util.Constants
plugins {
alias(libs.plugins.ackeecz.security.android.application)
@@ -8,33 +7,12 @@ plugins {
alias(libs.plugins.ackeecz.security.testing.protobuf)
}
-private val includeArtifactsTestsProperty = "includeTests"
-private val artifactsTestsPackage = "io.github.ackeecz.security.sample.*"
-
android {
namespace = "io.github.ackeecz.security.sample"
defaultConfig {
applicationId = "io.github.ackeecz.security"
}
-
- @Suppress("UnstableApiUsage")
- testOptions {
- unitTests.all {
- it.filter {
- // By default (when property is not set) we exclude artifacts tests, because they rely
- // on artifacts to be published, so we do not want them to run together with all other
- // tests using classic Gradle test tasks like testDebugUnitTest. We want to run them
- // only in a special custom task that sets this property and run this task only under
- // certain special conditions, like during pre-publish check on published artifacts to
- // Maven local before real publishing.
- if (!project.hasProperty(includeArtifactsTestsProperty)) {
- excludeTestsMatching(artifactsTestsPackage)
- isFailOnNoMatchingTests = false
- }
- }
- }
- }
}
@Suppress("UseTomlInstead")
@@ -54,14 +32,3 @@ dependencies {
testImplementation(libs.bouncyCastle.bcpkix)
}
-
-/**
- * Tests published artifacts. This verifies things like correctly published artifacts including BOM
- * or binary compatibility of the dependent artifacts.
- */
-tasks.register(Constants.ARTIFACTS_TESTS_TASK_NAME) {
- group = Constants.ACKEE_TASKS_GROUP
- description = "Tests published artifacts of the library"
- ext.set(includeArtifactsTestsProperty, true)
- dependsOn("testDebugUnitTest")
-}
diff --git a/build-logic/logic/src/main/kotlin/io/github/ackeecz/security/plugin/RegisterPreflightChecksPlugin.kt b/build-logic/logic/src/main/kotlin/io/github/ackeecz/security/plugin/RegisterPreflightChecksPlugin.kt
index e109f06..c1bb5cd 100644
--- a/build-logic/logic/src/main/kotlin/io/github/ackeecz/security/plugin/RegisterPreflightChecksPlugin.kt
+++ b/build-logic/logic/src/main/kotlin/io/github/ackeecz/security/plugin/RegisterPreflightChecksPlugin.kt
@@ -31,6 +31,8 @@ internal class RegisterPreflightChecksPlugin : Plugin {
private inner class RegisterPreMergeRequestCheck(private val currentProject: Project) {
operator fun invoke() {
+ // Changes to this task must be synchronized with the basic-preflight-check/action.yml action
+ // to run the same checks on the CI as well
currentProject.tasks.register(PRE_MERGE_REQUEST_CHECK_TASK_NAME) {
group = Constants.ACKEE_TASKS_GROUP
description = "Performs basic verifications before making a MR like running Detekt, tests, etc."
@@ -82,6 +84,8 @@ internal class RegisterPreflightChecksPlugin : Plugin {
private inner class RegisterPrePublishCheck(private val currentProject: Project) {
operator fun invoke() {
+ // Changes to this task must be synchronized with the deploy.yml workflow
+ // to run the same checks on the CI as well
currentProject.tasks.register(PRE_PUBLISH_CHECK_TASK_NAME) {
group = Constants.ACKEE_TASKS_GROUP
description = "Performs all necessary verifications before publishing new artifacts versions"
@@ -112,7 +116,7 @@ internal class RegisterPreflightChecksPlugin : Plugin {
// We need to publish the latest versions to Maven local first before we can run tests
// on published artifacts
project.executeGradleTask(taskName = "publishToMavenLocal")
- project.executeGradleTask(taskName = Constants.ARTIFACTS_TESTS_TASK_NAME)
+ project.executeGradleTask(taskName = ":$SAMPLE_APP_NAME:testDebugUnitTest")
}
}
@@ -124,7 +128,7 @@ internal class RegisterPreflightChecksPlugin : Plugin {
project = project,
)
when (result) {
- is ExecuteCommand.Result.Success -> logger.info(result.commandOutput)
+ is ExecuteCommand.Result.Success -> println(result.commandOutput)
is ExecuteCommand.Result.Error -> throw GradleException(result.commandOutput)
}
}
diff --git a/build-logic/logic/src/main/kotlin/io/github/ackeecz/security/util/Constants.kt b/build-logic/logic/src/main/kotlin/io/github/ackeecz/security/util/Constants.kt
index 24a114d..6b1ab5a 100644
--- a/build-logic/logic/src/main/kotlin/io/github/ackeecz/security/util/Constants.kt
+++ b/build-logic/logic/src/main/kotlin/io/github/ackeecz/security/util/Constants.kt
@@ -13,5 +13,4 @@ public object Constants {
public val JVM_TARGET: JvmTarget = JvmTarget.JVM_11
public const val ACKEE_TASKS_GROUP: String = "ackee"
- public const val ARTIFACTS_TESTS_TASK_NAME: String = "artifactsTests"
}
diff --git a/build-logic/logic/src/main/kotlin/io/github/ackeecz/security/verification/GetTag.kt b/build-logic/logic/src/main/kotlin/io/github/ackeecz/security/verification/GetTag.kt
index 5817276..5687dd8 100644
--- a/build-logic/logic/src/main/kotlin/io/github/ackeecz/security/verification/GetTag.kt
+++ b/build-logic/logic/src/main/kotlin/io/github/ackeecz/security/verification/GetTag.kt
@@ -16,6 +16,8 @@ internal interface GetTag {
companion object {
+ // Deploy Github workflow relies on this tag format, so if you need to change it, you need
+ // to change it there as well.
const val BOM_VERSION_TAG_PREFIX = "bom-"
}
}
diff --git a/build-logic/logic/src/test/kotlin/io/github/ackeecz/security/verification/GetReleaseDependentProjectsTest.kt b/build-logic/logic/src/test/kotlin/io/github/ackeecz/security/verification/GetReleaseDependentProjectsTest.kt
index fabbace..6147aa2 100644
--- a/build-logic/logic/src/test/kotlin/io/github/ackeecz/security/verification/GetReleaseDependentProjectsTest.kt
+++ b/build-logic/logic/src/test/kotlin/io/github/ackeecz/security/verification/GetReleaseDependentProjectsTest.kt
@@ -5,7 +5,7 @@ import io.github.ackeecz.security.testutil.addDependencies
import io.github.ackeecz.security.testutil.addImplementationDependencies
import io.github.ackeecz.security.testutil.buildProject
import io.kotest.core.spec.style.FunSpec
-import io.kotest.inspectors.forAll
+import io.kotest.datatest.withData
import io.kotest.matchers.collections.shouldBeEmpty
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
import org.gradle.api.Project
@@ -14,7 +14,7 @@ private lateinit var underTest: GetReleaseDependentProjects
internal class GetReleaseDependentProjectsTest : FunSpec({
- beforeEach {
+ beforeTest {
underTest = GetReleaseDependentProjects()
}
@@ -39,8 +39,8 @@ internal class GetReleaseDependentProjectsTest : FunSpec({
actual shouldContainProjectsExactlyInAnyOrder listOf(dependentProject1, dependentProject2)
}
- test("get dependent projects for release configurations") {
- listOf(
+ context("get dependent projects for release configurations") {
+ withData(
"api",
"compileOnly",
"compileOnlyApi",
@@ -51,7 +51,7 @@ internal class GetReleaseDependentProjectsTest : FunSpec({
"releaseImplementation",
"releaseRuntimeOnly",
"runtimeOnly",
- ).forAll { configuration ->
+ ) { configuration ->
val rootProject = buildProject(name = "root")
val checkedProject = buildProject(name = "checked", parent = rootProject)
val notDependentProject = buildProject(name = "not-dependent", parent = rootProject)
@@ -64,8 +64,8 @@ internal class GetReleaseDependentProjectsTest : FunSpec({
}
}
- test("get no dependent projects for non-release configurations") {
- listOf(
+ context("get no dependent projects for non-release configurations") {
+ withData(
"androidTestApi",
"androidTestImplementation",
"debugApi",
@@ -73,7 +73,7 @@ internal class GetReleaseDependentProjectsTest : FunSpec({
"testDebugImplementation",
"testFixturesImplementation",
"testImplementation",
- ).forAll { configuration ->
+ ) { configuration ->
val rootProject = buildProject(name = "root")
val checkedProject = buildProject(name = "checked", parent = rootProject)
buildProject(name = "dependent-with-non-release-configuration", parent = rootProject)
diff --git a/core/src/main/kotlin/io/github/ackeecz/security/core/MasterKey.kt b/core/src/main/kotlin/io/github/ackeecz/security/core/MasterKey.kt
index 002c9a5..5407658 100644
--- a/core/src/main/kotlin/io/github/ackeecz/security/core/MasterKey.kt
+++ b/core/src/main/kotlin/io/github/ackeecz/security/core/MasterKey.kt
@@ -1,3 +1,21 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * 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.
+ *
+ * This file is based on the original MasterKeys from Jetpack Security Crypto library
+ * https://developer.android.com/reference/kotlin/androidx/security/crypto/MasterKeys
+ */
package io.github.ackeecz.security.core
import android.security.keystore.KeyGenParameterSpec
diff --git a/jetpack/api/jetpack.api b/jetpack/api/jetpack.api
index 2a97852..7931a46 100644
--- a/jetpack/api/jetpack.api
+++ b/jetpack/api/jetpack.api
@@ -5,7 +5,7 @@ public final class io/github/ackeecz/security/jetpack/EncryptedFile {
}
public final class io/github/ackeecz/security/jetpack/EncryptedFile$Builder {
- public fun (Landroid/content/Context;Ljava/io/File;Lio/github/ackeecz/security/jetpack/EncryptedFile$FileEncryptionScheme;Lkotlin/jvm/functions/Function1;)V
+ public fun (Ljava/io/File;Landroid/content/Context;Lio/github/ackeecz/security/jetpack/EncryptedFile$FileEncryptionScheme;Lkotlin/jvm/functions/Function1;)V
public final fun build ()Lio/github/ackeecz/security/jetpack/EncryptedFile;
public final fun setBackgroundDispatcher (Lkotlinx/coroutines/CoroutineDispatcher;)Lio/github/ackeecz/security/jetpack/EncryptedFile$Builder;
public final fun setKeysetAlias (Ljava/lang/String;)Lio/github/ackeecz/security/jetpack/EncryptedFile$Builder;
@@ -38,7 +38,7 @@ public abstract interface class io/github/ackeecz/security/jetpack/EncryptedShar
}
public final class io/github/ackeecz/security/jetpack/EncryptedSharedPreferences$Companion {
- public final fun create (Landroid/content/Context;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lio/github/ackeecz/security/jetpack/EncryptedSharedPreferences$PrefKeyEncryptionScheme;Lio/github/ackeecz/security/jetpack/EncryptedSharedPreferences$PrefValueEncryptionScheme;)Lio/github/ackeecz/security/jetpack/EncryptedSharedPreferences;
+ public final fun create (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Landroid/content/Context;Lio/github/ackeecz/security/jetpack/EncryptedSharedPreferences$PrefKeyEncryptionScheme;Lio/github/ackeecz/security/jetpack/EncryptedSharedPreferences$PrefValueEncryptionScheme;)Lio/github/ackeecz/security/jetpack/EncryptedSharedPreferences;
}
public final class io/github/ackeecz/security/jetpack/EncryptedSharedPreferences$DefaultImpls {
diff --git a/jetpack/src/main/java/io/github/ackeecz/security/jetpack/EncryptedFile.kt b/jetpack/src/main/java/io/github/ackeecz/security/jetpack/EncryptedFile.kt
index a2e14b3..d4ed2b9 100644
--- a/jetpack/src/main/java/io/github/ackeecz/security/jetpack/EncryptedFile.kt
+++ b/jetpack/src/main/java/io/github/ackeecz/security/jetpack/EncryptedFile.kt
@@ -1,4 +1,3 @@
-// TODO Should this licence be here and for other JetSec original files?
/*
* Copyright 2018 The Android Open Source Project
*
@@ -13,6 +12,9 @@
* 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.
+ *
+ * This file is based on the original EncryptedFile from Jetpack Security Crypto library
+ * https://developer.android.com/reference/kotlin/androidx/security/crypto/EncryptedFile
*/
package io.github.ackeecz.security.jetpack
@@ -57,8 +59,8 @@ import java.security.GeneralSecurityException
* val getMasterKey = suspend { MasterKey.getOrCreate() }
* val file = File(context.filesDir, "secret_data")
* val encryptedFile = EncryptedFile.Builder(
- * context = context,
* file = file,
+ * context = context,
* encryptionScheme = EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB,
* getMasterKey = getMasterKey,
* ).build()
@@ -213,8 +215,8 @@ public class EncryptedFile private constructor(private val builder: Builder) {
}
public class Builder public constructor(
- context: Context,
internal val file: File,
+ context: Context,
internal val encryptionScheme: FileEncryptionScheme,
internal val getMasterKey: suspend () -> MasterKey,
) {
diff --git a/jetpack/src/main/java/io/github/ackeecz/security/jetpack/EncryptedSharedPreferences.kt b/jetpack/src/main/java/io/github/ackeecz/security/jetpack/EncryptedSharedPreferences.kt
index d9d21be..51381f4 100644
--- a/jetpack/src/main/java/io/github/ackeecz/security/jetpack/EncryptedSharedPreferences.kt
+++ b/jetpack/src/main/java/io/github/ackeecz/security/jetpack/EncryptedSharedPreferences.kt
@@ -12,6 +12,9 @@
* 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.
+ *
+ * This file is based on the original EncryptedSharedPreferences from Jetpack Security Crypto library
+ * https://developer.android.com/reference/kotlin/androidx/security/crypto/EncryptedSharedPreferences
*/
package io.github.ackeecz.security.jetpack
@@ -66,15 +69,15 @@ private const val VALUE_KEYSET_ALIAS = "__androidx_security_crypto_encrypted_pre
*
* Basic use of the class:
*```
- * val sharedPreferences = EncryptedSharedPreferences.create(
- * context = context,
+ * val encryptedSharedPreferences = EncryptedSharedPreferences.create(
* fileName = "secret_shared_prefs",
* getMasterKey = { MasterKey.getOrCreate() },
+ * context = context,
* prefKeyEncryptionScheme = EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
* prefValueEncryptionScheme = EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
* )
* // Use EncryptedSharedPreferences and Editor as you would normally use SharedPreferences
- * sharedPreferences.edit {
+ * encryptedSharedPreferences.edit {
* putString("secret_key", "secret_value")
* }
*```
@@ -248,16 +251,16 @@ public interface EncryptedSharedPreferences {
* @throws IOException when [fileName] can not be used
*/
public fun create(
- context: Context,
fileName: String,
getMasterKey: suspend () -> MasterKey,
+ context: Context,
prefKeyEncryptionScheme: PrefKeyEncryptionScheme,
prefValueEncryptionScheme: PrefValueEncryptionScheme,
): EncryptedSharedPreferences {
return create(
- context = context,
fileName = fileName,
getMasterKey = getMasterKey,
+ context = context,
prefKeyEncryptionScheme = prefKeyEncryptionScheme,
prefValueEncryptionScheme = prefValueEncryptionScheme,
weakReferenceFactory = WeakReferenceFactory(),
@@ -268,18 +271,18 @@ public interface EncryptedSharedPreferences {
@Suppress("LongParameterList")
@VisibleForTesting
internal fun create(
- context: Context,
fileName: String,
getMasterKey: suspend () -> MasterKey,
+ context: Context,
prefKeyEncryptionScheme: PrefKeyEncryptionScheme,
prefValueEncryptionScheme: PrefValueEncryptionScheme,
weakReferenceFactory: WeakReferenceFactory,
defaultDispatcher: CoroutineDispatcher,
): EncryptedSharedPreferences {
return EncryptedSharedPreferencesImpl(
- context = context,
fileName = fileName,
getMasterKey = getMasterKey,
+ context = context,
prefKeyEncryptionScheme = prefKeyEncryptionScheme,
prefValueEncryptionScheme = prefValueEncryptionScheme,
weakReferenceFactory = weakReferenceFactory,
diff --git a/jetpack/src/test/java/io/github/ackeecz/security/jetpack/EncryptedFileTest.kt b/jetpack/src/test/java/io/github/ackeecz/security/jetpack/EncryptedFileTest.kt
index 8dce47a..923c7bd 100644
--- a/jetpack/src/test/java/io/github/ackeecz/security/jetpack/EncryptedFileTest.kt
+++ b/jetpack/src/test/java/io/github/ackeecz/security/jetpack/EncryptedFileTest.kt
@@ -42,7 +42,7 @@ internal class EncryptedFileTest : AndroidTestWithKeyStore() {
encryptionScheme: EncryptedFile.FileEncryptionScheme = EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB,
getMasterKey: suspend () -> MasterKey = { MasterKey.getOrCreate() },
): EncryptedFile.Builder {
- return EncryptedFile.Builder(context, file, encryptionScheme, getMasterKey)
+ return EncryptedFile.Builder(file, context, encryptionScheme, getMasterKey)
.setBackgroundDispatcher(coroutineRule.testDispatcher)
}