From 959a56d7ffbc20db60721d7a09f399c8bdefe07e Mon Sep 17 00:00:00 2001 From: Harsh Shandilya Date: Thu, 10 Aug 2023 03:21:47 +0530 Subject: refactor: un-flatten module structure --- CONTRIBUTING.md | 8 +- app/build.gradle.kts | 6 +- crypto-common/build.gradle.kts | 7 - crypto-common/lint-baseline.xml | 4 - .../app/passwordstore/crypto/CryptoHandler.kt | 44 ---- .../app/passwordstore/crypto/CryptoOptions.kt | 8 - .../kotlin/app/passwordstore/crypto/KeyManager.kt | 41 ---- .../passwordstore/crypto/errors/CryptoException.kt | 48 ----- crypto-pgpainless/build.gradle.kts | 18 -- crypto-pgpainless/lint-baseline.xml | 4 - .../kotlin/app/passwordstore/crypto/KeyUtils.kt | 52 ----- .../app/passwordstore/crypto/PGPDecryptOptions.kt | 21 -- .../app/passwordstore/crypto/PGPEncryptOptions.kt | 35 --- .../app/passwordstore/crypto/PGPIdentifier.kt | 128 ----------- .../main/kotlin/app/passwordstore/crypto/PGPKey.kt | 12 -- .../app/passwordstore/crypto/PGPKeyManager.kt | 154 -------------- .../crypto/PGPainlessCryptoHandler.kt | 139 ------------ .../app/passwordstore/crypto/CryptoConstants.kt | 14 -- .../app/passwordstore/crypto/KeyUtilsTest.kt | 35 --- .../app/passwordstore/crypto/PGPIdentifierTest.kt | 41 ---- .../app/passwordstore/crypto/PGPKeyManagerTest.kt | 236 --------------------- .../crypto/PGPainlessCryptoHandlerTest.kt | 148 ------------- .../kotlin/app/passwordstore/crypto/TestUtils.kt | 32 --- crypto-pgpainless/src/test/resources/aead_pub | 51 ----- crypto-pgpainless/src/test/resources/aead_sec | 107 ---------- .../src/test/resources/alice_owner@example_com | 16 -- .../src/test/resources/bobby_owner@example_com | 16 -- crypto-pgpainless/src/test/resources/public_key | 21 -- .../test/resources/public_key_multiple_identities | 51 ----- crypto-pgpainless/src/test/resources/secret_key | 26 --- .../test/resources/secret_key_multiple_identities | 93 -------- crypto/common/build.gradle.kts | 7 + crypto/common/lint-baseline.xml | 4 + .../app/passwordstore/crypto/CryptoHandler.kt | 44 ++++ .../app/passwordstore/crypto/CryptoOptions.kt | 8 + .../kotlin/app/passwordstore/crypto/KeyManager.kt | 41 ++++ .../passwordstore/crypto/errors/CryptoException.kt | 48 +++++ crypto/pgpainless/build.gradle.kts | 18 ++ crypto/pgpainless/lint-baseline.xml | 4 + .../kotlin/app/passwordstore/crypto/KeyUtils.kt | 52 +++++ .../app/passwordstore/crypto/PGPDecryptOptions.kt | 21 ++ .../app/passwordstore/crypto/PGPEncryptOptions.kt | 35 +++ .../app/passwordstore/crypto/PGPIdentifier.kt | 128 +++++++++++ .../main/kotlin/app/passwordstore/crypto/PGPKey.kt | 12 ++ .../app/passwordstore/crypto/PGPKeyManager.kt | 154 ++++++++++++++ .../crypto/PGPainlessCryptoHandler.kt | 139 ++++++++++++ .../app/passwordstore/crypto/CryptoConstants.kt | 14 ++ .../app/passwordstore/crypto/KeyUtilsTest.kt | 35 +++ .../app/passwordstore/crypto/PGPIdentifierTest.kt | 41 ++++ .../app/passwordstore/crypto/PGPKeyManagerTest.kt | 236 +++++++++++++++++++++ .../crypto/PGPainlessCryptoHandlerTest.kt | 148 +++++++++++++ .../kotlin/app/passwordstore/crypto/TestUtils.kt | 32 +++ crypto/pgpainless/src/test/resources/aead_pub | 51 +++++ crypto/pgpainless/src/test/resources/aead_sec | 107 ++++++++++ .../src/test/resources/alice_owner@example_com | 16 ++ .../src/test/resources/bobby_owner@example_com | 16 ++ crypto/pgpainless/src/test/resources/public_key | 21 ++ .../test/resources/public_key_multiple_identities | 51 +++++ crypto/pgpainless/src/test/resources/secret_key | 26 +++ .../test/resources/secret_key_multiple_identities | 93 ++++++++ format-common/build.gradle.kts | 18 -- format-common/lint-baseline.xml | 4 - .../passwordstore/data/passfile/PasswordEntry.kt | 231 -------------------- .../kotlin/app/passwordstore/data/passfile/Totp.kt | 6 - .../app/passwordstore/util/time/UserClock.kt | 26 --- .../main/kotlin/app/passwordstore/util/totp/Otp.kt | 90 -------- .../app/passwordstore/util/totp/TotpFinder.kt | 29 --- .../app/passwordstore/util/totp/UriTotpFinder.kt | 52 ----- .../data/passfile/PasswordEntryTest.kt | 212 ------------------ .../app/passwordstore/util/time/TestUserClock.kt | 33 --- .../kotlin/app/passwordstore/util/totp/OtpTest.kt | 168 --------------- .../passwordstore/util/totp/UriTotpFinderTest.kt | 55 ----- format/common/build.gradle.kts | 18 ++ format/common/lint-baseline.xml | 4 + .../passwordstore/data/passfile/PasswordEntry.kt | 231 ++++++++++++++++++++ .../kotlin/app/passwordstore/data/passfile/Totp.kt | 6 + .../app/passwordstore/util/time/UserClock.kt | 26 +++ .../main/kotlin/app/passwordstore/util/totp/Otp.kt | 90 ++++++++ .../app/passwordstore/util/totp/TotpFinder.kt | 29 +++ .../app/passwordstore/util/totp/UriTotpFinder.kt | 52 +++++ .../data/passfile/PasswordEntryTest.kt | 212 ++++++++++++++++++ .../app/passwordstore/util/time/TestUserClock.kt | 33 +++ .../kotlin/app/passwordstore/util/totp/OtpTest.kt | 168 +++++++++++++++ .../passwordstore/util/totp/UriTotpFinderTest.kt | 55 +++++ settings.gradle.kts | 8 +- ui-compose/build.gradle.kts | 28 --- ui-compose/lint-baseline.xml | 4 - ui-compose/src/main/AndroidManifest.xml | 6 - .../main/kotlin/app/passwordstore/ui/APSAppBar.kt | 40 ---- .../app/passwordstore/ui/compose/PasswordField.kt | 82 ------- .../app/passwordstore/ui/compose/theme/Color.kt | 58 ----- .../app/passwordstore/ui/compose/theme/Theme.kt | 77 ------- .../app/passwordstore/ui/compose/theme/Type.kt | 144 ------------- .../app/passwordstore/ui/compose/theme/utils.kt | 27 --- .../main/res/drawable/baseline_visibility_24.xml | 10 - .../res/drawable/baseline_visibility_off_24.xml | 10 - .../src/main/res/drawable/ic_content_copy.xml | 15 -- ui-compose/src/main/res/font/manrope.xml | 28 --- ui-compose/src/main/res/font/manrope_bold.ttf | Bin 92228 -> 0 bytes ui-compose/src/main/res/font/manrope_extrabold.ttf | Bin 93144 -> 0 bytes .../src/main/res/font/manrope_extralight.ttf | Bin 92272 -> 0 bytes ui-compose/src/main/res/font/manrope_light.ttf | Bin 92400 -> 0 bytes ui-compose/src/main/res/font/manrope_medium.ttf | Bin 92464 -> 0 bytes ui-compose/src/main/res/font/manrope_regular.ttf | Bin 92376 -> 0 bytes ui-compose/src/main/res/font/manrope_semibold.ttf | Bin 92380 -> 0 bytes ui/compose/build.gradle.kts | 28 +++ ui/compose/lint-baseline.xml | 4 + ui/compose/src/main/AndroidManifest.xml | 6 + .../main/kotlin/app/passwordstore/ui/APSAppBar.kt | 40 ++++ .../app/passwordstore/ui/compose/PasswordField.kt | 82 +++++++ .../app/passwordstore/ui/compose/theme/Color.kt | 58 +++++ .../app/passwordstore/ui/compose/theme/Theme.kt | 77 +++++++ .../app/passwordstore/ui/compose/theme/Type.kt | 144 +++++++++++++ .../app/passwordstore/ui/compose/theme/utils.kt | 27 +++ .../main/res/drawable/baseline_visibility_24.xml | 10 + .../res/drawable/baseline_visibility_off_24.xml | 10 + .../src/main/res/drawable/ic_content_copy.xml | 15 ++ ui/compose/src/main/res/font/manrope.xml | 28 +++ ui/compose/src/main/res/font/manrope_bold.ttf | Bin 0 -> 92228 bytes ui/compose/src/main/res/font/manrope_extrabold.ttf | Bin 0 -> 93144 bytes .../src/main/res/font/manrope_extralight.ttf | Bin 0 -> 92272 bytes ui/compose/src/main/res/font/manrope_light.ttf | Bin 0 -> 92400 bytes ui/compose/src/main/res/font/manrope_medium.ttf | Bin 0 -> 92464 bytes ui/compose/src/main/res/font/manrope_regular.ttf | Bin 0 -> 92376 bytes ui/compose/src/main/res/font/manrope_semibold.ttf | Bin 0 -> 92380 bytes 125 files changed, 3066 insertions(+), 3066 deletions(-) delete mode 100644 crypto-common/build.gradle.kts delete mode 100644 crypto-common/lint-baseline.xml delete mode 100644 crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt delete mode 100644 crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoOptions.kt delete mode 100644 crypto-common/src/main/kotlin/app/passwordstore/crypto/KeyManager.kt delete mode 100644 crypto-common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt delete mode 100644 crypto-pgpainless/build.gradle.kts delete mode 100644 crypto-pgpainless/lint-baseline.xml delete mode 100644 crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt delete mode 100644 crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPDecryptOptions.kt delete mode 100644 crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPEncryptOptions.kt delete mode 100644 crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt delete mode 100644 crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKey.kt delete mode 100644 crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt delete mode 100644 crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt delete mode 100644 crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/CryptoConstants.kt delete mode 100644 crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt delete mode 100644 crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPIdentifierTest.kt delete mode 100644 crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt delete mode 100644 crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt delete mode 100644 crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/TestUtils.kt delete mode 100644 crypto-pgpainless/src/test/resources/aead_pub delete mode 100644 crypto-pgpainless/src/test/resources/aead_sec delete mode 100644 crypto-pgpainless/src/test/resources/alice_owner@example_com delete mode 100644 crypto-pgpainless/src/test/resources/bobby_owner@example_com delete mode 100644 crypto-pgpainless/src/test/resources/public_key delete mode 100644 crypto-pgpainless/src/test/resources/public_key_multiple_identities delete mode 100644 crypto-pgpainless/src/test/resources/secret_key delete mode 100644 crypto-pgpainless/src/test/resources/secret_key_multiple_identities create mode 100644 crypto/common/build.gradle.kts create mode 100644 crypto/common/lint-baseline.xml create mode 100644 crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt create mode 100644 crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoOptions.kt create mode 100644 crypto/common/src/main/kotlin/app/passwordstore/crypto/KeyManager.kt create mode 100644 crypto/common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt create mode 100644 crypto/pgpainless/build.gradle.kts create mode 100644 crypto/pgpainless/lint-baseline.xml create mode 100644 crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt create mode 100644 crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPDecryptOptions.kt create mode 100644 crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPEncryptOptions.kt create mode 100644 crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt create mode 100644 crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKey.kt create mode 100644 crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt create mode 100644 crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt create mode 100644 crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/CryptoConstants.kt create mode 100644 crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt create mode 100644 crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPIdentifierTest.kt create mode 100644 crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt create mode 100644 crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt create mode 100644 crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/TestUtils.kt create mode 100644 crypto/pgpainless/src/test/resources/aead_pub create mode 100644 crypto/pgpainless/src/test/resources/aead_sec create mode 100644 crypto/pgpainless/src/test/resources/alice_owner@example_com create mode 100644 crypto/pgpainless/src/test/resources/bobby_owner@example_com create mode 100644 crypto/pgpainless/src/test/resources/public_key create mode 100644 crypto/pgpainless/src/test/resources/public_key_multiple_identities create mode 100644 crypto/pgpainless/src/test/resources/secret_key create mode 100644 crypto/pgpainless/src/test/resources/secret_key_multiple_identities delete mode 100644 format-common/build.gradle.kts delete mode 100644 format-common/lint-baseline.xml delete mode 100644 format-common/src/main/kotlin/app/passwordstore/data/passfile/PasswordEntry.kt delete mode 100644 format-common/src/main/kotlin/app/passwordstore/data/passfile/Totp.kt delete mode 100644 format-common/src/main/kotlin/app/passwordstore/util/time/UserClock.kt delete mode 100644 format-common/src/main/kotlin/app/passwordstore/util/totp/Otp.kt delete mode 100644 format-common/src/main/kotlin/app/passwordstore/util/totp/TotpFinder.kt delete mode 100644 format-common/src/main/kotlin/app/passwordstore/util/totp/UriTotpFinder.kt delete mode 100644 format-common/src/test/kotlin/app/passwordstore/data/passfile/PasswordEntryTest.kt delete mode 100644 format-common/src/test/kotlin/app/passwordstore/util/time/TestUserClock.kt delete mode 100644 format-common/src/test/kotlin/app/passwordstore/util/totp/OtpTest.kt delete mode 100644 format-common/src/test/kotlin/app/passwordstore/util/totp/UriTotpFinderTest.kt create mode 100644 format/common/build.gradle.kts create mode 100644 format/common/lint-baseline.xml create mode 100644 format/common/src/main/kotlin/app/passwordstore/data/passfile/PasswordEntry.kt create mode 100644 format/common/src/main/kotlin/app/passwordstore/data/passfile/Totp.kt create mode 100644 format/common/src/main/kotlin/app/passwordstore/util/time/UserClock.kt create mode 100644 format/common/src/main/kotlin/app/passwordstore/util/totp/Otp.kt create mode 100644 format/common/src/main/kotlin/app/passwordstore/util/totp/TotpFinder.kt create mode 100644 format/common/src/main/kotlin/app/passwordstore/util/totp/UriTotpFinder.kt create mode 100644 format/common/src/test/kotlin/app/passwordstore/data/passfile/PasswordEntryTest.kt create mode 100644 format/common/src/test/kotlin/app/passwordstore/util/time/TestUserClock.kt create mode 100644 format/common/src/test/kotlin/app/passwordstore/util/totp/OtpTest.kt create mode 100644 format/common/src/test/kotlin/app/passwordstore/util/totp/UriTotpFinderTest.kt delete mode 100644 ui-compose/build.gradle.kts delete mode 100644 ui-compose/lint-baseline.xml delete mode 100644 ui-compose/src/main/AndroidManifest.xml delete mode 100644 ui-compose/src/main/kotlin/app/passwordstore/ui/APSAppBar.kt delete mode 100644 ui-compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt delete mode 100644 ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Color.kt delete mode 100644 ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Theme.kt delete mode 100644 ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Type.kt delete mode 100644 ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/utils.kt delete mode 100644 ui-compose/src/main/res/drawable/baseline_visibility_24.xml delete mode 100644 ui-compose/src/main/res/drawable/baseline_visibility_off_24.xml delete mode 100644 ui-compose/src/main/res/drawable/ic_content_copy.xml delete mode 100644 ui-compose/src/main/res/font/manrope.xml delete mode 100644 ui-compose/src/main/res/font/manrope_bold.ttf delete mode 100644 ui-compose/src/main/res/font/manrope_extrabold.ttf delete mode 100644 ui-compose/src/main/res/font/manrope_extralight.ttf delete mode 100644 ui-compose/src/main/res/font/manrope_light.ttf delete mode 100644 ui-compose/src/main/res/font/manrope_medium.ttf delete mode 100644 ui-compose/src/main/res/font/manrope_regular.ttf delete mode 100644 ui-compose/src/main/res/font/manrope_semibold.ttf create mode 100644 ui/compose/build.gradle.kts create mode 100644 ui/compose/lint-baseline.xml create mode 100644 ui/compose/src/main/AndroidManifest.xml create mode 100644 ui/compose/src/main/kotlin/app/passwordstore/ui/APSAppBar.kt create mode 100644 ui/compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt create mode 100644 ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Color.kt create mode 100644 ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Theme.kt create mode 100644 ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Type.kt create mode 100644 ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/utils.kt create mode 100644 ui/compose/src/main/res/drawable/baseline_visibility_24.xml create mode 100644 ui/compose/src/main/res/drawable/baseline_visibility_off_24.xml create mode 100644 ui/compose/src/main/res/drawable/ic_content_copy.xml create mode 100644 ui/compose/src/main/res/font/manrope.xml create mode 100644 ui/compose/src/main/res/font/manrope_bold.ttf create mode 100644 ui/compose/src/main/res/font/manrope_extrabold.ttf create mode 100644 ui/compose/src/main/res/font/manrope_extralight.ttf create mode 100644 ui/compose/src/main/res/font/manrope_light.ttf create mode 100644 ui/compose/src/main/res/font/manrope_medium.ttf create mode 100644 ui/compose/src/main/res/font/manrope_regular.ttf create mode 100644 ui/compose/src/main/res/font/manrope_semibold.ttf diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8feab911..9674dbfa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,15 +23,15 @@ The source code is split across 12 modules and 1 subproject. - `build-logic` and its modules host the Gradle build logic for the project. - `autofill-parser` is the aptly named parser for Android's Autofill structures that also deals with trust and feature detection for browsers. - `coroutine-utils` is a helper libraries that allow for effective usage and testing of [Kotlin Coroutines](https://kotlinlang.org/docs/coroutines-overview.html). -- `crypto-common` is the foundation of our new, extensible cryptography APIs that adds the ability to introduce new cryptographic backends to APS with minimal effort. -- `crypto-pgpainless` is the first of our new backends that implements the APIs defined in `crypto-common` to offer PGP cryptography through the [PGPainless](https://gh.pgpainless.org/) library. -- `format-common` handles parsing the `pass` file format. +- `crypto/common` is the foundation of our new, extensible cryptography APIs that adds the ability to introduce new cryptographic backends to APS with minimal effort. +- `crypto/pgpainless` is the first of our new backends that implements the APIs defined in `crypto-common` to offer PGP cryptography through the [PGPainless](https://gh.pgpainless.org/) library. +- `format/common` handles parsing the `pass` file format. - `openpgp-ktx` contains the now defunct glue code that was used by APS to interact with OpenKeychain. - `passgen/diceware` is our new password generator that implements the [Diceware](https://theworld.com/~reinhold/diceware.html) algorithm. - `passgen/random` contains the default password generator. - `sentry-stub` contains no-op variants of [Sentry](https://sentry.io/) APIs that we use to ensure the FOSS-only, telemetry-free variant of APS continues to compile in absence of Sentry dependencies. - `ssh` has exactly what you think it does. -- `ui-compose` has the theming code for building UI components in [Jetpack Compose](https://developer.android.com/jetpack/compose). +- `ui/compose` has the theming code for building UI components in [Jetpack Compose](https://developer.android.com/jetpack/compose). - `app` is everything else that constitutes APS. In most scenarios, the `app` directory is where you'd be contributing changes to. While most of the code has been rewritten and documented, there are still gnarly "legacy" parts that might be challenging to understand at a glance. Please get in touch via the [Discussions](https://github.com/android-password-store/Android-Password-Store/discussions) page with any questions you have, and we'd love to explain and improve things. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b7e93c57..4f825aad 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -43,12 +43,12 @@ dependencies { coreLibraryDesugaring(libs.android.desugarJdkLibs) implementation(projects.autofillParser) implementation(projects.coroutineUtils) - implementation(projects.cryptoPgpainless) - implementation(projects.formatCommon) + implementation(projects.crypto.pgpainless) + implementation(projects.format.common) implementation(projects.passgen.diceware) implementation(projects.passgen.random) implementation(projects.ssh) - implementation(projects.uiCompose) + implementation(projects.ui.compose) implementation(libs.androidx.activity.ktx) implementation(libs.androidx.activity.compose) implementation(libs.androidx.appcompat) diff --git a/crypto-common/build.gradle.kts b/crypto-common/build.gradle.kts deleted file mode 100644 index 110664bd..00000000 --- a/crypto-common/build.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -plugins { id("com.github.android-password-store.kotlin-jvm-library") } - -dependencies { implementation(libs.thirdparty.kotlinResult) } diff --git a/crypto-common/lint-baseline.xml b/crypto-common/lint-baseline.xml deleted file mode 100644 index 2ed8a3bb..00000000 --- a/crypto-common/lint-baseline.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt b/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt deleted file mode 100644 index 898cf058..00000000 --- a/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.crypto - -import app.passwordstore.crypto.errors.CryptoHandlerException -import com.github.michaelbull.result.Result -import java.io.InputStream -import java.io.OutputStream - -/** Generic interface to implement cryptographic operations on top of. */ -public interface CryptoHandler { - - /** - * Decrypt the given [ciphertextStream] using a set of potential [keys] and [passphrase], and - * writes the resultant plaintext to [outputStream]. The returned [Result] should be checked to - * ensure it is **not** an instance of [com.github.michaelbull.result.Err] before the contents of - * [outputStream] are used. - */ - public fun decrypt( - keys: List, - passphrase: String, - ciphertextStream: InputStream, - outputStream: OutputStream, - options: DecryptOpts, - ): Result - - /** - * Encrypt the given [plaintextStream] to the provided [keys], and writes the encrypted ciphertext - * to [outputStream]. The returned [Result] should be checked to ensure it is **not** an instance - * of [com.github.michaelbull.result.Err] before the contents of [outputStream] are used. - */ - public fun encrypt( - keys: List, - plaintextStream: InputStream, - outputStream: OutputStream, - options: EncOpts, - ): Result - - /** Given a [fileName], return whether this instance can handle it. */ - public fun canHandle(fileName: String): Boolean -} diff --git a/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoOptions.kt b/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoOptions.kt deleted file mode 100644 index 1e60cdba..00000000 --- a/crypto-common/src/main/kotlin/app/passwordstore/crypto/CryptoOptions.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.passwordstore.crypto - -/** Defines the contract for a grab-bag of options for individual cryptographic operations. */ -public interface CryptoOptions { - - /** Returns a [Boolean] indicating if the [option] is enabled for this operation. */ - public fun isOptionEnabled(option: String): Boolean -} diff --git a/crypto-common/src/main/kotlin/app/passwordstore/crypto/KeyManager.kt b/crypto-common/src/main/kotlin/app/passwordstore/crypto/KeyManager.kt deleted file mode 100644 index c9db3734..00000000 --- a/crypto-common/src/main/kotlin/app/passwordstore/crypto/KeyManager.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.crypto - -import com.github.michaelbull.result.Result - -/** - * [KeyManager] defines a contract for implementing a management system for [Key]s as they would be - * used by an implementation of [CryptoHandler] to obtain eligible public or private keys as - * required. - */ -public interface KeyManager { - - /** - * Inserts a [key] into the store. If the key already exists, this method will return - * [app.passwordstore.crypto.errors.KeyAlreadyExistsException] unless [replace] is `true`. - */ - public suspend fun addKey(key: Key, replace: Boolean = false): Result - - /** Finds a key for [identifier] in the store and deletes it. */ - public suspend fun removeKey(identifier: KeyIdentifier): Result - - /** - * Get a [Key] for the given [id]. The actual semantics of what [id] is are left to individual - * implementations to figure out for themselves. For example, in GPG this can be a full - * hexadecimal key ID, an email, a short hex key ID, and probably a few more things. - */ - public suspend fun getKeyById(id: KeyIdentifier): Result - - /** Returns all keys currently in the store as a [List]. */ - public suspend fun getAllKeys(): Result, Throwable> - - /** - * Get a stable identifier for the given [key]. The returned key ID should be suitable to be used - * as an identifier for the cryptographic identity tied to this key. - */ - public suspend fun getKeyId(key: Key): KeyIdentifier? -} diff --git a/crypto-common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt b/crypto-common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt deleted file mode 100644 index eb64541e..00000000 --- a/crypto-common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt +++ /dev/null @@ -1,48 +0,0 @@ -package app.passwordstore.crypto.errors - -import app.passwordstore.crypto.KeyManager - -public sealed class CryptoException(message: String? = null, cause: Throwable? = null) : - Exception(message, cause) - -/** Sealed exception types for [KeyManager]. */ -public sealed class KeyManagerException(message: String? = null) : CryptoException(message) - -/** Store contains no keys. */ -public data object NoKeysAvailableException : KeyManagerException("No keys were found") - -/** Key directory does not exist or cannot be accessed. */ -public data object KeyDirectoryUnavailableException : - KeyManagerException("Key directory does not exist") - -/** Failed to delete given key. */ -public data object KeyDeletionFailedException : KeyManagerException("Couldn't delete the key file") - -/** Failed to parse the key as a known type. */ -public data object InvalidKeyException : - KeyManagerException("Given key cannot be parsed as a known key type") - -/** Key failed the [app.passwordstore.crypto.KeyUtils.isKeyUsable] test. */ -public data object UnusableKeyException : - KeyManagerException("Given key is not usable for encryption - is it using AEAD?") - -/** No key matching `keyId` could be found. */ -public class KeyNotFoundException(keyId: String) : - KeyManagerException("No key found with id: $keyId") - -/** Attempting to add another key for `keyId` without requesting a replace. */ -public class KeyAlreadyExistsException(keyId: String) : - KeyManagerException("Pre-existing key was found for $keyId") - -/** Sealed exception types for [app.passwordstore.crypto.CryptoHandler]. */ -public sealed class CryptoHandlerException(message: String? = null, cause: Throwable? = null) : - CryptoException(message, cause) - -/** The passphrase provided for decryption was incorrect. */ -public class IncorrectPassphraseException(cause: Throwable) : CryptoHandlerException(null, cause) - -/** No keys were passed to the encrypt/decrypt operation. */ -public data object NoKeysProvidedException : CryptoHandlerException(null, null) - -/** An unexpected error that cannot be mapped to a known type. */ -public class UnknownError(cause: Throwable) : CryptoHandlerException(null, cause) diff --git a/crypto-pgpainless/build.gradle.kts b/crypto-pgpainless/build.gradle.kts deleted file mode 100644 index 22363f23..00000000 --- a/crypto-pgpainless/build.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -plugins { id("com.github.android-password-store.kotlin-jvm-library") } - -dependencies { - api(projects.cryptoCommon) - implementation(projects.coroutineUtils) - implementation(libs.androidx.annotation) - implementation(libs.dagger.hilt.core) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.thirdparty.kotlinResult) - implementation(libs.thirdparty.pgpainless) - testImplementation(libs.bundles.testDependencies) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.testing.testparameterinjector) -} diff --git a/crypto-pgpainless/lint-baseline.xml b/crypto-pgpainless/lint-baseline.xml deleted file mode 100644 index 2ed8a3bb..00000000 --- a/crypto-pgpainless/lint-baseline.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt deleted file mode 100644 index 5b23dc18..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.crypto - -import app.passwordstore.crypto.PGPIdentifier.KeyId -import app.passwordstore.crypto.PGPIdentifier.UserId -import com.github.michaelbull.result.get -import com.github.michaelbull.result.runCatching -import org.bouncycastle.openpgp.PGPKeyRing -import org.pgpainless.PGPainless -import org.pgpainless.key.parsing.KeyRingReader - -/** Utility methods to deal with [PGPKey]s. */ -public object KeyUtils { - /** - * Attempts to parse a [PGPKeyRing] from a given [key]. The key is first tried as a secret key and - * then as a public one before the method gives up and returns null. - */ - public fun tryParseKeyring(key: PGPKey): PGPKeyRing? { - return runCatching { KeyRingReader.readKeyRing(key.contents.inputStream()) }.get() - } - - /** Parses a [PGPKeyRing] from the given [key] and calculates its long key ID */ - public fun tryGetId(key: PGPKey): KeyId? { - val keyRing = tryParseKeyring(key) ?: return null - return KeyId(keyRing.publicKey.keyID) - } - - /** - * Attempts to parse the given [PGPKey] into a [PGPKeyRing] and obtains the [UserId] of the - * corresponding public key. - */ - public fun tryGetEmail(key: PGPKey): UserId? { - val keyRing = tryParseKeyring(key) ?: return null - return UserId(keyRing.publicKey.userIDs.next()) - } - - /** - * Tests if the given [key] can be used for encryption, which is a bare minimum necessity for the - * app. - */ - public fun isKeyUsable(key: PGPKey): Boolean { - return runCatching { - val keyRing = tryParseKeyring(key) ?: return false - PGPainless.inspectKeyRing(keyRing).isUsableForEncryption - } - .get() != null - } -} diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPDecryptOptions.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPDecryptOptions.kt deleted file mode 100644 index 15ce92f0..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPDecryptOptions.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.passwordstore.crypto - -/** [CryptoOptions] implementation for PGPainless decrypt operations. */ -public class PGPDecryptOptions -private constructor( - private val values: Map, -) : CryptoOptions { - - override fun isOptionEnabled(option: String): Boolean { - return values.getOrDefault(option, false) - } - - /** Implementation of a builder pattern for [PGPDecryptOptions]. */ - public class Builder { - - /** Build the final [PGPDecryptOptions] object. */ - public fun build(): PGPDecryptOptions { - return PGPDecryptOptions(emptyMap()) - } - } -} diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPEncryptOptions.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPEncryptOptions.kt deleted file mode 100644 index 90de6b51..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPEncryptOptions.kt +++ /dev/null @@ -1,35 +0,0 @@ -package app.passwordstore.crypto - -/** [CryptoOptions] implementation for PGPainless encrypt operations. */ -public class PGPEncryptOptions -private constructor( - private val values: Map, -) : CryptoOptions { - - internal companion object { - const val ASCII_ARMOR = "ASCII_ARMOR" - } - - override fun isOptionEnabled(option: String): Boolean { - return values.getOrDefault(option, false) - } - - /** Implementation of a builder pattern for [PGPEncryptOptions]. */ - public class Builder { - private val optionsMap = mutableMapOf() - - /** - * Toggle whether the encryption operation output will be ASCII armored or in OpenPGP's binary - * format. - */ - public fun withAsciiArmor(enabled: Boolean): Builder { - optionsMap[ASCII_ARMOR] = enabled - return this - } - - /** Build the final [PGPEncryptOptions] object. */ - public fun build(): PGPEncryptOptions { - return PGPEncryptOptions(optionsMap) - } - } -} diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt deleted file mode 100644 index 98a1de2e..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.crypto - -import java.util.Locale -import java.util.regex.Pattern - -/** Supertype for valid identifiers of PGP keys. */ -public sealed class PGPIdentifier { - - /** A [PGPIdentifier] that represents either a long key ID or a fingerprint. */ - public data class KeyId(val id: Long) : PGPIdentifier() { - override fun toString(): String { - return convertKeyIdToHex(id) - } - - /** Convert a [Long] key ID to a formatted string. */ - private fun convertKeyIdToHex(keyId: Long): String { - return convertKeyIdToHex32bit(keyId shr HEX_32_BIT_COUNT) + convertKeyIdToHex32bit(keyId) - } - - /** - * Converts [keyId] to an unsigned [Long] then uses [java.lang.Long.toHexString] to convert it - * to a lowercase hex ID. - */ - private fun convertKeyIdToHex32bit(keyId: Long): String { - var hexString = java.lang.Long.toHexString(keyId and HEX_32_BITMASK).lowercase(Locale.ENGLISH) - while (hexString.length < HEX_32_STRING_LENGTH) { - hexString = "0$hexString" - } - return hexString - } - } - - /** - * A [PGPIdentifier] that represents the textual name/email combination corresponding to a key. - * Despite the [email] property in this class, the value is not guaranteed to be a valid email. - */ - public data class UserId(val email: String) : PGPIdentifier() { - override fun toString(): String { - return email - } - } - - public companion object { - private const val HEX_RADIX = 16 - private const val HEX_32_BIT_COUNT = 32 - private const val HEX_32_BITMASK = 0xffffffffL - private const val HEX_32_STRING_LENGTH = 8 - private const val TRUNCATED_FINGERPRINT_LENGTH = 16 - - /** - * Attempts to parse an untyped String identifier into a concrete subtype of [PGPIdentifier]. - */ - @Suppress("ReturnCount") - public fun fromString(identifier: String): PGPIdentifier? { - if (identifier.isEmpty()) return null - // Match long key IDs: - // FF22334455667788 or 0xFF22334455667788 - val maybeLongKeyId = - identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F\\d]{16}".toRegex()) } - if (maybeLongKeyId != null) { - val keyId = maybeLongKeyId.toULong(HEX_RADIX) - return KeyId(keyId.toLong()) - } - - // Match fingerprints: - // FF223344556677889900112233445566778899 or 0xFF223344556677889900112233445566778899 - val maybeFingerprint = - identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F\\d]{40}".toRegex()) } - if (maybeFingerprint != null) { - // Truncating to the long key ID is not a security issue since OpenKeychain only - // accepts - // non-ambiguous key IDs. - val keyId = maybeFingerprint.takeLast(TRUNCATED_FINGERPRINT_LENGTH).toULong(HEX_RADIX) - return KeyId(keyId.toLong()) - } - - return splitUserId(identifier)?.let { UserId(it) } - } - - private object UserIdRegex { - val PATTERN: Pattern = Pattern.compile("^(.*?)(?: \\((.*)\\))?(?: <(.*)>)?$") - const val NAME = 1 - const val EMAIL = 3 - } - - private object EmailRegex { - val PATTERN: Pattern = Pattern.compile("^\"]*@[^<>\"]*[.]?[^<>\"]*)\"?>?$") - const val EMAIL = 1 - } - - /** - * Takes a 'Name (Comment) ' user ID in any of its permutations and attempts to extract - * an email from it. - */ - @Suppress("NestedBlockDepth") - private fun splitUserId(userId: String): String? { - if (userId.isNotEmpty()) { - val matcher = UserIdRegex.PATTERN.matcher(userId) - if (matcher.matches()) { - var name = - if (matcher.group(UserIdRegex.NAME)?.isEmpty() == true) null - else matcher.group(UserIdRegex.NAME) - var email = matcher.group(UserIdRegex.EMAIL) - if (email != null && name != null) { - val emailMatcher = EmailRegex.PATTERN.matcher(name) - if (emailMatcher.matches() && email == emailMatcher.group(EmailRegex.EMAIL)) { - email = emailMatcher.group(EmailRegex.EMAIL) - name = null - } - } - if (email == null && name != null) { - val emailMatcher = EmailRegex.PATTERN.matcher(name) - if (emailMatcher.matches()) { - email = emailMatcher.group(EmailRegex.EMAIL) - } - } - return email - } - } - return null - } - } -} diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKey.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKey.kt deleted file mode 100644 index a33655d4..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKey.kt +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.crypto - -/** - * A simple value class wrapping over a [ByteArray] that can represent a PGP key. The implementation - * details of public/ private parts as well as identities are deferred to [PGPKeyManager]. - */ -@JvmInline public value class PGPKey(public val contents: ByteArray) diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt deleted file mode 100644 index aed1acf2..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -@file:Suppress("BlockingMethodInNonBlockingContext") - -package app.passwordstore.crypto - -import androidx.annotation.VisibleForTesting -import app.passwordstore.crypto.KeyUtils.isKeyUsable -import app.passwordstore.crypto.KeyUtils.tryGetId -import app.passwordstore.crypto.KeyUtils.tryParseKeyring -import app.passwordstore.crypto.errors.InvalidKeyException -import app.passwordstore.crypto.errors.KeyAlreadyExistsException -import app.passwordstore.crypto.errors.KeyDeletionFailedException -import app.passwordstore.crypto.errors.KeyDirectoryUnavailableException -import app.passwordstore.crypto.errors.KeyNotFoundException -import app.passwordstore.crypto.errors.NoKeysAvailableException -import app.passwordstore.crypto.errors.UnusableKeyException -import app.passwordstore.util.coroutines.runSuspendCatching -import com.github.michaelbull.result.Result -import com.github.michaelbull.result.unwrap -import java.io.File -import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.withContext -import org.bouncycastle.openpgp.PGPPublicKeyRing -import org.bouncycastle.openpgp.PGPSecretKeyRing -import org.pgpainless.PGPainless -import org.pgpainless.util.selection.userid.SelectUserId - -public class PGPKeyManager -@Inject -constructor( - filesDir: String, - private val dispatcher: CoroutineDispatcher, -) : KeyManager { - - private val keyDir = File(filesDir, KEY_DIR_NAME) - - /** @see KeyManager.addKey */ - override suspend fun addKey(key: PGPKey, replace: Boolean): Result = - withContext(dispatcher) { - runSuspendCatching { - if (!keyDirExists()) throw KeyDirectoryUnavailableException - val incomingKeyRing = tryParseKeyring(key) ?: throw InvalidKeyException - if (!isKeyUsable(key)) throw UnusableKeyException - val keyFile = File(keyDir, "${tryGetId(key)}.$KEY_EXTENSION") - if (keyFile.exists()) { - val existingKeyBytes = keyFile.readBytes() - val existingKeyRing = - tryParseKeyring(PGPKey(existingKeyBytes)) ?: throw InvalidKeyException - when { - existingKeyRing is PGPPublicKeyRing && incomingKeyRing is PGPSecretKeyRing -> { - keyFile.writeBytes(key.contents) - return@runSuspendCatching key - } - existingKeyRing is PGPPublicKeyRing && incomingKeyRing is PGPPublicKeyRing -> { - val updatedPublicKey = PGPainless.mergeCertificate(existingKeyRing, incomingKeyRing) - val keyBytes = PGPainless.asciiArmor(updatedPublicKey).encodeToByteArray() - keyFile.writeBytes(keyBytes) - return@runSuspendCatching key - } - } - // Check for replace flag first and if it is false, throw an error - if (!replace) - throw KeyAlreadyExistsException( - tryGetId(key)?.toString() ?: "Failed to retrieve key ID" - ) - if (!keyFile.delete()) throw KeyDeletionFailedException - } - - keyFile.writeBytes(key.contents) - - key - } - } - - /** @see KeyManager.removeKey */ - override suspend fun removeKey(identifier: PGPIdentifier): Result = - withContext(dispatcher) { - runSuspendCatching { - if (!keyDirExists()) throw KeyDirectoryUnavailableException - val key = getKeyById(identifier).unwrap() - val keyFile = File(keyDir, "${tryGetId(key)}.$KEY_EXTENSION") - if (keyFile.exists()) { - if (!keyFile.delete()) throw KeyDeletionFailedException - } - } - } - - /** @see KeyManager.getKeyById */ - override suspend fun getKeyById(id: PGPIdentifier): Result = - withContext(dispatcher) { - runSuspendCatching { - if (!keyDirExists()) throw KeyDirectoryUnavailableException - val keyFiles = keyDir.listFiles() - if (keyFiles.isNullOrEmpty()) throw NoKeysAvailableException - val keys = keyFiles.map { file -> PGPKey(file.readBytes()) } - - val matchResult = - when (id) { - is PGPIdentifier.KeyId -> { - val keyIdMatch = - keys - .map { key -> key to tryGetId(key) } - .firstOrNull { (_, keyId) -> keyId?.id == id.id } - keyIdMatch?.first - } - is PGPIdentifier.UserId -> { - val selector = SelectUserId.byEmail(id.email) - val userIdMatch = - keys - .map { key -> key to tryParseKeyring(key) } - .firstOrNull { (_, keyRing) -> selector.firstMatch(keyRing) != null } - userIdMatch?.first - } - } - - if (matchResult != null) { - return@runSuspendCatching matchResult - } - - throw KeyNotFoundException("$id") - } - } - - /** @see KeyManager.getAllKeys */ - override suspend fun getAllKeys(): Result, Throwable> = - withContext(dispatcher) { - runSuspendCatching { - if (!keyDirExists()) throw KeyDirectoryUnavailableException - val keyFiles = keyDir.listFiles() - if (keyFiles.isNullOrEmpty()) return@runSuspendCatching emptyList() - keyFiles.map { keyFile -> PGPKey(keyFile.readBytes()) }.toList() - } - } - - /** @see KeyManager.getKeyById */ - override suspend fun getKeyId(key: PGPKey): PGPIdentifier? = tryGetId(key) - - /** Checks if [keyDir] exists and attempts to create it if not. */ - private fun keyDirExists(): Boolean { - return keyDir.exists() || keyDir.mkdirs() - } - - public companion object { - - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal const val KEY_DIR_NAME: String = "keys" - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - internal const val KEY_EXTENSION: String = "key" - } -} diff --git a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt b/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt deleted file mode 100644 index a7087acf..00000000 --- a/crypto-pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.crypto - -import app.passwordstore.crypto.errors.CryptoHandlerException -import app.passwordstore.crypto.errors.IncorrectPassphraseException -import app.passwordstore.crypto.errors.NoKeysProvidedException -import app.passwordstore.crypto.errors.UnknownError -import com.github.michaelbull.result.Result -import com.github.michaelbull.result.mapError -import com.github.michaelbull.result.runCatching -import java.io.InputStream -import java.io.OutputStream -import javax.inject.Inject -import org.bouncycastle.openpgp.PGPPublicKeyRing -import org.bouncycastle.openpgp.PGPPublicKeyRingCollection -import org.bouncycastle.openpgp.PGPSecretKeyRing -import org.bouncycastle.openpgp.PGPSecretKeyRingCollection -import org.bouncycastle.util.io.Streams -import org.pgpainless.PGPainless -import org.pgpainless.decryption_verification.ConsumerOptions -import org.pgpainless.encryption_signing.EncryptionOptions -import org.pgpainless.encryption_signing.ProducerOptions -import org.pgpainless.exception.WrongPassphraseException -import org.pgpainless.key.protection.SecretKeyRingProtector -import org.pgpainless.util.Passphrase - -public class PGPainlessCryptoHandler @Inject constructor() : - CryptoHandler { - - /** - * Decrypts the given [ciphertextStream] using [PGPainless] and writes the decrypted output to - * [outputStream]. The provided [passphrase] is wrapped in a [SecretKeyRingProtector] and the - * [keys] argument is defensively checked to ensure it has at least one key present. - * - * @see CryptoHandler.decrypt - */ - public override fun decrypt( - keys: List, - passphrase: String, - ciphertextStream: InputStream, - outputStream: OutputStream, - options: PGPDecryptOptions, - ): Result = - runCatching { - if (keys.isEmpty()) { - throw NoKeysProvidedException - } - val keyringCollection = - keys - .map { key -> PGPainless.readKeyRing().secretKeyRing(key.contents) } - .run(::PGPSecretKeyRingCollection) - val protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(passphrase)) - val decryptionStream = - PGPainless.decryptAndOrVerify() - .onInputStream(ciphertextStream) - .withOptions( - ConsumerOptions() - .addDecryptionKeys(keyringCollection, protector) - .addDecryptionPassphrase(Passphrase.fromPassword(passphrase)) - ) - Streams.pipeAll(decryptionStream, outputStream) - decryptionStream.close() - keyringCollection.forEach { keyRing -> - check(decryptionStream.metadata.isEncryptedFor(keyRing)) { - "Stream should be encrypted for ${keyRing.secretKey.keyID} but wasn't" - } - } - return@runCatching - } - .mapError { error -> - when (error) { - is WrongPassphraseException -> IncorrectPassphraseException(error) - is CryptoHandlerException -> error - else -> UnknownError(error) - } - } - - /** - * Encrypts the provided [plaintextStream] and writes the encrypted output to [outputStream]. The - * [keys] argument is defensively checked to contain at least one key. - * - * @see CryptoHandler.encrypt - */ - public override fun encrypt( - keys: List, - plaintextStream: InputStream, - outputStream: OutputStream, - options: PGPEncryptOptions, - ): Result = - runCatching { - if (keys.isEmpty()) { - throw NoKeysProvidedException - } - val publicKeyRings = - keys.mapNotNull(KeyUtils::tryParseKeyring).mapNotNull { keyRing -> - when (keyRing) { - is PGPPublicKeyRing -> keyRing - is PGPSecretKeyRing -> PGPainless.extractCertificate(keyRing) - else -> null - } - } - require(keys.size == publicKeyRings.size) { - "Failed to parse all keys: ${keys.size} keys were provided but only ${publicKeyRings.size} were valid" - } - if (publicKeyRings.isEmpty()) { - throw NoKeysProvidedException - } - val publicKeyRingCollection = PGPPublicKeyRingCollection(publicKeyRings) - val encryptionOptions = EncryptionOptions().addRecipients(publicKeyRingCollection) - val producerOptions = - ProducerOptions.encrypt(encryptionOptions) - .setAsciiArmor(options.isOptionEnabled(PGPEncryptOptions.ASCII_ARMOR)) - val encryptionStream = - PGPainless.encryptAndOrSign().onOutputStream(outputStream).withOptions(producerOptions) - Streams.pipeAll(plaintextStream, encryptionStream) - encryptionStream.close() - val result = encryptionStream.result - publicKeyRingCollection.forEach { keyRing -> - require(result.isEncryptedFor(keyRing)) { - "Stream should be encrypted for ${keyRing.publicKey.keyID} but wasn't" - } - } - } - .mapError { error -> - when (error) { - is CryptoHandlerException -> error - else -> UnknownError(error) - } - } - - /** Runs a naive check on the extension for the given [fileName] to check if it is a PGP file. */ - public override fun canHandle(fileName: String): Boolean { - return fileName.substringAfterLast('.', "") == "gpg" - } -} diff --git a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/CryptoConstants.kt b/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/CryptoConstants.kt deleted file mode 100644 index d827e169..00000000 --- a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/CryptoConstants.kt +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.crypto - -object CryptoConstants { - const val KEY_PASSPHRASE = "hunter2" - const val PLAIN_TEXT = "encryption worthy content" - const val KEY_NAME = "John Doe" - const val KEY_EMAIL = "john.doe@example.com" - const val KEY_ID = 0x08edf7567183ce27 -} diff --git a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt b/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt deleted file mode 100644 index a0f84402..00000000 --- a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -package app.passwordstore.crypto - -import app.passwordstore.crypto.KeyUtils.isKeyUsable -import app.passwordstore.crypto.KeyUtils.tryGetId -import app.passwordstore.crypto.KeyUtils.tryParseKeyring -import app.passwordstore.crypto.TestUtils.AllKeys -import app.passwordstore.crypto.TestUtils.getArmoredSecretKeyWithMultipleIdentities -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertNotNull -import org.bouncycastle.openpgp.PGPSecretKeyRing - -class KeyUtilsTest { - @Test - fun parseKeyWithMultipleIdentities() { - val key = PGPKey(getArmoredSecretKeyWithMultipleIdentities()) - val keyring = tryParseKeyring(key) - assertNotNull(keyring) - assertIs(keyring) - val keyId = tryGetId(key) - assertNotNull(keyId) - assertIs(keyId) - assertEquals("b950ae2813841585", keyId.toString()) - } - - @Test - fun isKeyUsable() { - val params = AllKeys.entries.map { it to (it != AllKeys.AEAD_PUB && it != AllKeys.AEAD_SEC) } - params.forEach { (allKeys, isUsable) -> - val key = PGPKey(allKeys.keyMaterial) - assertEquals(isUsable, isKeyUsable(key), "${allKeys.name} failed expectation:") - } - } -} diff --git a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPIdentifierTest.kt b/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPIdentifierTest.kt deleted file mode 100644 index efc6e0ba..00000000 --- a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPIdentifierTest.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.crypto - -import kotlin.test.Test -import kotlin.test.assertNotNull -import kotlin.test.assertTrue - -class PGPIdentifierTest { - - @Test - fun parseHexKeyIdWithout0xPrefix() { - val identifier = PGPIdentifier.fromString("79E8208280490C77") - assertNotNull(identifier) - assertTrue { identifier is PGPIdentifier.KeyId } - } - - @Test - fun parseHexKeyId() { - val identifier = PGPIdentifier.fromString("0x79E8208280490C77") - assertNotNull(identifier) - assertTrue { identifier is PGPIdentifier.KeyId } - } - - @Test - fun parseValidEmail() { - val identifier = PGPIdentifier.fromString("john.doe@example.org") - assertNotNull(identifier) - assertTrue { identifier is PGPIdentifier.UserId } - } - - @Test - fun parseEmailWithoutTLD() { - val identifier = PGPIdentifier.fromString("john.doe@example") - assertNotNull(identifier) - assertTrue { identifier is PGPIdentifier.UserId } - } -} diff --git a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt b/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt deleted file mode 100644 index 85cf8e1b..00000000 --- a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt +++ /dev/null @@ -1,236 +0,0 @@ -package app.passwordstore.crypto - -import app.passwordstore.crypto.KeyUtils.tryGetId -import app.passwordstore.crypto.PGPIdentifier.KeyId -import app.passwordstore.crypto.PGPIdentifier.UserId -import app.passwordstore.crypto.errors.KeyAlreadyExistsException -import app.passwordstore.crypto.errors.KeyNotFoundException -import app.passwordstore.crypto.errors.NoKeysAvailableException -import app.passwordstore.crypto.errors.UnusableKeyException -import com.github.michaelbull.result.Err -import com.github.michaelbull.result.Ok -import com.github.michaelbull.result.unwrap -import com.github.michaelbull.result.unwrapError -import java.io.File -import kotlin.test.Test -import kotlin.test.assertContentEquals -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertNotEquals -import kotlin.test.assertNotNull -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Rule -import org.junit.rules.TemporaryFolder - -class PGPKeyManagerTest { - - @get:Rule val temporaryFolder: TemporaryFolder = TemporaryFolder() - private val dispatcher = StandardTestDispatcher() - private val scope = TestScope(dispatcher) - private val filesDir by unsafeLazy { temporaryFolder.root } - private val keysDir by unsafeLazy { File(filesDir, PGPKeyManager.KEY_DIR_NAME) } - private val keyManager by unsafeLazy { PGPKeyManager(filesDir.absolutePath, dispatcher) } - private val secretKey = PGPKey(TestUtils.getArmoredSecretKey()) - private val publicKey = PGPKey(TestUtils.getArmoredPublicKey()) - - private fun unsafeLazy(initializer: () -> T) = - lazy(LazyThreadSafetyMode.NONE) { initializer.invoke() } - - @Test - fun addKey() = - runTest(dispatcher) { - // Check if the key id returned is correct - val keyId = keyManager.getKeyId(keyManager.addKey(secretKey).unwrap()) - assertEquals(KeyId(CryptoConstants.KEY_ID), keyId) - // Check if the keys directory have one file - assertEquals(1, filesDir.list()?.size) - // Check if the file name is correct - val keyFile = keysDir.listFiles()?.first() - assertEquals(keyFile?.name, "$keyId.${PGPKeyManager.KEY_EXTENSION}") - } - - @Test - fun addKeyWithoutReplaceFlag() = - runTest(dispatcher) { - // Check adding the keys twice - keyManager.addKey(secretKey, false).unwrap() - val error = keyManager.addKey(secretKey, false).unwrapError() - - assertIs(error) - } - - @Test - fun addKeyWithReplaceFlag() = - runTest(dispatcher) { - // Check adding the keys twice - keyManager.addKey(secretKey, true).unwrap() - val keyId = keyManager.getKeyId(keyManager.addKey(secretKey, true).unwrap()) - - assertEquals(KeyId(CryptoConstants.KEY_ID), keyId) - } - - @Test - fun addKeyWithUnusableKey() = - runTest(dispatcher) { - val error = keyManager.addKey(PGPKey(TestUtils.getAEADSecretKey())).unwrapError() - assertEquals(UnusableKeyException, error) - } - - @Test - fun removeKey() = - runTest(dispatcher) { - // Add key using KeyManager - keyManager.addKey(secretKey).unwrap() - // Remove key - keyManager.removeKey(tryGetId(secretKey)!!).unwrap() - // Check that no keys remain - val keys = keyManager.getAllKeys().unwrap() - assertEquals(0, keys.size) - } - - @Test - fun getKeyById() = - runTest(dispatcher) { - // Add key using KeyManager - keyManager.addKey(secretKey).unwrap() - val keyId = keyManager.getKeyId(secretKey) - assertNotNull(keyId) - assertEquals(KeyId(CryptoConstants.KEY_ID), keyManager.getKeyId(secretKey)) - // Check returned key id matches the expected id and the created key id - val returnedKey = keyManager.getKeyById(keyId).unwrap() - assertEquals(keyManager.getKeyId(secretKey), keyManager.getKeyId(returnedKey)) - } - - @Test - fun getKeyByFullUserId() = - runTest(dispatcher) { - keyManager.addKey(secretKey).unwrap() - val keyId = "${CryptoConstants.KEY_NAME} <${CryptoConstants.KEY_EMAIL}>" - val returnedKey = keyManager.getKeyById(UserId(keyId)).unwrap() - assertEquals(keyManager.getKeyId(secretKey), keyManager.getKeyId(returnedKey)) - } - - @Test - fun getKeyByEmailUserId() = - runTest(dispatcher) { - keyManager.addKey(secretKey).unwrap() - val keyId = CryptoConstants.KEY_EMAIL - val returnedKey = keyManager.getKeyById(UserId(keyId)).unwrap() - assertEquals(keyManager.getKeyId(secretKey), keyManager.getKeyId(returnedKey)) - } - - @Test - fun getNonExistentKey() = - runTest(dispatcher) { - // Add key using KeyManager - keyManager.addKey(secretKey).unwrap() - val keyId = KeyId(0x08edf7567183ce44) - // Check returned key - val error = keyManager.getKeyById(keyId).unwrapError() - assertIs(error) - assertEquals("No key found with id: $keyId", error.message) - } - - @Test - fun findNonExistentKey() = - runTest(dispatcher) { - // Check returned key - val error = keyManager.getKeyById(KeyId(0x08edf7567183ce44)).unwrapError() - assertIs(error) - assertEquals("No keys were found", error.message) - } - - @Test - fun getAllKeys() = - runTest(dispatcher) { - // Check if KeyManager returns no key - val noKeyList = keyManager.getAllKeys().unwrap() - assertEquals(0, noKeyList.size) - // Add key using KeyManager - keyManager.addKey(secretKey).unwrap() - keyManager.addKey(PGPKey(TestUtils.getArmoredSecretKeyWithMultipleIdentities())).unwrap() - // Check if KeyManager returns one key - val singleKeyList = keyManager.getAllKeys().unwrap() - assertEquals(2, singleKeyList.size) - } - - @Test - fun getMultipleIdentityKeyWithAllIdentities() = - runTest(dispatcher) { - val key = PGPKey(TestUtils.getArmoredSecretKeyWithMultipleIdentities()) - keyManager.addKey(key).unwrap() - val johnKey = keyManager.getKeyById(UserId("john@doe.org")).unwrap() - val janeKey = keyManager.getKeyById(UserId("jane@doe.org")).unwrap() - - assertContentEquals(johnKey.contents, janeKey.contents) - } - - @Test - fun replaceSecretKeyWithPublicKey() = - runTest(dispatcher) { - assertIs>(keyManager.addKey(secretKey)) - assertIs>(keyManager.addKey(publicKey)) - } - - @Test - fun replacePublicKeyWithSecretKey() = - runTest(dispatcher) { - assertIs>(keyManager.addKey(publicKey)) - assertIs>(keyManager.addKey(secretKey)) - } - - @Test - fun replacePublicKeyWithPublicKey() = - runTest(dispatcher) { - assertIs>(keyManager.addKey(publicKey)) - assertIs>(keyManager.addKey(publicKey)) - val allKeys = keyManager.getAllKeys() - assertIs>>(allKeys) - assertEquals(1, allKeys.value.size) - val key = allKeys.value[0] - assertContentEquals(publicKey.contents, key.contents) - } - - @Test - fun replaceSecretKeyWithSecretKey() = - runTest(dispatcher) { - assertIs>(keyManager.addKey(secretKey)) - assertIs>(keyManager.addKey(secretKey)) - } - - @Test - fun addMultipleKeysWithSameEmail() = - runTest(dispatcher) { - val alice = - PGPKey(this::class.java.classLoader.getResource("alice_owner@example_com")!!.readBytes()) - val bobby = - PGPKey(this::class.java.classLoader.getResource("bobby_owner@example_com")!!.readBytes()) - assertIs>(keyManager.addKey(alice)) - assertIs>(keyManager.addKey(bobby)) - - keyManager.getAllKeys().apply { - assertIs>>(this) - assertEquals(2, this.value.size) - } - val longKeyIds = - arrayOf( - KeyId(-7087927403306410599), // Alice - KeyId(-961222705095032109), // Bobby - ) - val userIds = - arrayOf( - UserId("Alice "), - UserId("Bobby "), - ) - - for (idCollection in arrayOf(longKeyIds, userIds)) { - val alice1 = keyManager.getKeyById(idCollection[0]) - val bobby1 = keyManager.getKeyById(idCollection[1]) - assertIs>(alice1) - assertIs>(bobby1) - assertNotEquals(alice1.value.contents, bobby1.value.contents) - } - } -} diff --git a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt b/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt deleted file mode 100644 index 8bf6ba1e..00000000 --- a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt +++ /dev/null @@ -1,148 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -@file:Suppress("JUnitMalformedDeclaration") // The test runner takes care of it - -package app.passwordstore.crypto - -import app.passwordstore.crypto.CryptoConstants.KEY_PASSPHRASE -import app.passwordstore.crypto.CryptoConstants.PLAIN_TEXT -import app.passwordstore.crypto.errors.IncorrectPassphraseException -import com.github.michaelbull.result.Err -import com.github.michaelbull.result.Ok -import com.github.michaelbull.result.getError -import com.google.testing.junit.testparameterinjector.TestParameter -import com.google.testing.junit.testparameterinjector.TestParameterInjector -import java.io.ByteArrayOutputStream -import kotlin.test.Test -import kotlin.test.assertContains -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertIs -import kotlin.test.assertTrue -import org.junit.runner.RunWith -import org.pgpainless.PGPainless -import org.pgpainless.decryption_verification.MessageInspector - -@Suppress("Unused") // Test runner handles it internally -enum class EncryptionKey(val keySet: List) { - PUBLIC(listOf(PGPKey(TestUtils.getArmoredPublicKey()))), - SECRET(listOf(PGPKey(TestUtils.getArmoredSecretKey()))), - ALL(listOf(PGPKey(TestUtils.getArmoredPublicKey()), PGPKey(TestUtils.getArmoredSecretKey()))), -} - -@RunWith(TestParameterInjector::class) -class PGPainlessCryptoHandlerTest { - - private val cryptoHandler = PGPainlessCryptoHandler() - private val secretKey = PGPKey(TestUtils.getArmoredSecretKey()) - - @Test - fun encryptAndDecrypt(@TestParameter encryptionKey: EncryptionKey) { - val ciphertextStream = ByteArrayOutputStream() - val encryptRes = - cryptoHandler.encrypt( - encryptionKey.keySet, - PLAIN_TEXT.byteInputStream(Charsets.UTF_8), - ciphertextStream, - PGPEncryptOptions.Builder().build(), - ) - assertIs>(encryptRes) - val plaintextStream = ByteArrayOutputStream() - val decryptRes = - cryptoHandler.decrypt( - listOf(secretKey), - KEY_PASSPHRASE, - ciphertextStream.toByteArray().inputStream(), - plaintextStream, - PGPDecryptOptions.Builder().build(), - ) - assertIs>(decryptRes) - assertEquals(PLAIN_TEXT, plaintextStream.toString(Charsets.UTF_8)) - } - - @Test - fun decryptWithWrongPassphrase(@TestParameter encryptionKey: EncryptionKey) { - val ciphertextStream = ByteArrayOutputStream() - val encryptRes = - cryptoHandler.encrypt( - encryptionKey.keySet, - PLAIN_TEXT.byteInputStream(Charsets.UTF_8), - ciphertextStream, - PGPEncryptOptions.Builder().build(), - ) - assertIs>(encryptRes) - val plaintextStream = ByteArrayOutputStream() - val result = - cryptoHandler.decrypt( - listOf(secretKey), - "very incorrect passphrase", - ciphertextStream.toByteArray().inputStream(), - plaintextStream, - PGPDecryptOptions.Builder().build(), - ) - assertIs>(result) - assertIs(result.getError()) - } - - @Test - fun encryptAsciiArmored(@TestParameter encryptionKey: EncryptionKey) { - val ciphertextStream = ByteArrayOutputStream() - val encryptRes = - cryptoHandler.encrypt( - encryptionKey.keySet, - PLAIN_TEXT.byteInputStream(Charsets.UTF_8), - ciphertextStream, - PGPEncryptOptions.Builder().withAsciiArmor(true).build(), - ) - assertIs>(encryptRes) - val ciphertext = ciphertextStream.toString(Charsets.UTF_8) - assertContains(ciphertext, "Version: PGPainless") - assertContains(ciphertext, "-----BEGIN PGP MESSAGE-----") - assertContains(ciphertext, "-----END PGP MESSAGE-----") - } - - @Test - fun encryptMultiple() { - val alice = - PGPainless.generateKeyRing().modernKeyRing("Alice ", KEY_PASSPHRASE) - val bob = PGPainless.generateKeyRing().modernKeyRing("Bob ", KEY_PASSPHRASE) - val aliceKey = PGPKey(PGPainless.asciiArmor(alice).encodeToByteArray()) - val bobKey = PGPKey(PGPainless.asciiArmor(bob).encodeToByteArray()) - val ciphertextStream = ByteArrayOutputStream() - val encryptRes = - cryptoHandler.encrypt( - listOf(aliceKey, bobKey), - PLAIN_TEXT.byteInputStream(Charsets.UTF_8), - ciphertextStream, - PGPEncryptOptions.Builder().withAsciiArmor(true).build(), - ) - assertIs>(encryptRes) - val message = ciphertextStream.toByteArray().decodeToString() - val info = MessageInspector.determineEncryptionInfoForMessage(message) - assertTrue(info.isEncrypted) - assertEquals(2, info.keyIds.size) - assertFalse(info.isSignedOnly) - for (key in listOf(aliceKey, bobKey)) { - val ciphertextStreamCopy = message.byteInputStream() - val plaintextStream = ByteArrayOutputStream() - val res = - cryptoHandler.decrypt( - listOf(key), - KEY_PASSPHRASE, - ciphertextStreamCopy, - plaintextStream, - PGPDecryptOptions.Builder().build(), - ) - assertIs>(res) - } - } - - @Test - fun canHandleFiltersFormats() { - assertFalse { cryptoHandler.canHandle("example.com") } - assertTrue { cryptoHandler.canHandle("example.com.gpg") } - assertFalse { cryptoHandler.canHandle("example.com.asc") } - } -} diff --git a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/TestUtils.kt b/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/TestUtils.kt deleted file mode 100644 index 90b98ac9..00000000 --- a/crypto-pgpainless/src/test/kotlin/app/passwordstore/crypto/TestUtils.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -@file:Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") - -package app.passwordstore.crypto - -object TestUtils { - fun getArmoredSecretKey() = this::class.java.classLoader.getResource("secret_key").readBytes() - - fun getArmoredPublicKey() = this::class.java.classLoader.getResource("public_key").readBytes() - - fun getArmoredSecretKeyWithMultipleIdentities() = - this::class.java.classLoader.getResource("secret_key_multiple_identities").readBytes() - - fun getArmoredPublicKeyWithMultipleIdentities() = - this::class.java.classLoader.getResource("public_key_multiple_identities").readBytes() - - fun getAEADPublicKey() = this::class.java.classLoader.getResource("aead_pub").readBytes() - - fun getAEADSecretKey() = this::class.java.classLoader.getResource("aead_sec").readBytes() - - enum class AllKeys(val keyMaterial: ByteArray) { - ARMORED_SEC(getArmoredSecretKey()), - ARMORED_PUB(getArmoredPublicKey()), - MULTIPLE_IDENTITIES_SEC(getArmoredSecretKeyWithMultipleIdentities()), - MULTIPLE_IDENTITIES_PUB(getArmoredPublicKeyWithMultipleIdentities()), - AEAD_SEC(getAEADSecretKey()), - AEAD_PUB(getAEADPublicKey()), - } -} diff --git a/crypto-pgpainless/src/test/resources/aead_pub b/crypto-pgpainless/src/test/resources/aead_pub deleted file mode 100644 index f6ae1e82..00000000 --- a/crypto-pgpainless/src/test/resources/aead_pub +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -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 -=lwhh ------END PGP PUBLIC KEY BLOCK----- diff --git a/crypto-pgpainless/src/test/resources/aead_sec b/crypto-pgpainless/src/test/resources/aead_sec deleted file mode 100644 index 8e13ac0e..00000000 --- a/crypto-pgpainless/src/test/resources/aead_sec +++ /dev/null @@ -1,107 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- - -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 -iHcmZE4pREmRPz9CinzsQXCm5yqJkucSfXtqn4c= -=m5E4 ------END PGP PRIVATE KEY BLOCK----- diff --git a/crypto-pgpainless/src/test/resources/alice_owner@example_com b/crypto-pgpainless/src/test/resources/alice_owner@example_com deleted file mode 100644 index d2612b0d..00000000 --- a/crypto-pgpainless/src/test/resources/alice_owner@example_com +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- - -lIYEY2to0BYJKwYBBAHaRw8BAQdAuI+Z2XyvQv6qBnA06ZAoKArgfMXFN783oYWl -Vh1DqpT+BwMCSqjdffS3e+X/Kfnv30tVSSrb8j2nX2C+P0ODVvS7xWs8MG8TN33d -NJXWUkfct513yADC520EL2KpXPU6GThIxsYmxBXPdyBb3CAiQQDJWLQZQWxpY2Ug -PG93bmVyQGV4YW1wbGUuY29tPoiQBBMWCAA4FiEEMqSK1ESF5td0+gNunaKflIQq -HZkFAmNraNACGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQnaKflIQqHZk6 -sAD/Xx8MQbtXcKPJi/0UGXkyRHEYbcC+zzhECFalHsQsNh0A/3Naih9zRixXt3v0 -JVCv7fbKaXpfKdGi8tj9muSa+5QBnIsEY2to0BIKKwYBBAGXVQEFAQEHQJqcQkrg -shQO4tyyshE9Ng74LAdu4zRD/yb9aQet+61BAwEIB/4HAwIEJ6OJJ8OAFf/0IyRK -Frmj2UHklu1UT1P2JPF5RPzgPlAxZB1eGHFcCQoJX/ro2AsbQ5KZUwraSs1QgX5b -GKAdcyJqFwtbz+pkTpBvOWS6iHgEGBYIACAWIQQypIrURIXm13T6A26dop+UhCod -mQUCY2to0AIbDAAKCRCdop+UhCodmYTSAP0U5Q6clPUsFcjIcwKA+x5G1Q+wzODx -7/pUS2Vg+cKOMAEAuY5wW5k0eCuWMC/uzXy8l2a3BwsMN3nlApuGk0zOcwM= -=ApnA ------END PGP PRIVATE KEY BLOCK----- diff --git a/crypto-pgpainless/src/test/resources/bobby_owner@example_com b/crypto-pgpainless/src/test/resources/bobby_owner@example_com deleted file mode 100644 index 6bd548e7..00000000 --- a/crypto-pgpainless/src/test/resources/bobby_owner@example_com +++ /dev/null @@ -1,16 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- - -lIYEY2tpShYJKwYBBAHaRw8BAQdA0syygn/sjv82T226XDe7ZmsJ897HQ88pruR6 -uMSdtYP+BwMCAaOsMuRJoq3/eHOl9Df1jlr3zfIBdw0hQrmcJ2qVOS4xQGDegjLW -Bbqnmjw7cCRUN1knjHdMWYwrnm8G9YmhOhhwHwdmhxw/LJOA00SyVLQZQm9iYnkg -PG93bmVyQGV4YW1wbGUuY29tPoiQBBMWCAA4FiEEPnwcCqistth5tnEB8qkNCuDF -QtMFAmNraUoCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ8qkNCuDFQtOx -NAD+MzrKYoQQxgLWkqf08Jhc58sa+2xeZBI3Sq0o+huMql0A/39hfDlJnzD61gEZ -vwOeZuTNb+LH23ha2uG8UpoMx4oLnIsEY2tpShIKKwYBBAGXVQEFAQEHQIVF4gjr -Wpw7KN/IYZQdml9Rn7zlBMsXNIxXhcMVjxlZAwEIB/4HAwJenw1L/ZS8WP+e3uzg -khxkk1dQ3fZbTaR90z0wzLDGngVJO1J2XmfIPnTeU8conEeak8Yyt8+85QdM9MK0 -ch0MyhEa/8hRgtCL8Fo3XkLKiHgEGBYIACAWIQQ+fBwKqKy22Hm2cQHyqQ0K4MVC -0wUCY2tpSgIbDAAKCRDyqQ0K4MVC04+aAQDEW/aasrpOYw35DIddH/Wp4tSrWi65 -kv18HvDPl/c6KwEAw6ZxYsfWmxMtzY6efTIzVnvb4T3OZEVWG6XetZoDTAI= -=OFsg ------END PGP PRIVATE KEY BLOCK----- diff --git a/crypto-pgpainless/src/test/resources/public_key b/crypto-pgpainless/src/test/resources/public_key deleted file mode 100644 index 987bac6f..00000000 --- a/crypto-pgpainless/src/test/resources/public_key +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: PGPainless -Comment: BC98 82EF 93DC 22F8 D7D4 47AD 08ED F756 7183 CE27 -Comment: John Doe - -mDMEYT33+BYJKwYBBAHaRw8BAQdAoofwCvOfKJ4pGxEO4s64wFD+QnePpNY5zXgW -TTOFb2+0H0pvaG4gRG9lIDxqb2huLmRvZUBleGFtcGxlLmNvbT6IeAQTFgoAIAUC -YT33+AIbAQUWAgMBAAQLCQgHBRUKCQgLAh4BAhkBAAoJEAjt91Zxg84n5dYA/AiA -BqBdt2ItWgDPLCNEqt9wIMgRpkDrAMtXXyyLSkWsAQCoowpenGsq5fxhuRcS3w6Q -s+/Qw1GqnoidxhioR9J+ALg4BGE99/gSCisGAQQBl1UBBQEBB0C7eFVsFUif4q9S -taBI6JAwsI+hQSAo3I6V4jU3rix8XwMBCAeIdQQYFgoAHQUCYT33+AIbDAUWAgMB -AAQLCQgHBRUKCQgLAh4BAAoJEAjt91Zxg84nmn4BALmD8WYxTdrJqUZUE1TcFvzG -5r0//rPM8Vut5X+KwUXjAQDWVP22KaA8VXpevSxkS3n/ti0KjQVKEFzGbmwB2dTT -CbgzBGE99/gWCSsGAQQB2kcPAQEHQJXfqDjCO9L4qBu62/UPpQ5q0638kG8+AGf/ -hJH2q2BTiNUEGBYKAH0FAmE99/gCGwIFFgIDAQAECwkIBwUVCgkICwIeAV8gBBkW -CgAGBQJhPff4AAoJEGSLoii3QC8mrhcBALzpJQTHF8cJJRA9+DQ3qZ85Eu217MJi -x1aYA1i0zyP5AQD/jN/aBsSTqAHF+zU8/ezzHeoilyBYgxLS9Q2qelDeDAAKCRAI -7fdWcYPOJ7aHAP9EBq0rzV3c6GtVl8bPnk+llpV/1aodxTSnijQtVSMuMAD+JMUD -Jd2bimlhuVwpu0DFiF7IF64SAxmVifTwsTWYiQs= -=jGlC ------END PGP PUBLIC KEY BLOCK----- diff --git a/crypto-pgpainless/src/test/resources/public_key_multiple_identities b/crypto-pgpainless/src/test/resources/public_key_multiple_identities deleted file mode 100644 index 2cc896c2..00000000 --- a/crypto-pgpainless/src/test/resources/public_key_multiple_identities +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- - -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 -GorvBkqXcIR7 -=dL2N ------END PGP PUBLIC KEY BLOCK----- diff --git a/crypto-pgpainless/src/test/resources/secret_key b/crypto-pgpainless/src/test/resources/secret_key deleted file mode 100644 index 61334b01..00000000 --- a/crypto-pgpainless/src/test/resources/secret_key +++ /dev/null @@ -1,26 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: PGPainless -Comment: BC98 82EF 93DC 22F8 D7D4 47AD 08ED F756 7183 CE27 -Comment: John Doe - -lIYEYT33+BYJKwYBBAHaRw8BAQdAoofwCvOfKJ4pGxEO4s64wFD+QnePpNY5zXgW -TTOFb2/+CQMCh3Bp60ThtX9g8u+uxtuLdeeU5UC14Ox4zVD/x2L7sUzN94XVocOn -WVJTIgeZ1CBhrsSOMg5grj0Zwf1YODlBpZ85V8stPebpjZ2mCZUz1rQfSm9obiBE -b2UgPGpvaG4uZG9lQGV4YW1wbGUuY29tPoh4BBMWCgAgBQJhPff4AhsBBRYCAwEA -BAsJCAcFFQoJCAsCHgECGQEACgkQCO33VnGDzifl1gD8CIAGoF23Yi1aAM8sI0Sq -33AgyBGmQOsAy1dfLItKRawBAKijCl6cayrl/GG5FxLfDpCz79DDUaqeiJ3GGKhH -0n4AnIsEYT33+BIKKwYBBAGXVQEFAQEHQLt4VWwVSJ/ir1K1oEjokDCwj6FBICjc -jpXiNTeuLHxfAwEIB/4JAwKHcGnrROG1f2AcnEUWhC2rDrztJB3JK7pe+PVJbMaK -O2eYKLiBZOT6Dy1rexMi0vS19IMYLf1V2qgsO9phoglOD+m95tr8Ha9FhfbpJjua -iHUEGBYKAB0FAmE99/gCGwwFFgIDAQAECwkIBwUVCgkICwIeAQAKCRAI7fdWcYPO -J5p+AQC5g/FmMU3ayalGVBNU3Bb8xua9P/6zzPFbreV/isFF4wEA1lT9timgPFV6 -Xr0sZEt5/7YtCo0FShBcxm5sAdnU0wmchgRhPff4FgkrBgEEAdpHDwEBB0CV36g4 -wjvS+Kgbutv1D6UOatOt/JBvPgBn/4SR9qtgU/4JAwKHcGnrROG1f2A1hnm2UXZL -Go/tPJo3pJCJDLClIKi7I5RoHruafuQ2ODvznLbCnbuft9B2cA5MZUMFCk6nBvoU -k6hwGWxOSNJIOmrCx+PMiNUEGBYKAH0FAmE99/gCGwIFFgIDAQAECwkIBwUVCgkI -CwIeAV8gBBkWCgAGBQJhPff4AAoJEGSLoii3QC8mrhcBALzpJQTHF8cJJRA9+DQ3 -qZ85Eu217MJix1aYA1i0zyP5AQD/jN/aBsSTqAHF+zU8/ezzHeoilyBYgxLS9Q2q -elDeDAAKCRAI7fdWcYPOJ7aHAP9EBq0rzV3c6GtVl8bPnk+llpV/1aodxTSnijQt -VSMuMAD+JMUDJd2bimlhuVwpu0DFiF7IF64SAxmVifTwsTWYiQs= -=/dDf ------END PGP PRIVATE KEY BLOCK----- diff --git a/crypto-pgpainless/src/test/resources/secret_key_multiple_identities b/crypto-pgpainless/src/test/resources/secret_key_multiple_identities deleted file mode 100644 index 5da8ac81..00000000 --- a/crypto-pgpainless/src/test/resources/secret_key_multiple_identities +++ /dev/null @@ -1,93 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- - -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 -hmaBvZKKiR6/QRMyZMQ34dx9GorvBkqXcIR7 -=8IuC ------END PGP PRIVATE KEY BLOCK----- diff --git a/crypto/common/build.gradle.kts b/crypto/common/build.gradle.kts new file mode 100644 index 00000000..110664bd --- /dev/null +++ b/crypto/common/build.gradle.kts @@ -0,0 +1,7 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +plugins { id("com.github.android-password-store.kotlin-jvm-library") } + +dependencies { implementation(libs.thirdparty.kotlinResult) } diff --git a/crypto/common/lint-baseline.xml b/crypto/common/lint-baseline.xml new file mode 100644 index 00000000..2ed8a3bb --- /dev/null +++ b/crypto/common/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt b/crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt new file mode 100644 index 00000000..898cf058 --- /dev/null +++ b/crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoHandler.kt @@ -0,0 +1,44 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.crypto + +import app.passwordstore.crypto.errors.CryptoHandlerException +import com.github.michaelbull.result.Result +import java.io.InputStream +import java.io.OutputStream + +/** Generic interface to implement cryptographic operations on top of. */ +public interface CryptoHandler { + + /** + * Decrypt the given [ciphertextStream] using a set of potential [keys] and [passphrase], and + * writes the resultant plaintext to [outputStream]. The returned [Result] should be checked to + * ensure it is **not** an instance of [com.github.michaelbull.result.Err] before the contents of + * [outputStream] are used. + */ + public fun decrypt( + keys: List, + passphrase: String, + ciphertextStream: InputStream, + outputStream: OutputStream, + options: DecryptOpts, + ): Result + + /** + * Encrypt the given [plaintextStream] to the provided [keys], and writes the encrypted ciphertext + * to [outputStream]. The returned [Result] should be checked to ensure it is **not** an instance + * of [com.github.michaelbull.result.Err] before the contents of [outputStream] are used. + */ + public fun encrypt( + keys: List, + plaintextStream: InputStream, + outputStream: OutputStream, + options: EncOpts, + ): Result + + /** Given a [fileName], return whether this instance can handle it. */ + public fun canHandle(fileName: String): Boolean +} diff --git a/crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoOptions.kt b/crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoOptions.kt new file mode 100644 index 00000000..1e60cdba --- /dev/null +++ b/crypto/common/src/main/kotlin/app/passwordstore/crypto/CryptoOptions.kt @@ -0,0 +1,8 @@ +package app.passwordstore.crypto + +/** Defines the contract for a grab-bag of options for individual cryptographic operations. */ +public interface CryptoOptions { + + /** Returns a [Boolean] indicating if the [option] is enabled for this operation. */ + public fun isOptionEnabled(option: String): Boolean +} diff --git a/crypto/common/src/main/kotlin/app/passwordstore/crypto/KeyManager.kt b/crypto/common/src/main/kotlin/app/passwordstore/crypto/KeyManager.kt new file mode 100644 index 00000000..c9db3734 --- /dev/null +++ b/crypto/common/src/main/kotlin/app/passwordstore/crypto/KeyManager.kt @@ -0,0 +1,41 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.crypto + +import com.github.michaelbull.result.Result + +/** + * [KeyManager] defines a contract for implementing a management system for [Key]s as they would be + * used by an implementation of [CryptoHandler] to obtain eligible public or private keys as + * required. + */ +public interface KeyManager { + + /** + * Inserts a [key] into the store. If the key already exists, this method will return + * [app.passwordstore.crypto.errors.KeyAlreadyExistsException] unless [replace] is `true`. + */ + public suspend fun addKey(key: Key, replace: Boolean = false): Result + + /** Finds a key for [identifier] in the store and deletes it. */ + public suspend fun removeKey(identifier: KeyIdentifier): Result + + /** + * Get a [Key] for the given [id]. The actual semantics of what [id] is are left to individual + * implementations to figure out for themselves. For example, in GPG this can be a full + * hexadecimal key ID, an email, a short hex key ID, and probably a few more things. + */ + public suspend fun getKeyById(id: KeyIdentifier): Result + + /** Returns all keys currently in the store as a [List]. */ + public suspend fun getAllKeys(): Result, Throwable> + + /** + * Get a stable identifier for the given [key]. The returned key ID should be suitable to be used + * as an identifier for the cryptographic identity tied to this key. + */ + public suspend fun getKeyId(key: Key): KeyIdentifier? +} diff --git a/crypto/common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt b/crypto/common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt new file mode 100644 index 00000000..eb64541e --- /dev/null +++ b/crypto/common/src/main/kotlin/app/passwordstore/crypto/errors/CryptoException.kt @@ -0,0 +1,48 @@ +package app.passwordstore.crypto.errors + +import app.passwordstore.crypto.KeyManager + +public sealed class CryptoException(message: String? = null, cause: Throwable? = null) : + Exception(message, cause) + +/** Sealed exception types for [KeyManager]. */ +public sealed class KeyManagerException(message: String? = null) : CryptoException(message) + +/** Store contains no keys. */ +public data object NoKeysAvailableException : KeyManagerException("No keys were found") + +/** Key directory does not exist or cannot be accessed. */ +public data object KeyDirectoryUnavailableException : + KeyManagerException("Key directory does not exist") + +/** Failed to delete given key. */ +public data object KeyDeletionFailedException : KeyManagerException("Couldn't delete the key file") + +/** Failed to parse the key as a known type. */ +public data object InvalidKeyException : + KeyManagerException("Given key cannot be parsed as a known key type") + +/** Key failed the [app.passwordstore.crypto.KeyUtils.isKeyUsable] test. */ +public data object UnusableKeyException : + KeyManagerException("Given key is not usable for encryption - is it using AEAD?") + +/** No key matching `keyId` could be found. */ +public class KeyNotFoundException(keyId: String) : + KeyManagerException("No key found with id: $keyId") + +/** Attempting to add another key for `keyId` without requesting a replace. */ +public class KeyAlreadyExistsException(keyId: String) : + KeyManagerException("Pre-existing key was found for $keyId") + +/** Sealed exception types for [app.passwordstore.crypto.CryptoHandler]. */ +public sealed class CryptoHandlerException(message: String? = null, cause: Throwable? = null) : + CryptoException(message, cause) + +/** The passphrase provided for decryption was incorrect. */ +public class IncorrectPassphraseException(cause: Throwable) : CryptoHandlerException(null, cause) + +/** No keys were passed to the encrypt/decrypt operation. */ +public data object NoKeysProvidedException : CryptoHandlerException(null, null) + +/** An unexpected error that cannot be mapped to a known type. */ +public class UnknownError(cause: Throwable) : CryptoHandlerException(null, cause) diff --git a/crypto/pgpainless/build.gradle.kts b/crypto/pgpainless/build.gradle.kts new file mode 100644 index 00000000..29f5bb1f --- /dev/null +++ b/crypto/pgpainless/build.gradle.kts @@ -0,0 +1,18 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +plugins { id("com.github.android-password-store.kotlin-jvm-library") } + +dependencies { + api(projects.crypto.common) + implementation(projects.coroutineUtils) + implementation(libs.androidx.annotation) + implementation(libs.dagger.hilt.core) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.thirdparty.kotlinResult) + implementation(libs.thirdparty.pgpainless) + testImplementation(libs.bundles.testDependencies) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.testing.testparameterinjector) +} diff --git a/crypto/pgpainless/lint-baseline.xml b/crypto/pgpainless/lint-baseline.xml new file mode 100644 index 00000000..2ed8a3bb --- /dev/null +++ b/crypto/pgpainless/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt new file mode 100644 index 00000000..5b23dc18 --- /dev/null +++ b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/KeyUtils.kt @@ -0,0 +1,52 @@ +/* + * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.crypto + +import app.passwordstore.crypto.PGPIdentifier.KeyId +import app.passwordstore.crypto.PGPIdentifier.UserId +import com.github.michaelbull.result.get +import com.github.michaelbull.result.runCatching +import org.bouncycastle.openpgp.PGPKeyRing +import org.pgpainless.PGPainless +import org.pgpainless.key.parsing.KeyRingReader + +/** Utility methods to deal with [PGPKey]s. */ +public object KeyUtils { + /** + * Attempts to parse a [PGPKeyRing] from a given [key]. The key is first tried as a secret key and + * then as a public one before the method gives up and returns null. + */ + public fun tryParseKeyring(key: PGPKey): PGPKeyRing? { + return runCatching { KeyRingReader.readKeyRing(key.contents.inputStream()) }.get() + } + + /** Parses a [PGPKeyRing] from the given [key] and calculates its long key ID */ + public fun tryGetId(key: PGPKey): KeyId? { + val keyRing = tryParseKeyring(key) ?: return null + return KeyId(keyRing.publicKey.keyID) + } + + /** + * Attempts to parse the given [PGPKey] into a [PGPKeyRing] and obtains the [UserId] of the + * corresponding public key. + */ + public fun tryGetEmail(key: PGPKey): UserId? { + val keyRing = tryParseKeyring(key) ?: return null + return UserId(keyRing.publicKey.userIDs.next()) + } + + /** + * Tests if the given [key] can be used for encryption, which is a bare minimum necessity for the + * app. + */ + public fun isKeyUsable(key: PGPKey): Boolean { + return runCatching { + val keyRing = tryParseKeyring(key) ?: return false + PGPainless.inspectKeyRing(keyRing).isUsableForEncryption + } + .get() != null + } +} diff --git a/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPDecryptOptions.kt b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPDecryptOptions.kt new file mode 100644 index 00000000..15ce92f0 --- /dev/null +++ b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPDecryptOptions.kt @@ -0,0 +1,21 @@ +package app.passwordstore.crypto + +/** [CryptoOptions] implementation for PGPainless decrypt operations. */ +public class PGPDecryptOptions +private constructor( + private val values: Map, +) : CryptoOptions { + + override fun isOptionEnabled(option: String): Boolean { + return values.getOrDefault(option, false) + } + + /** Implementation of a builder pattern for [PGPDecryptOptions]. */ + public class Builder { + + /** Build the final [PGPDecryptOptions] object. */ + public fun build(): PGPDecryptOptions { + return PGPDecryptOptions(emptyMap()) + } + } +} diff --git a/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPEncryptOptions.kt b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPEncryptOptions.kt new file mode 100644 index 00000000..90de6b51 --- /dev/null +++ b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPEncryptOptions.kt @@ -0,0 +1,35 @@ +package app.passwordstore.crypto + +/** [CryptoOptions] implementation for PGPainless encrypt operations. */ +public class PGPEncryptOptions +private constructor( + private val values: Map, +) : CryptoOptions { + + internal companion object { + const val ASCII_ARMOR = "ASCII_ARMOR" + } + + override fun isOptionEnabled(option: String): Boolean { + return values.getOrDefault(option, false) + } + + /** Implementation of a builder pattern for [PGPEncryptOptions]. */ + public class Builder { + private val optionsMap = mutableMapOf() + + /** + * Toggle whether the encryption operation output will be ASCII armored or in OpenPGP's binary + * format. + */ + public fun withAsciiArmor(enabled: Boolean): Builder { + optionsMap[ASCII_ARMOR] = enabled + return this + } + + /** Build the final [PGPEncryptOptions] object. */ + public fun build(): PGPEncryptOptions { + return PGPEncryptOptions(optionsMap) + } + } +} diff --git a/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt new file mode 100644 index 00000000..98a1de2e --- /dev/null +++ b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPIdentifier.kt @@ -0,0 +1,128 @@ +/* + * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.crypto + +import java.util.Locale +import java.util.regex.Pattern + +/** Supertype for valid identifiers of PGP keys. */ +public sealed class PGPIdentifier { + + /** A [PGPIdentifier] that represents either a long key ID or a fingerprint. */ + public data class KeyId(val id: Long) : PGPIdentifier() { + override fun toString(): String { + return convertKeyIdToHex(id) + } + + /** Convert a [Long] key ID to a formatted string. */ + private fun convertKeyIdToHex(keyId: Long): String { + return convertKeyIdToHex32bit(keyId shr HEX_32_BIT_COUNT) + convertKeyIdToHex32bit(keyId) + } + + /** + * Converts [keyId] to an unsigned [Long] then uses [java.lang.Long.toHexString] to convert it + * to a lowercase hex ID. + */ + private fun convertKeyIdToHex32bit(keyId: Long): String { + var hexString = java.lang.Long.toHexString(keyId and HEX_32_BITMASK).lowercase(Locale.ENGLISH) + while (hexString.length < HEX_32_STRING_LENGTH) { + hexString = "0$hexString" + } + return hexString + } + } + + /** + * A [PGPIdentifier] that represents the textual name/email combination corresponding to a key. + * Despite the [email] property in this class, the value is not guaranteed to be a valid email. + */ + public data class UserId(val email: String) : PGPIdentifier() { + override fun toString(): String { + return email + } + } + + public companion object { + private const val HEX_RADIX = 16 + private const val HEX_32_BIT_COUNT = 32 + private const val HEX_32_BITMASK = 0xffffffffL + private const val HEX_32_STRING_LENGTH = 8 + private const val TRUNCATED_FINGERPRINT_LENGTH = 16 + + /** + * Attempts to parse an untyped String identifier into a concrete subtype of [PGPIdentifier]. + */ + @Suppress("ReturnCount") + public fun fromString(identifier: String): PGPIdentifier? { + if (identifier.isEmpty()) return null + // Match long key IDs: + // FF22334455667788 or 0xFF22334455667788 + val maybeLongKeyId = + identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F\\d]{16}".toRegex()) } + if (maybeLongKeyId != null) { + val keyId = maybeLongKeyId.toULong(HEX_RADIX) + return KeyId(keyId.toLong()) + } + + // Match fingerprints: + // FF223344556677889900112233445566778899 or 0xFF223344556677889900112233445566778899 + val maybeFingerprint = + identifier.removePrefix("0x").takeIf { it.matches("[a-fA-F\\d]{40}".toRegex()) } + if (maybeFingerprint != null) { + // Truncating to the long key ID is not a security issue since OpenKeychain only + // accepts + // non-ambiguous key IDs. + val keyId = maybeFingerprint.takeLast(TRUNCATED_FINGERPRINT_LENGTH).toULong(HEX_RADIX) + return KeyId(keyId.toLong()) + } + + return splitUserId(identifier)?.let { UserId(it) } + } + + private object UserIdRegex { + val PATTERN: Pattern = Pattern.compile("^(.*?)(?: \\((.*)\\))?(?: <(.*)>)?$") + const val NAME = 1 + const val EMAIL = 3 + } + + private object EmailRegex { + val PATTERN: Pattern = Pattern.compile("^\"]*@[^<>\"]*[.]?[^<>\"]*)\"?>?$") + const val EMAIL = 1 + } + + /** + * Takes a 'Name (Comment) ' user ID in any of its permutations and attempts to extract + * an email from it. + */ + @Suppress("NestedBlockDepth") + private fun splitUserId(userId: String): String? { + if (userId.isNotEmpty()) { + val matcher = UserIdRegex.PATTERN.matcher(userId) + if (matcher.matches()) { + var name = + if (matcher.group(UserIdRegex.NAME)?.isEmpty() == true) null + else matcher.group(UserIdRegex.NAME) + var email = matcher.group(UserIdRegex.EMAIL) + if (email != null && name != null) { + val emailMatcher = EmailRegex.PATTERN.matcher(name) + if (emailMatcher.matches() && email == emailMatcher.group(EmailRegex.EMAIL)) { + email = emailMatcher.group(EmailRegex.EMAIL) + name = null + } + } + if (email == null && name != null) { + val emailMatcher = EmailRegex.PATTERN.matcher(name) + if (emailMatcher.matches()) { + email = emailMatcher.group(EmailRegex.EMAIL) + } + } + return email + } + } + return null + } + } +} diff --git a/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKey.kt b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKey.kt new file mode 100644 index 00000000..a33655d4 --- /dev/null +++ b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKey.kt @@ -0,0 +1,12 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.crypto + +/** + * A simple value class wrapping over a [ByteArray] that can represent a PGP key. The implementation + * details of public/ private parts as well as identities are deferred to [PGPKeyManager]. + */ +@JvmInline public value class PGPKey(public val contents: ByteArray) diff --git a/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt new file mode 100644 index 00000000..aed1acf2 --- /dev/null +++ b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPKeyManager.kt @@ -0,0 +1,154 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +@file:Suppress("BlockingMethodInNonBlockingContext") + +package app.passwordstore.crypto + +import androidx.annotation.VisibleForTesting +import app.passwordstore.crypto.KeyUtils.isKeyUsable +import app.passwordstore.crypto.KeyUtils.tryGetId +import app.passwordstore.crypto.KeyUtils.tryParseKeyring +import app.passwordstore.crypto.errors.InvalidKeyException +import app.passwordstore.crypto.errors.KeyAlreadyExistsException +import app.passwordstore.crypto.errors.KeyDeletionFailedException +import app.passwordstore.crypto.errors.KeyDirectoryUnavailableException +import app.passwordstore.crypto.errors.KeyNotFoundException +import app.passwordstore.crypto.errors.NoKeysAvailableException +import app.passwordstore.crypto.errors.UnusableKeyException +import app.passwordstore.util.coroutines.runSuspendCatching +import com.github.michaelbull.result.Result +import com.github.michaelbull.result.unwrap +import java.io.File +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.withContext +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.pgpainless.PGPainless +import org.pgpainless.util.selection.userid.SelectUserId + +public class PGPKeyManager +@Inject +constructor( + filesDir: String, + private val dispatcher: CoroutineDispatcher, +) : KeyManager { + + private val keyDir = File(filesDir, KEY_DIR_NAME) + + /** @see KeyManager.addKey */ + override suspend fun addKey(key: PGPKey, replace: Boolean): Result = + withContext(dispatcher) { + runSuspendCatching { + if (!keyDirExists()) throw KeyDirectoryUnavailableException + val incomingKeyRing = tryParseKeyring(key) ?: throw InvalidKeyException + if (!isKeyUsable(key)) throw UnusableKeyException + val keyFile = File(keyDir, "${tryGetId(key)}.$KEY_EXTENSION") + if (keyFile.exists()) { + val existingKeyBytes = keyFile.readBytes() + val existingKeyRing = + tryParseKeyring(PGPKey(existingKeyBytes)) ?: throw InvalidKeyException + when { + existingKeyRing is PGPPublicKeyRing && incomingKeyRing is PGPSecretKeyRing -> { + keyFile.writeBytes(key.contents) + return@runSuspendCatching key + } + existingKeyRing is PGPPublicKeyRing && incomingKeyRing is PGPPublicKeyRing -> { + val updatedPublicKey = PGPainless.mergeCertificate(existingKeyRing, incomingKeyRing) + val keyBytes = PGPainless.asciiArmor(updatedPublicKey).encodeToByteArray() + keyFile.writeBytes(keyBytes) + return@runSuspendCatching key + } + } + // Check for replace flag first and if it is false, throw an error + if (!replace) + throw KeyAlreadyExistsException( + tryGetId(key)?.toString() ?: "Failed to retrieve key ID" + ) + if (!keyFile.delete()) throw KeyDeletionFailedException + } + + keyFile.writeBytes(key.contents) + + key + } + } + + /** @see KeyManager.removeKey */ + override suspend fun removeKey(identifier: PGPIdentifier): Result = + withContext(dispatcher) { + runSuspendCatching { + if (!keyDirExists()) throw KeyDirectoryUnavailableException + val key = getKeyById(identifier).unwrap() + val keyFile = File(keyDir, "${tryGetId(key)}.$KEY_EXTENSION") + if (keyFile.exists()) { + if (!keyFile.delete()) throw KeyDeletionFailedException + } + } + } + + /** @see KeyManager.getKeyById */ + override suspend fun getKeyById(id: PGPIdentifier): Result = + withContext(dispatcher) { + runSuspendCatching { + if (!keyDirExists()) throw KeyDirectoryUnavailableException + val keyFiles = keyDir.listFiles() + if (keyFiles.isNullOrEmpty()) throw NoKeysAvailableException + val keys = keyFiles.map { file -> PGPKey(file.readBytes()) } + + val matchResult = + when (id) { + is PGPIdentifier.KeyId -> { + val keyIdMatch = + keys + .map { key -> key to tryGetId(key) } + .firstOrNull { (_, keyId) -> keyId?.id == id.id } + keyIdMatch?.first + } + is PGPIdentifier.UserId -> { + val selector = SelectUserId.byEmail(id.email) + val userIdMatch = + keys + .map { key -> key to tryParseKeyring(key) } + .firstOrNull { (_, keyRing) -> selector.firstMatch(keyRing) != null } + userIdMatch?.first + } + } + + if (matchResult != null) { + return@runSuspendCatching matchResult + } + + throw KeyNotFoundException("$id") + } + } + + /** @see KeyManager.getAllKeys */ + override suspend fun getAllKeys(): Result, Throwable> = + withContext(dispatcher) { + runSuspendCatching { + if (!keyDirExists()) throw KeyDirectoryUnavailableException + val keyFiles = keyDir.listFiles() + if (keyFiles.isNullOrEmpty()) return@runSuspendCatching emptyList() + keyFiles.map { keyFile -> PGPKey(keyFile.readBytes()) }.toList() + } + } + + /** @see KeyManager.getKeyById */ + override suspend fun getKeyId(key: PGPKey): PGPIdentifier? = tryGetId(key) + + /** Checks if [keyDir] exists and attempts to create it if not. */ + private fun keyDirExists(): Boolean { + return keyDir.exists() || keyDir.mkdirs() + } + + public companion object { + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val KEY_DIR_NAME: String = "keys" + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + internal const val KEY_EXTENSION: String = "key" + } +} diff --git a/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt new file mode 100644 index 00000000..a7087acf --- /dev/null +++ b/crypto/pgpainless/src/main/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandler.kt @@ -0,0 +1,139 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.crypto + +import app.passwordstore.crypto.errors.CryptoHandlerException +import app.passwordstore.crypto.errors.IncorrectPassphraseException +import app.passwordstore.crypto.errors.NoKeysProvidedException +import app.passwordstore.crypto.errors.UnknownError +import com.github.michaelbull.result.Result +import com.github.michaelbull.result.mapError +import com.github.michaelbull.result.runCatching +import java.io.InputStream +import java.io.OutputStream +import javax.inject.Inject +import org.bouncycastle.openpgp.PGPPublicKeyRing +import org.bouncycastle.openpgp.PGPPublicKeyRingCollection +import org.bouncycastle.openpgp.PGPSecretKeyRing +import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.bouncycastle.util.io.Streams +import org.pgpainless.PGPainless +import org.pgpainless.decryption_verification.ConsumerOptions +import org.pgpainless.encryption_signing.EncryptionOptions +import org.pgpainless.encryption_signing.ProducerOptions +import org.pgpainless.exception.WrongPassphraseException +import org.pgpainless.key.protection.SecretKeyRingProtector +import org.pgpainless.util.Passphrase + +public class PGPainlessCryptoHandler @Inject constructor() : + CryptoHandler { + + /** + * Decrypts the given [ciphertextStream] using [PGPainless] and writes the decrypted output to + * [outputStream]. The provided [passphrase] is wrapped in a [SecretKeyRingProtector] and the + * [keys] argument is defensively checked to ensure it has at least one key present. + * + * @see CryptoHandler.decrypt + */ + public override fun decrypt( + keys: List, + passphrase: String, + ciphertextStream: InputStream, + outputStream: OutputStream, + options: PGPDecryptOptions, + ): Result = + runCatching { + if (keys.isEmpty()) { + throw NoKeysProvidedException + } + val keyringCollection = + keys + .map { key -> PGPainless.readKeyRing().secretKeyRing(key.contents) } + .run(::PGPSecretKeyRingCollection) + val protector = SecretKeyRingProtector.unlockAnyKeyWith(Passphrase.fromPassword(passphrase)) + val decryptionStream = + PGPainless.decryptAndOrVerify() + .onInputStream(ciphertextStream) + .withOptions( + ConsumerOptions() + .addDecryptionKeys(keyringCollection, protector) + .addDecryptionPassphrase(Passphrase.fromPassword(passphrase)) + ) + Streams.pipeAll(decryptionStream, outputStream) + decryptionStream.close() + keyringCollection.forEach { keyRing -> + check(decryptionStream.metadata.isEncryptedFor(keyRing)) { + "Stream should be encrypted for ${keyRing.secretKey.keyID} but wasn't" + } + } + return@runCatching + } + .mapError { error -> + when (error) { + is WrongPassphraseException -> IncorrectPassphraseException(error) + is CryptoHandlerException -> error + else -> UnknownError(error) + } + } + + /** + * Encrypts the provided [plaintextStream] and writes the encrypted output to [outputStream]. The + * [keys] argument is defensively checked to contain at least one key. + * + * @see CryptoHandler.encrypt + */ + public override fun encrypt( + keys: List, + plaintextStream: InputStream, + outputStream: OutputStream, + options: PGPEncryptOptions, + ): Result = + runCatching { + if (keys.isEmpty()) { + throw NoKeysProvidedException + } + val publicKeyRings = + keys.mapNotNull(KeyUtils::tryParseKeyring).mapNotNull { keyRing -> + when (keyRing) { + is PGPPublicKeyRing -> keyRing + is PGPSecretKeyRing -> PGPainless.extractCertificate(keyRing) + else -> null + } + } + require(keys.size == publicKeyRings.size) { + "Failed to parse all keys: ${keys.size} keys were provided but only ${publicKeyRings.size} were valid" + } + if (publicKeyRings.isEmpty()) { + throw NoKeysProvidedException + } + val publicKeyRingCollection = PGPPublicKeyRingCollection(publicKeyRings) + val encryptionOptions = EncryptionOptions().addRecipients(publicKeyRingCollection) + val producerOptions = + ProducerOptions.encrypt(encryptionOptions) + .setAsciiArmor(options.isOptionEnabled(PGPEncryptOptions.ASCII_ARMOR)) + val encryptionStream = + PGPainless.encryptAndOrSign().onOutputStream(outputStream).withOptions(producerOptions) + Streams.pipeAll(plaintextStream, encryptionStream) + encryptionStream.close() + val result = encryptionStream.result + publicKeyRingCollection.forEach { keyRing -> + require(result.isEncryptedFor(keyRing)) { + "Stream should be encrypted for ${keyRing.publicKey.keyID} but wasn't" + } + } + } + .mapError { error -> + when (error) { + is CryptoHandlerException -> error + else -> UnknownError(error) + } + } + + /** Runs a naive check on the extension for the given [fileName] to check if it is a PGP file. */ + public override fun canHandle(fileName: String): Boolean { + return fileName.substringAfterLast('.', "") == "gpg" + } +} diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/CryptoConstants.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/CryptoConstants.kt new file mode 100644 index 00000000..d827e169 --- /dev/null +++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/CryptoConstants.kt @@ -0,0 +1,14 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.crypto + +object CryptoConstants { + const val KEY_PASSPHRASE = "hunter2" + const val PLAIN_TEXT = "encryption worthy content" + const val KEY_NAME = "John Doe" + const val KEY_EMAIL = "john.doe@example.com" + const val KEY_ID = 0x08edf7567183ce27 +} diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt new file mode 100644 index 00000000..a0f84402 --- /dev/null +++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/KeyUtilsTest.kt @@ -0,0 +1,35 @@ +package app.passwordstore.crypto + +import app.passwordstore.crypto.KeyUtils.isKeyUsable +import app.passwordstore.crypto.KeyUtils.tryGetId +import app.passwordstore.crypto.KeyUtils.tryParseKeyring +import app.passwordstore.crypto.TestUtils.AllKeys +import app.passwordstore.crypto.TestUtils.getArmoredSecretKeyWithMultipleIdentities +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotNull +import org.bouncycastle.openpgp.PGPSecretKeyRing + +class KeyUtilsTest { + @Test + fun parseKeyWithMultipleIdentities() { + val key = PGPKey(getArmoredSecretKeyWithMultipleIdentities()) + val keyring = tryParseKeyring(key) + assertNotNull(keyring) + assertIs(keyring) + val keyId = tryGetId(key) + assertNotNull(keyId) + assertIs(keyId) + assertEquals("b950ae2813841585", keyId.toString()) + } + + @Test + fun isKeyUsable() { + val params = AllKeys.entries.map { it to (it != AllKeys.AEAD_PUB && it != AllKeys.AEAD_SEC) } + params.forEach { (allKeys, isUsable) -> + val key = PGPKey(allKeys.keyMaterial) + assertEquals(isUsable, isKeyUsable(key), "${allKeys.name} failed expectation:") + } + } +} diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPIdentifierTest.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPIdentifierTest.kt new file mode 100644 index 00000000..efc6e0ba --- /dev/null +++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPIdentifierTest.kt @@ -0,0 +1,41 @@ +/* + * Copyright © 2014-2022 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.crypto + +import kotlin.test.Test +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class PGPIdentifierTest { + + @Test + fun parseHexKeyIdWithout0xPrefix() { + val identifier = PGPIdentifier.fromString("79E8208280490C77") + assertNotNull(identifier) + assertTrue { identifier is PGPIdentifier.KeyId } + } + + @Test + fun parseHexKeyId() { + val identifier = PGPIdentifier.fromString("0x79E8208280490C77") + assertNotNull(identifier) + assertTrue { identifier is PGPIdentifier.KeyId } + } + + @Test + fun parseValidEmail() { + val identifier = PGPIdentifier.fromString("john.doe@example.org") + assertNotNull(identifier) + assertTrue { identifier is PGPIdentifier.UserId } + } + + @Test + fun parseEmailWithoutTLD() { + val identifier = PGPIdentifier.fromString("john.doe@example") + assertNotNull(identifier) + assertTrue { identifier is PGPIdentifier.UserId } + } +} diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt new file mode 100644 index 00000000..85cf8e1b --- /dev/null +++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPKeyManagerTest.kt @@ -0,0 +1,236 @@ +package app.passwordstore.crypto + +import app.passwordstore.crypto.KeyUtils.tryGetId +import app.passwordstore.crypto.PGPIdentifier.KeyId +import app.passwordstore.crypto.PGPIdentifier.UserId +import app.passwordstore.crypto.errors.KeyAlreadyExistsException +import app.passwordstore.crypto.errors.KeyNotFoundException +import app.passwordstore.crypto.errors.NoKeysAvailableException +import app.passwordstore.crypto.errors.UnusableKeyException +import com.github.michaelbull.result.Err +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.unwrap +import com.github.michaelbull.result.unwrapError +import java.io.File +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Rule +import org.junit.rules.TemporaryFolder + +class PGPKeyManagerTest { + + @get:Rule val temporaryFolder: TemporaryFolder = TemporaryFolder() + private val dispatcher = StandardTestDispatcher() + private val scope = TestScope(dispatcher) + private val filesDir by unsafeLazy { temporaryFolder.root } + private val keysDir by unsafeLazy { File(filesDir, PGPKeyManager.KEY_DIR_NAME) } + private val keyManager by unsafeLazy { PGPKeyManager(filesDir.absolutePath, dispatcher) } + private val secretKey = PGPKey(TestUtils.getArmoredSecretKey()) + private val publicKey = PGPKey(TestUtils.getArmoredPublicKey()) + + private fun unsafeLazy(initializer: () -> T) = + lazy(LazyThreadSafetyMode.NONE) { initializer.invoke() } + + @Test + fun addKey() = + runTest(dispatcher) { + // Check if the key id returned is correct + val keyId = keyManager.getKeyId(keyManager.addKey(secretKey).unwrap()) + assertEquals(KeyId(CryptoConstants.KEY_ID), keyId) + // Check if the keys directory have one file + assertEquals(1, filesDir.list()?.size) + // Check if the file name is correct + val keyFile = keysDir.listFiles()?.first() + assertEquals(keyFile?.name, "$keyId.${PGPKeyManager.KEY_EXTENSION}") + } + + @Test + fun addKeyWithoutReplaceFlag() = + runTest(dispatcher) { + // Check adding the keys twice + keyManager.addKey(secretKey, false).unwrap() + val error = keyManager.addKey(secretKey, false).unwrapError() + + assertIs(error) + } + + @Test + fun addKeyWithReplaceFlag() = + runTest(dispatcher) { + // Check adding the keys twice + keyManager.addKey(secretKey, true).unwrap() + val keyId = keyManager.getKeyId(keyManager.addKey(secretKey, true).unwrap()) + + assertEquals(KeyId(CryptoConstants.KEY_ID), keyId) + } + + @Test + fun addKeyWithUnusableKey() = + runTest(dispatcher) { + val error = keyManager.addKey(PGPKey(TestUtils.getAEADSecretKey())).unwrapError() + assertEquals(UnusableKeyException, error) + } + + @Test + fun removeKey() = + runTest(dispatcher) { + // Add key using KeyManager + keyManager.addKey(secretKey).unwrap() + // Remove key + keyManager.removeKey(tryGetId(secretKey)!!).unwrap() + // Check that no keys remain + val keys = keyManager.getAllKeys().unwrap() + assertEquals(0, keys.size) + } + + @Test + fun getKeyById() = + runTest(dispatcher) { + // Add key using KeyManager + keyManager.addKey(secretKey).unwrap() + val keyId = keyManager.getKeyId(secretKey) + assertNotNull(keyId) + assertEquals(KeyId(CryptoConstants.KEY_ID), keyManager.getKeyId(secretKey)) + // Check returned key id matches the expected id and the created key id + val returnedKey = keyManager.getKeyById(keyId).unwrap() + assertEquals(keyManager.getKeyId(secretKey), keyManager.getKeyId(returnedKey)) + } + + @Test + fun getKeyByFullUserId() = + runTest(dispatcher) { + keyManager.addKey(secretKey).unwrap() + val keyId = "${CryptoConstants.KEY_NAME} <${CryptoConstants.KEY_EMAIL}>" + val returnedKey = keyManager.getKeyById(UserId(keyId)).unwrap() + assertEquals(keyManager.getKeyId(secretKey), keyManager.getKeyId(returnedKey)) + } + + @Test + fun getKeyByEmailUserId() = + runTest(dispatcher) { + keyManager.addKey(secretKey).unwrap() + val keyId = CryptoConstants.KEY_EMAIL + val returnedKey = keyManager.getKeyById(UserId(keyId)).unwrap() + assertEquals(keyManager.getKeyId(secretKey), keyManager.getKeyId(returnedKey)) + } + + @Test + fun getNonExistentKey() = + runTest(dispatcher) { + // Add key using KeyManager + keyManager.addKey(secretKey).unwrap() + val keyId = KeyId(0x08edf7567183ce44) + // Check returned key + val error = keyManager.getKeyById(keyId).unwrapError() + assertIs(error) + assertEquals("No key found with id: $keyId", error.message) + } + + @Test + fun findNonExistentKey() = + runTest(dispatcher) { + // Check returned key + val error = keyManager.getKeyById(KeyId(0x08edf7567183ce44)).unwrapError() + assertIs(error) + assertEquals("No keys were found", error.message) + } + + @Test + fun getAllKeys() = + runTest(dispatcher) { + // Check if KeyManager returns no key + val noKeyList = keyManager.getAllKeys().unwrap() + assertEquals(0, noKeyList.size) + // Add key using KeyManager + keyManager.addKey(secretKey).unwrap() + keyManager.addKey(PGPKey(TestUtils.getArmoredSecretKeyWithMultipleIdentities())).unwrap() + // Check if KeyManager returns one key + val singleKeyList = keyManager.getAllKeys().unwrap() + assertEquals(2, singleKeyList.size) + } + + @Test + fun getMultipleIdentityKeyWithAllIdentities() = + runTest(dispatcher) { + val key = PGPKey(TestUtils.getArmoredSecretKeyWithMultipleIdentities()) + keyManager.addKey(key).unwrap() + val johnKey = keyManager.getKeyById(UserId("john@doe.org")).unwrap() + val janeKey = keyManager.getKeyById(UserId("jane@doe.org")).unwrap() + + assertContentEquals(johnKey.contents, janeKey.contents) + } + + @Test + fun replaceSecretKeyWithPublicKey() = + runTest(dispatcher) { + assertIs>(keyManager.addKey(secretKey)) + assertIs>(keyManager.addKey(publicKey)) + } + + @Test + fun replacePublicKeyWithSecretKey() = + runTest(dispatcher) { + assertIs>(keyManager.addKey(publicKey)) + assertIs>(keyManager.addKey(secretKey)) + } + + @Test + fun replacePublicKeyWithPublicKey() = + runTest(dispatcher) { + assertIs>(keyManager.addKey(publicKey)) + assertIs>(keyManager.addKey(publicKey)) + val allKeys = keyManager.getAllKeys() + assertIs>>(allKeys) + assertEquals(1, allKeys.value.size) + val key = allKeys.value[0] + assertContentEquals(publicKey.contents, key.contents) + } + + @Test + fun replaceSecretKeyWithSecretKey() = + runTest(dispatcher) { + assertIs>(keyManager.addKey(secretKey)) + assertIs>(keyManager.addKey(secretKey)) + } + + @Test + fun addMultipleKeysWithSameEmail() = + runTest(dispatcher) { + val alice = + PGPKey(this::class.java.classLoader.getResource("alice_owner@example_com")!!.readBytes()) + val bobby = + PGPKey(this::class.java.classLoader.getResource("bobby_owner@example_com")!!.readBytes()) + assertIs>(keyManager.addKey(alice)) + assertIs>(keyManager.addKey(bobby)) + + keyManager.getAllKeys().apply { + assertIs>>(this) + assertEquals(2, this.value.size) + } + val longKeyIds = + arrayOf( + KeyId(-7087927403306410599), // Alice + KeyId(-961222705095032109), // Bobby + ) + val userIds = + arrayOf( + UserId("Alice "), + UserId("Bobby "), + ) + + for (idCollection in arrayOf(longKeyIds, userIds)) { + val alice1 = keyManager.getKeyById(idCollection[0]) + val bobby1 = keyManager.getKeyById(idCollection[1]) + assertIs>(alice1) + assertIs>(bobby1) + assertNotEquals(alice1.value.contents, bobby1.value.contents) + } + } +} diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt new file mode 100644 index 00000000..8bf6ba1e --- /dev/null +++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/PGPainlessCryptoHandlerTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +@file:Suppress("JUnitMalformedDeclaration") // The test runner takes care of it + +package app.passwordstore.crypto + +import app.passwordstore.crypto.CryptoConstants.KEY_PASSPHRASE +import app.passwordstore.crypto.CryptoConstants.PLAIN_TEXT +import app.passwordstore.crypto.errors.IncorrectPassphraseException +import com.github.michaelbull.result.Err +import com.github.michaelbull.result.Ok +import com.github.michaelbull.result.getError +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import java.io.ByteArrayOutputStream +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertIs +import kotlin.test.assertTrue +import org.junit.runner.RunWith +import org.pgpainless.PGPainless +import org.pgpainless.decryption_verification.MessageInspector + +@Suppress("Unused") // Test runner handles it internally +enum class EncryptionKey(val keySet: List) { + PUBLIC(listOf(PGPKey(TestUtils.getArmoredPublicKey()))), + SECRET(listOf(PGPKey(TestUtils.getArmoredSecretKey()))), + ALL(listOf(PGPKey(TestUtils.getArmoredPublicKey()), PGPKey(TestUtils.getArmoredSecretKey()))), +} + +@RunWith(TestParameterInjector::class) +class PGPainlessCryptoHandlerTest { + + private val cryptoHandler = PGPainlessCryptoHandler() + private val secretKey = PGPKey(TestUtils.getArmoredSecretKey()) + + @Test + fun encryptAndDecrypt(@TestParameter encryptionKey: EncryptionKey) { + val ciphertextStream = ByteArrayOutputStream() + val encryptRes = + cryptoHandler.encrypt( + encryptionKey.keySet, + PLAIN_TEXT.byteInputStream(Charsets.UTF_8), + ciphertextStream, + PGPEncryptOptions.Builder().build(), + ) + assertIs>(encryptRes) + val plaintextStream = ByteArrayOutputStream() + val decryptRes = + cryptoHandler.decrypt( + listOf(secretKey), + KEY_PASSPHRASE, + ciphertextStream.toByteArray().inputStream(), + plaintextStream, + PGPDecryptOptions.Builder().build(), + ) + assertIs>(decryptRes) + assertEquals(PLAIN_TEXT, plaintextStream.toString(Charsets.UTF_8)) + } + + @Test + fun decryptWithWrongPassphrase(@TestParameter encryptionKey: EncryptionKey) { + val ciphertextStream = ByteArrayOutputStream() + val encryptRes = + cryptoHandler.encrypt( + encryptionKey.keySet, + PLAIN_TEXT.byteInputStream(Charsets.UTF_8), + ciphertextStream, + PGPEncryptOptions.Builder().build(), + ) + assertIs>(encryptRes) + val plaintextStream = ByteArrayOutputStream() + val result = + cryptoHandler.decrypt( + listOf(secretKey), + "very incorrect passphrase", + ciphertextStream.toByteArray().inputStream(), + plaintextStream, + PGPDecryptOptions.Builder().build(), + ) + assertIs>(result) + assertIs(result.getError()) + } + + @Test + fun encryptAsciiArmored(@TestParameter encryptionKey: EncryptionKey) { + val ciphertextStream = ByteArrayOutputStream() + val encryptRes = + cryptoHandler.encrypt( + encryptionKey.keySet, + PLAIN_TEXT.byteInputStream(Charsets.UTF_8), + ciphertextStream, + PGPEncryptOptions.Builder().withAsciiArmor(true).build(), + ) + assertIs>(encryptRes) + val ciphertext = ciphertextStream.toString(Charsets.UTF_8) + assertContains(ciphertext, "Version: PGPainless") + assertContains(ciphertext, "-----BEGIN PGP MESSAGE-----") + assertContains(ciphertext, "-----END PGP MESSAGE-----") + } + + @Test + fun encryptMultiple() { + val alice = + PGPainless.generateKeyRing().modernKeyRing("Alice ", KEY_PASSPHRASE) + val bob = PGPainless.generateKeyRing().modernKeyRing("Bob ", KEY_PASSPHRASE) + val aliceKey = PGPKey(PGPainless.asciiArmor(alice).encodeToByteArray()) + val bobKey = PGPKey(PGPainless.asciiArmor(bob).encodeToByteArray()) + val ciphertextStream = ByteArrayOutputStream() + val encryptRes = + cryptoHandler.encrypt( + listOf(aliceKey, bobKey), + PLAIN_TEXT.byteInputStream(Charsets.UTF_8), + ciphertextStream, + PGPEncryptOptions.Builder().withAsciiArmor(true).build(), + ) + assertIs>(encryptRes) + val message = ciphertextStream.toByteArray().decodeToString() + val info = MessageInspector.determineEncryptionInfoForMessage(message) + assertTrue(info.isEncrypted) + assertEquals(2, info.keyIds.size) + assertFalse(info.isSignedOnly) + for (key in listOf(aliceKey, bobKey)) { + val ciphertextStreamCopy = message.byteInputStream() + val plaintextStream = ByteArrayOutputStream() + val res = + cryptoHandler.decrypt( + listOf(key), + KEY_PASSPHRASE, + ciphertextStreamCopy, + plaintextStream, + PGPDecryptOptions.Builder().build(), + ) + assertIs>(res) + } + } + + @Test + fun canHandleFiltersFormats() { + assertFalse { cryptoHandler.canHandle("example.com") } + assertTrue { cryptoHandler.canHandle("example.com.gpg") } + assertFalse { cryptoHandler.canHandle("example.com.asc") } + } +} diff --git a/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/TestUtils.kt b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/TestUtils.kt new file mode 100644 index 00000000..90b98ac9 --- /dev/null +++ b/crypto/pgpainless/src/test/kotlin/app/passwordstore/crypto/TestUtils.kt @@ -0,0 +1,32 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +@file:Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") + +package app.passwordstore.crypto + +object TestUtils { + fun getArmoredSecretKey() = this::class.java.classLoader.getResource("secret_key").readBytes() + + fun getArmoredPublicKey() = this::class.java.classLoader.getResource("public_key").readBytes() + + fun getArmoredSecretKeyWithMultipleIdentities() = + this::class.java.classLoader.getResource("secret_key_multiple_identities").readBytes() + + fun getArmoredPublicKeyWithMultipleIdentities() = + this::class.java.classLoader.getResource("public_key_multiple_identities").readBytes() + + fun getAEADPublicKey() = this::class.java.classLoader.getResource("aead_pub").readBytes() + + fun getAEADSecretKey() = this::class.java.classLoader.getResource("aead_sec").readBytes() + + enum class AllKeys(val keyMaterial: ByteArray) { + ARMORED_SEC(getArmoredSecretKey()), + ARMORED_PUB(getArmoredPublicKey()), + MULTIPLE_IDENTITIES_SEC(getArmoredSecretKeyWithMultipleIdentities()), + MULTIPLE_IDENTITIES_PUB(getArmoredPublicKeyWithMultipleIdentities()), + AEAD_SEC(getAEADSecretKey()), + AEAD_PUB(getAEADPublicKey()), + } +} diff --git a/crypto/pgpainless/src/test/resources/aead_pub b/crypto/pgpainless/src/test/resources/aead_pub new file mode 100644 index 00000000..f6ae1e82 --- /dev/null +++ b/crypto/pgpainless/src/test/resources/aead_pub @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=lwhh +-----END PGP PUBLIC KEY BLOCK----- diff --git a/crypto/pgpainless/src/test/resources/aead_sec b/crypto/pgpainless/src/test/resources/aead_sec new file mode 100644 index 00000000..8e13ac0e --- /dev/null +++ b/crypto/pgpainless/src/test/resources/aead_sec @@ -0,0 +1,107 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lQdGBGNGwwsBEADW2F2sEPBx6Kpl6Rs5gzhuEA3PN5HvCllAkX4DTTFvjnMLkV3v +ZSSEiQgQKFecJllLzZISq5HXvb/72VcaouPdAVS2yCXB/+PgfkONldo6JOYP4GK8 +E9KEBHWjywqsT1tK/6HaPJLGYMvuIzmn4ETELF55Y4SVuLMJ3IJ2DGhiq7PIo6+R +7GDXfjMXDOpF6cBMy/MG2WDue+y/rgSaq3WDoZUc0Rp3VPpozCuqOt095IPIJzOO +cGkwE8wEM2CImRFULKLyPl5yCVw8e1yAKFD+aBu5XoB3T+PheiwgMQydtpzWkI6q +Al97Ql5B483Ios8B8AU2SOhACZr8q0jZMgFtqwBOwNMsFUqWtj3gC/5DVFa+N8pe +1yg1VRHSooxzosjiy40AdGow5gSNoL2HUkjF+C5N1RGehR/6yQ75RZ5J6IexMg9C +2oTGszaZA+gscZB3+aeStU6vMfuEC+NXM3E1YmFn2Og2XfDDx7O40pf7wgqJ4DTk +EleZDltyKr0XDZ2EwlvY1uB6HfzP+8M+2hDfJxmEU0BjpMNSW2RiaLyGSYOSWJDR +PZrxXXiLjrxIECZ0uAuLfdoZjFs8AvtC4KuWCAb14MIZWa0zxdMxV5jVJ9+apDqv +k2X0FMtMi/ADgT5vxKm5slssGfCH/Az/4sZDVeWBgmtoKqQjYZBhLieCXwARAQAB +/gcDAuz+/Grb/fqi8jgaoNn9vH/8+Qv4LnCgXs+buQBy+6Udy71zj/AdmrJ1yqD6 +4Dls/xDoUv0ZL79y4KaTVjrh959v7Aq+ZdIqNZ//aRqy51A1tKcbHXNElxgYmpQQ +XvWFH7RgrUrDCv4OtxwH9w3KrAuafJBRvzK1eaFEz8MqIbKOHFwGD8DjTZRVke9d +Cmah9jAHF4n5Jqm2en+lyRQYlWYYYa6/3b8BXWf3AP2EiOA5WBCqIHHM4u3KmGfV +OYi2Zq4LBJi+k5SaiamkJg62JM4vvYdt53n65iAZe/zokM6h0VZyFVKraqu1ylkb +WJK+hFAN079UnUWhT8vnkByXlGzkqf10FYG54VFPZvV0cTtazcuQkLdSaNwLLSyR +vXelyezokTOUxtuU9NfdiUa2VyU5GrdkMLvkvJ7OcGJu38WsHaRqYnVx98IyceZA +Ljw8z31nnpYVJXncPyX6aCqgUDttEUFar7Bfy8Hp2Zj92s77exHHzMVFAg+tD23L +2Akw/tvVaBZYpIqYSB2rVRJ8mrp/JSS2NR0lwf2ij1B2xk9uVzESOReb4kL1hF21 +GqGjTQfynQ2dfQYbRuK6rLArR0rw6pNnZzVGnBcxkznlzyOnAZTqxBQqZQIGG9mu +O+3TPcrfeQnqTgo/7L3uwijl//P1EI1gXGvTTmk/b/MitawlN4h+8mLTIqLvX1Qd +/JjrGsZ15j54f9mlnuFO7heUhAKacMzCZDwiY6WswWgr3oCz1gQWmuyFvik+9a/u +6R8PWFU3MQ6HvuklrDYYxfyn0uaYUdRqZicmFOmNcWPhfH2vFHPcpEeIATAo2Aa7 +xB3+etKtGFpak0MeW24YPcRIHs2LjbqzQlySdlCP5ehct+jfb8d7Z97Bum0pxPVY +t7n4K5Aw9M4WrtrqNIhgNVsX1wZeisV7aNzeLw+FJL8pE0kCz79fLwL+tYVIEBxK +xtHCAOHp7tj+l0R3ng33PhL3/qOy7i3Yz9SCK30B6jiaWUQMcq/C0SD1Dd8m6neZ +wZyiD+pxg55jm1wk4qLyGvhx2CoXIwxQ9SJ/JXRY6s45me+owGlmUlSqwOZQ69t+ +kiL3NKUQ55KG/4DXcNoUsjwtmZabh+3w5Kmj5Wcbm5SSTRo9CMci/Vox6c7HzCP0 +Mxx8c+mdJbN5gXKasI/W39h6s859zh0aehZc9TbJrZJT1SwFi5DS3NPJc3oacTao +ZF1q93VWppckDw9a8ufoPwTax3hwvDAwCktrqH3rA87qeaPk8hRWaIF+hY0Y6Hkc +3F+6WKouPwnOhM8DWk6E46FQJuzrgdn/9tRMI4ZlG0uceh27RcY39Zx8PnDSGudX +Bog/fDeyx+MyCYhQ3JYWbd0GJ4cBeQQgG/jhQcIE3PGx2FXeoPPw+luY0DfPkAms +Sa652De9Ajd2Z+f0EoE27nfvRKItrc0njAwp06Gdfgj7npkomLu8WdhgXfKn823p +Gt9QLca/UruO1bmBj3F0peLpsZp/JLvSAdvpy2P9mDouXtuAoHj1Cc3+SD3PC0pw +4jTvtOpGHD/WMcGHMUmmv9NlZrqsOw4XwJYZAprz/zbDudAofyzIMtr7/8hRkhNk +J8AZ7OejCkZ+GtVfz1xxubY2/sP+dOOGvMXT2lrKAFqv1xC0T2hDnlsFNS4DZ6g7 +HHhCEdGSzQWeLFGx6+5bhz/C3+TfvRyudyoOwv+ueKTPXMeJg7pzmd7fQQGpmlh+ +7MqrIGsHCUOGzeX/+tGdbf94b2vGP0i+wATt50B2myM1xcP/PgU2LK60H0pvaG4g +RG9lIDxqb2huLmRvZUBleGFtcGxlLmNvbT6JAk4EEwECADgWIQT7B3A/907HBEEr +P1kmZLQXJ+oWzgUCY0bDCwIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAm +ZLQXJ+oWzokpD/wNzzL1c4zwL5fsClFCkOj1RKxoL5XerbrMuLAG2OWPrVWPcHO5 +Rddnq/AhpETjHv4+tRpLtz84m6kKrqLMoQ/f5aGgXBGlnX3Sz0kFjudp6h+Y7yUo +Uriq6ArGlIFaBzEdX49Bw0RtHV3DIbWE56mQ4fbx1Ilx5ZPOcAJo2F1fHZuUDlKG +6m9boYeYIIycHEeHKwCzEg40259iwCuD/RhYETgS79UtiRL3BMepTKz2tsWd68xL +ybMWWiwSlHKrqy6MLbjXpbmXBWUtNOYU9H7PzxJlf7SMQ4zHCQQd+yWo4xzY9Mpc +uAhoJLlN2TqHxwq61DnOeOM9jPbkfRm50PUe6GOQkdsk34xpZlZYGrW2HcSAldeT +PJ5w/N3VYqJUGu+lD/16JTi4FTPkwfV9PuPv/lPhLR7+kp3MRW/6/8ePUPWt2P5d +cLswBGjJuxRIh4TkrnBHvQokSwHM7/8QJjTBivh61zc3KH6fEBvElU0rIYI2ViqT +bgz+EOsMqtGXeweYx9z5iTFHg2E8CGhvimNSNy2spuTgsh/H2KQjhxBWorPze2u7 +se0D1SLgObG+qazOFoaROQwOeO/e431ix5l8V8c8hZU3oR4B6/MyW/bhYHqBt1Vy +MVlv845yzudt8cLAJpSSkzbngcq/ksO6BDoBbYiM48PfmsNmKawqgfR91p0HRgRj +RsMLARAAyB/ChlXg1ofksTPF08TGokEAWaeoRIdXiXPH+Y+CO1GRGg6wbkZRbZ+W +RNk0p/KdK4JiT3+zTrql8/e+Op7RfaGtsb+kejhuLJp1igQtA2lwIlk59LjMMiIw +EsbJw53s4CulpuWl7x+ZAtqJDBfFWMz83NRCSoPPEVkFr2u8R2g8lD9X8IYb+vs7 +/EqP4APRQa1jE9XMhmmyW1PZBCy9kY5VRDIkAqhpEXW4gggyHEzdWy4s8civ3VRs +yoQaJVjVzxiI+1oKsDq3z2Cm0MY1zwfctq7Fh0Cye6IDHpVu+dB14muuTDUAO8Hl +EJvPV4I00XpGRh6i5weRy2RE1yGPHYtHLN3Iz0ROwM2QkXyFfshqlIMg4MjT/Qzm +It55FauALwxzS1F5HgEs7pmaWd/jsZ+NIu9rcouom0G03RYoXHKKkvNA10RmUp+m +gKjbA1dmNwGH7pSqG7p9NLIY0RMt6qhdh6VhW3ZTN9qKb6+PSou56n+3ZDvMC/ey +oOW9ryz8z60eEUMvOB6MiCghNfwjs/1YWol3FD3n9C8qeYq0WbMSEZjdI6T0gLxU +K9KJzGR4m1TRbXqw923hKeXqWkEjiLwajT201Zu/7iWhcrRB7MZlUGgqpbUBZh4V +9BdtEfP4uH5BIu+1vOj+VEbf10Z2ZlQtsAvMf8GSezEA1xx4KVMAEQEAAf4HAwJg +S0ZLmwgEXfLvsEfP/i8NZPOVUWQmvdAefWo5BL4hNYHdls1CUs8oTbYNDKvwUL7x +euv3b/zF8gwzT2AFwPfiJrT1FoB9+gGrCL0eRcwlWZLSGhvqNJJtpHHMxly3J3es +inj8VCiptXcPVt/SMUvOY6kKXCjGwE1haxM0FqABYkBTSTJJ7pllCjSMmbHfO5m8 +7SnGfemKxaDGRNcDcEylr2DwvPPEPsp7b+/KCxstGEHyitGMr0vHjls1v91+87/p +DJQqEVFjetRM0qC7X9PYGTO60njyuAk/YVF3YWnMcXsx+NPIDjlW9E6Etc9KuM6m +oirSVleXR2CMrD/b08xvsU3vRnj5XtA3Piyk0UBYD8jIDi6zKxeUKYWDRDCNR8Cx +869uEkaQ1WzT/O0nIZqdFU5bG+gG92NMtVUa8vNDsMSaFiX3NP1WFJo6muTgRM2x +vPvRUs8DSPwIHlPPXH9dLFyNMD04r5IW1Ll56kbxg+aPtsNztnlCUYivWnXTDq70 +dAFoU7LxxkI6V4AXH2pQbruQOBpKXHoNZKCkrQi7jN3EW5F5+oAi1ecOcWzyX9Xa +aAZg9ihNSFNjvo4cf66RgO+tD4dGBeM1Xx7kHVvGl4LvFk6IileaYAY+TvFQkHPR +P0psgVq+iZIvO2UX+m719Y0f4SE6utvLJ1Kl09WDOVy6R2N7x2PGyVDbh/h1+lEE +NlGS5biBF9tZd28Rh6AbfoZUT51a6uDN7j52HIYPS1eAJA61ApopffY7+tRZd2Wi +WYxCew/ASblT4vM9LmSiu/4kH0sJYJBjRWuCSbsy52rbUgNWZoEdvlVOYdckDHUG +uVD+eal3C6Z8AQCaissUZUOc+QiIPO2tiZ19nmFBPN7HSwat+9uM43r32FGDLkYl +W79yV90D5vjSst/Sa1B1JoTHWviWwAu4zPObRipA8JtPEP+piVp94+qdfV/nm+PD +TdFF3ccAynFdkOycrouTOHHMl2+HpT+XKG27/8301S4oVEb4SwbsrH+0coQnUi0H +ll50IVdaDyGxre46vxXWsbxASPlmRUoD2ByBLmHJmqE8EJiNZ2lsPBfHffkntSqT +I4l1d/dORTkU56FCLkTY6ZTCpB4oE9S/rMjRWZ+dz9Y8vhmkmSzeYOd+TFMnkuS3 +epy1yxgXsU9ri6S3Ir9j3bAfirL/1LJAblSdQBBVWIDZ3AHcSp2rSxfxSbIabmEC +Oqa8VFWqV2Z19H/HrjI5ZxqgkCFeaAsydhJ/3QgKuirM1klLgCpzNukObeuW2ZRn +Fl3NnD8cu08ie/YCUf41x1oxfK4kBHbn2eXKNji7jEWL/oRBKObOXO23pNj2B7hw +ArGmRjdx8j55HeEtMu0EPoFnrW44R0pcRwKrJywlkyhitQ9c/Nx3t6wAbHx7/sfZ +ft0jWezTdba4w/GyA1W38OZvU5ul1flt4lTClAuFv2bK8f7vmPT+bU/fRPqt/d0R +7Zc9LE5fcchL9AYrE46ixApnpSLhaPwpBZ82p1XKntPvU0WzVeVr3C1x9N+tuLQT +FB805hUr3u7Qi/uuDISh+p0Kw8TimkPNHMZ9UoJjCrGJpVuv+CoQPESRf0gTC4oM +RHS7BWuplZ6Di01RUFRLkk4+ytIxLH8+lS4q2iKK5CFbvjuL5a9Xp4mmXrqfkhlN +uRUePZDPIfU24k/MO1mfPgnwaMJTMeSTlcPaEdgzcg+r02kDFXExEWgY/Modq+kx +ls+Bbtru/dUTbsFaUT8QeLio7EcX2n8a0BF/Uo4EvqLhb8A6iQI2BBgBAgAgFiEE ++wdwP/dOxwRBKz9ZJmS0FyfqFs4FAmNGwwsCGwwACgkQJmS0FyfqFs472A//Uygk +E4h3rkk9A6XfCayBW7iPluO2TAh3pbLvjloAM5nCQp0y2jJI3a2QoegVPFSJbH2q +ebbOaidAKc4Mt2iYPB0z35OnKDrLk2Y4CrHp29nlbmQD6gotk93Hnc6jgEfUGZro +VGePs+1H6WT9d26FLO0VhFNTmR2o6hYGqbhitFRHvbTTnTb4P3572MFK22VfPM4I +L/kuR6BXXiixQogLxb2tDSLTprDWgPuhPbVLV0tvavj7j/9JULTNX68W0nb0sgOj +JPbQrHETKJysQD1n+moSFTJ8/n1DwBZm5Wi6qQRMjP4jBSY7IOkmTeNFEGX9bjap +L9rzbdBBm9QhBUwWq/FS9Hrq/d0qwnVLxGoO5Hk+HGfJd5mttlqT5uxIBb9IT0dO +di18YBs9NcKsjDWeW7/8hFLt+H+NUqWMiSDudFtsU6+kNcDgq0b+JEDs9KoypekY +2ddA5ag0s95n7SOwMuxtjs37xhT8uL9tzDf0xaa6sGHNVay5cVQCbxT5MpNESgpo +jhXXtTMjw7vFsct8/oIwgHJ2xYSxs8QS4PrOGJQrt2DYTOorstTcIJHxjqRHWDFi +0B31R5mormVb4OaLE36LJH8rdqKrFQ3JkgCqYDmIMOrpM6Kh/hapaUIfVeZqNRAJ +iHcmZE4pREmRPz9CinzsQXCm5yqJkucSfXtqn4c= +=m5E4 +-----END PGP PRIVATE KEY BLOCK----- diff --git a/crypto/pgpainless/src/test/resources/alice_owner@example_com b/crypto/pgpainless/src/test/resources/alice_owner@example_com new file mode 100644 index 00000000..d2612b0d --- /dev/null +++ b/crypto/pgpainless/src/test/resources/alice_owner@example_com @@ -0,0 +1,16 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lIYEY2to0BYJKwYBBAHaRw8BAQdAuI+Z2XyvQv6qBnA06ZAoKArgfMXFN783oYWl +Vh1DqpT+BwMCSqjdffS3e+X/Kfnv30tVSSrb8j2nX2C+P0ODVvS7xWs8MG8TN33d +NJXWUkfct513yADC520EL2KpXPU6GThIxsYmxBXPdyBb3CAiQQDJWLQZQWxpY2Ug +PG93bmVyQGV4YW1wbGUuY29tPoiQBBMWCAA4FiEEMqSK1ESF5td0+gNunaKflIQq +HZkFAmNraNACGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQnaKflIQqHZk6 +sAD/Xx8MQbtXcKPJi/0UGXkyRHEYbcC+zzhECFalHsQsNh0A/3Naih9zRixXt3v0 +JVCv7fbKaXpfKdGi8tj9muSa+5QBnIsEY2to0BIKKwYBBAGXVQEFAQEHQJqcQkrg +shQO4tyyshE9Ng74LAdu4zRD/yb9aQet+61BAwEIB/4HAwIEJ6OJJ8OAFf/0IyRK +Frmj2UHklu1UT1P2JPF5RPzgPlAxZB1eGHFcCQoJX/ro2AsbQ5KZUwraSs1QgX5b +GKAdcyJqFwtbz+pkTpBvOWS6iHgEGBYIACAWIQQypIrURIXm13T6A26dop+UhCod +mQUCY2to0AIbDAAKCRCdop+UhCodmYTSAP0U5Q6clPUsFcjIcwKA+x5G1Q+wzODx +7/pUS2Vg+cKOMAEAuY5wW5k0eCuWMC/uzXy8l2a3BwsMN3nlApuGk0zOcwM= +=ApnA +-----END PGP PRIVATE KEY BLOCK----- diff --git a/crypto/pgpainless/src/test/resources/bobby_owner@example_com b/crypto/pgpainless/src/test/resources/bobby_owner@example_com new file mode 100644 index 00000000..6bd548e7 --- /dev/null +++ b/crypto/pgpainless/src/test/resources/bobby_owner@example_com @@ -0,0 +1,16 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +lIYEY2tpShYJKwYBBAHaRw8BAQdA0syygn/sjv82T226XDe7ZmsJ897HQ88pruR6 +uMSdtYP+BwMCAaOsMuRJoq3/eHOl9Df1jlr3zfIBdw0hQrmcJ2qVOS4xQGDegjLW +Bbqnmjw7cCRUN1knjHdMWYwrnm8G9YmhOhhwHwdmhxw/LJOA00SyVLQZQm9iYnkg +PG93bmVyQGV4YW1wbGUuY29tPoiQBBMWCAA4FiEEPnwcCqistth5tnEB8qkNCuDF +QtMFAmNraUoCGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ8qkNCuDFQtOx +NAD+MzrKYoQQxgLWkqf08Jhc58sa+2xeZBI3Sq0o+huMql0A/39hfDlJnzD61gEZ +vwOeZuTNb+LH23ha2uG8UpoMx4oLnIsEY2tpShIKKwYBBAGXVQEFAQEHQIVF4gjr +Wpw7KN/IYZQdml9Rn7zlBMsXNIxXhcMVjxlZAwEIB/4HAwJenw1L/ZS8WP+e3uzg +khxkk1dQ3fZbTaR90z0wzLDGngVJO1J2XmfIPnTeU8conEeak8Yyt8+85QdM9MK0 +ch0MyhEa/8hRgtCL8Fo3XkLKiHgEGBYIACAWIQQ+fBwKqKy22Hm2cQHyqQ0K4MVC +0wUCY2tpSgIbDAAKCRDyqQ0K4MVC04+aAQDEW/aasrpOYw35DIddH/Wp4tSrWi65 +kv18HvDPl/c6KwEAw6ZxYsfWmxMtzY6efTIzVnvb4T3OZEVWG6XetZoDTAI= +=OFsg +-----END PGP PRIVATE KEY BLOCK----- diff --git a/crypto/pgpainless/src/test/resources/public_key b/crypto/pgpainless/src/test/resources/public_key new file mode 100644 index 00000000..987bac6f --- /dev/null +++ b/crypto/pgpainless/src/test/resources/public_key @@ -0,0 +1,21 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- +Version: PGPainless +Comment: BC98 82EF 93DC 22F8 D7D4 47AD 08ED F756 7183 CE27 +Comment: John Doe + +mDMEYT33+BYJKwYBBAHaRw8BAQdAoofwCvOfKJ4pGxEO4s64wFD+QnePpNY5zXgW +TTOFb2+0H0pvaG4gRG9lIDxqb2huLmRvZUBleGFtcGxlLmNvbT6IeAQTFgoAIAUC +YT33+AIbAQUWAgMBAAQLCQgHBRUKCQgLAh4BAhkBAAoJEAjt91Zxg84n5dYA/AiA +BqBdt2ItWgDPLCNEqt9wIMgRpkDrAMtXXyyLSkWsAQCoowpenGsq5fxhuRcS3w6Q +s+/Qw1GqnoidxhioR9J+ALg4BGE99/gSCisGAQQBl1UBBQEBB0C7eFVsFUif4q9S +taBI6JAwsI+hQSAo3I6V4jU3rix8XwMBCAeIdQQYFgoAHQUCYT33+AIbDAUWAgMB +AAQLCQgHBRUKCQgLAh4BAAoJEAjt91Zxg84nmn4BALmD8WYxTdrJqUZUE1TcFvzG +5r0//rPM8Vut5X+KwUXjAQDWVP22KaA8VXpevSxkS3n/ti0KjQVKEFzGbmwB2dTT +CbgzBGE99/gWCSsGAQQB2kcPAQEHQJXfqDjCO9L4qBu62/UPpQ5q0638kG8+AGf/ +hJH2q2BTiNUEGBYKAH0FAmE99/gCGwIFFgIDAQAECwkIBwUVCgkICwIeAV8gBBkW +CgAGBQJhPff4AAoJEGSLoii3QC8mrhcBALzpJQTHF8cJJRA9+DQ3qZ85Eu217MJi +x1aYA1i0zyP5AQD/jN/aBsSTqAHF+zU8/ezzHeoilyBYgxLS9Q2qelDeDAAKCRAI +7fdWcYPOJ7aHAP9EBq0rzV3c6GtVl8bPnk+llpV/1aodxTSnijQtVSMuMAD+JMUD +Jd2bimlhuVwpu0DFiF7IF64SAxmVifTwsTWYiQs= +=jGlC +-----END PGP PUBLIC KEY BLOCK----- diff --git a/crypto/pgpainless/src/test/resources/public_key_multiple_identities b/crypto/pgpainless/src/test/resources/public_key_multiple_identities new file mode 100644 index 00000000..2cc896c2 --- /dev/null +++ b/crypto/pgpainless/src/test/resources/public_key_multiple_identities @@ -0,0 +1,51 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +GorvBkqXcIR7 +=dL2N +-----END PGP PUBLIC KEY BLOCK----- diff --git a/crypto/pgpainless/src/test/resources/secret_key b/crypto/pgpainless/src/test/resources/secret_key new file mode 100644 index 00000000..61334b01 --- /dev/null +++ b/crypto/pgpainless/src/test/resources/secret_key @@ -0,0 +1,26 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- +Version: PGPainless +Comment: BC98 82EF 93DC 22F8 D7D4 47AD 08ED F756 7183 CE27 +Comment: John Doe + +lIYEYT33+BYJKwYBBAHaRw8BAQdAoofwCvOfKJ4pGxEO4s64wFD+QnePpNY5zXgW +TTOFb2/+CQMCh3Bp60ThtX9g8u+uxtuLdeeU5UC14Ox4zVD/x2L7sUzN94XVocOn +WVJTIgeZ1CBhrsSOMg5grj0Zwf1YODlBpZ85V8stPebpjZ2mCZUz1rQfSm9obiBE +b2UgPGpvaG4uZG9lQGV4YW1wbGUuY29tPoh4BBMWCgAgBQJhPff4AhsBBRYCAwEA +BAsJCAcFFQoJCAsCHgECGQEACgkQCO33VnGDzifl1gD8CIAGoF23Yi1aAM8sI0Sq +33AgyBGmQOsAy1dfLItKRawBAKijCl6cayrl/GG5FxLfDpCz79DDUaqeiJ3GGKhH +0n4AnIsEYT33+BIKKwYBBAGXVQEFAQEHQLt4VWwVSJ/ir1K1oEjokDCwj6FBICjc +jpXiNTeuLHxfAwEIB/4JAwKHcGnrROG1f2AcnEUWhC2rDrztJB3JK7pe+PVJbMaK +O2eYKLiBZOT6Dy1rexMi0vS19IMYLf1V2qgsO9phoglOD+m95tr8Ha9FhfbpJjua +iHUEGBYKAB0FAmE99/gCGwwFFgIDAQAECwkIBwUVCgkICwIeAQAKCRAI7fdWcYPO +J5p+AQC5g/FmMU3ayalGVBNU3Bb8xua9P/6zzPFbreV/isFF4wEA1lT9timgPFV6 +Xr0sZEt5/7YtCo0FShBcxm5sAdnU0wmchgRhPff4FgkrBgEEAdpHDwEBB0CV36g4 +wjvS+Kgbutv1D6UOatOt/JBvPgBn/4SR9qtgU/4JAwKHcGnrROG1f2A1hnm2UXZL +Go/tPJo3pJCJDLClIKi7I5RoHruafuQ2ODvznLbCnbuft9B2cA5MZUMFCk6nBvoU +k6hwGWxOSNJIOmrCx+PMiNUEGBYKAH0FAmE99/gCGwIFFgIDAQAECwkIBwUVCgkI +CwIeAV8gBBkWCgAGBQJhPff4AAoJEGSLoii3QC8mrhcBALzpJQTHF8cJJRA9+DQ3 +qZ85Eu217MJix1aYA1i0zyP5AQD/jN/aBsSTqAHF+zU8/ezzHeoilyBYgxLS9Q2q +elDeDAAKCRAI7fdWcYPOJ7aHAP9EBq0rzV3c6GtVl8bPnk+llpV/1aodxTSnijQt +VSMuMAD+JMUDJd2bimlhuVwpu0DFiF7IF64SAxmVifTwsTWYiQs= +=/dDf +-----END PGP PRIVATE KEY BLOCK----- diff --git a/crypto/pgpainless/src/test/resources/secret_key_multiple_identities b/crypto/pgpainless/src/test/resources/secret_key_multiple_identities new file mode 100644 index 00000000..5da8ac81 --- /dev/null +++ b/crypto/pgpainless/src/test/resources/secret_key_multiple_identities @@ -0,0 +1,93 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +hmaBvZKKiR6/QRMyZMQ34dx9GorvBkqXcIR7 +=8IuC +-----END PGP PRIVATE KEY BLOCK----- diff --git a/format-common/build.gradle.kts b/format-common/build.gradle.kts deleted file mode 100644 index b4ac58b6..00000000 --- a/format-common/build.gradle.kts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -plugins { id("com.github.android-password-store.kotlin-jvm-library") } - -dependencies { - api(libs.kotlinx.coroutines.core) - api(libs.thirdparty.kotlinResult) - implementation(projects.coroutineUtils) - implementation(libs.androidx.annotation) - implementation(libs.dagger.hilt.core) - implementation(libs.thirdparty.commons.codec) - implementation(libs.thirdparty.uri) - testImplementation(libs.bundles.testDependencies) - testImplementation(libs.kotlinx.coroutines.test) - testImplementation(libs.testing.turbine) -} diff --git a/format-common/lint-baseline.xml b/format-common/lint-baseline.xml deleted file mode 100644 index 2ed8a3bb..00000000 --- a/format-common/lint-baseline.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/format-common/src/main/kotlin/app/passwordstore/data/passfile/PasswordEntry.kt b/format-common/src/main/kotlin/app/passwordstore/data/passfile/PasswordEntry.kt deleted file mode 100644 index 1618374f..00000000 --- a/format-common/src/main/kotlin/app/passwordstore/data/passfile/PasswordEntry.kt +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.data.passfile - -import androidx.annotation.VisibleForTesting -import app.passwordstore.util.time.UserClock -import app.passwordstore.util.totp.Otp -import app.passwordstore.util.totp.TotpFinder -import com.github.michaelbull.result.mapBoth -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject -import kotlin.collections.set -import kotlin.coroutines.coroutineContext -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.isActive - -/** Represents a single entry in the password store. */ -public class PasswordEntry -@AssistedInject -constructor( - /** A time source used to calculate the TOTP */ - private val clock: UserClock, - /** [TotpFinder] implementation to extract data from a TOTP URI */ - private val totpFinder: TotpFinder, - /** The content of this entry, as an array of bytes. */ - @Assisted bytes: ByteArray, -) { - - private val content = bytes.decodeToString() - - /** The password text for this entry. Can be null. */ - public val password: String? - - /** The username for this entry. Can be null. */ - public val username: String? - - /** A [String] to [String] [Map] of the extra content of this entry, in a key:value fashion. */ - public val extraContent: Map - - /** - * Direct [String] representation of the extra content of this entry, before any transforms are - * applied. Only use this when the extra content is required in a formatting-preserving manner. - */ - public val extraContentString: String - - /** - * A [Flow] providing the current TOTP. It will start emitting only when collected. If this entry - * does not have a TOTP secret, the flow will never emit. Users should call [hasTotp] before - * collection to check if it is valid to collect this [Flow]. - */ - public val totp: Flow = flow { - require(totpSecret != null) { "Cannot collect this flow without a TOTP secret" } - do { - val otp = calculateTotp() - emit(otp) - delay(THOUSAND_MILLIS.milliseconds) - } while (coroutineContext.isActive) - } - - /** Obtain the [Totp.value] for this [PasswordEntry] at the current time. */ - public val currentOtp: String - get() = calculateTotp().value - - /** - * String representation of [extraContent] but with authentication related data such as TOTP URIs - * and usernames stripped. - */ - public val extraContentWithoutAuthData: String - private val totpSecret: String? - - init { - val (foundPassword, passContent) = findAndStripPassword(content.split("\n".toRegex())) - password = foundPassword - extraContentString = passContent.joinToString("\n") - extraContentWithoutAuthData = generateExtraContentWithoutAuthData() - extraContent = generateExtraContentPairs() - username = findUsername() - totpSecret = totpFinder.findSecret(content) - } - - public fun hasTotp(): Boolean { - return totpSecret != null - } - - @Suppress("ReturnCount") - private fun findAndStripPassword(passContent: List): Pair> { - if (TotpFinder.TOTP_FIELDS.any { passContent[0].startsWith(it) }) return Pair(null, passContent) - for (line in passContent) { - for (prefix in PASSWORD_FIELDS) { - if (line.startsWith(prefix, ignoreCase = true)) { - return Pair(line.substring(prefix.length).trimStart(), passContent.minus(line)) - } - } - } - return Pair(passContent[0], passContent.minus(passContent[0])) - } - - private fun generateExtraContentWithoutAuthData(): String { - var foundUsername = false - return extraContentString - .lineSequence() - .filter { line -> - return@filter when { - USERNAME_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } && - !foundUsername -> { - foundUsername = true - false - } - TotpFinder.TOTP_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } -> { - false - } - else -> { - true - } - } - } - .joinToString(separator = "\n") - } - - private fun generateExtraContentPairs(): Map { - fun MutableMap.putOrAppend(key: String, value: String) { - if (value.isEmpty()) return - val existing = this[key] - this[key] = - if (existing == null) { - value - } else { - "$existing\n$value" - } - } - - val items = mutableMapOf() - extraContentWithoutAuthData.lines().forEach { line -> - // Split the line on ':' and save all the parts into an array - // "ABC : DEF:GHI" --> ["ABC", "DEF", "GHI"] - val splitArray = line.split(":") - // Take the first element of the array. This will be the key for the key-value pair. - // ["ABC ", " DEF", "GHI"] -> key = "ABC" - val key = splitArray.first().trimEnd() - // Remove the first element from the array and join the rest of the string again with - // ':' as separator. - // ["ABC ", " DEF", "GHI"] -> value = "DEF:GHI" - val value = splitArray.drop(1).joinToString(":").trimStart() - - if (key.isNotEmpty() && value.isNotEmpty()) { - // If both key and value are not empty, we can form a pair with this so add it to - // the map. - // key = "ABC", value = "DEF:GHI" - items[key] = value - } else { - // If either key or value is empty, we were not able to form proper key-value pair. - // So append the original line into an "EXTRA CONTENT" map entry - items.putOrAppend(EXTRA_CONTENT, line) - } - } - - return items - } - - private fun findUsername(): String? { - extraContentString.splitToSequence("\n").forEach { line -> - for (prefix in USERNAME_FIELDS) { - if (line.startsWith(prefix, ignoreCase = true)) - return line.substring(prefix.length).trimStart() - } - } - return null - } - - private fun calculateTotp(): Totp { - val digits = totpFinder.findDigits(content) - val totpPeriod = totpFinder.findPeriod(content) - val totpAlgorithm = totpFinder.findAlgorithm(content) - val issuer = totpFinder.findIssuer(content) - val millis = clock.millis() - val remainingTime = (totpPeriod - ((millis / THOUSAND_MILLIS) % totpPeriod)).seconds - Otp.calculateCode( - totpSecret!!, - millis / (THOUSAND_MILLIS * totpPeriod), - totpAlgorithm, - digits, - issuer - ) - .mapBoth( - { code -> - return Totp(code, remainingTime) - }, - { throwable -> throw throwable } - ) - } - - @AssistedFactory - public interface Factory { - public fun create(bytes: ByteArray): PasswordEntry - } - - public companion object { - - private const val EXTRA_CONTENT = "Extra Content" - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - public val USERNAME_FIELDS: Array = - arrayOf( - "login:", - "username:", - "user:", - "account:", - "email:", - "mail:", - "name:", - "handle:", - "id:", - "identity:", - ) - @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) - public val PASSWORD_FIELDS: Array = - arrayOf( - "password:", - "secret:", - "pass:", - ) - private const val THOUSAND_MILLIS = 1000L - } -} diff --git a/format-common/src/main/kotlin/app/passwordstore/data/passfile/Totp.kt b/format-common/src/main/kotlin/app/passwordstore/data/passfile/Totp.kt deleted file mode 100644 index c279360f..00000000 --- a/format-common/src/main/kotlin/app/passwordstore/data/passfile/Totp.kt +++ /dev/null @@ -1,6 +0,0 @@ -package app.passwordstore.data.passfile - -import kotlin.time.Duration - -/** Holder for a TOTP secret and the duration for which it is valid. */ -public data class Totp(public val value: String, public val remainingTime: Duration) diff --git a/format-common/src/main/kotlin/app/passwordstore/util/time/UserClock.kt b/format-common/src/main/kotlin/app/passwordstore/util/time/UserClock.kt deleted file mode 100644 index 4ffeb6a6..00000000 --- a/format-common/src/main/kotlin/app/passwordstore/util/time/UserClock.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.util.time - -import java.time.Clock -import java.time.Instant -import java.time.ZoneId -import javax.inject.Inject - -/** - * A subclass of [Clock] that uses [Clock.systemDefaultZone] to get a clock that works for the - * user's current time zone. - */ -public open class UserClock @Inject constructor() : Clock() { - - private val clock = systemDefaultZone() - - override fun withZone(zone: ZoneId): Clock = clock.withZone(zone) - - override fun getZone(): ZoneId = clock.zone - - override fun instant(): Instant = clock.instant() -} diff --git a/format-common/src/main/kotlin/app/passwordstore/util/totp/Otp.kt b/format-common/src/main/kotlin/app/passwordstore/util/totp/Otp.kt deleted file mode 100644 index 10e771fe..00000000 --- a/format-common/src/main/kotlin/app/passwordstore/util/totp/Otp.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.util.totp - -import com.github.michaelbull.result.Err -import com.github.michaelbull.result.Result -import com.github.michaelbull.result.runCatching -import java.nio.ByteBuffer -import java.util.Locale -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec -import kotlin.experimental.and -import org.apache.commons.codec.binary.Base32 - -public object Otp { - - private val BASE_32 = Base32() - private val STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY".toCharArray() - private const val BYTE_BUFFER_CAPACITY = 8 - private const val END_INDEX_OFFSET = 4 - private const val STEAM_GUARD_DIGITS = 5 - private const val MINIMUM_DIGITS = 6 - private const val MAXIMUM_DIGITS = 10 - private const val ALPHABET_LENGTH = 26 - private const val MOST_SIGNIFICANT_BYTE = 0x7f - - public fun calculateCode( - secret: String, - counter: Long, - algorithm: String, - digits: String, - issuer: String?, - ): Result = runCatching { - val algo = "Hmac${algorithm.uppercase(Locale.ROOT)}" - val decodedSecret = BASE_32.decode(secret) - val secretKey = SecretKeySpec(decodedSecret, algo) - val digest = - Mac.getInstance(algo).run { - init(secretKey) - doFinal(ByteBuffer.allocate(BYTE_BUFFER_CAPACITY).putLong(counter).array()) - } - // Least significant 4 bits are used as an offset into the digest. - val offset = (digest.last() and 0xf).toInt() - // Extract 32 bits at the offset and clear the most significant bit. - val code = digest.copyOfRange(offset, offset.plus(END_INDEX_OFFSET)) - code[0] = (MOST_SIGNIFICANT_BYTE and code[0].toInt()).toByte() - val codeInt = ByteBuffer.wrap(code).int - check(codeInt > 0) - // SteamGuard is a horrible OTP implementation that generates non-standard 5 digit OTPs as - // well - // as uses a custom character set. - if (digits == "s" || issuer == "Steam") { - var remainingCodeInt = codeInt - buildString { - repeat(STEAM_GUARD_DIGITS) { - append(STEAM_ALPHABET[remainingCodeInt % STEAM_ALPHABET.size]) - remainingCodeInt /= ALPHABET_LENGTH - } - } - } else { - // Base 10, 6 to 10 digits - val numDigits = digits.toIntOrNull() - when { - numDigits == null -> { - return Err(IllegalArgumentException("Digits specifier has to be either 's' or numeric")) - } - numDigits < MINIMUM_DIGITS -> { - return Err( - IllegalArgumentException("TOTP codes have to be at least $MINIMUM_DIGITS digits long") - ) - } - numDigits > MAXIMUM_DIGITS -> { - return Err( - IllegalArgumentException("TOTP codes can be at most $MAXIMUM_DIGITS digits long") - ) - } - else -> { - // 2^31 = 2_147_483_648, so we can extract at most 10 digits with the first one - // always being 0, 1, or 2. Pad with leading zeroes. - val codeStringBase10 = codeInt.toString(MAXIMUM_DIGITS).padStart(MAXIMUM_DIGITS, '0') - check(codeStringBase10.length == MAXIMUM_DIGITS) - codeStringBase10.takeLast(numDigits) - } - } - } - } -} diff --git a/format-common/src/main/kotlin/app/passwordstore/util/totp/TotpFinder.kt b/format-common/src/main/kotlin/app/passwordstore/util/totp/TotpFinder.kt deleted file mode 100644 index d53d76dc..00000000 --- a/format-common/src/main/kotlin/app/passwordstore/util/totp/TotpFinder.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.util.totp - -/** Defines a class that can extract relevant parts of a TOTP URL for use by the app. */ -public interface TotpFinder { - - /** Get the TOTP secret from the given extra content. */ - public fun findSecret(content: String): String? - - /** Get the number of digits required in the final OTP. */ - public fun findDigits(content: String): String - - /** Get the TOTP timeout period. */ - public fun findPeriod(content: String): Long - - /** Get the algorithm for the TOTP secret. */ - public fun findAlgorithm(content: String): String - - /** Get the issuer for the TOTP secret, if any. */ - public fun findIssuer(content: String): String? - - public companion object { - public val TOTP_FIELDS: Array = arrayOf("otpauth://totp", "totp:") - } -} diff --git a/format-common/src/main/kotlin/app/passwordstore/util/totp/UriTotpFinder.kt b/format-common/src/main/kotlin/app/passwordstore/util/totp/UriTotpFinder.kt deleted file mode 100644 index bb97c90c..00000000 --- a/format-common/src/main/kotlin/app/passwordstore/util/totp/UriTotpFinder.kt +++ /dev/null @@ -1,52 +0,0 @@ -package app.passwordstore.util.totp - -import com.eygraber.uri.Uri -import javax.inject.Inject - -/** [Uri] backed TOTP URL parser. */ -public class UriTotpFinder @Inject constructor() : TotpFinder { - - private companion object { - private const val DEFAULT_TOTP_PERIOD = 30L - } - - override fun findSecret(content: String): String? { - content.split("\n".toRegex()).forEach { line -> - if (line.startsWith(TotpFinder.TOTP_FIELDS[0])) { - return Uri.parse(line).getQueryParameter("secret") - } - if (line.startsWith(TotpFinder.TOTP_FIELDS[1], ignoreCase = true)) { - return line.split(": *".toRegex(), 2).toTypedArray()[1] - } - } - return null - } - - override fun findDigits(content: String): String { - return getQueryParameter(content, "digits") ?: "6" - } - - override fun findPeriod(content: String): Long { - return getQueryParameter(content, "period")?.toLongOrNull() ?: DEFAULT_TOTP_PERIOD - } - - override fun findAlgorithm(content: String): String { - return getQueryParameter(content, "algorithm") ?: "sha1" - } - - override fun findIssuer(content: String): String? { - return getQueryParameter(content, "issuer") ?: Uri.parse(content).authority - } - - private fun getQueryParameter(content: String, parameterName: String): String? { - content.split("\n".toRegex()).forEach { line -> - val uri = Uri.parse(line) - if ( - line.startsWith(TotpFinder.TOTP_FIELDS[0]) && uri.getQueryParameter(parameterName) != null - ) { - return uri.getQueryParameter(parameterName) - } - } - return null - } -} diff --git a/format-common/src/test/kotlin/app/passwordstore/data/passfile/PasswordEntryTest.kt b/format-common/src/test/kotlin/app/passwordstore/data/passfile/PasswordEntryTest.kt deleted file mode 100644 index 865a13c1..00000000 --- a/format-common/src/test/kotlin/app/passwordstore/data/passfile/PasswordEntryTest.kt +++ /dev/null @@ -1,212 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.data.passfile - -import app.cash.turbine.Event -import app.cash.turbine.test -import app.passwordstore.util.time.TestUserClock -import app.passwordstore.util.time.UserClock -import app.passwordstore.util.totp.UriTotpFinder -import java.util.Locale -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.delay -import kotlinx.coroutines.test.runTest - -class PasswordEntryTest { - - private val totpFinder = UriTotpFinder() - - private fun makeEntry(content: String, clock: UserClock = fakeClock) = - PasswordEntry( - clock, - totpFinder, - content.encodeToByteArray(), - ) - - @Test - fun getPassword() { - assertEquals("fooooo", makeEntry("fooooo\nbla\n").password) - assertEquals("fooooo", makeEntry("fooooo\nbla").password) - assertEquals("fooooo", makeEntry("fooooo\n").password) - assertEquals("fooooo", makeEntry("fooooo").password) - assertEquals("", makeEntry("\nblubb\n").password) - assertEquals("", makeEntry("\nblubb").password) - assertEquals("", makeEntry("\n").password) - assertEquals("", makeEntry("").password) - for (field in PasswordEntry.PASSWORD_FIELDS) { - assertEquals("fooooo", makeEntry("\n$field fooooo").password) - assertEquals("fooooo", makeEntry("\n${field.uppercase(Locale.getDefault())} fooooo").password) - assertEquals("fooooo", makeEntry("GOPASS-SECRET-1.0\n$field fooooo").password) - assertEquals("fooooo", makeEntry("someFirstLine\nUsername: bar\n$field fooooo").password) - } - } - - @Test - fun getExtraContent() { - assertEquals("bla\n", makeEntry("fooooo\nbla\n").extraContentString) - assertEquals("bla", makeEntry("fooooo\nbla").extraContentString) - assertEquals("", makeEntry("fooooo\n").extraContentString) - assertEquals("", makeEntry("fooooo").extraContentString) - assertEquals("blubb\n", makeEntry("\nblubb\n").extraContentString) - assertEquals("blubb", makeEntry("\nblubb").extraContentString) - assertEquals("blubb", makeEntry("blubb\npassword: foo").extraContentString) - assertEquals("blubb", makeEntry("password: foo\nblubb").extraContentString) - assertEquals( - "blubb\nusername: bar", - makeEntry("blubb\npassword: foo\nusername: bar").extraContentString - ) - assertEquals("", makeEntry("\n").extraContentString) - assertEquals("", makeEntry("").extraContentString) - } - - @Test - fun parseExtraContentWithoutAuth() { - var entry = makeEntry("username: abc\npassword: abc\ntest: abcdef") - assertEquals(1, entry.extraContent.size) - assertTrue(entry.extraContent.containsKey("test")) - assertEquals("abcdef", entry.extraContent["test"]) - - entry = makeEntry("username: abc\npassword: abc\ntest: :abcdef:") - assertEquals(1, entry.extraContent.size) - assertTrue(entry.extraContent.containsKey("test")) - assertEquals(":abcdef:", entry.extraContent["test"]) - - entry = makeEntry("username: abc\npassword: abc\ntest : ::abc:def::") - assertEquals(1, entry.extraContent.size) - assertTrue(entry.extraContent.containsKey("test")) - assertEquals("::abc:def::", entry.extraContent["test"]) - - entry = makeEntry("username: abc\npassword: abc\ntest: abcdef\ntest2: ghijkl") - assertEquals(2, entry.extraContent.size) - assertTrue(entry.extraContent.containsKey("test2")) - assertEquals("ghijkl", entry.extraContent["test2"]) - - entry = makeEntry("username: abc\npassword: abc\ntest: abcdef\n: ghijkl\n mnopqr:") - assertEquals(2, entry.extraContent.size) - assertTrue(entry.extraContent.containsKey("Extra Content")) - assertEquals(": ghijkl\n mnopqr:", entry.extraContent["Extra Content"]) - - entry = makeEntry("username: abc\npassword: abc\n:\n\n") - assertEquals(1, entry.extraContent.size) - assertTrue(entry.extraContent.containsKey("Extra Content")) - assertEquals(":", entry.extraContent["Extra Content"]) - } - - @Test - fun getUsername() { - for (field in PasswordEntry.USERNAME_FIELDS) { - assertEquals("username", makeEntry("\n$field username").username) - assertEquals( - "username", - makeEntry("\n${field.uppercase(Locale.getDefault())} username").username - ) - } - assertEquals("username", makeEntry("secret\nextra\nlogin: username\ncontent\n").username) - assertEquals("username", makeEntry("\nextra\nusername: username\ncontent\n").username) - assertEquals("username", makeEntry("\nUSERNaMe: username\ncontent\n").username) - assertEquals("username", makeEntry("\nlogin: username").username) - assertEquals("foo@example.com", makeEntry("\nemail: foo@example.com").username) - assertEquals("username", makeEntry("\nidentity: username\nlogin: another_username").username) - assertEquals("username", makeEntry("\nLOGiN:username").username) - assertEquals("foo@example.com", makeEntry("pass\nmail: foo@example.com").username) - assertNull(makeEntry("secret\nextra\ncontent\n").username) - } - - @Test - fun hasUsername() { - assertNotNull(makeEntry("secret\nextra\nlogin: username\ncontent\n").username) - assertNull(makeEntry("secret\nextra\ncontent\n").username) - assertNull(makeEntry("secret\nlogin failed\n").username) - assertNull(makeEntry("\n").username) - assertNull(makeEntry("").username) - } - - @Test - fun generatesOtpFromTotpUri() = runTest { - val entry = makeEntry("secret\nextra\n$TOTP_URI") - assertTrue(entry.hasTotp()) - entry.totp.test { - val otp = expectMostRecentItem() - assertEquals("818800", otp.value) - assertEquals(30.seconds, otp.remainingTime) - cancelAndIgnoreRemainingEvents() - } - } - - /** - * Same as [generatesOtpFromTotpUri], but advances the clock by 5 seconds. This exercises the - * [Totp.remainingTime] calculation logic, and acts as a regression test to resolve the bug which - * blocked https://github.com/Android-Password-Store/Android-Password-Store/issues/1550. - */ - @Test - fun generatedOtpHasCorrectRemainingTime() = runTest { - val entry = makeEntry("secret\nextra\n$TOTP_URI", TestUserClock.withAddedSeconds(5)) - assertTrue(entry.hasTotp()) - entry.totp.test { - val otp = expectMostRecentItem() - assertEquals("818800", otp.value) - assertEquals(25.seconds, otp.remainingTime) - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun generatesOtpWithOnlyUriInFile() = runTest { - val entry = makeEntry(TOTP_URI) - assertNull(entry.password) - entry.totp.test { - val otp = expectMostRecentItem() - assertEquals("818800", otp.value) - assertEquals(30.seconds, otp.remainingTime) - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun emitsTotpEverySecond() = runTest { - val entry = makeEntry(TOTP_URI) - entry.totp.test { - delay(3000L) - val events = cancelAndConsumeRemainingEvents() - assertEquals(3, events.size) - assertTrue { events.all { event -> event is Event.Item } } - } - } - - @Test - fun onlyLooksForUriInFirstLine() { - val entry = makeEntry("id:\n$TOTP_URI") - assertNotNull(entry.password) - assertTrue(entry.hasTotp()) - assertNull(entry.username) - } - - // https://github.com/android-password-store/Android-Password-Store/issues/1190 - @Test - fun extraContentWithMultipleUsernameFields() { - val entry = makeEntry("pass\nuser: user\nid: id\n$TOTP_URI") - assertTrue(entry.extraContent.isNotEmpty()) - assertTrue(entry.hasTotp()) - assertNotNull(entry.username) - assertEquals("pass", entry.password) - assertEquals("user", entry.username) - assertEquals(mapOf("id" to "id"), entry.extraContent) - } - - companion object { - - @Suppress("MaxLineLength") - const val TOTP_URI = - "otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30" - - val fakeClock = TestUserClock() - } -} diff --git a/format-common/src/test/kotlin/app/passwordstore/util/time/TestUserClock.kt b/format-common/src/test/kotlin/app/passwordstore/util/time/TestUserClock.kt deleted file mode 100644 index 8a860a39..00000000 --- a/format-common/src/test/kotlin/app/passwordstore/util/time/TestUserClock.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.util.time - -import java.time.Clock -import java.time.Instant -import java.time.ZoneId -import java.time.ZoneOffset.UTC - -/** - * Implementation of [UserClock] that is fixed to [Instant.EPOCH] for deterministic time-based tests - */ -class TestUserClock(instant: Instant) : UserClock() { - - constructor() : this(Instant.EPOCH) - - private var clock = fixed(instant, UTC) - - override fun withZone(zone: ZoneId): Clock = clock.withZone(zone) - - override fun getZone(): ZoneId = UTC - - override fun instant(): Instant = clock.instant() - - companion object { - fun withAddedSeconds(secondsToAdd: Long): TestUserClock { - return TestUserClock(Instant.EPOCH.plusSeconds(secondsToAdd)) - } - } -} diff --git a/format-common/src/test/kotlin/app/passwordstore/util/totp/OtpTest.kt b/format-common/src/test/kotlin/app/passwordstore/util/totp/OtpTest.kt deleted file mode 100644 index 54ac492d..00000000 --- a/format-common/src/test/kotlin/app/passwordstore/util/totp/OtpTest.kt +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.util.totp - -import com.github.michaelbull.result.get -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull - -class OtpTest { - - private fun generateOtp( - counter: Long, - secret: String = "JBSWY3DPEHPK3PXP", - algorithm: String = "SHA1", - digits: String = "6", - issuer: String? = null, - ): String? { - return Otp.calculateCode(secret, counter, algorithm, digits, issuer).get() - } - - @Test - fun otpGeneration6Digits() { - assertEquals( - "953550", - generateOtp( - counter = 1593333298159 / (1000 * 30), - ) - ) - assertEquals( - "275379", - generateOtp( - counter = 1593333571918 / (1000 * 30), - ) - ) - assertEquals( - "867507", - generateOtp( - counter = 1593333600517 / (1000 * 57), - ) - ) - } - - @Test - fun otpGeneration10Digits() { - assertEquals( - "0740900914", - generateOtp( - counter = 1593333655044 / (1000 * 30), - digits = "10", - ) - ) - assertEquals( - "0070632029", - generateOtp( - counter = 1593333691405 / (1000 * 30), - digits = "10", - ) - ) - assertEquals( - "1017265882", - generateOtp( - counter = 1593333728893 / (1000 * 83), - digits = "10", - ) - ) - } - - @Test - fun otpGenerationIllegalInput() { - assertNull( - generateOtp( - counter = 10000, - algorithm = "SHA0", - digits = "10", - ) - ) - assertNull( - generateOtp( - counter = 10000, - digits = "a", - ) - ) - assertNull( - generateOtp( - counter = 10000, - algorithm = "SHA1", - digits = "5", - ) - ) - assertNull( - generateOtp( - counter = 10000, - digits = "11", - ) - ) - assertNull( - generateOtp( - counter = 10000, - secret = "JBSWY3DPEHPK3PXPAAAAB", - digits = "6", - ) - ) - } - - @Test - fun otpGenerationUnusualSecrets() { - assertEquals( - "127764", - generateOtp( - counter = 1593367111963 / (1000 * 30), - secret = "JBSWY3DPEHPK3PXPAAAAAAAA", - ) - ) - assertEquals( - "047515", - generateOtp( - counter = 1593367171420 / (1000 * 30), - secret = "JBSWY3DPEHPK3PXPAAAAA", - ) - ) - } - - @Test - fun otpGenerationUnpaddedSecrets() { - // Secret was generated with `echo 'string with some padding needed' | base32` - // We don't care for the resultant OTP's actual value, we just want both the padded and - // unpadded variant to generate the same one. - val unpaddedOtp = - generateOtp( - counter = 1593367171420 / (1000 * 30), - secret = "ON2HE2LOM4QHO2LUNAQHG33NMUQHAYLEMRUW4ZZANZSWKZDFMQFA", - ) - val paddedOtp = - generateOtp( - counter = 1593367171420 / (1000 * 30), - secret = "ON2HE2LOM4QHO2LUNAQHG33NMUQHAYLEMRUW4ZZANZSWKZDFMQFA====", - ) - - assertNotNull(unpaddedOtp) - assertNotNull(paddedOtp) - assertEquals(unpaddedOtp, paddedOtp) - } - - @Test - fun generateSteamTotp() { - val issuerOtp = - generateOtp( - counter = 48297900 / (1000 * 30), - secret = "STK7746GVMCHMNH5FBIAQXGPV3I7ZHRG", - issuer = "Steam", - ) - val digitsOtp = - generateOtp( - counter = 48297900 / (1000 * 30), - secret = "STK7746GVMCHMNH5FBIAQXGPV3I7ZHRG", - digits = "s", - ) - assertNotNull(issuerOtp) - assertNotNull(digitsOtp) - assertEquals("6M3CT", issuerOtp) - assertEquals("6M3CT", digitsOtp) - } -} diff --git a/format-common/src/test/kotlin/app/passwordstore/util/totp/UriTotpFinderTest.kt b/format-common/src/test/kotlin/app/passwordstore/util/totp/UriTotpFinderTest.kt deleted file mode 100644 index e462c490..00000000 --- a/format-common/src/test/kotlin/app/passwordstore/util/totp/UriTotpFinderTest.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ - -package app.passwordstore.util.totp - -import kotlin.test.Test -import kotlin.test.assertEquals - -class UriTotpFinderTest { - - private val totpFinder = UriTotpFinder() - - @Test - fun findSecret() { - assertEquals("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", totpFinder.findSecret(TOTP_URI)) - assertEquals( - "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", - totpFinder.findSecret("name\npassword\ntotp: HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ") - ) - assertEquals("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", totpFinder.findSecret(PASS_FILE_CONTENT)) - } - - @Test - fun findDigits() { - assertEquals("12", totpFinder.findDigits(TOTP_URI)) - assertEquals("12", totpFinder.findDigits(PASS_FILE_CONTENT)) - } - - @Test - fun findPeriod() { - assertEquals(25, totpFinder.findPeriod(TOTP_URI)) - assertEquals(25, totpFinder.findPeriod(PASS_FILE_CONTENT)) - } - - @Test - fun findAlgorithm() { - assertEquals("SHA256", totpFinder.findAlgorithm(TOTP_URI)) - assertEquals("SHA256", totpFinder.findAlgorithm(PASS_FILE_CONTENT)) - } - - @Test - fun findIssuer() { - assertEquals("ACME Co", totpFinder.findIssuer(TOTP_URI)) - assertEquals("ACME Co", totpFinder.findIssuer(PASS_FILE_CONTENT)) - } - - companion object { - - const val TOTP_URI = - "otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA256&digits=12&period=25" - const val PASS_FILE_CONTENT = "password\n$TOTP_URI" - } -} diff --git a/format/common/build.gradle.kts b/format/common/build.gradle.kts new file mode 100644 index 00000000..b4ac58b6 --- /dev/null +++ b/format/common/build.gradle.kts @@ -0,0 +1,18 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +plugins { id("com.github.android-password-store.kotlin-jvm-library") } + +dependencies { + api(libs.kotlinx.coroutines.core) + api(libs.thirdparty.kotlinResult) + implementation(projects.coroutineUtils) + implementation(libs.androidx.annotation) + implementation(libs.dagger.hilt.core) + implementation(libs.thirdparty.commons.codec) + implementation(libs.thirdparty.uri) + testImplementation(libs.bundles.testDependencies) + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.testing.turbine) +} diff --git a/format/common/lint-baseline.xml b/format/common/lint-baseline.xml new file mode 100644 index 00000000..2ed8a3bb --- /dev/null +++ b/format/common/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/format/common/src/main/kotlin/app/passwordstore/data/passfile/PasswordEntry.kt b/format/common/src/main/kotlin/app/passwordstore/data/passfile/PasswordEntry.kt new file mode 100644 index 00000000..1618374f --- /dev/null +++ b/format/common/src/main/kotlin/app/passwordstore/data/passfile/PasswordEntry.kt @@ -0,0 +1,231 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.data.passfile + +import androidx.annotation.VisibleForTesting +import app.passwordstore.util.time.UserClock +import app.passwordstore.util.totp.Otp +import app.passwordstore.util.totp.TotpFinder +import com.github.michaelbull.result.mapBoth +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlin.collections.set +import kotlin.coroutines.coroutineContext +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.isActive + +/** Represents a single entry in the password store. */ +public class PasswordEntry +@AssistedInject +constructor( + /** A time source used to calculate the TOTP */ + private val clock: UserClock, + /** [TotpFinder] implementation to extract data from a TOTP URI */ + private val totpFinder: TotpFinder, + /** The content of this entry, as an array of bytes. */ + @Assisted bytes: ByteArray, +) { + + private val content = bytes.decodeToString() + + /** The password text for this entry. Can be null. */ + public val password: String? + + /** The username for this entry. Can be null. */ + public val username: String? + + /** A [String] to [String] [Map] of the extra content of this entry, in a key:value fashion. */ + public val extraContent: Map + + /** + * Direct [String] representation of the extra content of this entry, before any transforms are + * applied. Only use this when the extra content is required in a formatting-preserving manner. + */ + public val extraContentString: String + + /** + * A [Flow] providing the current TOTP. It will start emitting only when collected. If this entry + * does not have a TOTP secret, the flow will never emit. Users should call [hasTotp] before + * collection to check if it is valid to collect this [Flow]. + */ + public val totp: Flow = flow { + require(totpSecret != null) { "Cannot collect this flow without a TOTP secret" } + do { + val otp = calculateTotp() + emit(otp) + delay(THOUSAND_MILLIS.milliseconds) + } while (coroutineContext.isActive) + } + + /** Obtain the [Totp.value] for this [PasswordEntry] at the current time. */ + public val currentOtp: String + get() = calculateTotp().value + + /** + * String representation of [extraContent] but with authentication related data such as TOTP URIs + * and usernames stripped. + */ + public val extraContentWithoutAuthData: String + private val totpSecret: String? + + init { + val (foundPassword, passContent) = findAndStripPassword(content.split("\n".toRegex())) + password = foundPassword + extraContentString = passContent.joinToString("\n") + extraContentWithoutAuthData = generateExtraContentWithoutAuthData() + extraContent = generateExtraContentPairs() + username = findUsername() + totpSecret = totpFinder.findSecret(content) + } + + public fun hasTotp(): Boolean { + return totpSecret != null + } + + @Suppress("ReturnCount") + private fun findAndStripPassword(passContent: List): Pair> { + if (TotpFinder.TOTP_FIELDS.any { passContent[0].startsWith(it) }) return Pair(null, passContent) + for (line in passContent) { + for (prefix in PASSWORD_FIELDS) { + if (line.startsWith(prefix, ignoreCase = true)) { + return Pair(line.substring(prefix.length).trimStart(), passContent.minus(line)) + } + } + } + return Pair(passContent[0], passContent.minus(passContent[0])) + } + + private fun generateExtraContentWithoutAuthData(): String { + var foundUsername = false + return extraContentString + .lineSequence() + .filter { line -> + return@filter when { + USERNAME_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } && + !foundUsername -> { + foundUsername = true + false + } + TotpFinder.TOTP_FIELDS.any { prefix -> line.startsWith(prefix, ignoreCase = true) } -> { + false + } + else -> { + true + } + } + } + .joinToString(separator = "\n") + } + + private fun generateExtraContentPairs(): Map { + fun MutableMap.putOrAppend(key: String, value: String) { + if (value.isEmpty()) return + val existing = this[key] + this[key] = + if (existing == null) { + value + } else { + "$existing\n$value" + } + } + + val items = mutableMapOf() + extraContentWithoutAuthData.lines().forEach { line -> + // Split the line on ':' and save all the parts into an array + // "ABC : DEF:GHI" --> ["ABC", "DEF", "GHI"] + val splitArray = line.split(":") + // Take the first element of the array. This will be the key for the key-value pair. + // ["ABC ", " DEF", "GHI"] -> key = "ABC" + val key = splitArray.first().trimEnd() + // Remove the first element from the array and join the rest of the string again with + // ':' as separator. + // ["ABC ", " DEF", "GHI"] -> value = "DEF:GHI" + val value = splitArray.drop(1).joinToString(":").trimStart() + + if (key.isNotEmpty() && value.isNotEmpty()) { + // If both key and value are not empty, we can form a pair with this so add it to + // the map. + // key = "ABC", value = "DEF:GHI" + items[key] = value + } else { + // If either key or value is empty, we were not able to form proper key-value pair. + // So append the original line into an "EXTRA CONTENT" map entry + items.putOrAppend(EXTRA_CONTENT, line) + } + } + + return items + } + + private fun findUsername(): String? { + extraContentString.splitToSequence("\n").forEach { line -> + for (prefix in USERNAME_FIELDS) { + if (line.startsWith(prefix, ignoreCase = true)) + return line.substring(prefix.length).trimStart() + } + } + return null + } + + private fun calculateTotp(): Totp { + val digits = totpFinder.findDigits(content) + val totpPeriod = totpFinder.findPeriod(content) + val totpAlgorithm = totpFinder.findAlgorithm(content) + val issuer = totpFinder.findIssuer(content) + val millis = clock.millis() + val remainingTime = (totpPeriod - ((millis / THOUSAND_MILLIS) % totpPeriod)).seconds + Otp.calculateCode( + totpSecret!!, + millis / (THOUSAND_MILLIS * totpPeriod), + totpAlgorithm, + digits, + issuer + ) + .mapBoth( + { code -> + return Totp(code, remainingTime) + }, + { throwable -> throw throwable } + ) + } + + @AssistedFactory + public interface Factory { + public fun create(bytes: ByteArray): PasswordEntry + } + + public companion object { + + private const val EXTRA_CONTENT = "Extra Content" + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public val USERNAME_FIELDS: Array = + arrayOf( + "login:", + "username:", + "user:", + "account:", + "email:", + "mail:", + "name:", + "handle:", + "id:", + "identity:", + ) + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public val PASSWORD_FIELDS: Array = + arrayOf( + "password:", + "secret:", + "pass:", + ) + private const val THOUSAND_MILLIS = 1000L + } +} diff --git a/format/common/src/main/kotlin/app/passwordstore/data/passfile/Totp.kt b/format/common/src/main/kotlin/app/passwordstore/data/passfile/Totp.kt new file mode 100644 index 00000000..c279360f --- /dev/null +++ b/format/common/src/main/kotlin/app/passwordstore/data/passfile/Totp.kt @@ -0,0 +1,6 @@ +package app.passwordstore.data.passfile + +import kotlin.time.Duration + +/** Holder for a TOTP secret and the duration for which it is valid. */ +public data class Totp(public val value: String, public val remainingTime: Duration) diff --git a/format/common/src/main/kotlin/app/passwordstore/util/time/UserClock.kt b/format/common/src/main/kotlin/app/passwordstore/util/time/UserClock.kt new file mode 100644 index 00000000..4ffeb6a6 --- /dev/null +++ b/format/common/src/main/kotlin/app/passwordstore/util/time/UserClock.kt @@ -0,0 +1,26 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.util.time + +import java.time.Clock +import java.time.Instant +import java.time.ZoneId +import javax.inject.Inject + +/** + * A subclass of [Clock] that uses [Clock.systemDefaultZone] to get a clock that works for the + * user's current time zone. + */ +public open class UserClock @Inject constructor() : Clock() { + + private val clock = systemDefaultZone() + + override fun withZone(zone: ZoneId): Clock = clock.withZone(zone) + + override fun getZone(): ZoneId = clock.zone + + override fun instant(): Instant = clock.instant() +} diff --git a/format/common/src/main/kotlin/app/passwordstore/util/totp/Otp.kt b/format/common/src/main/kotlin/app/passwordstore/util/totp/Otp.kt new file mode 100644 index 00000000..10e771fe --- /dev/null +++ b/format/common/src/main/kotlin/app/passwordstore/util/totp/Otp.kt @@ -0,0 +1,90 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.util.totp + +import com.github.michaelbull.result.Err +import com.github.michaelbull.result.Result +import com.github.michaelbull.result.runCatching +import java.nio.ByteBuffer +import java.util.Locale +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec +import kotlin.experimental.and +import org.apache.commons.codec.binary.Base32 + +public object Otp { + + private val BASE_32 = Base32() + private val STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY".toCharArray() + private const val BYTE_BUFFER_CAPACITY = 8 + private const val END_INDEX_OFFSET = 4 + private const val STEAM_GUARD_DIGITS = 5 + private const val MINIMUM_DIGITS = 6 + private const val MAXIMUM_DIGITS = 10 + private const val ALPHABET_LENGTH = 26 + private const val MOST_SIGNIFICANT_BYTE = 0x7f + + public fun calculateCode( + secret: String, + counter: Long, + algorithm: String, + digits: String, + issuer: String?, + ): Result = runCatching { + val algo = "Hmac${algorithm.uppercase(Locale.ROOT)}" + val decodedSecret = BASE_32.decode(secret) + val secretKey = SecretKeySpec(decodedSecret, algo) + val digest = + Mac.getInstance(algo).run { + init(secretKey) + doFinal(ByteBuffer.allocate(BYTE_BUFFER_CAPACITY).putLong(counter).array()) + } + // Least significant 4 bits are used as an offset into the digest. + val offset = (digest.last() and 0xf).toInt() + // Extract 32 bits at the offset and clear the most significant bit. + val code = digest.copyOfRange(offset, offset.plus(END_INDEX_OFFSET)) + code[0] = (MOST_SIGNIFICANT_BYTE and code[0].toInt()).toByte() + val codeInt = ByteBuffer.wrap(code).int + check(codeInt > 0) + // SteamGuard is a horrible OTP implementation that generates non-standard 5 digit OTPs as + // well + // as uses a custom character set. + if (digits == "s" || issuer == "Steam") { + var remainingCodeInt = codeInt + buildString { + repeat(STEAM_GUARD_DIGITS) { + append(STEAM_ALPHABET[remainingCodeInt % STEAM_ALPHABET.size]) + remainingCodeInt /= ALPHABET_LENGTH + } + } + } else { + // Base 10, 6 to 10 digits + val numDigits = digits.toIntOrNull() + when { + numDigits == null -> { + return Err(IllegalArgumentException("Digits specifier has to be either 's' or numeric")) + } + numDigits < MINIMUM_DIGITS -> { + return Err( + IllegalArgumentException("TOTP codes have to be at least $MINIMUM_DIGITS digits long") + ) + } + numDigits > MAXIMUM_DIGITS -> { + return Err( + IllegalArgumentException("TOTP codes can be at most $MAXIMUM_DIGITS digits long") + ) + } + else -> { + // 2^31 = 2_147_483_648, so we can extract at most 10 digits with the first one + // always being 0, 1, or 2. Pad with leading zeroes. + val codeStringBase10 = codeInt.toString(MAXIMUM_DIGITS).padStart(MAXIMUM_DIGITS, '0') + check(codeStringBase10.length == MAXIMUM_DIGITS) + codeStringBase10.takeLast(numDigits) + } + } + } + } +} diff --git a/format/common/src/main/kotlin/app/passwordstore/util/totp/TotpFinder.kt b/format/common/src/main/kotlin/app/passwordstore/util/totp/TotpFinder.kt new file mode 100644 index 00000000..d53d76dc --- /dev/null +++ b/format/common/src/main/kotlin/app/passwordstore/util/totp/TotpFinder.kt @@ -0,0 +1,29 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.util.totp + +/** Defines a class that can extract relevant parts of a TOTP URL for use by the app. */ +public interface TotpFinder { + + /** Get the TOTP secret from the given extra content. */ + public fun findSecret(content: String): String? + + /** Get the number of digits required in the final OTP. */ + public fun findDigits(content: String): String + + /** Get the TOTP timeout period. */ + public fun findPeriod(content: String): Long + + /** Get the algorithm for the TOTP secret. */ + public fun findAlgorithm(content: String): String + + /** Get the issuer for the TOTP secret, if any. */ + public fun findIssuer(content: String): String? + + public companion object { + public val TOTP_FIELDS: Array = arrayOf("otpauth://totp", "totp:") + } +} diff --git a/format/common/src/main/kotlin/app/passwordstore/util/totp/UriTotpFinder.kt b/format/common/src/main/kotlin/app/passwordstore/util/totp/UriTotpFinder.kt new file mode 100644 index 00000000..bb97c90c --- /dev/null +++ b/format/common/src/main/kotlin/app/passwordstore/util/totp/UriTotpFinder.kt @@ -0,0 +1,52 @@ +package app.passwordstore.util.totp + +import com.eygraber.uri.Uri +import javax.inject.Inject + +/** [Uri] backed TOTP URL parser. */ +public class UriTotpFinder @Inject constructor() : TotpFinder { + + private companion object { + private const val DEFAULT_TOTP_PERIOD = 30L + } + + override fun findSecret(content: String): String? { + content.split("\n".toRegex()).forEach { line -> + if (line.startsWith(TotpFinder.TOTP_FIELDS[0])) { + return Uri.parse(line).getQueryParameter("secret") + } + if (line.startsWith(TotpFinder.TOTP_FIELDS[1], ignoreCase = true)) { + return line.split(": *".toRegex(), 2).toTypedArray()[1] + } + } + return null + } + + override fun findDigits(content: String): String { + return getQueryParameter(content, "digits") ?: "6" + } + + override fun findPeriod(content: String): Long { + return getQueryParameter(content, "period")?.toLongOrNull() ?: DEFAULT_TOTP_PERIOD + } + + override fun findAlgorithm(content: String): String { + return getQueryParameter(content, "algorithm") ?: "sha1" + } + + override fun findIssuer(content: String): String? { + return getQueryParameter(content, "issuer") ?: Uri.parse(content).authority + } + + private fun getQueryParameter(content: String, parameterName: String): String? { + content.split("\n".toRegex()).forEach { line -> + val uri = Uri.parse(line) + if ( + line.startsWith(TotpFinder.TOTP_FIELDS[0]) && uri.getQueryParameter(parameterName) != null + ) { + return uri.getQueryParameter(parameterName) + } + } + return null + } +} diff --git a/format/common/src/test/kotlin/app/passwordstore/data/passfile/PasswordEntryTest.kt b/format/common/src/test/kotlin/app/passwordstore/data/passfile/PasswordEntryTest.kt new file mode 100644 index 00000000..865a13c1 --- /dev/null +++ b/format/common/src/test/kotlin/app/passwordstore/data/passfile/PasswordEntryTest.kt @@ -0,0 +1,212 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.data.passfile + +import app.cash.turbine.Event +import app.cash.turbine.test +import app.passwordstore.util.time.TestUserClock +import app.passwordstore.util.time.UserClock +import app.passwordstore.util.totp.UriTotpFinder +import java.util.Locale +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.delay +import kotlinx.coroutines.test.runTest + +class PasswordEntryTest { + + private val totpFinder = UriTotpFinder() + + private fun makeEntry(content: String, clock: UserClock = fakeClock) = + PasswordEntry( + clock, + totpFinder, + content.encodeToByteArray(), + ) + + @Test + fun getPassword() { + assertEquals("fooooo", makeEntry("fooooo\nbla\n").password) + assertEquals("fooooo", makeEntry("fooooo\nbla").password) + assertEquals("fooooo", makeEntry("fooooo\n").password) + assertEquals("fooooo", makeEntry("fooooo").password) + assertEquals("", makeEntry("\nblubb\n").password) + assertEquals("", makeEntry("\nblubb").password) + assertEquals("", makeEntry("\n").password) + assertEquals("", makeEntry("").password) + for (field in PasswordEntry.PASSWORD_FIELDS) { + assertEquals("fooooo", makeEntry("\n$field fooooo").password) + assertEquals("fooooo", makeEntry("\n${field.uppercase(Locale.getDefault())} fooooo").password) + assertEquals("fooooo", makeEntry("GOPASS-SECRET-1.0\n$field fooooo").password) + assertEquals("fooooo", makeEntry("someFirstLine\nUsername: bar\n$field fooooo").password) + } + } + + @Test + fun getExtraContent() { + assertEquals("bla\n", makeEntry("fooooo\nbla\n").extraContentString) + assertEquals("bla", makeEntry("fooooo\nbla").extraContentString) + assertEquals("", makeEntry("fooooo\n").extraContentString) + assertEquals("", makeEntry("fooooo").extraContentString) + assertEquals("blubb\n", makeEntry("\nblubb\n").extraContentString) + assertEquals("blubb", makeEntry("\nblubb").extraContentString) + assertEquals("blubb", makeEntry("blubb\npassword: foo").extraContentString) + assertEquals("blubb", makeEntry("password: foo\nblubb").extraContentString) + assertEquals( + "blubb\nusername: bar", + makeEntry("blubb\npassword: foo\nusername: bar").extraContentString + ) + assertEquals("", makeEntry("\n").extraContentString) + assertEquals("", makeEntry("").extraContentString) + } + + @Test + fun parseExtraContentWithoutAuth() { + var entry = makeEntry("username: abc\npassword: abc\ntest: abcdef") + assertEquals(1, entry.extraContent.size) + assertTrue(entry.extraContent.containsKey("test")) + assertEquals("abcdef", entry.extraContent["test"]) + + entry = makeEntry("username: abc\npassword: abc\ntest: :abcdef:") + assertEquals(1, entry.extraContent.size) + assertTrue(entry.extraContent.containsKey("test")) + assertEquals(":abcdef:", entry.extraContent["test"]) + + entry = makeEntry("username: abc\npassword: abc\ntest : ::abc:def::") + assertEquals(1, entry.extraContent.size) + assertTrue(entry.extraContent.containsKey("test")) + assertEquals("::abc:def::", entry.extraContent["test"]) + + entry = makeEntry("username: abc\npassword: abc\ntest: abcdef\ntest2: ghijkl") + assertEquals(2, entry.extraContent.size) + assertTrue(entry.extraContent.containsKey("test2")) + assertEquals("ghijkl", entry.extraContent["test2"]) + + entry = makeEntry("username: abc\npassword: abc\ntest: abcdef\n: ghijkl\n mnopqr:") + assertEquals(2, entry.extraContent.size) + assertTrue(entry.extraContent.containsKey("Extra Content")) + assertEquals(": ghijkl\n mnopqr:", entry.extraContent["Extra Content"]) + + entry = makeEntry("username: abc\npassword: abc\n:\n\n") + assertEquals(1, entry.extraContent.size) + assertTrue(entry.extraContent.containsKey("Extra Content")) + assertEquals(":", entry.extraContent["Extra Content"]) + } + + @Test + fun getUsername() { + for (field in PasswordEntry.USERNAME_FIELDS) { + assertEquals("username", makeEntry("\n$field username").username) + assertEquals( + "username", + makeEntry("\n${field.uppercase(Locale.getDefault())} username").username + ) + } + assertEquals("username", makeEntry("secret\nextra\nlogin: username\ncontent\n").username) + assertEquals("username", makeEntry("\nextra\nusername: username\ncontent\n").username) + assertEquals("username", makeEntry("\nUSERNaMe: username\ncontent\n").username) + assertEquals("username", makeEntry("\nlogin: username").username) + assertEquals("foo@example.com", makeEntry("\nemail: foo@example.com").username) + assertEquals("username", makeEntry("\nidentity: username\nlogin: another_username").username) + assertEquals("username", makeEntry("\nLOGiN:username").username) + assertEquals("foo@example.com", makeEntry("pass\nmail: foo@example.com").username) + assertNull(makeEntry("secret\nextra\ncontent\n").username) + } + + @Test + fun hasUsername() { + assertNotNull(makeEntry("secret\nextra\nlogin: username\ncontent\n").username) + assertNull(makeEntry("secret\nextra\ncontent\n").username) + assertNull(makeEntry("secret\nlogin failed\n").username) + assertNull(makeEntry("\n").username) + assertNull(makeEntry("").username) + } + + @Test + fun generatesOtpFromTotpUri() = runTest { + val entry = makeEntry("secret\nextra\n$TOTP_URI") + assertTrue(entry.hasTotp()) + entry.totp.test { + val otp = expectMostRecentItem() + assertEquals("818800", otp.value) + assertEquals(30.seconds, otp.remainingTime) + cancelAndIgnoreRemainingEvents() + } + } + + /** + * Same as [generatesOtpFromTotpUri], but advances the clock by 5 seconds. This exercises the + * [Totp.remainingTime] calculation logic, and acts as a regression test to resolve the bug which + * blocked https://github.com/Android-Password-Store/Android-Password-Store/issues/1550. + */ + @Test + fun generatedOtpHasCorrectRemainingTime() = runTest { + val entry = makeEntry("secret\nextra\n$TOTP_URI", TestUserClock.withAddedSeconds(5)) + assertTrue(entry.hasTotp()) + entry.totp.test { + val otp = expectMostRecentItem() + assertEquals("818800", otp.value) + assertEquals(25.seconds, otp.remainingTime) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun generatesOtpWithOnlyUriInFile() = runTest { + val entry = makeEntry(TOTP_URI) + assertNull(entry.password) + entry.totp.test { + val otp = expectMostRecentItem() + assertEquals("818800", otp.value) + assertEquals(30.seconds, otp.remainingTime) + cancelAndIgnoreRemainingEvents() + } + } + + @Test + fun emitsTotpEverySecond() = runTest { + val entry = makeEntry(TOTP_URI) + entry.totp.test { + delay(3000L) + val events = cancelAndConsumeRemainingEvents() + assertEquals(3, events.size) + assertTrue { events.all { event -> event is Event.Item } } + } + } + + @Test + fun onlyLooksForUriInFirstLine() { + val entry = makeEntry("id:\n$TOTP_URI") + assertNotNull(entry.password) + assertTrue(entry.hasTotp()) + assertNull(entry.username) + } + + // https://github.com/android-password-store/Android-Password-Store/issues/1190 + @Test + fun extraContentWithMultipleUsernameFields() { + val entry = makeEntry("pass\nuser: user\nid: id\n$TOTP_URI") + assertTrue(entry.extraContent.isNotEmpty()) + assertTrue(entry.hasTotp()) + assertNotNull(entry.username) + assertEquals("pass", entry.password) + assertEquals("user", entry.username) + assertEquals(mapOf("id" to "id"), entry.extraContent) + } + + companion object { + + @Suppress("MaxLineLength") + const val TOTP_URI = + "otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA1&digits=6&period=30" + + val fakeClock = TestUserClock() + } +} diff --git a/format/common/src/test/kotlin/app/passwordstore/util/time/TestUserClock.kt b/format/common/src/test/kotlin/app/passwordstore/util/time/TestUserClock.kt new file mode 100644 index 00000000..8a860a39 --- /dev/null +++ b/format/common/src/test/kotlin/app/passwordstore/util/time/TestUserClock.kt @@ -0,0 +1,33 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.util.time + +import java.time.Clock +import java.time.Instant +import java.time.ZoneId +import java.time.ZoneOffset.UTC + +/** + * Implementation of [UserClock] that is fixed to [Instant.EPOCH] for deterministic time-based tests + */ +class TestUserClock(instant: Instant) : UserClock() { + + constructor() : this(Instant.EPOCH) + + private var clock = fixed(instant, UTC) + + override fun withZone(zone: ZoneId): Clock = clock.withZone(zone) + + override fun getZone(): ZoneId = UTC + + override fun instant(): Instant = clock.instant() + + companion object { + fun withAddedSeconds(secondsToAdd: Long): TestUserClock { + return TestUserClock(Instant.EPOCH.plusSeconds(secondsToAdd)) + } + } +} diff --git a/format/common/src/test/kotlin/app/passwordstore/util/totp/OtpTest.kt b/format/common/src/test/kotlin/app/passwordstore/util/totp/OtpTest.kt new file mode 100644 index 00000000..54ac492d --- /dev/null +++ b/format/common/src/test/kotlin/app/passwordstore/util/totp/OtpTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.util.totp + +import com.github.michaelbull.result.get +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class OtpTest { + + private fun generateOtp( + counter: Long, + secret: String = "JBSWY3DPEHPK3PXP", + algorithm: String = "SHA1", + digits: String = "6", + issuer: String? = null, + ): String? { + return Otp.calculateCode(secret, counter, algorithm, digits, issuer).get() + } + + @Test + fun otpGeneration6Digits() { + assertEquals( + "953550", + generateOtp( + counter = 1593333298159 / (1000 * 30), + ) + ) + assertEquals( + "275379", + generateOtp( + counter = 1593333571918 / (1000 * 30), + ) + ) + assertEquals( + "867507", + generateOtp( + counter = 1593333600517 / (1000 * 57), + ) + ) + } + + @Test + fun otpGeneration10Digits() { + assertEquals( + "0740900914", + generateOtp( + counter = 1593333655044 / (1000 * 30), + digits = "10", + ) + ) + assertEquals( + "0070632029", + generateOtp( + counter = 1593333691405 / (1000 * 30), + digits = "10", + ) + ) + assertEquals( + "1017265882", + generateOtp( + counter = 1593333728893 / (1000 * 83), + digits = "10", + ) + ) + } + + @Test + fun otpGenerationIllegalInput() { + assertNull( + generateOtp( + counter = 10000, + algorithm = "SHA0", + digits = "10", + ) + ) + assertNull( + generateOtp( + counter = 10000, + digits = "a", + ) + ) + assertNull( + generateOtp( + counter = 10000, + algorithm = "SHA1", + digits = "5", + ) + ) + assertNull( + generateOtp( + counter = 10000, + digits = "11", + ) + ) + assertNull( + generateOtp( + counter = 10000, + secret = "JBSWY3DPEHPK3PXPAAAAB", + digits = "6", + ) + ) + } + + @Test + fun otpGenerationUnusualSecrets() { + assertEquals( + "127764", + generateOtp( + counter = 1593367111963 / (1000 * 30), + secret = "JBSWY3DPEHPK3PXPAAAAAAAA", + ) + ) + assertEquals( + "047515", + generateOtp( + counter = 1593367171420 / (1000 * 30), + secret = "JBSWY3DPEHPK3PXPAAAAA", + ) + ) + } + + @Test + fun otpGenerationUnpaddedSecrets() { + // Secret was generated with `echo 'string with some padding needed' | base32` + // We don't care for the resultant OTP's actual value, we just want both the padded and + // unpadded variant to generate the same one. + val unpaddedOtp = + generateOtp( + counter = 1593367171420 / (1000 * 30), + secret = "ON2HE2LOM4QHO2LUNAQHG33NMUQHAYLEMRUW4ZZANZSWKZDFMQFA", + ) + val paddedOtp = + generateOtp( + counter = 1593367171420 / (1000 * 30), + secret = "ON2HE2LOM4QHO2LUNAQHG33NMUQHAYLEMRUW4ZZANZSWKZDFMQFA====", + ) + + assertNotNull(unpaddedOtp) + assertNotNull(paddedOtp) + assertEquals(unpaddedOtp, paddedOtp) + } + + @Test + fun generateSteamTotp() { + val issuerOtp = + generateOtp( + counter = 48297900 / (1000 * 30), + secret = "STK7746GVMCHMNH5FBIAQXGPV3I7ZHRG", + issuer = "Steam", + ) + val digitsOtp = + generateOtp( + counter = 48297900 / (1000 * 30), + secret = "STK7746GVMCHMNH5FBIAQXGPV3I7ZHRG", + digits = "s", + ) + assertNotNull(issuerOtp) + assertNotNull(digitsOtp) + assertEquals("6M3CT", issuerOtp) + assertEquals("6M3CT", digitsOtp) + } +} diff --git a/format/common/src/test/kotlin/app/passwordstore/util/totp/UriTotpFinderTest.kt b/format/common/src/test/kotlin/app/passwordstore/util/totp/UriTotpFinderTest.kt new file mode 100644 index 00000000..e462c490 --- /dev/null +++ b/format/common/src/test/kotlin/app/passwordstore/util/totp/UriTotpFinderTest.kt @@ -0,0 +1,55 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ + +package app.passwordstore.util.totp + +import kotlin.test.Test +import kotlin.test.assertEquals + +class UriTotpFinderTest { + + private val totpFinder = UriTotpFinder() + + @Test + fun findSecret() { + assertEquals("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", totpFinder.findSecret(TOTP_URI)) + assertEquals( + "HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", + totpFinder.findSecret("name\npassword\ntotp: HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ") + ) + assertEquals("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", totpFinder.findSecret(PASS_FILE_CONTENT)) + } + + @Test + fun findDigits() { + assertEquals("12", totpFinder.findDigits(TOTP_URI)) + assertEquals("12", totpFinder.findDigits(PASS_FILE_CONTENT)) + } + + @Test + fun findPeriod() { + assertEquals(25, totpFinder.findPeriod(TOTP_URI)) + assertEquals(25, totpFinder.findPeriod(PASS_FILE_CONTENT)) + } + + @Test + fun findAlgorithm() { + assertEquals("SHA256", totpFinder.findAlgorithm(TOTP_URI)) + assertEquals("SHA256", totpFinder.findAlgorithm(PASS_FILE_CONTENT)) + } + + @Test + fun findIssuer() { + assertEquals("ACME Co", totpFinder.findIssuer(TOTP_URI)) + assertEquals("ACME Co", totpFinder.findIssuer(PASS_FILE_CONTENT)) + } + + companion object { + + const val TOTP_URI = + "otpauth://totp/ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=SHA256&digits=12&period=25" + const val PASS_FILE_CONTENT = "password\n$TOTP_URI" + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b8b830b7..18984e5a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -180,11 +180,11 @@ include("autofill-parser") include("coroutine-utils") -include("crypto-common") +include("crypto:common") -include("crypto-pgpainless") +include("crypto:pgpainless") -include("format-common") +include("format:common") include("passgen:diceware") @@ -192,6 +192,6 @@ include("passgen:random") include("sentry-stub") -include("ui-compose") +include("ui:compose") include("ssh") diff --git a/ui-compose/build.gradle.kts b/ui-compose/build.gradle.kts deleted file mode 100644 index 21f1e469..00000000 --- a/ui-compose/build.gradle.kts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. - * SPDX-License-Identifier: GPL-3.0-only - */ -plugins { - id("com.github.android-password-store.android-library") - id("com.github.android-password-store.kotlin-android") -} - -android { - buildFeatures { - compose = true - androidResources = true - } - composeOptions { - useLiveLiterals = false - kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() - } - namespace = "app.passwordstore.ui.compose" -} - -dependencies { - api(platform(libs.compose.bom)) - api(libs.compose.foundation.core) - api(libs.compose.foundation.layout) - api(libs.compose.material3) - api(libs.compose.ui.core) -} diff --git a/ui-compose/lint-baseline.xml b/ui-compose/lint-baseline.xml deleted file mode 100644 index 2ed8a3bb..00000000 --- a/ui-compose/lint-baseline.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/ui-compose/src/main/AndroidManifest.xml b/ui-compose/src/main/AndroidManifest.xml deleted file mode 100644 index e3cc34c8..00000000 --- a/ui-compose/src/main/AndroidManifest.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - diff --git a/ui-compose/src/main/kotlin/app/passwordstore/ui/APSAppBar.kt b/ui-compose/src/main/kotlin/app/passwordstore/ui/APSAppBar.kt deleted file mode 100644 index 94b646b7..00000000 --- a/ui-compose/src/main/kotlin/app/passwordstore/ui/APSAppBar.kt +++ /dev/null @@ -1,40 +0,0 @@ -package app.passwordstore.ui - -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.unit.dp - -@Composable -@OptIn(ExperimentalMaterial3Api::class) -public fun APSAppBar( - title: String, - backgroundColor: Color, - navigationIcon: Painter?, - onNavigationIconClick: (() -> Unit)?, - modifier: Modifier = Modifier, -) { - TopAppBar( - title = { Text(text = title) }, - navigationIcon = { - if (navigationIcon != null) { - IconButton(onClick = { onNavigationIconClick?.invoke() }) { - Icon( - painter = navigationIcon, - contentDescription = null, - ) - } - } - }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = backgroundColor), - modifier = modifier.shadow(8.dp), - ) -} diff --git a/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt b/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt deleted file mode 100644 index d2a04777..00000000 --- a/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt +++ /dev/null @@ -1,82 +0,0 @@ -package app.passwordstore.ui.compose - -import androidx.annotation.StringRes -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation - -@Composable -public fun PasswordField( - value: String, - label: String, - initialVisibility: Boolean, - modifier: Modifier = Modifier, - readOnly: Boolean = false, -) { - var visible by remember { mutableStateOf(initialVisibility) } - TextField( - value = value, - onValueChange = {}, - readOnly = readOnly, - label = { Text(label) }, - visualTransformation = - if (visible) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { - ToggleButton( - visible = visible, - contentDescription = "Toggle password visibility", - onButtonClick = { visible = !visible }, - ) - }, - modifier = modifier, - ) -} - -@Composable -private fun ToggleButton( - visible: Boolean, - contentDescription: String, - onButtonClick: () -> Unit, - modifier: Modifier = Modifier, -) { - IconButton(onClick = onButtonClick, modifier = modifier) { - val icon = - if (visible) painterResource(id = R.drawable.baseline_visibility_off_24) - else painterResource(id = R.drawable.baseline_visibility_24) - Icon( - painter = icon, - contentDescription = contentDescription, - ) - } -} - -@Composable -public fun CopyButton( - textToCopy: String, - @StringRes buttonLabelRes: Int, - modifier: Modifier = Modifier, -) { - val clipboard = LocalClipboardManager.current - IconButton( - onClick = { clipboard.setText(AnnotatedString(textToCopy)) }, - modifier = modifier, - ) { - Icon( - painter = painterResource(R.drawable.ic_content_copy), - contentDescription = stringResource(buttonLabelRes), - ) - } -} diff --git a/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Color.kt b/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Color.kt deleted file mode 100644 index 7709d136..00000000 --- a/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Color.kt +++ /dev/null @@ -1,58 +0,0 @@ -package app.passwordstore.ui.compose.theme - -import androidx.compose.ui.graphics.Color - -internal val md_theme_light_primary = Color(0xFF006591) -internal val md_theme_light_onPrimary = Color(0xFFffffff) -internal val md_theme_light_primaryContainer = Color(0xFFc7e6ff) -internal val md_theme_light_onPrimaryContainer = Color(0xFF001e30) -internal val md_theme_light_secondary = Color(0xFF4f606e) -internal val md_theme_light_onSecondary = Color(0xFFffffff) -internal val md_theme_light_secondaryContainer = Color(0xFFd3e5f5) -internal val md_theme_light_onSecondaryContainer = Color(0xFF0b1d29) -internal val md_theme_light_tertiary = Color(0xFF006494) -internal val md_theme_light_onTertiary = Color(0xFFffffff) -internal val md_theme_light_tertiaryContainer = Color(0xFFc8e6ff) -internal val md_theme_light_onTertiaryContainer = Color(0xFF001e31) -internal val md_theme_light_error = Color(0xFFba1b1b) -internal val md_theme_light_errorContainer = Color(0xFFffdad4) -internal val md_theme_light_onError = Color(0xFFffffff) -internal val md_theme_light_onErrorContainer = Color(0xFF410001) -internal val md_theme_light_background = Color(0xFFfcfcff) -internal val md_theme_light_onBackground = Color(0xFF1a1c1e) -internal val md_theme_light_surface = Color(0xFFfcfcff) -internal val md_theme_light_onSurface = Color(0xFF1a1c1e) -internal val md_theme_light_surfaceVariant = Color(0xFFdde3ea) -internal val md_theme_light_onSurfaceVariant = Color(0xFF41474d) -internal val md_theme_light_outline = Color(0xFF72787e) -internal val md_theme_light_inverseOnSurface = Color(0xFFf0f0f3) -internal val md_theme_light_inverseSurface = Color(0xFF2e3133) - -internal val md_theme_dark_primary = Color(0xFF85ceff) -internal val md_theme_dark_onPrimary = Color(0xFF00344e) -internal val md_theme_dark_primaryContainer = Color(0xFF004c6f) -internal val md_theme_dark_onPrimaryContainer = Color(0xFFc7e6ff) -internal val md_theme_dark_secondary = Color(0xFFb7c9d9) -internal val md_theme_dark_onSecondary = Color(0xFF21323e) -internal val md_theme_dark_secondaryContainer = Color(0xFF384956) -internal val md_theme_dark_onSecondaryContainer = Color(0xFFd3e5f5) -internal val md_theme_dark_tertiary = Color(0xFF8aceff) -internal val md_theme_dark_onTertiary = Color(0xFF003450) -internal val md_theme_dark_tertiaryContainer = Color(0xFF004b70) -internal val md_theme_dark_onTertiaryContainer = Color(0xFFc8e6ff) -internal val md_theme_dark_error = Color(0xFFffb4a9) -internal val md_theme_dark_errorContainer = Color(0xFF930006) -internal val md_theme_dark_onError = Color(0xFF680003) -internal val md_theme_dark_onErrorContainer = Color(0xFFffdad4) -internal val md_theme_dark_background = Color(0xFF1a1c1e) -internal val md_theme_dark_onBackground = Color(0xFFe1e2e5) -internal val md_theme_dark_surface = Color(0xFF1a1c1e) -internal val md_theme_dark_onSurface = Color(0xFFe1e2e5) -internal val md_theme_dark_surfaceVariant = Color(0xFF41474d) -internal val md_theme_dark_onSurfaceVariant = Color(0xFFc1c7ce) -internal val md_theme_dark_outline = Color(0xFF8b9197) -internal val md_theme_dark_inverseOnSurface = Color(0xFF1a1c1e) -internal val md_theme_dark_inverseSurface = Color(0xFFe1e2e5) - -internal val seed = Color(0xFF003e5b) -internal val error = Color(0xFFba1b1b) diff --git a/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Theme.kt b/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Theme.kt deleted file mode 100644 index b11aa7c1..00000000 --- a/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Theme.kt +++ /dev/null @@ -1,77 +0,0 @@ -package app.passwordstore.ui.compose.theme - -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable - -internal val LightThemeColors = - lightColorScheme( - primary = md_theme_light_primary, - onPrimary = md_theme_light_onPrimary, - primaryContainer = md_theme_light_primaryContainer, - onPrimaryContainer = md_theme_light_onPrimaryContainer, - secondary = md_theme_light_secondary, - onSecondary = md_theme_light_onSecondary, - secondaryContainer = md_theme_light_secondaryContainer, - onSecondaryContainer = md_theme_light_onSecondaryContainer, - tertiary = md_theme_light_tertiary, - onTertiary = md_theme_light_onTertiary, - tertiaryContainer = md_theme_light_tertiaryContainer, - onTertiaryContainer = md_theme_light_onTertiaryContainer, - error = md_theme_light_error, - errorContainer = md_theme_light_errorContainer, - onError = md_theme_light_onError, - onErrorContainer = md_theme_light_onErrorContainer, - background = md_theme_light_background, - onBackground = md_theme_light_onBackground, - surface = md_theme_light_surface, - onSurface = md_theme_light_onSurface, - surfaceVariant = md_theme_light_surfaceVariant, - onSurfaceVariant = md_theme_light_onSurfaceVariant, - outline = md_theme_light_outline, - inverseOnSurface = md_theme_light_inverseOnSurface, - inverseSurface = md_theme_light_inverseSurface, - ) -internal val DarkThemeColors = - darkColorScheme( - primary = md_theme_dark_primary, - onPrimary = md_theme_dark_onPrimary, - primaryContainer = md_theme_dark_primaryContainer, - onPrimaryContainer = md_theme_dark_onPrimaryContainer, - secondary = md_theme_dark_secondary, - onSecondary = md_theme_dark_onSecondary, - secondaryContainer = md_theme_dark_secondaryContainer, - onSecondaryContainer = md_theme_dark_onSecondaryContainer, - tertiary = md_theme_dark_tertiary, - onTertiary = md_theme_dark_onTertiary, - tertiaryContainer = md_theme_dark_tertiaryContainer, - onTertiaryContainer = md_theme_dark_onTertiaryContainer, - error = md_theme_dark_error, - errorContainer = md_theme_dark_errorContainer, - onError = md_theme_dark_onError, - onErrorContainer = md_theme_dark_onErrorContainer, - background = md_theme_dark_background, - onBackground = md_theme_dark_onBackground, - surface = md_theme_dark_surface, - onSurface = md_theme_dark_onSurface, - surfaceVariant = md_theme_dark_surfaceVariant, - onSurfaceVariant = md_theme_dark_onSurfaceVariant, - outline = md_theme_dark_outline, - inverseOnSurface = md_theme_dark_inverseOnSurface, - inverseSurface = md_theme_dark_inverseSurface, - ) - -@Composable -public fun APSTheme( - colors: ColorScheme, - content: @Composable () -> Unit, -) { - MaterialTheme(colorScheme = colors, typography = AppTypography, content = content) -} - -@Composable -public fun APSThemePreview(content: @Composable () -> Unit) { - MaterialTheme(colorScheme = LightThemeColors, typography = AppTypography, content = content) -} diff --git a/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Type.kt b/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Type.kt deleted file mode 100644 index b9a07d5d..00000000 --- a/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Type.kt +++ /dev/null @@ -1,144 +0,0 @@ -package app.passwordstore.ui.compose.theme - -import androidx.compose.material3.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.Font -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp -import app.passwordstore.ui.compose.R - -internal val Manrope = - FontFamily( - Font(R.font.manrope_bold, FontWeight.Bold), - Font(R.font.manrope_extrabold, FontWeight.ExtraBold), - Font(R.font.manrope_extralight, FontWeight.ExtraLight), - Font(R.font.manrope_light, FontWeight.Light), - Font(R.font.manrope_medium, FontWeight.Medium), - Font(R.font.manrope_regular, FontWeight.Normal), - Font(R.font.manrope_semibold, FontWeight.SemiBold), - ) - -internal val AppTypography = - Typography( - displayLarge = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.W400, - fontSize = 57.sp, - lineHeight = 64.sp, - letterSpacing = (-0.25).sp, - ), - displayMedium = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.W400, - fontSize = 45.sp, - lineHeight = 52.sp, - letterSpacing = 0.sp, - ), - displaySmall = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.W400, - fontSize = 36.sp, - lineHeight = 44.sp, - letterSpacing = 0.sp, - ), - headlineLarge = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.W400, - fontSize = 32.sp, - lineHeight = 40.sp, - letterSpacing = 0.sp, - ), - headlineMedium = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.W400, - fontSize = 28.sp, - lineHeight = 36.sp, - letterSpacing = 0.sp, - ), - headlineSmall = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.W400, - fontSize = 24.sp, - lineHeight = 32.sp, - letterSpacing = 0.sp, - ), - titleLarge = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.W400, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp, - ), - titleMedium = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.Medium, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.1.sp, - ), - titleSmall = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp, - ), - labelLarge = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.1.sp, - ), - bodyLarge = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.W400, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp, - ), - bodyMedium = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.W400, - fontSize = 14.sp, - lineHeight = 20.sp, - letterSpacing = 0.25.sp, - ), - bodySmall = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.W400, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.4.sp, - ), - labelMedium = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.Medium, - fontSize = 12.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp, - ), - labelSmall = - TextStyle( - fontFamily = Manrope, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp, - ), - ) diff --git a/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/utils.kt b/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/utils.kt deleted file mode 100644 index 096862dc..00000000 --- a/ui-compose/src/main/kotlin/app/passwordstore/ui/compose/theme/utils.kt +++ /dev/null @@ -1,27 +0,0 @@ -package app.passwordstore.ui.compose.theme - -import android.content.Context -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.runtime.Composable - -@Composable -public fun decideColorScheme(context: Context): ColorScheme { - val isDarkTheme = isSystemInDarkTheme() - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - if (isDarkTheme) { - dynamicDarkColorScheme(context) - } else { - dynamicLightColorScheme(context) - } - } else { - if (isDarkTheme) { - DarkThemeColors - } else { - LightThemeColors - } - } -} diff --git a/ui-compose/src/main/res/drawable/baseline_visibility_24.xml b/ui-compose/src/main/res/drawable/baseline_visibility_24.xml deleted file mode 100644 index e732f005..00000000 --- a/ui-compose/src/main/res/drawable/baseline_visibility_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/ui-compose/src/main/res/drawable/baseline_visibility_off_24.xml b/ui-compose/src/main/res/drawable/baseline_visibility_off_24.xml deleted file mode 100644 index a5cad715..00000000 --- a/ui-compose/src/main/res/drawable/baseline_visibility_off_24.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - diff --git a/ui-compose/src/main/res/drawable/ic_content_copy.xml b/ui-compose/src/main/res/drawable/ic_content_copy.xml deleted file mode 100644 index 8afc9846..00000000 --- a/ui-compose/src/main/res/drawable/ic_content_copy.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - diff --git a/ui-compose/src/main/res/font/manrope.xml b/ui-compose/src/main/res/font/manrope.xml deleted file mode 100644 index c1ecac5c..00000000 --- a/ui-compose/src/main/res/font/manrope.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - diff --git a/ui-compose/src/main/res/font/manrope_bold.ttf b/ui-compose/src/main/res/font/manrope_bold.ttf deleted file mode 100644 index 8bbf0bd1..00000000 Binary files a/ui-compose/src/main/res/font/manrope_bold.ttf and /dev/null differ diff --git a/ui-compose/src/main/res/font/manrope_extrabold.ttf b/ui-compose/src/main/res/font/manrope_extrabold.ttf deleted file mode 100644 index 3f68dffc..00000000 Binary files a/ui-compose/src/main/res/font/manrope_extrabold.ttf and /dev/null differ diff --git a/ui-compose/src/main/res/font/manrope_extralight.ttf b/ui-compose/src/main/res/font/manrope_extralight.ttf deleted file mode 100644 index 9d21d775..00000000 Binary files a/ui-compose/src/main/res/font/manrope_extralight.ttf and /dev/null differ diff --git a/ui-compose/src/main/res/font/manrope_light.ttf b/ui-compose/src/main/res/font/manrope_light.ttf deleted file mode 100644 index f255257a..00000000 Binary files a/ui-compose/src/main/res/font/manrope_light.ttf and /dev/null differ diff --git a/ui-compose/src/main/res/font/manrope_medium.ttf b/ui-compose/src/main/res/font/manrope_medium.ttf deleted file mode 100644 index c73d7741..00000000 Binary files a/ui-compose/src/main/res/font/manrope_medium.ttf and /dev/null differ diff --git a/ui-compose/src/main/res/font/manrope_regular.ttf b/ui-compose/src/main/res/font/manrope_regular.ttf deleted file mode 100644 index c02b01be..00000000 Binary files a/ui-compose/src/main/res/font/manrope_regular.ttf and /dev/null differ diff --git a/ui-compose/src/main/res/font/manrope_semibold.ttf b/ui-compose/src/main/res/font/manrope_semibold.ttf deleted file mode 100644 index 30ee0310..00000000 Binary files a/ui-compose/src/main/res/font/manrope_semibold.ttf and /dev/null differ diff --git a/ui/compose/build.gradle.kts b/ui/compose/build.gradle.kts new file mode 100644 index 00000000..21f1e469 --- /dev/null +++ b/ui/compose/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright © 2014-2021 The Android Password Store Authors. All Rights Reserved. + * SPDX-License-Identifier: GPL-3.0-only + */ +plugins { + id("com.github.android-password-store.android-library") + id("com.github.android-password-store.kotlin-android") +} + +android { + buildFeatures { + compose = true + androidResources = true + } + composeOptions { + useLiveLiterals = false + kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() + } + namespace = "app.passwordstore.ui.compose" +} + +dependencies { + api(platform(libs.compose.bom)) + api(libs.compose.foundation.core) + api(libs.compose.foundation.layout) + api(libs.compose.material3) + api(libs.compose.ui.core) +} diff --git a/ui/compose/lint-baseline.xml b/ui/compose/lint-baseline.xml new file mode 100644 index 00000000..2ed8a3bb --- /dev/null +++ b/ui/compose/lint-baseline.xml @@ -0,0 +1,4 @@ + + + + diff --git a/ui/compose/src/main/AndroidManifest.xml b/ui/compose/src/main/AndroidManifest.xml new file mode 100644 index 00000000..e3cc34c8 --- /dev/null +++ b/ui/compose/src/main/AndroidManifest.xml @@ -0,0 +1,6 @@ + + + diff --git a/ui/compose/src/main/kotlin/app/passwordstore/ui/APSAppBar.kt b/ui/compose/src/main/kotlin/app/passwordstore/ui/APSAppBar.kt new file mode 100644 index 00000000..94b646b7 --- /dev/null +++ b/ui/compose/src/main/kotlin/app/passwordstore/ui/APSAppBar.kt @@ -0,0 +1,40 @@ +package app.passwordstore.ui + +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +public fun APSAppBar( + title: String, + backgroundColor: Color, + navigationIcon: Painter?, + onNavigationIconClick: (() -> Unit)?, + modifier: Modifier = Modifier, +) { + TopAppBar( + title = { Text(text = title) }, + navigationIcon = { + if (navigationIcon != null) { + IconButton(onClick = { onNavigationIconClick?.invoke() }) { + Icon( + painter = navigationIcon, + contentDescription = null, + ) + } + } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = backgroundColor), + modifier = modifier.shadow(8.dp), + ) +} diff --git a/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt b/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt new file mode 100644 index 00000000..d2a04777 --- /dev/null +++ b/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/PasswordField.kt @@ -0,0 +1,82 @@ +package app.passwordstore.ui.compose + +import androidx.annotation.StringRes +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation + +@Composable +public fun PasswordField( + value: String, + label: String, + initialVisibility: Boolean, + modifier: Modifier = Modifier, + readOnly: Boolean = false, +) { + var visible by remember { mutableStateOf(initialVisibility) } + TextField( + value = value, + onValueChange = {}, + readOnly = readOnly, + label = { Text(label) }, + visualTransformation = + if (visible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + ToggleButton( + visible = visible, + contentDescription = "Toggle password visibility", + onButtonClick = { visible = !visible }, + ) + }, + modifier = modifier, + ) +} + +@Composable +private fun ToggleButton( + visible: Boolean, + contentDescription: String, + onButtonClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton(onClick = onButtonClick, modifier = modifier) { + val icon = + if (visible) painterResource(id = R.drawable.baseline_visibility_off_24) + else painterResource(id = R.drawable.baseline_visibility_24) + Icon( + painter = icon, + contentDescription = contentDescription, + ) + } +} + +@Composable +public fun CopyButton( + textToCopy: String, + @StringRes buttonLabelRes: Int, + modifier: Modifier = Modifier, +) { + val clipboard = LocalClipboardManager.current + IconButton( + onClick = { clipboard.setText(AnnotatedString(textToCopy)) }, + modifier = modifier, + ) { + Icon( + painter = painterResource(R.drawable.ic_content_copy), + contentDescription = stringResource(buttonLabelRes), + ) + } +} diff --git a/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Color.kt b/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Color.kt new file mode 100644 index 00000000..7709d136 --- /dev/null +++ b/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Color.kt @@ -0,0 +1,58 @@ +package app.passwordstore.ui.compose.theme + +import androidx.compose.ui.graphics.Color + +internal val md_theme_light_primary = Color(0xFF006591) +internal val md_theme_light_onPrimary = Color(0xFFffffff) +internal val md_theme_light_primaryContainer = Color(0xFFc7e6ff) +internal val md_theme_light_onPrimaryContainer = Color(0xFF001e30) +internal val md_theme_light_secondary = Color(0xFF4f606e) +internal val md_theme_light_onSecondary = Color(0xFFffffff) +internal val md_theme_light_secondaryContainer = Color(0xFFd3e5f5) +internal val md_theme_light_onSecondaryContainer = Color(0xFF0b1d29) +internal val md_theme_light_tertiary = Color(0xFF006494) +internal val md_theme_light_onTertiary = Color(0xFFffffff) +internal val md_theme_light_tertiaryContainer = Color(0xFFc8e6ff) +internal val md_theme_light_onTertiaryContainer = Color(0xFF001e31) +internal val md_theme_light_error = Color(0xFFba1b1b) +internal val md_theme_light_errorContainer = Color(0xFFffdad4) +internal val md_theme_light_onError = Color(0xFFffffff) +internal val md_theme_light_onErrorContainer = Color(0xFF410001) +internal val md_theme_light_background = Color(0xFFfcfcff) +internal val md_theme_light_onBackground = Color(0xFF1a1c1e) +internal val md_theme_light_surface = Color(0xFFfcfcff) +internal val md_theme_light_onSurface = Color(0xFF1a1c1e) +internal val md_theme_light_surfaceVariant = Color(0xFFdde3ea) +internal val md_theme_light_onSurfaceVariant = Color(0xFF41474d) +internal val md_theme_light_outline = Color(0xFF72787e) +internal val md_theme_light_inverseOnSurface = Color(0xFFf0f0f3) +internal val md_theme_light_inverseSurface = Color(0xFF2e3133) + +internal val md_theme_dark_primary = Color(0xFF85ceff) +internal val md_theme_dark_onPrimary = Color(0xFF00344e) +internal val md_theme_dark_primaryContainer = Color(0xFF004c6f) +internal val md_theme_dark_onPrimaryContainer = Color(0xFFc7e6ff) +internal val md_theme_dark_secondary = Color(0xFFb7c9d9) +internal val md_theme_dark_onSecondary = Color(0xFF21323e) +internal val md_theme_dark_secondaryContainer = Color(0xFF384956) +internal val md_theme_dark_onSecondaryContainer = Color(0xFFd3e5f5) +internal val md_theme_dark_tertiary = Color(0xFF8aceff) +internal val md_theme_dark_onTertiary = Color(0xFF003450) +internal val md_theme_dark_tertiaryContainer = Color(0xFF004b70) +internal val md_theme_dark_onTertiaryContainer = Color(0xFFc8e6ff) +internal val md_theme_dark_error = Color(0xFFffb4a9) +internal val md_theme_dark_errorContainer = Color(0xFF930006) +internal val md_theme_dark_onError = Color(0xFF680003) +internal val md_theme_dark_onErrorContainer = Color(0xFFffdad4) +internal val md_theme_dark_background = Color(0xFF1a1c1e) +internal val md_theme_dark_onBackground = Color(0xFFe1e2e5) +internal val md_theme_dark_surface = Color(0xFF1a1c1e) +internal val md_theme_dark_onSurface = Color(0xFFe1e2e5) +internal val md_theme_dark_surfaceVariant = Color(0xFF41474d) +internal val md_theme_dark_onSurfaceVariant = Color(0xFFc1c7ce) +internal val md_theme_dark_outline = Color(0xFF8b9197) +internal val md_theme_dark_inverseOnSurface = Color(0xFF1a1c1e) +internal val md_theme_dark_inverseSurface = Color(0xFFe1e2e5) + +internal val seed = Color(0xFF003e5b) +internal val error = Color(0xFFba1b1b) diff --git a/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Theme.kt b/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Theme.kt new file mode 100644 index 00000000..b11aa7c1 --- /dev/null +++ b/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Theme.kt @@ -0,0 +1,77 @@ +package app.passwordstore.ui.compose.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +internal val LightThemeColors = + lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + ) +internal val DarkThemeColors = + darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + ) + +@Composable +public fun APSTheme( + colors: ColorScheme, + content: @Composable () -> Unit, +) { + MaterialTheme(colorScheme = colors, typography = AppTypography, content = content) +} + +@Composable +public fun APSThemePreview(content: @Composable () -> Unit) { + MaterialTheme(colorScheme = LightThemeColors, typography = AppTypography, content = content) +} diff --git a/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Type.kt b/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Type.kt new file mode 100644 index 00000000..b9a07d5d --- /dev/null +++ b/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/Type.kt @@ -0,0 +1,144 @@ +package app.passwordstore.ui.compose.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import app.passwordstore.ui.compose.R + +internal val Manrope = + FontFamily( + Font(R.font.manrope_bold, FontWeight.Bold), + Font(R.font.manrope_extrabold, FontWeight.ExtraBold), + Font(R.font.manrope_extralight, FontWeight.ExtraLight), + Font(R.font.manrope_light, FontWeight.Light), + Font(R.font.manrope_medium, FontWeight.Medium), + Font(R.font.manrope_regular, FontWeight.Normal), + Font(R.font.manrope_semibold, FontWeight.SemiBold), + ) + +internal val AppTypography = + Typography( + displayLarge = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.W400, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp, + ), + displayMedium = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.W400, + fontSize = 45.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp, + ), + displaySmall = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.W400, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp, + ), + headlineLarge = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.W400, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp, + ), + headlineMedium = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.W400, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp, + ), + headlineSmall = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.W400, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp, + ), + titleLarge = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.W400, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp, + ), + titleMedium = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.1.sp, + ), + titleSmall = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + labelLarge = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + bodyLarge = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.W400, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + ), + bodyMedium = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.W400, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ), + bodySmall = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.W400, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + ), + labelMedium = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), + labelSmall = + TextStyle( + fontFamily = Manrope, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + ), + ) diff --git a/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/utils.kt b/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/utils.kt new file mode 100644 index 00000000..096862dc --- /dev/null +++ b/ui/compose/src/main/kotlin/app/passwordstore/ui/compose/theme/utils.kt @@ -0,0 +1,27 @@ +package app.passwordstore.ui.compose.theme + +import android.content.Context +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.runtime.Composable + +@Composable +public fun decideColorScheme(context: Context): ColorScheme { + val isDarkTheme = isSystemInDarkTheme() + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (isDarkTheme) { + dynamicDarkColorScheme(context) + } else { + dynamicLightColorScheme(context) + } + } else { + if (isDarkTheme) { + DarkThemeColors + } else { + LightThemeColors + } + } +} diff --git a/ui/compose/src/main/res/drawable/baseline_visibility_24.xml b/ui/compose/src/main/res/drawable/baseline_visibility_24.xml new file mode 100644 index 00000000..e732f005 --- /dev/null +++ b/ui/compose/src/main/res/drawable/baseline_visibility_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/ui/compose/src/main/res/drawable/baseline_visibility_off_24.xml b/ui/compose/src/main/res/drawable/baseline_visibility_off_24.xml new file mode 100644 index 00000000..a5cad715 --- /dev/null +++ b/ui/compose/src/main/res/drawable/baseline_visibility_off_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/ui/compose/src/main/res/drawable/ic_content_copy.xml b/ui/compose/src/main/res/drawable/ic_content_copy.xml new file mode 100644 index 00000000..8afc9846 --- /dev/null +++ b/ui/compose/src/main/res/drawable/ic_content_copy.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/ui/compose/src/main/res/font/manrope.xml b/ui/compose/src/main/res/font/manrope.xml new file mode 100644 index 00000000..c1ecac5c --- /dev/null +++ b/ui/compose/src/main/res/font/manrope.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + diff --git a/ui/compose/src/main/res/font/manrope_bold.ttf b/ui/compose/src/main/res/font/manrope_bold.ttf new file mode 100644 index 00000000..8bbf0bd1 Binary files /dev/null and b/ui/compose/src/main/res/font/manrope_bold.ttf differ diff --git a/ui/compose/src/main/res/font/manrope_extrabold.ttf b/ui/compose/src/main/res/font/manrope_extrabold.ttf new file mode 100644 index 00000000..3f68dffc Binary files /dev/null and b/ui/compose/src/main/res/font/manrope_extrabold.ttf differ diff --git a/ui/compose/src/main/res/font/manrope_extralight.ttf b/ui/compose/src/main/res/font/manrope_extralight.ttf new file mode 100644 index 00000000..9d21d775 Binary files /dev/null and b/ui/compose/src/main/res/font/manrope_extralight.ttf differ diff --git a/ui/compose/src/main/res/font/manrope_light.ttf b/ui/compose/src/main/res/font/manrope_light.ttf new file mode 100644 index 00000000..f255257a Binary files /dev/null and b/ui/compose/src/main/res/font/manrope_light.ttf differ diff --git a/ui/compose/src/main/res/font/manrope_medium.ttf b/ui/compose/src/main/res/font/manrope_medium.ttf new file mode 100644 index 00000000..c73d7741 Binary files /dev/null and b/ui/compose/src/main/res/font/manrope_medium.ttf differ diff --git a/ui/compose/src/main/res/font/manrope_regular.ttf b/ui/compose/src/main/res/font/manrope_regular.ttf new file mode 100644 index 00000000..c02b01be Binary files /dev/null and b/ui/compose/src/main/res/font/manrope_regular.ttf differ diff --git a/ui/compose/src/main/res/font/manrope_semibold.ttf b/ui/compose/src/main/res/font/manrope_semibold.ttf new file mode 100644 index 00000000..30ee0310 Binary files /dev/null and b/ui/compose/src/main/res/font/manrope_semibold.ttf differ -- cgit v1.2.3