From a29341b312a3259302bb8f8227542a9905d55f33 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 14 Aug 2026 11:46:55 +0200 Subject: [PATCH 01/20] refactor: move :migration:legacy-data to :legacy-migration --- app/build.gradle.kts | 2 +- .../build.gradle.kts | 2 +- .../consumer-rules.pro | 0 .../1.json | 0 .../2.json | 0 .../3.json | 0 .../data/crypto/LegacyAesGcmCipher.kt | 4 +- .../data/json/LegacyDetailJson.kt | 2 +- .../data/json/LegacyDetailParser.kt | 10 ++--- .../data/local/dao/LegacyElementDao.kt | 10 ++--- .../AndroidLegacyDatabaseProvider.kt | 4 +- .../data/local/datasource/LegacyDatabase.kt | 12 +++--- .../datasource/LegacyDatabaseProvider.kt | 2 +- .../local/entity/LegacySecureElementEntity.kt | 2 +- .../entity/LegacySecureElementTagCrossRef.kt | 2 +- .../data/local/entity/LegacyTagEntity.kt | 2 +- .../local/migration/LegacyMigration2To3.kt | 2 +- .../data/local/pojo/LegacyElementWithTags.kt | 8 ++-- .../data/mapper/LegacyElementMapper.kt | 10 ++--- .../repository/LegacyItemRepositoryImpl.kt | 28 ++++++------- .../repository/LegacyKeyRepositoryImpl.kt | 4 +- .../di/LegacyDatabaseModule.kt | 6 +-- .../di/LegacyMigrationModule.kt | 6 +-- .../domain/crypto/LegacyCipher.kt | 2 +- .../domain/mapper/LegacyItemConverter.kt | 10 ++--- .../domain/model/LegacyDetail.kt | 2 +- .../domain/model/LegacyFailureReason.kt | 2 +- .../domain/model/LegacyItem.kt | 2 +- .../domain/model/LegacyMigrationOutcome.kt | 2 +- .../domain/model/LegacyMigrationReport.kt | 2 +- .../domain/model/LegacyReadFailure.kt | 2 +- .../domain/model/LegacyStrength.kt | 2 +- .../domain/repository/LegacyItemRepository.kt | 8 ++-- .../domain/repository/LegacyKeyRepository.kt | 2 +- .../domain/usecase/LegacyDataImportStarter.kt | 2 +- .../domain/usecase/LegacyImportRunner.kt | 6 +-- .../usecase/MigrateLegacyDataUseCase.kt | 22 +++++----- .../LegacyMigrationEndToEndTest.kt | 34 ++++++++-------- .../LegacyMigrationRealDatabaseTest.kt | 22 +++++----- .../data/crypto/LegacyAesGcmCipherTest.kt | 4 +- .../data/json/LegacyDetailParserTest.kt | 6 +-- .../data/local/LegacyDatabaseOpenTest.kt | 4 +- .../data/local/LegacyMigrationTest.kt | 8 ++-- .../data/local/LegacySchemaIdentityTest.kt | 2 +- .../data/local/LegacySchemaSeed.kt | 6 +-- .../data/local/LegacyTestContext.kt | 6 +-- .../LegacyItemRepositoryImplTest.kt | 38 +++++++++--------- .../domain/mapper/LegacyItemConverterTest.kt | 16 ++++---- .../domain/usecase/LegacyImportRunnerTest.kt | 10 ++--- .../usecase/MigrateLegacyDataUseCaseTest.kt | 30 +++++++------- .../legacy-fixtures/secure_element_database | Bin .../secure_element_database-shm | Bin .../secure_element_database-wal | Bin .../data/FakeLegacyCipher.kt | 6 +-- .../data/FakeLegacyDatabase.kt | 6 +-- .../data/FakeLegacyDatabaseProvider.kt | 6 +-- .../data/FakeLegacyElementDao.kt | 12 +++--- .../data/FakeLegacyItemRepository.kt | 8 ++-- .../data/FakeLegacyKeyRepository.kt | 4 +- .../data/FakeRegistrableDomainResolver.kt | 2 +- .../data/LegacyV1Encryption.kt | 2 +- settings.gradle.kts | 2 +- 62 files changed, 209 insertions(+), 209 deletions(-) rename {migration/legacy-data => legacy-migration}/build.gradle.kts (96%) rename {migration/legacy-data => legacy-migration}/consumer-rules.pro (100%) rename {migration/legacy-data/schemas/de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase => legacy-migration/schemas/de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase}/1.json (100%) rename {migration/legacy-data/schemas/de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase => legacy-migration/schemas/de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase}/2.json (100%) rename {migration/legacy-data/schemas/de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase => legacy-migration/schemas/de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase}/3.json (100%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/crypto/LegacyAesGcmCipher.kt (88%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/json/LegacyDetailJson.kt (94%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/json/LegacyDetailParser.kt (85%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/local/dao/LegacyElementDao.kt (68%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/local/datasource/AndroidLegacyDatabaseProvider.kt (93%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/local/datasource/LegacyDatabase.kt (77%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/local/datasource/LegacyDatabaseProvider.kt (89%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/local/entity/LegacySecureElementEntity.kt (97%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/local/entity/LegacySecureElementTagCrossRef.kt (92%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/local/entity/LegacyTagEntity.kt (82%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/local/migration/LegacyMigration2To3.kt (98%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/local/pojo/LegacyElementWithTags.kt (64%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/mapper/LegacyElementMapper.kt (73%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/repository/LegacyItemRepositoryImpl.kt (81%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/repository/LegacyKeyRepositoryImpl.kt (88%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/di/LegacyDatabaseModule.kt (56%) rename migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/di/MigrationLegacyDataModule.kt => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt (59%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/crypto/LegacyCipher.kt (89%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/mapper/LegacyItemConverter.kt (94%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/model/LegacyDetail.kt (96%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/model/LegacyFailureReason.kt (90%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/model/LegacyItem.kt (88%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/model/LegacyMigrationOutcome.kt (96%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/model/LegacyMigrationReport.kt (92%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/model/LegacyReadFailure.kt (96%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/model/LegacyStrength.kt (81%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/repository/LegacyItemRepository.kt (85%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/repository/LegacyKeyRepository.kt (89%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/usecase/LegacyDataImportStarter.kt (97%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/usecase/LegacyImportRunner.kt (95%) rename {migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/usecase/MigrateLegacyDataUseCase.kt (92%) rename {migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration}/LegacyMigrationEndToEndTest.kt (93%) rename {migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration}/LegacyMigrationRealDatabaseTest.kt (92%) rename {migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration}/data/crypto/LegacyAesGcmCipherTest.kt (94%) rename {migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration}/data/json/LegacyDetailParserTest.kt (95%) rename {migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration}/data/local/LegacyDatabaseOpenTest.kt (97%) rename {migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration}/data/local/LegacyMigrationTest.kt (95%) rename {migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration}/data/local/LegacySchemaIdentityTest.kt (97%) rename {migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration}/data/local/LegacySchemaSeed.kt (93%) rename {migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration}/data/local/LegacyTestContext.kt (89%) rename {migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration}/data/repository/LegacyItemRepositoryImplTest.kt (90%) rename {migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration}/domain/mapper/LegacyItemConverterTest.kt (94%) rename {migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration}/domain/usecase/LegacyImportRunnerTest.kt (96%) rename {migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration}/domain/usecase/MigrateLegacyDataUseCaseTest.kt (93%) rename {migration/legacy-data => legacy-migration}/src/test/resources/legacy-fixtures/secure_element_database (100%) rename {migration/legacy-data => legacy-migration}/src/test/resources/legacy-fixtures/secure_element_database-shm (100%) rename {migration/legacy-data => legacy-migration}/src/test/resources/legacy-fixtures/secure_element_database-wal (100%) rename {migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration}/data/FakeLegacyCipher.kt (84%) rename {migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration}/data/FakeLegacyDatabase.kt (79%) rename {migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration}/data/FakeLegacyDatabaseProvider.kt (83%) rename {migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration}/data/FakeLegacyElementDao.kt (77%) rename {migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration}/data/FakeLegacyItemRepository.kt (87%) rename {migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration}/data/FakeLegacyKeyRepository.kt (84%) rename {migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration}/data/FakeRegistrableDomainResolver.kt (93%) rename {migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data => legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration}/data/LegacyV1Encryption.kt (93%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ffb70bed1..0450e2bce 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -120,7 +120,7 @@ dependencies { implementation(projects.feature.backup) implementation(projects.feature.onboarding) implementation(projects.migration.createAccess) - implementation(projects.migration.legacyData) + implementation(projects.legacyMigration) implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.core.ktx) diff --git a/migration/legacy-data/build.gradle.kts b/legacy-migration/build.gradle.kts similarity index 96% rename from migration/legacy-data/build.gradle.kts rename to legacy-migration/build.gradle.kts index e6e1aafa3..9875d1fb1 100644 --- a/migration/legacy-data/build.gradle.kts +++ b/legacy-migration/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } android { - namespace = "de.davis.keygo.migration.legacy_data" + namespace = "de.davis.keygo.legacy_migration" testFixtures { enable = true diff --git a/migration/legacy-data/consumer-rules.pro b/legacy-migration/consumer-rules.pro similarity index 100% rename from migration/legacy-data/consumer-rules.pro rename to legacy-migration/consumer-rules.pro diff --git a/migration/legacy-data/schemas/de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase/1.json b/legacy-migration/schemas/de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase/1.json similarity index 100% rename from migration/legacy-data/schemas/de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase/1.json rename to legacy-migration/schemas/de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase/1.json diff --git a/migration/legacy-data/schemas/de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase/2.json b/legacy-migration/schemas/de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase/2.json similarity index 100% rename from migration/legacy-data/schemas/de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase/2.json rename to legacy-migration/schemas/de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase/2.json diff --git a/migration/legacy-data/schemas/de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase/3.json b/legacy-migration/schemas/de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase/3.json similarity index 100% rename from migration/legacy-data/schemas/de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase/3.json rename to legacy-migration/schemas/de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase/3.json diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/crypto/LegacyAesGcmCipher.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipher.kt similarity index 88% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/crypto/LegacyAesGcmCipher.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipher.kt index 5b378bafc..dfbab3a7a 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/crypto/LegacyAesGcmCipher.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipher.kt @@ -1,6 +1,6 @@ -package de.davis.keygo.migration.legacy_data.data.crypto +package de.davis.keygo.legacy_migration.data.crypto -import de.davis.keygo.migration.legacy_data.domain.crypto.LegacyCipher +import de.davis.keygo.legacy_migration.domain.crypto.LegacyCipher import org.koin.core.annotation.Single import javax.crypto.Cipher import javax.crypto.SecretKey diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/json/LegacyDetailJson.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/json/LegacyDetailJson.kt similarity index 94% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/json/LegacyDetailJson.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/json/LegacyDetailJson.kt index c500a47f8..4a93d6907 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/json/LegacyDetailJson.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/json/LegacyDetailJson.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.data.json +package de.davis.keygo.legacy_migration.data.json import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/json/LegacyDetailParser.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/json/LegacyDetailParser.kt similarity index 85% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/json/LegacyDetailParser.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/json/LegacyDetailParser.kt index 8a4bb4198..6f2dcae58 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/json/LegacyDetailParser.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/json/LegacyDetailParser.kt @@ -1,9 +1,9 @@ -package de.davis.keygo.migration.legacy_data.data.json +package de.davis.keygo.legacy_migration.data.json -import de.davis.keygo.migration.legacy_data.domain.model.LEGACY_TYPE_CREDIT_CARD -import de.davis.keygo.migration.legacy_data.domain.model.LEGACY_TYPE_PASSWORD -import de.davis.keygo.migration.legacy_data.domain.model.LegacyDetail -import de.davis.keygo.migration.legacy_data.domain.model.LegacyStrength +import de.davis.keygo.legacy_migration.domain.model.LEGACY_TYPE_CREDIT_CARD +import de.davis.keygo.legacy_migration.domain.model.LEGACY_TYPE_PASSWORD +import de.davis.keygo.legacy_migration.domain.model.LegacyDetail +import de.davis.keygo.legacy_migration.domain.model.LegacyStrength import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/dao/LegacyElementDao.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/dao/LegacyElementDao.kt similarity index 68% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/dao/LegacyElementDao.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/dao/LegacyElementDao.kt index d018bec94..ef81b037a 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/dao/LegacyElementDao.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/dao/LegacyElementDao.kt @@ -1,13 +1,13 @@ -package de.davis.keygo.migration.legacy_data.data.local.dao +package de.davis.keygo.legacy_migration.data.local.dao import androidx.room3.Dao import androidx.room3.Insert import androidx.room3.Query import androidx.room3.Transaction -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacySecureElementEntity -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacySecureElementTagCrossRef -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacyTagEntity -import de.davis.keygo.migration.legacy_data.data.local.pojo.LegacyElementWithTags +import de.davis.keygo.legacy_migration.data.local.entity.LegacySecureElementEntity +import de.davis.keygo.legacy_migration.data.local.entity.LegacySecureElementTagCrossRef +import de.davis.keygo.legacy_migration.data.local.entity.LegacyTagEntity +import de.davis.keygo.legacy_migration.data.local.pojo.LegacyElementWithTags @Dao internal interface LegacyElementDao { diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/datasource/AndroidLegacyDatabaseProvider.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/datasource/AndroidLegacyDatabaseProvider.kt similarity index 93% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/datasource/AndroidLegacyDatabaseProvider.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/datasource/AndroidLegacyDatabaseProvider.kt index b47aeb6d1..49febcd69 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/datasource/AndroidLegacyDatabaseProvider.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/datasource/AndroidLegacyDatabaseProvider.kt @@ -1,9 +1,9 @@ -package de.davis.keygo.migration.legacy_data.data.local.datasource +package de.davis.keygo.legacy_migration.data.local.datasource import android.content.Context import androidx.room3.Room import androidx.sqlite.SQLiteDriver -import de.davis.keygo.migration.legacy_data.data.local.migration.LegacyMigration2To3 +import de.davis.keygo.legacy_migration.data.local.migration.LegacyMigration2To3 /** * Opens the inherited file. Never creates one. diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/datasource/LegacyDatabase.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/datasource/LegacyDatabase.kt similarity index 77% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/datasource/LegacyDatabase.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/datasource/LegacyDatabase.kt index 20d176836..a6f480f24 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/datasource/LegacyDatabase.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/datasource/LegacyDatabase.kt @@ -1,12 +1,12 @@ -package de.davis.keygo.migration.legacy_data.data.local.datasource +package de.davis.keygo.legacy_migration.data.local.datasource import androidx.room3.AutoMigration import androidx.room3.Database import androidx.room3.RoomDatabase -import de.davis.keygo.migration.legacy_data.data.local.dao.LegacyElementDao -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacySecureElementEntity -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacySecureElementTagCrossRef -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacyTagEntity +import de.davis.keygo.legacy_migration.data.local.dao.LegacyElementDao +import de.davis.keygo.legacy_migration.data.local.entity.LegacySecureElementEntity +import de.davis.keygo.legacy_migration.data.local.entity.LegacySecureElementTagCrossRef +import de.davis.keygo.legacy_migration.data.local.entity.LegacyTagEntity internal const val LEGACY_DATABASE_NAME = "secure_element_database" @@ -19,7 +19,7 @@ internal const val LEGACY_DATABASE_NAME = "secure_element_database" * 1-to-2 only adds columns, so Room generates it from v1's own exported `1.json`. 2-to-3 is a table * recreate that a row with a NULL `title` or `data` would abort, so it is hand written instead and * registered on the builder rather than here: see - * [de.davis.keygo.migration.legacy_data.data.local.migration.LegacyMigration2To3]. + * [de.davis.keygo.legacy_migration.data.local.migration.LegacyMigration2To3]. * * Opening this over an inherited file is a one-way door, but only once the open succeeds. The first * query runs the migrations, which permanently rewrite the file to version 3 and drop its diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/datasource/LegacyDatabaseProvider.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/datasource/LegacyDatabaseProvider.kt similarity index 89% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/datasource/LegacyDatabaseProvider.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/datasource/LegacyDatabaseProvider.kt index 37e7ea0cb..f8968afc1 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/datasource/LegacyDatabaseProvider.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/datasource/LegacyDatabaseProvider.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.data.local.datasource +package de.davis.keygo.legacy_migration.data.local.datasource internal interface LegacyDatabaseProvider { diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/entity/LegacySecureElementEntity.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/entity/LegacySecureElementEntity.kt similarity index 97% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/entity/LegacySecureElementEntity.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/entity/LegacySecureElementEntity.kt index b58ccb5c6..6c0135f28 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/entity/LegacySecureElementEntity.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/entity/LegacySecureElementEntity.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.data.local.entity +package de.davis.keygo.legacy_migration.data.local.entity import androidx.room3.ColumnInfo import androidx.room3.Embedded diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/entity/LegacySecureElementTagCrossRef.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/entity/LegacySecureElementTagCrossRef.kt similarity index 92% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/entity/LegacySecureElementTagCrossRef.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/entity/LegacySecureElementTagCrossRef.kt index 09b1cbd77..46a6d4272 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/entity/LegacySecureElementTagCrossRef.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/entity/LegacySecureElementTagCrossRef.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.data.local.entity +package de.davis.keygo.legacy_migration.data.local.entity import androidx.room3.Entity import androidx.room3.ForeignKey diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/entity/LegacyTagEntity.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/entity/LegacyTagEntity.kt similarity index 82% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/entity/LegacyTagEntity.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/entity/LegacyTagEntity.kt index f9a7d433a..1020d4e42 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/entity/LegacyTagEntity.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/entity/LegacyTagEntity.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.data.local.entity +package de.davis.keygo.legacy_migration.data.local.entity import androidx.room3.Entity import androidx.room3.Index diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/migration/LegacyMigration2To3.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/migration/LegacyMigration2To3.kt similarity index 98% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/migration/LegacyMigration2To3.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/migration/LegacyMigration2To3.kt index eee520c9d..deb9176ad 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/migration/LegacyMigration2To3.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/migration/LegacyMigration2To3.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.data.local.migration +package de.davis.keygo.legacy_migration.data.local.migration import androidx.room3.migration.Migration import androidx.sqlite.SQLiteConnection diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/pojo/LegacyElementWithTags.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/pojo/LegacyElementWithTags.kt similarity index 64% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/pojo/LegacyElementWithTags.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/pojo/LegacyElementWithTags.kt index afd09f08c..33e33bdf5 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/local/pojo/LegacyElementWithTags.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/pojo/LegacyElementWithTags.kt @@ -1,11 +1,11 @@ -package de.davis.keygo.migration.legacy_data.data.local.pojo +package de.davis.keygo.legacy_migration.data.local.pojo import androidx.room3.Embedded import androidx.room3.Junction import androidx.room3.Relation -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacySecureElementEntity -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacySecureElementTagCrossRef -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacyTagEntity +import de.davis.keygo.legacy_migration.data.local.entity.LegacySecureElementEntity +import de.davis.keygo.legacy_migration.data.local.entity.LegacySecureElementTagCrossRef +import de.davis.keygo.legacy_migration.data.local.entity.LegacyTagEntity internal data class LegacyElementWithTags( @Embedded val element: LegacySecureElementEntity, diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/mapper/LegacyElementMapper.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/mapper/LegacyElementMapper.kt similarity index 73% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/mapper/LegacyElementMapper.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/mapper/LegacyElementMapper.kt index 1bc81722e..953ff576a 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/mapper/LegacyElementMapper.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/mapper/LegacyElementMapper.kt @@ -1,9 +1,9 @@ -package de.davis.keygo.migration.legacy_data.data.mapper +package de.davis.keygo.legacy_migration.data.mapper -import de.davis.keygo.migration.legacy_data.data.local.pojo.LegacyElementWithTags -import de.davis.keygo.migration.legacy_data.domain.model.LEGACY_TAG_PREFIX -import de.davis.keygo.migration.legacy_data.domain.model.LegacyDetail -import de.davis.keygo.migration.legacy_data.domain.model.LegacyItem +import de.davis.keygo.legacy_migration.data.local.pojo.LegacyElementWithTags +import de.davis.keygo.legacy_migration.domain.model.LEGACY_TAG_PREFIX +import de.davis.keygo.legacy_migration.domain.model.LegacyDetail +import de.davis.keygo.legacy_migration.domain.model.LegacyItem /** * Turns a decrypted, parsed row into a [LegacyItem]. diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/repository/LegacyItemRepositoryImpl.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImpl.kt similarity index 81% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/repository/LegacyItemRepositoryImpl.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImpl.kt index b1d2d8977..b023aa614 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/repository/LegacyItemRepositoryImpl.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImpl.kt @@ -1,21 +1,21 @@ -package de.davis.keygo.migration.legacy_data.data.repository +package de.davis.keygo.legacy_migration.data.repository import de.davis.keygo.core.util.Result import de.davis.keygo.core.util.asResult import de.davis.keygo.core.util.resultBinding -import de.davis.keygo.migration.legacy_data.data.json.LegacyDetailParser -import de.davis.keygo.migration.legacy_data.data.local.dao.LegacyElementDao -import de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabaseProvider -import de.davis.keygo.migration.legacy_data.data.local.pojo.LegacyElementWithTags -import de.davis.keygo.migration.legacy_data.data.mapper.toLegacyItem -import de.davis.keygo.migration.legacy_data.domain.crypto.LegacyCipher -import de.davis.keygo.migration.legacy_data.domain.model.LegacyFailureReason -import de.davis.keygo.migration.legacy_data.domain.model.LegacyItem -import de.davis.keygo.migration.legacy_data.domain.model.LegacyReadFailure -import de.davis.keygo.migration.legacy_data.domain.model.LegacyRowFailure -import de.davis.keygo.migration.legacy_data.domain.repository.LegacyItemRepository -import de.davis.keygo.migration.legacy_data.domain.repository.LegacyKeyRepository -import de.davis.keygo.migration.legacy_data.domain.repository.LegacyReadResult +import de.davis.keygo.legacy_migration.data.json.LegacyDetailParser +import de.davis.keygo.legacy_migration.data.local.dao.LegacyElementDao +import de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabaseProvider +import de.davis.keygo.legacy_migration.data.local.pojo.LegacyElementWithTags +import de.davis.keygo.legacy_migration.data.mapper.toLegacyItem +import de.davis.keygo.legacy_migration.domain.crypto.LegacyCipher +import de.davis.keygo.legacy_migration.domain.model.LegacyFailureReason +import de.davis.keygo.legacy_migration.domain.model.LegacyItem +import de.davis.keygo.legacy_migration.domain.model.LegacyReadFailure +import de.davis.keygo.legacy_migration.domain.model.LegacyRowFailure +import de.davis.keygo.legacy_migration.domain.repository.LegacyItemRepository +import de.davis.keygo.legacy_migration.domain.repository.LegacyKeyRepository +import de.davis.keygo.legacy_migration.domain.repository.LegacyReadResult import org.koin.core.annotation.Single import kotlin.coroutines.cancellation.CancellationException diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/repository/LegacyKeyRepositoryImpl.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyKeyRepositoryImpl.kt similarity index 88% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/repository/LegacyKeyRepositoryImpl.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyKeyRepositoryImpl.kt index 11112b162..0e5987f78 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/data/repository/LegacyKeyRepositoryImpl.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyKeyRepositoryImpl.kt @@ -1,6 +1,6 @@ -package de.davis.keygo.migration.legacy_data.data.repository +package de.davis.keygo.legacy_migration.data.repository -import de.davis.keygo.migration.legacy_data.domain.repository.LegacyKeyRepository +import de.davis.keygo.legacy_migration.domain.repository.LegacyKeyRepository import org.koin.core.annotation.Single import java.security.KeyStore import javax.crypto.SecretKey diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/di/LegacyDatabaseModule.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyDatabaseModule.kt similarity index 56% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/di/LegacyDatabaseModule.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyDatabaseModule.kt index c13f594aa..c4e9a35e5 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/di/LegacyDatabaseModule.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyDatabaseModule.kt @@ -1,8 +1,8 @@ -package de.davis.keygo.migration.legacy_data.di +package de.davis.keygo.legacy_migration.di import android.content.Context -import de.davis.keygo.migration.legacy_data.data.local.datasource.AndroidLegacyDatabaseProvider -import de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabaseProvider +import de.davis.keygo.legacy_migration.data.local.datasource.AndroidLegacyDatabaseProvider +import de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabaseProvider import org.koin.core.annotation.Module import org.koin.core.annotation.Single diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/di/MigrationLegacyDataModule.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt similarity index 59% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/di/MigrationLegacyDataModule.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt index 29fb92479..b5f7042ba 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/di/MigrationLegacyDataModule.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.di +package de.davis.keygo.legacy_migration.di import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Configuration @@ -6,5 +6,5 @@ import org.koin.core.annotation.Module @Module(includes = [LegacyDatabaseModule::class]) @Configuration -@ComponentScan("de.davis.keygo.migration.legacy_data") -object MigrationLegacyDataModule +@ComponentScan("de.davis.keygo.legacy_migration") +object LegacyMigrationModule diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/crypto/LegacyCipher.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/crypto/LegacyCipher.kt similarity index 89% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/crypto/LegacyCipher.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/crypto/LegacyCipher.kt index bd25cb2eb..a379bdd41 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/crypto/LegacyCipher.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/crypto/LegacyCipher.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.crypto +package de.davis.keygo.legacy_migration.domain.crypto import javax.crypto.SecretKey diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/mapper/LegacyItemConverter.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/mapper/LegacyItemConverter.kt similarity index 94% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/mapper/LegacyItemConverter.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/mapper/LegacyItemConverter.kt index 2dc65cea9..a9136e428 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/mapper/LegacyItemConverter.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/mapper/LegacyItemConverter.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.mapper +package de.davis.keygo.legacy_migration.domain.mapper import de.davis.keygo.core.item.domain.alias.ItemId import de.davis.keygo.core.item.domain.alias.VaultId @@ -16,10 +16,10 @@ import de.davis.keygo.core.item.domain.model.toYearMonthOrNull import de.davis.keygo.core.security.domain.crypto.CryptographicScope import de.davis.keygo.core.security.domain.crypto.encrypt import de.davis.keygo.core.util.domain.resolver.RegistrableDomainResolver -import de.davis.keygo.migration.legacy_data.domain.crypto.LegacyCipher -import de.davis.keygo.migration.legacy_data.domain.model.LegacyDetail -import de.davis.keygo.migration.legacy_data.domain.model.LegacyItem -import de.davis.keygo.migration.legacy_data.domain.model.LegacyStrength +import de.davis.keygo.legacy_migration.domain.crypto.LegacyCipher +import de.davis.keygo.legacy_migration.domain.model.LegacyDetail +import de.davis.keygo.legacy_migration.domain.model.LegacyItem +import de.davis.keygo.legacy_migration.domain.model.LegacyStrength import org.koin.core.annotation.Single import javax.crypto.SecretKey import kotlin.time.Clock diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyDetail.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyDetail.kt similarity index 96% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyDetail.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyDetail.kt index eced5bfad..b963ce33d 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyDetail.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyDetail.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.model +package de.davis.keygo.legacy_migration.domain.model /** The decoded contents of a v1 `SecureElement.data` blob, before any v2 types are involved. */ internal sealed interface LegacyDetail { diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyFailureReason.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyFailureReason.kt similarity index 90% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyFailureReason.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyFailureReason.kt index 63396eaf5..697c29759 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyFailureReason.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyFailureReason.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.model +package de.davis.keygo.legacy_migration.domain.model /** Why one v1 row could not be imported. The row stays in the legacy database either way. */ enum class LegacyFailureReason { diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyItem.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyItem.kt similarity index 88% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyItem.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyItem.kt index 1301f70e0..f2c32699d 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyItem.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyItem.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.model +package de.davis.keygo.legacy_migration.domain.model /** A single v1 row, decrypted and parsed, with its user tags already filtered. */ internal data class LegacyItem( diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyMigrationOutcome.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyMigrationOutcome.kt similarity index 96% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyMigrationOutcome.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyMigrationOutcome.kt index 5b96a191f..ba399f638 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyMigrationOutcome.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyMigrationOutcome.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.model +package de.davis.keygo.legacy_migration.domain.model sealed interface LegacyMigrationOutcome { diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyMigrationReport.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyMigrationReport.kt similarity index 92% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyMigrationReport.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyMigrationReport.kt index 88a0c7ff2..2f213a220 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyMigrationReport.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyMigrationReport.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.model +package de.davis.keygo.legacy_migration.domain.model data class LegacyRowFailure( val legacyId: Long, diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyReadFailure.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyReadFailure.kt similarity index 96% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyReadFailure.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyReadFailure.kt index c41b68dc7..94153faac 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyReadFailure.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyReadFailure.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.model +package de.davis.keygo.legacy_migration.domain.model /** * Why a whole read could not run. Nothing is imported, and the legacy file is left exactly as it diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyStrength.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyStrength.kt similarity index 81% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyStrength.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyStrength.kt index 377dca34a..54ac8e010 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/model/LegacyStrength.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/LegacyStrength.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.model +package de.davis.keygo.legacy_migration.domain.model /** * v1's `Strength`, in v1's declaration order. The ordinal is load-bearing: the older JSON form diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/repository/LegacyItemRepository.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyItemRepository.kt similarity index 85% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/repository/LegacyItemRepository.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyItemRepository.kt index 8ec4573cd..506fa67d7 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/repository/LegacyItemRepository.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyItemRepository.kt @@ -1,9 +1,9 @@ -package de.davis.keygo.migration.legacy_data.domain.repository +package de.davis.keygo.legacy_migration.domain.repository import de.davis.keygo.core.util.Result -import de.davis.keygo.migration.legacy_data.domain.model.LegacyItem -import de.davis.keygo.migration.legacy_data.domain.model.LegacyReadFailure -import de.davis.keygo.migration.legacy_data.domain.model.LegacyRowFailure +import de.davis.keygo.legacy_migration.domain.model.LegacyItem +import de.davis.keygo.legacy_migration.domain.model.LegacyReadFailure +import de.davis.keygo.legacy_migration.domain.model.LegacyRowFailure import javax.crypto.SecretKey internal data class LegacyReadResult( diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/repository/LegacyKeyRepository.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyKeyRepository.kt similarity index 89% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/repository/LegacyKeyRepository.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyKeyRepository.kt index 70c8da367..a93fc6e90 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/repository/LegacyKeyRepository.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyKeyRepository.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.repository +package de.davis.keygo.legacy_migration.domain.repository import javax.crypto.SecretKey diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/LegacyDataImportStarter.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyDataImportStarter.kt similarity index 97% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/LegacyDataImportStarter.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyDataImportStarter.kt index 7aed9dd18..567583fe4 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/LegacyDataImportStarter.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyDataImportStarter.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.usecase +package de.davis.keygo.legacy_migration.domain.usecase import android.util.Log import de.davis.keygo.core.security.domain.Session diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/LegacyImportRunner.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunner.kt similarity index 95% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/LegacyImportRunner.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunner.kt index d96df5a79..2e4032540 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/LegacyImportRunner.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunner.kt @@ -1,7 +1,7 @@ -package de.davis.keygo.migration.legacy_data.domain.usecase +package de.davis.keygo.legacy_migration.domain.usecase -import de.davis.keygo.migration.legacy_data.domain.model.LegacyMigrationOutcome -import de.davis.keygo.migration.legacy_data.domain.model.LegacyMigrationReport +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationReport import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.util.concurrent.atomic.AtomicBoolean diff --git a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/MigrateLegacyDataUseCase.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCase.kt similarity index 92% rename from migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/MigrateLegacyDataUseCase.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCase.kt index f1a3463be..751d7d1ee 100644 --- a/migration/legacy-data/src/main/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/MigrateLegacyDataUseCase.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCase.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.usecase +package de.davis.keygo.legacy_migration.domain.usecase import de.davis.keygo.core.item.domain.alias.ItemId import de.davis.keygo.core.item.domain.alias.VaultId @@ -17,16 +17,16 @@ import de.davis.keygo.core.util.Result import de.davis.keygo.core.util.getOrNull import de.davis.keygo.core.util.isSuccess import de.davis.keygo.core.util.onFailure -import de.davis.keygo.migration.legacy_data.domain.mapper.LegacyItemConverter -import de.davis.keygo.migration.legacy_data.domain.model.LegacyFailureReason -import de.davis.keygo.migration.legacy_data.domain.model.LegacyItem -import de.davis.keygo.migration.legacy_data.domain.model.LegacyMigrationException -import de.davis.keygo.migration.legacy_data.domain.model.LegacyMigrationOutcome -import de.davis.keygo.migration.legacy_data.domain.model.LegacyMigrationReport -import de.davis.keygo.migration.legacy_data.domain.model.LegacyReadFailure -import de.davis.keygo.migration.legacy_data.domain.model.LegacyRowFailure -import de.davis.keygo.migration.legacy_data.domain.repository.LegacyItemRepository -import de.davis.keygo.migration.legacy_data.domain.repository.LegacyKeyRepository +import de.davis.keygo.legacy_migration.domain.mapper.LegacyItemConverter +import de.davis.keygo.legacy_migration.domain.model.LegacyFailureReason +import de.davis.keygo.legacy_migration.domain.model.LegacyItem +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationException +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationReport +import de.davis.keygo.legacy_migration.domain.model.LegacyReadFailure +import de.davis.keygo.legacy_migration.domain.model.LegacyRowFailure +import de.davis.keygo.legacy_migration.domain.repository.LegacyItemRepository +import de.davis.keygo.legacy_migration.domain.repository.LegacyKeyRepository import de.davisalessandro.keygo.rust.ItemAad import kotlinx.coroutines.flow.first import org.koin.core.annotation.Single diff --git a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/LegacyMigrationEndToEndTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/LegacyMigrationEndToEndTest.kt similarity index 93% rename from migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/LegacyMigrationEndToEndTest.kt rename to legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/LegacyMigrationEndToEndTest.kt index efb45f699..733928797 100644 --- a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/LegacyMigrationEndToEndTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/LegacyMigrationEndToEndTest.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data +package de.davis.keygo.legacy_migration import androidx.room3.Room import androidx.sqlite.driver.bundled.BundledSQLiteDriver @@ -22,22 +22,22 @@ import de.davis.keygo.core.security.domain.crypto.model.WrappedItemKeyInformatio import de.davis.keygo.core.security.domain.crypto.model.WrappedVaultKeyInformation import de.davis.keygo.core.util.Result import de.davis.keygo.core.util.assertSuccess -import de.davis.keygo.migration.legacy_data.data.FakeLegacyDatabaseProvider -import de.davis.keygo.migration.legacy_data.data.FakeLegacyKeyRepository -import de.davis.keygo.migration.legacy_data.data.FakeRegistrableDomainResolver -import de.davis.keygo.migration.legacy_data.data.crypto.LegacyAesGcmCipher -import de.davis.keygo.migration.legacy_data.data.encryptLikeV1 -import de.davis.keygo.migration.legacy_data.data.json.LegacyDetailParser -import de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacySecureElementEntity -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacySecureElementTagCrossRef -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacyTagEntity -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacyTimestamps -import de.davis.keygo.migration.legacy_data.data.repository.LegacyItemRepositoryImpl -import de.davis.keygo.migration.legacy_data.domain.mapper.LegacyItemConverter -import de.davis.keygo.migration.legacy_data.domain.model.LegacyFailureReason -import de.davis.keygo.migration.legacy_data.domain.model.LegacyMigrationOutcome -import de.davis.keygo.migration.legacy_data.domain.usecase.MigrateLegacyDataUseCase +import de.davis.keygo.legacy_migration.data.FakeLegacyDatabaseProvider +import de.davis.keygo.legacy_migration.data.FakeLegacyKeyRepository +import de.davis.keygo.legacy_migration.data.FakeRegistrableDomainResolver +import de.davis.keygo.legacy_migration.data.crypto.LegacyAesGcmCipher +import de.davis.keygo.legacy_migration.data.encryptLikeV1 +import de.davis.keygo.legacy_migration.data.json.LegacyDetailParser +import de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase +import de.davis.keygo.legacy_migration.data.local.entity.LegacySecureElementEntity +import de.davis.keygo.legacy_migration.data.local.entity.LegacySecureElementTagCrossRef +import de.davis.keygo.legacy_migration.data.local.entity.LegacyTagEntity +import de.davis.keygo.legacy_migration.data.local.entity.LegacyTimestamps +import de.davis.keygo.legacy_migration.data.repository.LegacyItemRepositoryImpl +import de.davis.keygo.legacy_migration.domain.mapper.LegacyItemConverter +import de.davis.keygo.legacy_migration.domain.model.LegacyFailureReason +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome +import de.davis.keygo.legacy_migration.domain.usecase.MigrateLegacyDataUseCase import de.davisalessandro.keygo.rust.ItemAad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first diff --git a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/LegacyMigrationRealDatabaseTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/LegacyMigrationRealDatabaseTest.kt similarity index 92% rename from migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/LegacyMigrationRealDatabaseTest.kt rename to legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/LegacyMigrationRealDatabaseTest.kt index 6eee051c5..059fb7c73 100644 --- a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/LegacyMigrationRealDatabaseTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/LegacyMigrationRealDatabaseTest.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data +package de.davis.keygo.legacy_migration import androidx.room3.Room import androidx.sqlite.driver.bundled.BundledSQLiteDriver @@ -18,16 +18,16 @@ import de.davis.keygo.core.security.domain.crypto.decrypt import de.davis.keygo.core.security.domain.crypto.model.WrappedItemKeyInformation import de.davis.keygo.core.security.domain.crypto.model.WrappedVaultKeyInformation import de.davis.keygo.core.util.assertSuccess -import de.davis.keygo.migration.legacy_data.data.FakeLegacyDatabaseProvider -import de.davis.keygo.migration.legacy_data.data.FakeLegacyKeyRepository -import de.davis.keygo.migration.legacy_data.data.FakeRegistrableDomainResolver -import de.davis.keygo.migration.legacy_data.data.crypto.LegacyAesGcmCipher -import de.davis.keygo.migration.legacy_data.data.json.LegacyDetailParser -import de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase -import de.davis.keygo.migration.legacy_data.data.repository.LegacyItemRepositoryImpl -import de.davis.keygo.migration.legacy_data.domain.mapper.LegacyItemConverter -import de.davis.keygo.migration.legacy_data.domain.model.LegacyMigrationOutcome -import de.davis.keygo.migration.legacy_data.domain.usecase.MigrateLegacyDataUseCase +import de.davis.keygo.legacy_migration.data.FakeLegacyDatabaseProvider +import de.davis.keygo.legacy_migration.data.FakeLegacyKeyRepository +import de.davis.keygo.legacy_migration.data.FakeRegistrableDomainResolver +import de.davis.keygo.legacy_migration.data.crypto.LegacyAesGcmCipher +import de.davis.keygo.legacy_migration.data.json.LegacyDetailParser +import de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase +import de.davis.keygo.legacy_migration.data.repository.LegacyItemRepositoryImpl +import de.davis.keygo.legacy_migration.domain.mapper.LegacyItemConverter +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome +import de.davis.keygo.legacy_migration.domain.usecase.MigrateLegacyDataUseCase import de.davisalessandro.keygo.rust.ItemAad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first diff --git a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/crypto/LegacyAesGcmCipherTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipherTest.kt similarity index 94% rename from migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/crypto/LegacyAesGcmCipherTest.kt rename to legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipherTest.kt index fcad2cf70..91ef17159 100644 --- a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/crypto/LegacyAesGcmCipherTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipherTest.kt @@ -1,6 +1,6 @@ -package de.davis.keygo.migration.legacy_data.data.crypto +package de.davis.keygo.legacy_migration.data.crypto -import de.davis.keygo.migration.legacy_data.data.encryptLikeV1 +import de.davis.keygo.legacy_migration.data.encryptLikeV1 import java.security.SecureRandom import javax.crypto.Cipher import javax.crypto.KeyGenerator diff --git a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/json/LegacyDetailParserTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/json/LegacyDetailParserTest.kt similarity index 95% rename from migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/json/LegacyDetailParserTest.kt rename to legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/json/LegacyDetailParserTest.kt index 4cff958fa..0b022981d 100644 --- a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/json/LegacyDetailParserTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/json/LegacyDetailParserTest.kt @@ -1,7 +1,7 @@ -package de.davis.keygo.migration.legacy_data.data.json +package de.davis.keygo.legacy_migration.data.json -import de.davis.keygo.migration.legacy_data.domain.model.LegacyDetail -import de.davis.keygo.migration.legacy_data.domain.model.LegacyStrength +import de.davis.keygo.legacy_migration.domain.model.LegacyDetail +import de.davis.keygo.legacy_migration.domain.model.LegacyStrength import kotlin.test.Test import kotlin.test.assertContentEquals import kotlin.test.assertEquals diff --git a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacyDatabaseOpenTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacyDatabaseOpenTest.kt similarity index 97% rename from migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacyDatabaseOpenTest.kt rename to legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacyDatabaseOpenTest.kt index 3ca67f6b5..b8e92dd73 100644 --- a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacyDatabaseOpenTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacyDatabaseOpenTest.kt @@ -1,9 +1,9 @@ -package de.davis.keygo.migration.legacy_data.data.local +package de.davis.keygo.legacy_migration.data.local import androidx.sqlite.SQLiteConnection import androidx.sqlite.driver.bundled.BundledSQLiteDriver import androidx.sqlite.execSQL -import de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase +import de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase import kotlinx.coroutines.test.runTest import java.io.File import java.nio.file.Files diff --git a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacyMigrationTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacyMigrationTest.kt similarity index 95% rename from migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacyMigrationTest.kt rename to legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacyMigrationTest.kt index 4418117cd..7c6901b43 100644 --- a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacyMigrationTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacyMigrationTest.kt @@ -1,10 +1,10 @@ -package de.davis.keygo.migration.legacy_data.data.local +package de.davis.keygo.legacy_migration.data.local import androidx.sqlite.SQLiteConnection import androidx.sqlite.driver.bundled.BundledSQLiteDriver import androidx.sqlite.execSQL -import de.davis.keygo.migration.legacy_data.data.local.datasource.AndroidLegacyDatabaseProvider -import de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase +import de.davis.keygo.legacy_migration.data.local.datasource.AndroidLegacyDatabaseProvider +import de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase import kotlinx.coroutines.test.runTest import java.io.File import java.nio.file.Files @@ -23,7 +23,7 @@ import kotlin.test.assertTrue * throws `Migration didn't properly handle ...` on any drift in a column name, affinity, * nullability, default, primary key, index or foreign key. So every test here that reaches a row * has already proved the DDL in - * [de.davis.keygo.migration.legacy_data.data.local.migration.LegacyMigration2To3] is a faithful copy + * [de.davis.keygo.legacy_migration.data.local.migration.LegacyMigration2To3] is a faithful copy * of v1's. Opening through [AndroidLegacyDatabaseProvider] rather than a bare builder means the * tests also fail if production ever forgets to register the migration. */ diff --git a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacySchemaIdentityTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacySchemaIdentityTest.kt similarity index 97% rename from migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacySchemaIdentityTest.kt rename to legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacySchemaIdentityTest.kt index 0d6e95290..9b47fa52f 100644 --- a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacySchemaIdentityTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacySchemaIdentityTest.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.data.local +package de.davis.keygo.legacy_migration.data.local import kotlinx.serialization.json.Json import kotlinx.serialization.json.jsonObject diff --git a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacySchemaSeed.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacySchemaSeed.kt similarity index 93% rename from migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacySchemaSeed.kt rename to legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacySchemaSeed.kt index fb82f8c93..a39975650 100644 --- a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacySchemaSeed.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacySchemaSeed.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.data.local +package de.davis.keygo.legacy_migration.data.local import android.app.Instrumentation import android.content.Context @@ -6,7 +6,7 @@ import android.content.res.AssetManager import androidx.room3.testing.MigrationTestHelper import androidx.sqlite.SQLiteConnection import androidx.sqlite.driver.bundled.BundledSQLiteDriver -import de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase +import de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.runBlocking @@ -55,7 +55,7 @@ private fun schemaInstrumentation(): Instrumentation { * * Only the helper's `createDatabase` is used. Its `runMigrationsAndValidate` is deliberately left * alone, because it takes the migration list from the caller: the tests instead open the seeded file - * through [de.davis.keygo.migration.legacy_data.data.local.datasource.AndroidLegacyDatabaseProvider], + * through [de.davis.keygo.legacy_migration.data.local.datasource.AndroidLegacyDatabaseProvider], * which validates the post-migration schema through real Room *and* fails if production ever stops * registering the hand-written 2-to-3. See [LegacyMigrationTest]. * diff --git a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacyTestContext.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacyTestContext.kt similarity index 89% rename from migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacyTestContext.kt rename to legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacyTestContext.kt index 26a806904..db4ebc2fb 100644 --- a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/local/LegacyTestContext.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/local/LegacyTestContext.kt @@ -1,10 +1,10 @@ -package de.davis.keygo.migration.legacy_data.data.local +package de.davis.keygo.legacy_migration.data.local import android.content.Context import android.content.ContextWrapper import androidx.sqlite.driver.bundled.BundledSQLiteDriver -import de.davis.keygo.migration.legacy_data.data.local.datasource.AndroidLegacyDatabaseProvider -import de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase +import de.davis.keygo.legacy_migration.data.local.datasource.AndroidLegacyDatabaseProvider +import de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase import java.io.File import kotlin.test.assertNotNull diff --git a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/repository/LegacyItemRepositoryImplTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImplTest.kt similarity index 90% rename from migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/repository/LegacyItemRepositoryImplTest.kt rename to legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImplTest.kt index b0567eca9..36c88ee52 100644 --- a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/data/repository/LegacyItemRepositoryImplTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImplTest.kt @@ -1,28 +1,28 @@ -package de.davis.keygo.migration.legacy_data.data.repository +package de.davis.keygo.legacy_migration.data.repository import androidx.room3.Room import androidx.sqlite.driver.bundled.BundledSQLiteDriver import androidx.sqlite.execSQL import de.davis.keygo.core.util.assertFailure import de.davis.keygo.core.util.assertSuccess -import de.davis.keygo.migration.legacy_data.data.FakeLegacyCipher -import de.davis.keygo.migration.legacy_data.data.FakeLegacyDatabase -import de.davis.keygo.migration.legacy_data.data.FakeLegacyDatabaseProvider -import de.davis.keygo.migration.legacy_data.data.FakeLegacyElementDao -import de.davis.keygo.migration.legacy_data.data.FakeLegacyKeyRepository -import de.davis.keygo.migration.legacy_data.data.json.LegacyDetailParser -import de.davis.keygo.migration.legacy_data.data.local.datasource.AndroidLegacyDatabaseProvider -import de.davis.keygo.migration.legacy_data.data.local.datasource.LEGACY_DATABASE_NAME -import de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase -import de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabaseProvider -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacySecureElementEntity -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacySecureElementTagCrossRef -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacyTagEntity -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacyTimestamps -import de.davis.keygo.migration.legacy_data.data.local.legacyContext -import de.davis.keygo.migration.legacy_data.domain.model.LegacyDetail -import de.davis.keygo.migration.legacy_data.domain.model.LegacyFailureReason -import de.davis.keygo.migration.legacy_data.domain.model.LegacyReadFailure +import de.davis.keygo.legacy_migration.data.FakeLegacyCipher +import de.davis.keygo.legacy_migration.data.FakeLegacyDatabase +import de.davis.keygo.legacy_migration.data.FakeLegacyDatabaseProvider +import de.davis.keygo.legacy_migration.data.FakeLegacyElementDao +import de.davis.keygo.legacy_migration.data.FakeLegacyKeyRepository +import de.davis.keygo.legacy_migration.data.json.LegacyDetailParser +import de.davis.keygo.legacy_migration.data.local.datasource.AndroidLegacyDatabaseProvider +import de.davis.keygo.legacy_migration.data.local.datasource.LEGACY_DATABASE_NAME +import de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase +import de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabaseProvider +import de.davis.keygo.legacy_migration.data.local.entity.LegacySecureElementEntity +import de.davis.keygo.legacy_migration.data.local.entity.LegacySecureElementTagCrossRef +import de.davis.keygo.legacy_migration.data.local.entity.LegacyTagEntity +import de.davis.keygo.legacy_migration.data.local.entity.LegacyTimestamps +import de.davis.keygo.legacy_migration.data.local.legacyContext +import de.davis.keygo.legacy_migration.domain.model.LegacyDetail +import de.davis.keygo.legacy_migration.domain.model.LegacyFailureReason +import de.davis.keygo.legacy_migration.domain.model.LegacyReadFailure import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.test.runTest import java.io.File diff --git a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/domain/mapper/LegacyItemConverterTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/mapper/LegacyItemConverterTest.kt similarity index 94% rename from migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/domain/mapper/LegacyItemConverterTest.kt rename to legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/mapper/LegacyItemConverterTest.kt index 98124a9f1..54ea8c916 100644 --- a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/domain/mapper/LegacyItemConverterTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/mapper/LegacyItemConverterTest.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.mapper +package de.davis.keygo.legacy_migration.domain.mapper import de.davis.keygo.core.item.FakeItemRepository import de.davis.keygo.core.item.domain.alias.newItemId @@ -12,13 +12,13 @@ import de.davis.keygo.core.security.domain.crypto.decrypt import de.davis.keygo.core.security.domain.crypto.model.WrappedItemKeyInformation import de.davis.keygo.core.security.domain.crypto.model.WrappedVaultKeyInformation import de.davis.keygo.core.util.assertSuccess -import de.davis.keygo.migration.legacy_data.data.FAKE_LEGACY_KEY -import de.davis.keygo.migration.legacy_data.data.FakeLegacyCipher -import de.davis.keygo.migration.legacy_data.data.FakeRegistrableDomainResolver -import de.davis.keygo.migration.legacy_data.domain.crypto.LegacyCipher -import de.davis.keygo.migration.legacy_data.domain.model.LegacyDetail -import de.davis.keygo.migration.legacy_data.domain.model.LegacyItem -import de.davis.keygo.migration.legacy_data.domain.model.LegacyStrength +import de.davis.keygo.legacy_migration.data.FAKE_LEGACY_KEY +import de.davis.keygo.legacy_migration.data.FakeLegacyCipher +import de.davis.keygo.legacy_migration.data.FakeRegistrableDomainResolver +import de.davis.keygo.legacy_migration.domain.crypto.LegacyCipher +import de.davis.keygo.legacy_migration.domain.model.LegacyDetail +import de.davis.keygo.legacy_migration.domain.model.LegacyItem +import de.davis.keygo.legacy_migration.domain.model.LegacyStrength import de.davisalessandro.keygo.rust.ItemAad import kotlinx.coroutines.test.runTest import java.time.YearMonth diff --git a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/LegacyImportRunnerTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunnerTest.kt similarity index 96% rename from migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/LegacyImportRunnerTest.kt rename to legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunnerTest.kt index 0fdd2e217..0e2481b26 100644 --- a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/LegacyImportRunnerTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunnerTest.kt @@ -1,9 +1,9 @@ -package de.davis.keygo.migration.legacy_data.domain.usecase +package de.davis.keygo.legacy_migration.domain.usecase -import de.davis.keygo.migration.legacy_data.domain.model.LegacyFailureReason -import de.davis.keygo.migration.legacy_data.domain.model.LegacyMigrationOutcome -import de.davis.keygo.migration.legacy_data.domain.model.LegacyMigrationReport -import de.davis.keygo.migration.legacy_data.domain.model.LegacyRowFailure +import de.davis.keygo.legacy_migration.domain.model.LegacyFailureReason +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationReport +import de.davis.keygo.legacy_migration.domain.model.LegacyRowFailure import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope diff --git a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/MigrateLegacyDataUseCaseTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCaseTest.kt similarity index 93% rename from migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/MigrateLegacyDataUseCaseTest.kt rename to legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCaseTest.kt index 043517c3d..6dbc73b27 100644 --- a/migration/legacy-data/src/test/kotlin/de/davis/keygo/migration/legacy_data/domain/usecase/MigrateLegacyDataUseCaseTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCaseTest.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.domain.usecase +package de.davis.keygo.legacy_migration.domain.usecase import de.davis.keygo.core.item.FakeCreditCardRepository import de.davis.keygo.core.item.FakeItemRepository @@ -15,20 +15,20 @@ import de.davis.keygo.core.security.crypto.FakeCryptographicScopeProvider import de.davis.keygo.core.security.domain.crypto.CryptographicScopeProvider import de.davis.keygo.core.security.domain.model.CryptoScopeError import de.davis.keygo.core.util.Result -import de.davis.keygo.migration.legacy_data.data.FAKE_LEGACY_KEY -import de.davis.keygo.migration.legacy_data.data.FakeLegacyCipher -import de.davis.keygo.migration.legacy_data.data.FakeLegacyItemRepository -import de.davis.keygo.migration.legacy_data.data.FakeLegacyKeyRepository -import de.davis.keygo.migration.legacy_data.data.FakeRegistrableDomainResolver -import de.davis.keygo.migration.legacy_data.domain.crypto.LegacyCipher -import de.davis.keygo.migration.legacy_data.domain.mapper.LegacyItemConverter -import de.davis.keygo.migration.legacy_data.domain.model.LegacyDetail -import de.davis.keygo.migration.legacy_data.domain.model.LegacyFailureReason -import de.davis.keygo.migration.legacy_data.domain.model.LegacyItem -import de.davis.keygo.migration.legacy_data.domain.model.LegacyMigrationOutcome -import de.davis.keygo.migration.legacy_data.domain.model.LegacyReadFailure -import de.davis.keygo.migration.legacy_data.domain.model.LegacyRowFailure -import de.davis.keygo.migration.legacy_data.domain.repository.LegacyReadResult +import de.davis.keygo.legacy_migration.data.FAKE_LEGACY_KEY +import de.davis.keygo.legacy_migration.data.FakeLegacyCipher +import de.davis.keygo.legacy_migration.data.FakeLegacyItemRepository +import de.davis.keygo.legacy_migration.data.FakeLegacyKeyRepository +import de.davis.keygo.legacy_migration.data.FakeRegistrableDomainResolver +import de.davis.keygo.legacy_migration.domain.crypto.LegacyCipher +import de.davis.keygo.legacy_migration.domain.mapper.LegacyItemConverter +import de.davis.keygo.legacy_migration.domain.model.LegacyDetail +import de.davis.keygo.legacy_migration.domain.model.LegacyFailureReason +import de.davis.keygo.legacy_migration.domain.model.LegacyItem +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome +import de.davis.keygo.legacy_migration.domain.model.LegacyReadFailure +import de.davis.keygo.legacy_migration.domain.model.LegacyRowFailure +import de.davis.keygo.legacy_migration.domain.repository.LegacyReadResult import kotlinx.coroutines.flow.first import kotlinx.coroutines.test.runTest import kotlin.test.Test diff --git a/migration/legacy-data/src/test/resources/legacy-fixtures/secure_element_database b/legacy-migration/src/test/resources/legacy-fixtures/secure_element_database similarity index 100% rename from migration/legacy-data/src/test/resources/legacy-fixtures/secure_element_database rename to legacy-migration/src/test/resources/legacy-fixtures/secure_element_database diff --git a/migration/legacy-data/src/test/resources/legacy-fixtures/secure_element_database-shm b/legacy-migration/src/test/resources/legacy-fixtures/secure_element_database-shm similarity index 100% rename from migration/legacy-data/src/test/resources/legacy-fixtures/secure_element_database-shm rename to legacy-migration/src/test/resources/legacy-fixtures/secure_element_database-shm diff --git a/migration/legacy-data/src/test/resources/legacy-fixtures/secure_element_database-wal b/legacy-migration/src/test/resources/legacy-fixtures/secure_element_database-wal similarity index 100% rename from migration/legacy-data/src/test/resources/legacy-fixtures/secure_element_database-wal rename to legacy-migration/src/test/resources/legacy-fixtures/secure_element_database-wal diff --git a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyCipher.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyCipher.kt similarity index 84% rename from migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyCipher.kt rename to legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyCipher.kt index 79c8c6338..3b1a157b7 100644 --- a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyCipher.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyCipher.kt @@ -1,7 +1,7 @@ -package de.davis.keygo.migration.legacy_data.data +package de.davis.keygo.legacy_migration.data -import de.davis.keygo.migration.legacy_data.data.FakeLegacyCipher.Companion.FAIL -import de.davis.keygo.migration.legacy_data.domain.crypto.LegacyCipher +import de.davis.keygo.legacy_migration.data.FakeLegacyCipher.Companion.FAIL +import de.davis.keygo.legacy_migration.domain.crypto.LegacyCipher import javax.crypto.SecretKey /** diff --git a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyDatabase.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyDatabase.kt similarity index 79% rename from migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyDatabase.kt rename to legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyDatabase.kt index 5eb7611c9..23c822845 100644 --- a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyDatabase.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyDatabase.kt @@ -1,8 +1,8 @@ -package de.davis.keygo.migration.legacy_data.data +package de.davis.keygo.legacy_migration.data import androidx.room3.InvalidationTracker -import de.davis.keygo.migration.legacy_data.data.local.dao.LegacyElementDao -import de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase +import de.davis.keygo.legacy_migration.data.local.dao.LegacyElementDao +import de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase /** * A plain subclass rather than Room's builder, for tests that only ever reach [legacyElementDao]. diff --git a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyDatabaseProvider.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyDatabaseProvider.kt similarity index 83% rename from migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyDatabaseProvider.kt rename to legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyDatabaseProvider.kt index aae83b26e..3a1f34c61 100644 --- a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyDatabaseProvider.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyDatabaseProvider.kt @@ -1,7 +1,7 @@ -package de.davis.keygo.migration.legacy_data.data +package de.davis.keygo.legacy_migration.data -import de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabase -import de.davis.keygo.migration.legacy_data.data.local.datasource.LegacyDatabaseProvider +import de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabase +import de.davis.keygo.legacy_migration.data.local.datasource.LegacyDatabaseProvider import java.io.File /** diff --git a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyElementDao.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyElementDao.kt similarity index 77% rename from migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyElementDao.kt rename to legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyElementDao.kt index 9a96c7e6c..6f5df1a6d 100644 --- a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyElementDao.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyElementDao.kt @@ -1,10 +1,10 @@ -package de.davis.keygo.migration.legacy_data.data +package de.davis.keygo.legacy_migration.data -import de.davis.keygo.migration.legacy_data.data.local.dao.LegacyElementDao -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacySecureElementEntity -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacySecureElementTagCrossRef -import de.davis.keygo.migration.legacy_data.data.local.entity.LegacyTagEntity -import de.davis.keygo.migration.legacy_data.data.local.pojo.LegacyElementWithTags +import de.davis.keygo.legacy_migration.data.local.dao.LegacyElementDao +import de.davis.keygo.legacy_migration.data.local.entity.LegacySecureElementEntity +import de.davis.keygo.legacy_migration.data.local.entity.LegacySecureElementTagCrossRef +import de.davis.keygo.legacy_migration.data.local.entity.LegacyTagEntity +import de.davis.keygo.legacy_migration.data.local.pojo.LegacyElementWithTags /** * In-memory [LegacyElementDao] over a set of row ids, for the tests that are about counting and diff --git a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyItemRepository.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyItemRepository.kt similarity index 87% rename from migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyItemRepository.kt rename to legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyItemRepository.kt index ff489573c..fc3995dbe 100644 --- a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyItemRepository.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyItemRepository.kt @@ -1,9 +1,9 @@ -package de.davis.keygo.migration.legacy_data.data +package de.davis.keygo.legacy_migration.data import de.davis.keygo.core.util.Result -import de.davis.keygo.migration.legacy_data.domain.model.LegacyReadFailure -import de.davis.keygo.migration.legacy_data.domain.repository.LegacyItemRepository -import de.davis.keygo.migration.legacy_data.domain.repository.LegacyReadResult +import de.davis.keygo.legacy_migration.domain.model.LegacyReadFailure +import de.davis.keygo.legacy_migration.domain.repository.LegacyItemRepository +import de.davis.keygo.legacy_migration.domain.repository.LegacyReadResult /** * In-memory [LegacyItemRepository] that keeps the real contract: every read, prune and count can diff --git a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyKeyRepository.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyKeyRepository.kt similarity index 84% rename from migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyKeyRepository.kt rename to legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyKeyRepository.kt index 4670bbe05..d447ba931 100644 --- a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeLegacyKeyRepository.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyKeyRepository.kt @@ -1,6 +1,6 @@ -package de.davis.keygo.migration.legacy_data.data +package de.davis.keygo.legacy_migration.data -import de.davis.keygo.migration.legacy_data.domain.repository.LegacyKeyRepository +import de.davis.keygo.legacy_migration.domain.repository.LegacyKeyRepository import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec diff --git a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeRegistrableDomainResolver.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeRegistrableDomainResolver.kt similarity index 93% rename from migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeRegistrableDomainResolver.kt rename to legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeRegistrableDomainResolver.kt index 673f9f219..7bf837b87 100644 --- a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/FakeRegistrableDomainResolver.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeRegistrableDomainResolver.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.data +package de.davis.keygo.legacy_migration.data import de.davis.keygo.core.util.domain.resolver.RegistrableDomainResolver diff --git a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/LegacyV1Encryption.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/LegacyV1Encryption.kt similarity index 93% rename from migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/LegacyV1Encryption.kt rename to legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/LegacyV1Encryption.kt index 40da9f6ca..aa3590554 100644 --- a/migration/legacy-data/src/testFixtures/kotlin/de/davis/keygo/migration/legacy_data/data/LegacyV1Encryption.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/LegacyV1Encryption.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.legacy_data.data +package de.davis.keygo.legacy_migration.data import javax.crypto.Cipher import javax.crypto.SecretKey diff --git a/settings.gradle.kts b/settings.gradle.kts index c7e735f27..a3168757c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -31,7 +31,7 @@ include(":app") include(":automation-processor") include(":automation") include(":migration:create-access") -include(":migration:legacy-data") +include(":legacy-migration") include(":core:item") include(":core:util") include(":core:security") From c0b260f939745c2c8a51d7f9c7be46a33eca467a Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 14 Aug 2026 12:02:07 +0200 Subject: [PATCH 02/20] refactor: merge :migration:create-access into :legacy-migration --- app/build.gradle.kts | 1 - .../keygo/app/presentation/AppViewModel.kt | 2 +- feature/auth/build.gradle.kts | 4 +-- .../auth/presentation/AuthViewModel.kt | 6 ++-- .../auth/presentation/AuthViewModelTest.kt | 8 +++--- feature/onboarding/build.gradle.kts | 1 - legacy-migration/build.gradle.kts | 4 +++ .../datastore/MainPasswordSerializer.kt | 4 +-- .../data/mapper/MainPasswordMapper.kt | 8 ++++++ .../data/repository/HashValidatorImpl.kt | 4 +-- .../repository/LegacyKeyRepositoryImpl.kt | 7 +++-- .../repository/MainPasswordRepositoryImpl.kt | 14 +++++----- .../di/LegacyMigrationModule.kt | 7 ++++- .../di/MainPasswordDataStoreModule.kt | 27 ++++++++++++++++++ .../di/annotation/MainPasswordQualifier.kt | 2 +- .../domain/model/MainPassword.kt | 2 +- .../domain/repository/HashValidator.kt | 2 +- .../repository/MainPasswordRepository.kt | 10 +++++++ .../usecase/ClearMainPasswordUseCase.kt | 4 +-- .../domain/usecase/HasMainPasswordUseCase.kt | 4 +-- .../usecase/ValidateMainPasswordUseCase.kt | 6 ++-- .../src/main/proto/main_password.proto | 2 +- .../FakeMainPasswordRepository.kt | 6 ++-- .../MigrationTestUseCases.kt | 10 +++---- migration/create-access/.gitignore | 1 - migration/create-access/build.gradle.kts | 17 ----------- migration/create-access/consumer-rules.pro | 16 ----------- migration/create-access/proguard-rules.pro | 21 -------------- .../src/main/AndroidManifest.xml | 4 --- .../data/mapper/MainPasswordMapper.kt | 8 ------ .../di/MigrationCreateAccessModule.kt | 28 ------------------- .../repository/MainPasswordRepository.kt | 10 ------- settings.gradle.kts | 1 - 33 files changed, 100 insertions(+), 151 deletions(-) rename {migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/local/datasource/datastore/MainPasswordSerializer.kt (75%) create mode 100644 legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/mapper/MainPasswordMapper.kt rename {migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/repository/HashValidatorImpl.kt (81%) rename {migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/data/repository/MainPasswordRepositoryImpl.kt (57%) create mode 100644 legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/MainPasswordDataStoreModule.kt rename {migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/di/annotation/MainPasswordQualifier.kt (60%) rename {migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/model/MainPassword.kt (62%) rename {migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/repository/HashValidator.kt (67%) create mode 100644 legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/MainPasswordRepository.kt rename {migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/usecase/ClearMainPasswordUseCase.kt (62%) rename {migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/usecase/HasMainPasswordUseCase.kt (65%) rename {migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access => legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration}/domain/usecase/ValidateMainPasswordUseCase.kt (70%) rename {migration/create-access => legacy-migration}/src/main/proto/main_password.proto (70%) rename {migration/create-access/src/testFixtures/kotlin/de/davis/keygo/migration/create_access => legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration}/FakeMainPasswordRepository.kt (72%) rename {migration/create-access/src/testFixtures/kotlin/de/davis/keygo/migration/create_access => legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration}/MigrationTestUseCases.kt (57%) delete mode 100644 migration/create-access/.gitignore delete mode 100644 migration/create-access/build.gradle.kts delete mode 100644 migration/create-access/consumer-rules.pro delete mode 100644 migration/create-access/proguard-rules.pro delete mode 100644 migration/create-access/src/main/AndroidManifest.xml delete mode 100644 migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/data/mapper/MainPasswordMapper.kt delete mode 100644 migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/di/MigrationCreateAccessModule.kt delete mode 100644 migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/repository/MainPasswordRepository.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0450e2bce..72cec353d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -119,7 +119,6 @@ dependencies { implementation(projects.feature.settings) implementation(projects.feature.backup) implementation(projects.feature.onboarding) - implementation(projects.migration.createAccess) implementation(projects.legacyMigration) implementation(libs.androidx.core.splashscreen) diff --git a/app/src/main/kotlin/de/davis/keygo/app/presentation/AppViewModel.kt b/app/src/main/kotlin/de/davis/keygo/app/presentation/AppViewModel.kt index add7fd8e3..3e82a6e01 100644 --- a/app/src/main/kotlin/de/davis/keygo/app/presentation/AppViewModel.kt +++ b/app/src/main/kotlin/de/davis/keygo/app/presentation/AppViewModel.kt @@ -3,7 +3,7 @@ package de.davis.keygo.app.presentation import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.davis.keygo.core.identity.domain.repository.AccountRepository -import de.davis.keygo.migration.create_access.domain.usecase.HasMainPasswordUseCase +import de.davis.keygo.legacy_migration.domain.usecase.HasMainPasswordUseCase import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index b72e45587..53f9ffaa3 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -11,7 +11,7 @@ dependencies { implementation(projects.core.identity) implementation(projects.core.item) implementation(projects.core.ui) - implementation(projects.migration.createAccess) + implementation(projects.legacyMigration) implementation(libs.androidx.navigation.compose) @@ -20,5 +20,5 @@ dependencies { testImplementation(testFixtures(projects.core.item)) testImplementation(testFixtures(projects.core.security)) testImplementation(testFixtures(projects.rust)) - testImplementation(testFixtures(projects.migration.createAccess)) + testImplementation(testFixtures(projects.legacyMigration)) } diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt index 4b2c71afd..9e346148c 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt @@ -17,9 +17,9 @@ import de.davis.keygo.core.util.onSuccess import de.davis.keygo.feature.auth.presentation.model.AuthState import de.davis.keygo.feature.auth.presentation.model.AuthUIEvent import de.davis.keygo.feature.auth.presentation.model.BiometricRequest -import de.davis.keygo.migration.create_access.domain.usecase.ClearMainPasswordUseCase -import de.davis.keygo.migration.create_access.domain.usecase.HasMainPasswordUseCase -import de.davis.keygo.migration.create_access.domain.usecase.ValidateMainPasswordUseCase +import de.davis.keygo.legacy_migration.domain.usecase.ClearMainPasswordUseCase +import de.davis.keygo.legacy_migration.domain.usecase.HasMainPasswordUseCase +import de.davis.keygo.legacy_migration.domain.usecase.ValidateMainPasswordUseCase import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow diff --git a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt index adbe208b3..558a07e42 100644 --- a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt +++ b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt @@ -9,10 +9,10 @@ import de.davis.keygo.core.item.FakeVaultRepository import de.davis.keygo.core.security.crypto.FakeBiometricAvailabilityRepository import de.davis.keygo.core.security.crypto.FakeSession import de.davis.keygo.feature.auth.presentation.model.AuthState -import de.davis.keygo.migration.create_access.FakeMainPasswordRepository -import de.davis.keygo.migration.create_access.clearMainPasswordUseCase -import de.davis.keygo.migration.create_access.hasMainPasswordUseCase -import de.davis.keygo.migration.create_access.validateMainPasswordUseCase +import de.davis.keygo.legacy_migration.FakeMainPasswordRepository +import de.davis.keygo.legacy_migration.clearMainPasswordUseCase +import de.davis.keygo.legacy_migration.hasMainPasswordUseCase +import de.davis.keygo.legacy_migration.validateMainPasswordUseCase import de.davis.keygo.rust.FakeAccountManager import de.davis.keygo.rust.FakeKeyDeriver import de.davis.keygo.rust.FakeKeyWrapper diff --git a/feature/onboarding/build.gradle.kts b/feature/onboarding/build.gradle.kts index 98ffcb9ba..e30014b80 100644 --- a/feature/onboarding/build.gradle.kts +++ b/feature/onboarding/build.gradle.kts @@ -17,7 +17,6 @@ dependencies { implementation(projects.core.identity) implementation(projects.feature.backup) implementation(projects.feature.autofill) - implementation(projects.migration.createAccess) implementation(libs.androidx.navigation.compose) } diff --git a/legacy-migration/build.gradle.kts b/legacy-migration/build.gradle.kts index 9875d1fb1..e0266dd55 100644 --- a/legacy-migration/build.gradle.kts +++ b/legacy-migration/build.gradle.kts @@ -1,5 +1,6 @@ plugins { alias(libs.plugins.keygo.android.library) + alias(libs.plugins.keygo.android.protobuf) alias(libs.plugins.androidx.room3) alias(libs.plugins.google.ksp) alias(libs.plugins.kotlin.serialization) @@ -14,6 +15,9 @@ android { } dependencies { + implementation(libs.androidx.datastore) + implementation(libs.at.favre.bcrypt) + implementation(libs.androidx.room3.runtime) ksp(libs.androidx.room3.compiler) diff --git a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/data/local/datasource/datastore/MainPasswordSerializer.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/datasource/datastore/MainPasswordSerializer.kt similarity index 75% rename from migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/data/local/datasource/datastore/MainPasswordSerializer.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/datasource/datastore/MainPasswordSerializer.kt index cebf6bf3f..586685ab8 100644 --- a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/data/local/datasource/datastore/MainPasswordSerializer.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/local/datasource/datastore/MainPasswordSerializer.kt @@ -1,7 +1,7 @@ -package de.davis.keygo.migration.create_access.data.local.datasource.datastore +package de.davis.keygo.legacy_migration.data.local.datasource.datastore import androidx.datastore.core.Serializer -import de.davis.keygo.migration.create_access.data.local.model.ProtoMainPassword +import de.davis.keygo.legacy_migration.data.local.model.ProtoMainPassword import java.io.InputStream import java.io.OutputStream diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/mapper/MainPasswordMapper.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/mapper/MainPasswordMapper.kt new file mode 100644 index 000000000..acb5f9139 --- /dev/null +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/mapper/MainPasswordMapper.kt @@ -0,0 +1,8 @@ +package de.davis.keygo.legacy_migration.data.mapper + +import de.davis.keygo.legacy_migration.data.local.model.ProtoMainPassword +import de.davis.keygo.legacy_migration.domain.model.MainPassword +import java.time.Instant + +internal fun ProtoMainPassword.toDomain() = + MainPassword(hash, Instant.ofEpochSecond(createdAt.seconds, createdAt.nanos.toLong())) diff --git a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/data/repository/HashValidatorImpl.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/HashValidatorImpl.kt similarity index 81% rename from migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/data/repository/HashValidatorImpl.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/HashValidatorImpl.kt index 3064530e2..a2a574292 100644 --- a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/data/repository/HashValidatorImpl.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/HashValidatorImpl.kt @@ -1,8 +1,8 @@ -package de.davis.keygo.migration.create_access.data.repository +package de.davis.keygo.legacy_migration.data.repository import at.favre.lib.crypto.bcrypt.BCrypt import at.favre.lib.crypto.bcrypt.LongPasswordStrategies -import de.davis.keygo.migration.create_access.domain.repository.HashValidator +import de.davis.keygo.legacy_migration.domain.repository.HashValidator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.koin.core.annotation.Single diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyKeyRepositoryImpl.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyKeyRepositoryImpl.kt index 0e5987f78..7c8b63f06 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyKeyRepositoryImpl.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyKeyRepositoryImpl.kt @@ -5,6 +5,11 @@ import org.koin.core.annotation.Single import java.security.KeyStore import javax.crypto.SecretKey +/** + * v1's Keystore alias. The identity of a shipped v1 install's item key. It must never change. + */ +internal const val LEGACY_KEY_ALIAS = "password_manager_skey" + @Single internal class LegacyKeyRepositoryImpl : LegacyKeyRepository { @@ -27,7 +32,5 @@ internal class LegacyKeyRepositoryImpl : LegacyKeyRepository { private companion object { const val ANDROID_KEY_STORE = "AndroidKeyStore" - - const val LEGACY_KEY_ALIAS = "password_manager_skey" } } diff --git a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/data/repository/MainPasswordRepositoryImpl.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/MainPasswordRepositoryImpl.kt similarity index 57% rename from migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/data/repository/MainPasswordRepositoryImpl.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/MainPasswordRepositoryImpl.kt index 613725556..e83bf764f 100644 --- a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/data/repository/MainPasswordRepositoryImpl.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/MainPasswordRepositoryImpl.kt @@ -1,13 +1,13 @@ -package de.davis.keygo.migration.create_access.data.repository +package de.davis.keygo.legacy_migration.data.repository import androidx.datastore.core.DataStore import com.google.protobuf.timestamp -import de.davis.keygo.migration.create_access.data.local.model.ProtoMainPassword -import de.davis.keygo.migration.create_access.data.local.model.copy -import de.davis.keygo.migration.create_access.data.mapper.toDomain -import de.davis.keygo.migration.create_access.di.annotation.MainPasswordQualifier -import de.davis.keygo.migration.create_access.domain.model.MainPassword -import de.davis.keygo.migration.create_access.domain.repository.MainPasswordRepository +import de.davis.keygo.legacy_migration.data.local.model.ProtoMainPassword +import de.davis.keygo.legacy_migration.data.local.model.copy +import de.davis.keygo.legacy_migration.data.mapper.toDomain +import de.davis.keygo.legacy_migration.di.annotation.MainPasswordQualifier +import de.davis.keygo.legacy_migration.domain.model.MainPassword +import de.davis.keygo.legacy_migration.domain.repository.MainPasswordRepository import kotlinx.coroutines.flow.first import org.koin.core.annotation.Single diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt index b5f7042ba..542c46ea2 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt @@ -4,7 +4,12 @@ import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Configuration import org.koin.core.annotation.Module -@Module(includes = [LegacyDatabaseModule::class]) +@Module( + includes = [ + LegacyDatabaseModule::class, + MainPasswordDataStoreModule::class, + ], +) @Configuration @ComponentScan("de.davis.keygo.legacy_migration") object LegacyMigrationModule diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/MainPasswordDataStoreModule.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/MainPasswordDataStoreModule.kt new file mode 100644 index 000000000..6b409e7b3 --- /dev/null +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/MainPasswordDataStoreModule.kt @@ -0,0 +1,27 @@ +package de.davis.keygo.legacy_migration.di + +import android.content.Context +import androidx.datastore.dataStore +import de.davis.keygo.legacy_migration.data.local.datasource.datastore.MainPasswordSerializer +import de.davis.keygo.legacy_migration.di.annotation.MainPasswordQualifier +import org.koin.core.annotation.Module +import org.koin.core.annotation.Single + +/** + * The file name is the on-disk identity of a shipped v1 install's main password record. It must + * never change, whatever the module or package around it is called. + */ +internal const val MAIN_PASSWORD_DATA_STORE_NAME = "main-password.db" + +@Module +internal class MainPasswordDataStoreModule { + + private val Context.protoMainPasswordDataStore by dataStore( + fileName = MAIN_PASSWORD_DATA_STORE_NAME, + serializer = MainPasswordSerializer, + ) + + @Single + @MainPasswordQualifier + fun provideProtoMainPasswordDataStore(context: Context) = context.protoMainPasswordDataStore +} diff --git a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/di/annotation/MainPasswordQualifier.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/annotation/MainPasswordQualifier.kt similarity index 60% rename from migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/di/annotation/MainPasswordQualifier.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/annotation/MainPasswordQualifier.kt index 601dec055..11c1b7617 100644 --- a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/di/annotation/MainPasswordQualifier.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/annotation/MainPasswordQualifier.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.create_access.di.annotation +package de.davis.keygo.legacy_migration.di.annotation import org.koin.core.annotation.Named diff --git a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/model/MainPassword.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/MainPassword.kt similarity index 62% rename from migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/model/MainPassword.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/MainPassword.kt index 5f785bdf3..431497df5 100644 --- a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/model/MainPassword.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/MainPassword.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.create_access.domain.model +package de.davis.keygo.legacy_migration.domain.model import java.time.Instant diff --git a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/repository/HashValidator.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/HashValidator.kt similarity index 67% rename from migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/repository/HashValidator.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/HashValidator.kt index c37cd9297..e22127b82 100644 --- a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/repository/HashValidator.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/HashValidator.kt @@ -1,4 +1,4 @@ -package de.davis.keygo.migration.create_access.domain.repository +package de.davis.keygo.legacy_migration.domain.repository internal interface HashValidator { diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/MainPasswordRepository.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/MainPasswordRepository.kt new file mode 100644 index 000000000..a038593dd --- /dev/null +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/MainPasswordRepository.kt @@ -0,0 +1,10 @@ +package de.davis.keygo.legacy_migration.domain.repository + +import de.davis.keygo.legacy_migration.domain.model.MainPassword + +internal interface MainPasswordRepository { + + suspend fun getMainPassword(): MainPassword + + suspend fun clearMainPassword() +} diff --git a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/usecase/ClearMainPasswordUseCase.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/ClearMainPasswordUseCase.kt similarity index 62% rename from migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/usecase/ClearMainPasswordUseCase.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/ClearMainPasswordUseCase.kt index 583759ee3..90fe84bc1 100644 --- a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/usecase/ClearMainPasswordUseCase.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/ClearMainPasswordUseCase.kt @@ -1,6 +1,6 @@ -package de.davis.keygo.migration.create_access.domain.usecase +package de.davis.keygo.legacy_migration.domain.usecase -import de.davis.keygo.migration.create_access.domain.repository.MainPasswordRepository +import de.davis.keygo.legacy_migration.domain.repository.MainPasswordRepository import org.koin.core.annotation.Single @Single diff --git a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/usecase/HasMainPasswordUseCase.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/HasMainPasswordUseCase.kt similarity index 65% rename from migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/usecase/HasMainPasswordUseCase.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/HasMainPasswordUseCase.kt index 7f2dc54c6..ea08b069b 100644 --- a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/usecase/HasMainPasswordUseCase.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/HasMainPasswordUseCase.kt @@ -1,6 +1,6 @@ -package de.davis.keygo.migration.create_access.domain.usecase +package de.davis.keygo.legacy_migration.domain.usecase -import de.davis.keygo.migration.create_access.domain.repository.MainPasswordRepository +import de.davis.keygo.legacy_migration.domain.repository.MainPasswordRepository import org.koin.core.annotation.Single @Single diff --git a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/usecase/ValidateMainPasswordUseCase.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/ValidateMainPasswordUseCase.kt similarity index 70% rename from migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/usecase/ValidateMainPasswordUseCase.kt rename to legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/ValidateMainPasswordUseCase.kt index 456bd896b..9e2a76105 100644 --- a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/usecase/ValidateMainPasswordUseCase.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/ValidateMainPasswordUseCase.kt @@ -1,7 +1,7 @@ -package de.davis.keygo.migration.create_access.domain.usecase +package de.davis.keygo.legacy_migration.domain.usecase -import de.davis.keygo.migration.create_access.domain.repository.HashValidator -import de.davis.keygo.migration.create_access.domain.repository.MainPasswordRepository +import de.davis.keygo.legacy_migration.domain.repository.HashValidator +import de.davis.keygo.legacy_migration.domain.repository.MainPasswordRepository import org.koin.core.annotation.Single @Single diff --git a/migration/create-access/src/main/proto/main_password.proto b/legacy-migration/src/main/proto/main_password.proto similarity index 70% rename from migration/create-access/src/main/proto/main_password.proto rename to legacy-migration/src/main/proto/main_password.proto index ff270cabc..6ae79fef3 100644 --- a/migration/create-access/src/main/proto/main_password.proto +++ b/legacy-migration/src/main/proto/main_password.proto @@ -2,7 +2,7 @@ syntax = "proto3"; import "google/protobuf/timestamp.proto"; -option java_package = "de.davis.keygo.migration.create_access.data.local.model"; +option java_package = "de.davis.keygo.legacy_migration.data.local.model"; option java_multiple_files = true; message ProtoMainPassword { diff --git a/migration/create-access/src/testFixtures/kotlin/de/davis/keygo/migration/create_access/FakeMainPasswordRepository.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/FakeMainPasswordRepository.kt similarity index 72% rename from migration/create-access/src/testFixtures/kotlin/de/davis/keygo/migration/create_access/FakeMainPasswordRepository.kt rename to legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/FakeMainPasswordRepository.kt index 56d472e1d..237c1d656 100644 --- a/migration/create-access/src/testFixtures/kotlin/de/davis/keygo/migration/create_access/FakeMainPasswordRepository.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/FakeMainPasswordRepository.kt @@ -1,7 +1,7 @@ -package de.davis.keygo.migration.create_access +package de.davis.keygo.legacy_migration -import de.davis.keygo.migration.create_access.domain.model.MainPassword -import de.davis.keygo.migration.create_access.domain.repository.MainPasswordRepository +import de.davis.keygo.legacy_migration.domain.model.MainPassword +import de.davis.keygo.legacy_migration.domain.repository.MainPasswordRepository import java.time.Instant /** diff --git a/migration/create-access/src/testFixtures/kotlin/de/davis/keygo/migration/create_access/MigrationTestUseCases.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt similarity index 57% rename from migration/create-access/src/testFixtures/kotlin/de/davis/keygo/migration/create_access/MigrationTestUseCases.kt rename to legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt index de78a697f..a047cbed6 100644 --- a/migration/create-access/src/testFixtures/kotlin/de/davis/keygo/migration/create_access/MigrationTestUseCases.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt @@ -1,9 +1,9 @@ -package de.davis.keygo.migration.create_access +package de.davis.keygo.legacy_migration -import de.davis.keygo.migration.create_access.data.repository.HashValidatorImpl -import de.davis.keygo.migration.create_access.domain.usecase.ClearMainPasswordUseCase -import de.davis.keygo.migration.create_access.domain.usecase.HasMainPasswordUseCase -import de.davis.keygo.migration.create_access.domain.usecase.ValidateMainPasswordUseCase +import de.davis.keygo.legacy_migration.data.repository.HashValidatorImpl +import de.davis.keygo.legacy_migration.domain.usecase.ClearMainPasswordUseCase +import de.davis.keygo.legacy_migration.domain.usecase.HasMainPasswordUseCase +import de.davis.keygo.legacy_migration.domain.usecase.ValidateMainPasswordUseCase fun clearMainPasswordUseCase(repository: FakeMainPasswordRepository): ClearMainPasswordUseCase = ClearMainPasswordUseCase(repository.asMainPasswordRepository()) diff --git a/migration/create-access/.gitignore b/migration/create-access/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/migration/create-access/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/migration/create-access/build.gradle.kts b/migration/create-access/build.gradle.kts deleted file mode 100644 index 944332436..000000000 --- a/migration/create-access/build.gradle.kts +++ /dev/null @@ -1,17 +0,0 @@ -plugins { - alias(libs.plugins.keygo.android.library) - alias(libs.plugins.keygo.android.protobuf) -} - -android { - namespace = "de.davis.keygo.migration.create_access" - - testFixtures { - enable = true - } -} - -dependencies { - implementation(libs.androidx.datastore) - implementation(libs.at.favre.bcrypt) -} diff --git a/migration/create-access/consumer-rules.pro b/migration/create-access/consumer-rules.pro deleted file mode 100644 index 087733ca4..000000000 --- a/migration/create-access/consumer-rules.pro +++ /dev/null @@ -1,16 +0,0 @@ -# Protobuf Lite --keep class com.google.protobuf.** { *; } - -# Keep generated protobuf message fields --keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite { - ; -} - -# Keep protobuf enums --keepclassmembers enum * { - public static **[] values(); - public static ** valueOf(java.lang.String); -} - -# DataStore Proto --keep class androidx.datastore.** { *; } \ No newline at end of file diff --git a/migration/create-access/proguard-rules.pro b/migration/create-access/proguard-rules.pro deleted file mode 100644 index 481bb4348..000000000 --- a/migration/create-access/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/migration/create-access/src/main/AndroidManifest.xml b/migration/create-access/src/main/AndroidManifest.xml deleted file mode 100644 index 44008a433..000000000 --- a/migration/create-access/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/data/mapper/MainPasswordMapper.kt b/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/data/mapper/MainPasswordMapper.kt deleted file mode 100644 index 92451b051..000000000 --- a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/data/mapper/MainPasswordMapper.kt +++ /dev/null @@ -1,8 +0,0 @@ -package de.davis.keygo.migration.create_access.data.mapper - -import de.davis.keygo.migration.create_access.data.local.model.ProtoMainPassword -import de.davis.keygo.migration.create_access.domain.model.MainPassword -import java.time.Instant - -internal fun ProtoMainPassword.toDomain() = - MainPassword(hash, Instant.ofEpochSecond(createdAt.seconds, createdAt.nanos.toLong())) diff --git a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/di/MigrationCreateAccessModule.kt b/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/di/MigrationCreateAccessModule.kt deleted file mode 100644 index bb87b8664..000000000 --- a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/di/MigrationCreateAccessModule.kt +++ /dev/null @@ -1,28 +0,0 @@ -package de.davis.keygo.migration.create_access.di - -import android.content.Context -import androidx.datastore.dataStore -import de.davis.keygo.migration.create_access.data.local.datasource.datastore.MainPasswordSerializer -import de.davis.keygo.migration.create_access.di.annotation.MainPasswordQualifier -import org.koin.core.annotation.ComponentScan -import org.koin.core.annotation.Configuration -import org.koin.core.annotation.Module -import org.koin.core.annotation.Single - -@Module -@Configuration -@ComponentScan("de.davis.keygo.migration.create_access") -object MigrationCreateAccessModule { - - private const val DATA_STORE_NAME = "main-password.db" - - private val Context.protoMainPasswordDataStore by dataStore( - fileName = DATA_STORE_NAME, - serializer = MainPasswordSerializer, - ) - - @Single - @MainPasswordQualifier - internal fun provideProtoMainPasswordDataStore(context: Context) = - context.protoMainPasswordDataStore -} diff --git a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/repository/MainPasswordRepository.kt b/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/repository/MainPasswordRepository.kt deleted file mode 100644 index bb5ed527e..000000000 --- a/migration/create-access/src/main/kotlin/de/davis/keygo/migration/create_access/domain/repository/MainPasswordRepository.kt +++ /dev/null @@ -1,10 +0,0 @@ -package de.davis.keygo.migration.create_access.domain.repository - -import de.davis.keygo.migration.create_access.domain.model.MainPassword - -internal interface MainPasswordRepository { - - suspend fun getMainPassword(): MainPassword - - suspend fun clearMainPassword() -} diff --git a/settings.gradle.kts b/settings.gradle.kts index a3168757c..f5b0fe299 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -30,7 +30,6 @@ rootProject.name = "KeyGoV2" include(":app") include(":automation-processor") include(":automation") -include(":migration:create-access") include(":legacy-migration") include(":core:item") include(":core:util") From c0247e9ddeb7f33ac2ba164ab08582f34198e40e Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 14 Aug 2026 12:10:33 +0200 Subject: [PATCH 03/20] test: pin v1 on-disk identities across the module move --- .../legacy_migration/OnDiskIdentityTest.kt | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/OnDiskIdentityTest.kt diff --git a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/OnDiskIdentityTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/OnDiskIdentityTest.kt new file mode 100644 index 000000000..5d7bc0ed8 --- /dev/null +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/OnDiskIdentityTest.kt @@ -0,0 +1,57 @@ +package de.davis.keygo.legacy_migration + +import com.google.protobuf.timestamp +import de.davis.keygo.legacy_migration.data.local.datasource.LEGACY_DATABASE_NAME +import de.davis.keygo.legacy_migration.data.local.model.protoMainPassword +import de.davis.keygo.legacy_migration.data.repository.LEGACY_KEY_ALIAS +import de.davis.keygo.legacy_migration.di.MAIN_PASSWORD_DATA_STORE_NAME +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +/** + * The four names below are the identity of a shipped v1 install's data on disk. Renaming the module + * or the Kotlin package around them is safe. Renaming them is not: it orphans the user's database, + * their main password record or their item key, with no way back. + * + * These assertions look tautological on purpose. They are here so that changing one of the values + * cannot be done quietly. + */ +class OnDiskIdentityTest { + + @Test + fun `legacy room database keeps v1's file name`() { + assertEquals("secure_element_database", LEGACY_DATABASE_NAME) + } + + @Test + fun `main password datastore keeps v1's file name`() { + assertEquals("main-password.db", MAIN_PASSWORD_DATA_STORE_NAME) + } + + @Test + fun `legacy keystore alias keeps v1's value`() { + assertEquals("password_manager_skey", LEGACY_KEY_ALIAS) + } + + /** + * The proto's `java_package` moved with the module. That renames the generated Kotlin class and + * nothing else, so an existing `main-password.db` still parses. This pins the wire format that + * makes the claim true: field 1 is the hash, field 2 is the timestamp. + */ + @Test + fun `main password proto keeps v1's field numbers`() { + val encoded = protoMainPassword { + hash = "ab" + createdAt = timestamp { seconds = 1 } + }.toByteArray() + + assertContentEquals( + byteArrayOf( + 0x0A, 0x02, 0x61, 0x62, // field 1, length-delimited, "ab" + 0x12, 0x02, 0x08, 0x01, // field 2, length-delimited, Timestamp { seconds = 1 } + ), + encoded, + ) + } +} From 44904f5e4ee74d3ec90ec5927e3c6e376b321dff Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Fri, 14 Aug 2026 12:19:36 +0200 Subject: [PATCH 04/20] feat(migration): add RunPendingMigrationUseCase --- .../domain/model/MigrationResult.kt | 26 ++++ .../domain/usecase/LegacyDataImporter.kt | 16 ++ .../usecase/MigrateLegacyDataUseCase.kt | 8 +- .../usecase/RunPendingMigrationUseCase.kt | 62 ++++++++ .../usecase/RunPendingMigrationUseCaseTest.kt | 139 ++++++++++++++++++ .../legacy_migration/MigrationTestUseCases.kt | 22 +++ 6 files changed, 269 insertions(+), 4 deletions(-) create mode 100644 legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/MigrationResult.kt create mode 100644 legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyDataImporter.kt create mode 100644 legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt create mode 100644 legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/MigrationResult.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/MigrationResult.kt new file mode 100644 index 000000000..0bb789172 --- /dev/null +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/MigrationResult.kt @@ -0,0 +1,26 @@ +package de.davis.keygo.legacy_migration.domain.model + +/** + * How a call to `RunPendingMigrationUseCase` ended, in the terms the auth screen has to act on. + */ +sealed interface MigrationResult { + + /** + * No v1 marker: a clean install, or a migration that already finished. Nothing was opened, and + * this is the answer for the overwhelming majority of unlocks. + */ + data object NotPending : MigrationResult + + /** + * The import reached a verdict about the v1 file and the marker is gone. [skippedItems] counts + * rows that could not be read; those rows are still in the legacy file, which + * `MigrateLegacyDataUseCase` retains whenever anything failed. + */ + data class Completed(val skippedItems: Int) : MigrationResult + + /** + * The import could not reach a verdict. The marker is kept, the legacy file is untouched, and + * the next attempt retries. + */ + data class Incomplete(val cause: Throwable) : MigrationResult +} diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyDataImporter.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyDataImporter.kt new file mode 100644 index 000000000..0b9564f27 --- /dev/null +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyDataImporter.kt @@ -0,0 +1,16 @@ +package de.davis.keygo.legacy_migration.domain.usecase + +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome + +/** + * The one thing [RunPendingMigrationUseCase] needs of the v1 item import: run it, and say how it + * ended. + * + * Narrow on purpose. The sequencing this module exists to guarantee is worth testing on its own, + * and standing up Room, a SQLite driver, the Keystore and the whole v2 key hierarchy behind every + * one of those tests would say nothing extra about the sequencing. + */ +internal fun interface LegacyDataImporter { + + suspend operator fun invoke(): LegacyMigrationOutcome +} diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCase.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCase.kt index 751d7d1ee..cc676c948 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCase.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCase.kt @@ -46,8 +46,8 @@ import kotlin.coroutines.cancellation.CancellationException * file where it is, because a retry on the next unlock costs the user nothing and a wrong deletion * costs them everything. */ -@Single -class MigrateLegacyDataUseCase internal constructor( +@Single(binds = [LegacyDataImporter::class]) +internal class MigrateLegacyDataUseCase internal constructor( private val legacyItemRepository: LegacyItemRepository, private val legacyKeyRepository: LegacyKeyRepository, private val converter: LegacyItemConverter, @@ -56,9 +56,9 @@ class MigrateLegacyDataUseCase internal constructor( private val vaultContextRepository: VaultContextRepository, private val upsertVaultItem: UpsertVaultItemUseCase, private val transactionRunner: TransactionRunner, -) { +) : LegacyDataImporter { - suspend operator fun invoke(): LegacyMigrationOutcome = try { + override suspend operator fun invoke(): LegacyMigrationOutcome = try { migrate() } catch (e: CancellationException) { // See LegacyItemRepositoryImpl.withDao. diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt new file mode 100644 index 000000000..5e0d1cc0e --- /dev/null +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt @@ -0,0 +1,62 @@ +package de.davis.keygo.legacy_migration.domain.usecase + +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome +import de.davis.keygo.legacy_migration.domain.model.MigrationResult +import org.koin.core.annotation.Single +import kotlin.coroutines.cancellation.CancellationException + +/** + * Runs whatever is left of the v1 migration, in order, once a session is live. + * + * The v1 main password record is the marker for the whole migration, not just for the access half. + * It is read first, so an install that never ran v1 never opens the legacy path at all, and it is + * cleared last, and only once the import has said something definite about the v1 file. Between + * those two the user's credential is the only way back to a migration that did not finish, so + * dropping it early would strand them with rows still on disk and nothing to act on. + * + * Called after every path that establishes a session on the auth screen, which is the only door a + * migrating install has: both unlock paths need an account that does not exist yet while the + * migration is pending, so the autofill service and the passkey activities cannot reach this. + */ +@Single +class RunPendingMigrationUseCase internal constructor( + private val hasMainPassword: HasMainPasswordUseCase, + private val importLegacyData: LegacyDataImporter, + private val clearMainPassword: ClearMainPasswordUseCase, +) { + + suspend operator fun invoke(): MigrationResult { + if (!hasMainPassword()) return MigrationResult.NotPending + + val outcome = try { + importLegacyData() + } catch (e: CancellationException) { + // A run cancelled because the scope went away tells us nothing about the user's file, + // and it must not be able to answer for it. See LegacyItemRepositoryImpl.withDao. + throw e + } catch (e: Throwable) { + // Throwable and not Exception: MigrateLegacyDataUseCase catches Exception around its + // whole run, which leaves everything that is not one uncaught, and a module reaching + // Room, a native SQLite driver and the Keystore can raise a LinkageError or a + // NoClassDefFoundError on a device missing something it expected. This now runs inside + // viewModelScope, where that would end the process rather than the migration. + return MigrationResult.Incomplete(e) + } + + return when (outcome) { + is LegacyMigrationOutcome.Failed -> MigrationResult.Incomplete(outcome.cause) + + // Exhaustive rather than an else, so an outcome added later cannot default into the + // branch that drops the user's v1 credential. + LegacyMigrationOutcome.NothingToMigrate -> { + clearMainPassword() + MigrationResult.Completed(skippedItems = 0) + } + + is LegacyMigrationOutcome.Migrated -> { + clearMainPassword() + MigrationResult.Completed(skippedItems = outcome.report.failures.size) + } + } + } +} diff --git a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt new file mode 100644 index 000000000..9eb6622fd --- /dev/null +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt @@ -0,0 +1,139 @@ +package de.davis.keygo.legacy_migration.domain.usecase + +import de.davis.keygo.legacy_migration.FakeMainPasswordRepository +import de.davis.keygo.legacy_migration.clearMainPasswordUseCase +import de.davis.keygo.legacy_migration.domain.model.LegacyFailureReason +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationReport +import de.davis.keygo.legacy_migration.domain.model.LegacyRowFailure +import de.davis.keygo.legacy_migration.domain.model.MigrationResult +import de.davis.keygo.legacy_migration.hasMainPasswordUseCase +import kotlinx.coroutines.test.runTest +import kotlin.coroutines.cancellation.CancellationException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertIs +import kotlin.test.assertTrue + +class RunPendingMigrationUseCaseTest { + + private val mainPasswordRepository = FakeMainPasswordRepository(hash = "a-v1-bcrypt-hash") + + private var importsRun = 0 + + private fun useCase(importer: LegacyDataImporter) = RunPendingMigrationUseCase( + hasMainPassword = hasMainPasswordUseCase(mainPasswordRepository), + importLegacyData = LegacyDataImporter { + importsRun++ + importer() + }, + clearMainPassword = clearMainPasswordUseCase(mainPasswordRepository), + ) + + private fun migrated(failures: List = emptyList(), fileRetained: Boolean = false) = + LegacyMigrationOutcome.Migrated( + LegacyMigrationReport( + migratedItems = 3, + failures = failures, + fileRetained = fileRetained, + ), + ) + + private fun rowFailure(id: Long) = LegacyRowFailure( + legacyId = id, + title = "an account", + reason = LegacyFailureReason.Unreadable, + ) + + @Test + fun `clears the marker once the import reports rows migrated`() = runTest { + val result = useCase { migrated() }() + + assertEquals(MigrationResult.Completed(skippedItems = 0), result) + assertEquals("", mainPasswordRepository.hash) + } + + @Test + fun `clears the marker when there was nothing to migrate`() = runTest { + val result = useCase { LegacyMigrationOutcome.NothingToMigrate }() + + assertEquals(MigrationResult.Completed(skippedItems = 0), result) + assertEquals("", mainPasswordRepository.hash) + } + + @Test + fun `reports the rows that were skipped and still clears the marker`() = runTest { + val result = useCase { migrated(failures = listOf(rowFailure(1), rowFailure(2))) }() + + assertEquals(MigrationResult.Completed(skippedItems = 2), result) + assertEquals("", mainPasswordRepository.hash) + } + + /** + * A file left behind by a failed prune or a failed delete, rather than by a failed row. The + * rows are already in v2, so a retry would duplicate them; the marker goes and the stale file + * is accepted. This is the one place the design retries less than the code it replaced, and it + * is pinned here so the trade is deliberate rather than discovered. + */ + @Test + fun `clears the marker even when the legacy file could not be deleted`() = runTest { + val result = useCase { migrated(fileRetained = true) }() + + assertEquals(MigrationResult.Completed(skippedItems = 0), result) + assertEquals("", mainPasswordRepository.hash) + } + + /** + * The invariant the whole module exists to hold. A run that could not reach a verdict about the + * v1 file must leave the credential that gets the user back to it. + */ + @Test + fun `keeps the marker when the import failed`() = runTest { + val cause = IllegalStateException("could not read the legacy database") + + val result = useCase { LegacyMigrationOutcome.Failed(cause) }() + + assertIs(result) + assertEquals(cause, result.cause) + assertEquals("a-v1-bcrypt-hash", mainPasswordRepository.hash) + } + + /** + * The short circuit that keeps a clean install from opening the legacy path at all. Before this + * existed, every unlock on every install swept it once per process. + */ + @Test + fun `never touches the import when there is no marker`() = runTest { + mainPasswordRepository.hash = "" + + val result = useCase { migrated() }() + + assertEquals(MigrationResult.NotPending, result) + assertEquals(0, importsRun) + } + + /** + * MigrateLegacyDataUseCase catches Exception, which leaves everything that is not one uncaught. + * A module reaching Room, a native SQLite driver and the Keystore can raise a LinkageError on a + * device missing something it expected, and this now runs in viewModelScope where that would + * take the process down rather than being logged. + */ + @Test + fun `contains a throwable that is not an exception`() = runTest { + val result = useCase { throw NoClassDefFoundError("libsqlite") }() + + assertIs(result) + assertTrue(result.cause is NoClassDefFoundError) + assertEquals("a-v1-bcrypt-hash", mainPasswordRepository.hash) + } + + @Test + fun `lets cancellation through rather than reporting it as a verdict`() = runTest { + assertFailsWith { + useCase { throw CancellationException("unlock scope went away") }() + } + + assertEquals("a-v1-bcrypt-hash", mainPasswordRepository.hash) + } +} diff --git a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt index a047cbed6..3cafca909 100644 --- a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt @@ -1,8 +1,11 @@ package de.davis.keygo.legacy_migration import de.davis.keygo.legacy_migration.data.repository.HashValidatorImpl +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome import de.davis.keygo.legacy_migration.domain.usecase.ClearMainPasswordUseCase import de.davis.keygo.legacy_migration.domain.usecase.HasMainPasswordUseCase +import de.davis.keygo.legacy_migration.domain.usecase.LegacyDataImporter +import de.davis.keygo.legacy_migration.domain.usecase.RunPendingMigrationUseCase import de.davis.keygo.legacy_migration.domain.usecase.ValidateMainPasswordUseCase fun clearMainPasswordUseCase(repository: FakeMainPasswordRepository): ClearMainPasswordUseCase = @@ -13,3 +16,22 @@ fun hasMainPasswordUseCase(repository: FakeMainPasswordRepository): HasMainPassw fun validateMainPasswordUseCase(repository: FakeMainPasswordRepository): ValidateMainPasswordUseCase = ValidateMainPasswordUseCase(HashValidatorImpl(), repository.asMainPasswordRepository()) + +/** + * Builds the real sequencing over a fake marker and a canned import outcome, so a consumer outside + * this module can drive the auth screen through a finished, a partial and a failed migration + * without depending on anything internal to it. [onImport] fires once per run, for callers that + * need to see that a retry actually retried. + */ +fun runPendingMigrationUseCase( + repository: FakeMainPasswordRepository, + outcome: LegacyMigrationOutcome = LegacyMigrationOutcome.NothingToMigrate, + onImport: () -> Unit = {}, +): RunPendingMigrationUseCase = RunPendingMigrationUseCase( + hasMainPassword = hasMainPasswordUseCase(repository), + importLegacyData = LegacyDataImporter { + onImport() + outcome + }, + clearMainPassword = clearMainPasswordUseCase(repository), +) From 42e77b50549ed8d5ffac4679ec58bee3d06efaa6 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 16 Aug 2026 10:29:33 +0200 Subject: [PATCH 05/20] feat(auth): run the pending v1 migration before entering the app --- feature/auth/build.gradle.kts | 4 + .../feature/auth/presentation/AuthContent.kt | 73 +++++++++++ .../feature/auth/presentation/AuthScreen.kt | 4 +- .../auth/presentation/AuthViewModel.kt | 79 +++++++++--- .../auth/presentation/model/AuthState.kt | 15 +++ .../auth/presentation/model/AuthUIEvent.kt | 3 + feature/auth/src/main/res/values/strings.xml | 9 ++ .../auth/presentation/AuthViewModelTest.kt | 114 +++++++++++++++--- 8 files changed, 264 insertions(+), 37 deletions(-) diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index 53f9ffaa3..77b76cca0 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -5,6 +5,10 @@ plugins { android { namespace = "de.davis.keygo.feature.auth" + + testOptions { + unitTests.isReturnDefaultValues = true + } } dependencies { diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthContent.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthContent.kt index 07191375c..a5a501c65 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthContent.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthContent.kt @@ -75,6 +75,79 @@ fun AuthContent( } } + is AuthState.ImportingLegacyData -> { + Surface(modifier = Modifier.fillMaxSize()) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically), + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + ) { + ContainedLoadingIndicator() + + Text( + text = stringResource(R.string.importing_legacy_data), + style = MaterialTheme.typography.titleMedium, + ) + + Text( + text = stringResource(R.string.importing_legacy_data_description), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + ) + } + } + } + + is AuthState.MigrationSummary -> { + Surface(modifier = Modifier.fillMaxSize()) { + AlertDialog( + onDismissRequest = {}, + icon = { + Icon(imageVector = Icons.Default.AutoFixHigh, contentDescription = null) + }, + title = { Text(text = stringResource(R.string.migration_summary_title)) }, + text = { + Text( + text = stringResource( + R.string.migration_summary_description, + state.skippedItems, + ), + ) + }, + confirmButton = { + Button(onClick = { onEvent(AuthUIEvent.ContinueAfterMigration) }) { + Text(text = stringResource(R.string.continue_anyway)) + } + }, + ) + } + } + + is AuthState.MigrationFailed -> { + Surface(modifier = Modifier.fillMaxSize()) { + AlertDialog( + onDismissRequest = {}, + icon = { + Icon(imageVector = Icons.Default.AutoFixHigh, contentDescription = null) + }, + title = { Text(text = stringResource(R.string.migration_failed_title)) }, + text = { Text(text = stringResource(R.string.migration_failed_description)) }, + confirmButton = { + Button(onClick = { onEvent(AuthUIEvent.RetryMigration) }) { + Text(text = stringResource(R.string.retry)) + } + }, + dismissButton = { + FilledTonalButton(onClick = { onEvent(AuthUIEvent.ContinueAfterMigration) }) { + Text(text = stringResource(R.string.continue_anyway)) + } + }, + ) + } + } + is AuthState.Interactable -> InteractableAuthContent( state = state, onEvent = onEvent, diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthScreen.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthScreen.kt index c21829175..9034b97c7 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthScreen.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthScreen.kt @@ -36,7 +36,7 @@ fun AuthScreen(onSuccess: () -> Unit) { keyId = KeyId.BiometricVaultKek, mode = request.cryptoMode ).onSuccess { - viewModel.executeCreateAccessAndClearV1(request.password, it) + viewModel.executeCreateAccess(request.password, it) }.onFailure { Log.e("AuthScreen", "Failed to create cipher for biometric access: $it") // TODO: show error @@ -47,7 +47,7 @@ fun AuthScreen(onSuccess: () -> Unit) { biometricUnlockAdapter.useAdapter { biometricCryptoController.requestUnlockVault() }.onSuccess { - currentOnSuccess() + viewModel.onSessionEstablished() } } } diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt index 9e346148c..4b10dbe61 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt @@ -1,5 +1,6 @@ package de.davis.keygo.feature.auth.presentation +import android.util.Log import androidx.compose.foundation.text.input.TextFieldState import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -17,8 +18,9 @@ import de.davis.keygo.core.util.onSuccess import de.davis.keygo.feature.auth.presentation.model.AuthState import de.davis.keygo.feature.auth.presentation.model.AuthUIEvent import de.davis.keygo.feature.auth.presentation.model.BiometricRequest -import de.davis.keygo.legacy_migration.domain.usecase.ClearMainPasswordUseCase +import de.davis.keygo.legacy_migration.domain.model.MigrationResult import de.davis.keygo.legacy_migration.domain.usecase.HasMainPasswordUseCase +import de.davis.keygo.legacy_migration.domain.usecase.RunPendingMigrationUseCase import de.davis.keygo.legacy_migration.domain.usecase.ValidateMainPasswordUseCase import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow @@ -36,9 +38,9 @@ internal class AuthViewModel( accountRepository: AccountRepository, // ---- Migration ---- - hasV1MainPassword: HasMainPasswordUseCase, + private val hasV1MainPassword: HasMainPasswordUseCase, private val validateMainPassword: ValidateMainPasswordUseCase, - private val clearMainPasswordUseCase: ClearMainPasswordUseCase, + private val runPendingMigration: RunPendingMigrationUseCase, // ------------------- private val unlockWithPassword: UnlockWithPasswordUseCase, @@ -88,7 +90,7 @@ internal class AuthViewModel( } } - private val navigationEventChannel = Channel() + private val navigationEventChannel = Channel(Channel.BUFFERED) val navigationEvent = navigationEventChannel.receiveAsFlow() fun onEvent(event: AuthUIEvent) { @@ -138,6 +140,10 @@ internal class AuthViewModel( it.copy(useBiometrics = event.checked) } } + + AuthUIEvent.RetryMigration -> onSessionEstablished() + + AuthUIEvent.ContinueAfterMigration -> navigationEventChannel.trySend(Unit) } } @@ -146,7 +152,7 @@ internal class AuthViewModel( password: String ) { if (!authState.biometricsAvailable || !authState.useBiometrics) { - executeCreateAccessAndClearV1(password = password) + executeCreateAccess(password = password) return } @@ -159,7 +165,7 @@ internal class AuthViewModel( private fun loading( setLoading: Boolean = true, - block: suspend LoadingScope.() -> Unit + block: suspend LoadingScope.() -> Unit, ) { if (setLoading) _uiState.update { @@ -168,34 +174,67 @@ internal class AuthViewModel( } viewModelScope.launch { - _uiState.update { - if (it !is AuthState.Interactable) return@update it + val current = _uiState.value as? AuthState.Interactable ?: return@launch + + var sessionEstablished = false + val scope = LoadingScope( + state = current, + onSuccess = { sessionEstablished = true }, + ) + scope.block() + + _uiState.update { scope.updatedState.copyDefaultState(loading = false) } + + if (sessionEstablished) onSessionEstablished() + } + } - LoadingScope( - state = it, - onSuccess = { navigationEventChannel.trySend(Unit) }, - ).apply { - block() - }.updatedState.copyDefaultState(loading = false) + /** + * Run after every path that establishes a session, which is the only moment the import can + * happen: every secret it writes is re-encrypted under a key that hangs off the ARK. + * + * The marker is read here as well as inside the use case so the common case, an install with no + * v1 migration pending, never flips the screen into an import it is not going to run. + */ + fun onSessionEstablished() { + viewModelScope.launch { + if (!hasV1MainPassword()) { + navigationEventChannel.trySend(Unit) + return@launch + } + + _uiState.update { AuthState.ImportingLegacyData } + + when (val result = runPendingMigration()) { + MigrationResult.NotPending -> navigationEventChannel.trySend(Unit) + + is MigrationResult.Completed -> + if (result.skippedItems == 0) navigationEventChannel.trySend(Unit) + else _uiState.update { AuthState.MigrationSummary(result.skippedItems) } + + is MigrationResult.Incomplete -> { + Log.e(TAG, "v1 import did not finish", result.cause) + _uiState.update { AuthState.MigrationFailed } + } } } } - fun executeCreateAccessAndClearV1( + fun executeCreateAccess( password: String, - cipher: Cipher? = null + cipher: Cipher? = null, ) { loading { createAllAccesses( password = password, - biometricCipher = cipher - ).onSuccess { - clearMainPasswordUseCase() - }.handleAuthenticationResult() + biometricCipher = cipher, + ).handleAuthenticationResult() } } } +private const val TAG = "AuthViewModel" + private class LoadingScope( state: State, private val onSuccess: () -> Unit, diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/model/AuthState.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/model/AuthState.kt index 330a32306..2f4c9585d 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/model/AuthState.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/model/AuthState.kt @@ -46,4 +46,19 @@ sealed interface AuthState { val useBiometrics: Boolean = true, val showMigrationDialog: Boolean = true ) : Interactable + + /** The v1 import is running. Nothing on screen is interactable while it does. */ + data object ImportingLegacyData : AuthState + + /** + * The import finished but left rows behind. Shown once, because the rows are gone as far as the + * user is concerned and nothing else would ever tell them. + */ + data class MigrationSummary(val skippedItems: Int) : AuthState + + /** + * The import could not reach a verdict. The v1 password is still on disk, so both buttons are + * safe: Retry runs it again, Continue enters the app and leaves it for the next launch. + */ + data object MigrationFailed : AuthState } diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/model/AuthUIEvent.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/model/AuthUIEvent.kt index 03245614a..fe9efd2dd 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/model/AuthUIEvent.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/model/AuthUIEvent.kt @@ -8,4 +8,7 @@ sealed interface AuthUIEvent { data object CloseMigrationDialog : AuthUIEvent data class ToggleUseBiometrics(val checked: Boolean) : AuthUIEvent + + data object RetryMigration : AuthUIEvent + data object ContinueAfterMigration : AuthUIEvent } \ No newline at end of file diff --git a/feature/auth/src/main/res/values/strings.xml b/feature/auth/src/main/res/values/strings.xml index ed197a27e..114563069 100644 --- a/feature/auth/src/main/res/values/strings.xml +++ b/feature/auth/src/main/res/values/strings.xml @@ -18,4 +18,13 @@ Request biometric authentication Unlock your vault to add this authenticator code. + + Importing your data + Moving your items into your new vault. This happens once. + Import finished + %1$d of your items could not be read and were left behind. Everything else is in your vault. + Import incomplete + Your items could not be imported. Nothing was lost, and KeyGo will try again the next time you open it. + Retry + Continue \ No newline at end of file diff --git a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt index 558a07e42..c015f5104 100644 --- a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt +++ b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt @@ -9,9 +9,15 @@ import de.davis.keygo.core.item.FakeVaultRepository import de.davis.keygo.core.security.crypto.FakeBiometricAvailabilityRepository import de.davis.keygo.core.security.crypto.FakeSession import de.davis.keygo.feature.auth.presentation.model.AuthState +import de.davis.keygo.feature.auth.presentation.model.AuthUIEvent import de.davis.keygo.legacy_migration.FakeMainPasswordRepository -import de.davis.keygo.legacy_migration.clearMainPasswordUseCase +import de.davis.keygo.legacy_migration.domain.model.LegacyFailureReason +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationReport +import de.davis.keygo.legacy_migration.domain.model.LegacyRowFailure +import de.davis.keygo.legacy_migration.domain.usecase.RunPendingMigrationUseCase import de.davis.keygo.legacy_migration.hasMainPasswordUseCase +import de.davis.keygo.legacy_migration.runPendingMigrationUseCase import de.davis.keygo.legacy_migration.validateMainPasswordUseCase import de.davis.keygo.rust.FakeAccountManager import de.davis.keygo.rust.FakeKeyDeriver @@ -31,15 +37,17 @@ import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertIs /** * Regression tests for the v1-password retry lockout fixed in `97b15f3c`. * - * [AuthViewModel.executeCreateAccessAndClearV1] used to clear the v1 migration password as soon - * as the password was validated, before the account was actually created. If account creation - * then failed for any reason - most notably a failed/declined biometric prompt - the v1 password - * was already gone, so `HasMainPasswordUseCase` reported no pending migration and the user had no - * way to retry. The fix defers `clearMainPasswordUseCase()` until account creation succeeds. + * [AuthViewModel.executeCreateAccess] used to clear the v1 migration password as soon as the + * password was validated, before the account was actually created. If account creation then + * failed for any reason - most notably a failed/declined biometric prompt - the v1 password was + * already gone, so `HasMainPasswordUseCase` reported no pending migration and the user had no way + * to retry. Clearing the marker now belongs to [RunPendingMigrationUseCase], which runs only after + * a session exists and only once the import has said something definite about the v1 file. */ @OptIn(ExperimentalCoroutinesApi::class) class AuthViewModelTest { @@ -78,7 +86,6 @@ class AuthViewModelTest { // they can't be built here directly the way a plain fake dependency would be. private val hasV1MainPassword = hasMainPasswordUseCase(mainPasswordRepository) private val validateMainPassword = validateMainPasswordUseCase(mainPasswordRepository) - private val clearMainPassword = clearMainPasswordUseCase(mainPasswordRepository) @BeforeTest fun setUp() { @@ -93,14 +100,17 @@ class AuthViewModelTest { * [mainPasswordRepository]'s state, so seed a hash before calling this for the ViewModel to * resolve into `AuthState.Migrating`. */ - private fun TestScope.viewModel(): AuthViewModel { + private fun TestScope.viewModel( + runPendingMigration: RunPendingMigrationUseCase = + runPendingMigrationUseCase(mainPasswordRepository), + ): AuthViewModel { val vm = AuthViewModel( savedStateHandle = SavedStateHandle(), biometricAvailabilityRepository = biometricAvailability, accountRepository = accountRepository, hasV1MainPassword = hasV1MainPassword, validateMainPassword = validateMainPassword, - clearMainPasswordUseCase = clearMainPassword, + runPendingMigration = runPendingMigration, unlockWithPassword = unlockWithPassword, createAllAccesses = createAllAccesses, ) @@ -127,7 +137,7 @@ class AuthViewModelTest { // failure mode described in the bug report. val failingCipher = Cipher.getInstance("AES/GCM/NoPadding") - vm.executeCreateAccessAndClearV1(password = "correct-password", cipher = failingCipher) + vm.executeCreateAccess(password = "correct-password", cipher = failingCipher) vm.awaitIdle() assertEquals("original-v1-hash", mainPasswordRepository.hash) @@ -142,8 +152,8 @@ class AuthViewModelTest { init(Cipher.WRAP_MODE, biometricKek) } - vm.executeCreateAccessAndClearV1(password = "correct-password", cipher = cipher) - vm.awaitIdle() + vm.executeCreateAccess(password = "correct-password", cipher = cipher) + vm.navigationEvent.first() assertEquals("", mainPasswordRepository.hash) } @@ -155,7 +165,7 @@ class AuthViewModelTest { accountRepository.setFails = true val vm = viewModel() - vm.executeCreateAccessAndClearV1(password = "correct-password") + vm.executeCreateAccess(password = "correct-password") vm.awaitIdle() assertEquals("original-v1-hash", mainPasswordRepository.hash) @@ -166,9 +176,83 @@ class AuthViewModelTest { mainPasswordRepository.hash = "original-v1-hash" val vm = viewModel() - vm.executeCreateAccessAndClearV1(password = "correct-password") - vm.awaitIdle() + vm.executeCreateAccess(password = "correct-password") + vm.navigationEvent.first() assertEquals("", mainPasswordRepository.hash) } + + /** + * The descendant of the 97b15f3c regression test. There the v1 password was dropped before the + * account existed. Here it must not be dropped before the user's rows are across. + */ + @Test + fun `a failed import leaves the v1 password in place and offers a retry`() = + runTest(dispatcher) { + mainPasswordRepository.hash = "original-v1-hash" + val vm = viewModel( + runPendingMigration = runPendingMigrationUseCase( + repository = mainPasswordRepository, + outcome = LegacyMigrationOutcome.Failed(IllegalStateException("unreadable")), + ), + ) + + vm.executeCreateAccess(password = "correct-password") + vm.uiState.first { it is AuthState.MigrationFailed } + + assertEquals("original-v1-hash", mainPasswordRepository.hash) + } + + @Test + fun `retrying a failed import runs it again`() = runTest(dispatcher) { + mainPasswordRepository.hash = "original-v1-hash" + var runs = 0 + val vm = viewModel( + runPendingMigration = runPendingMigrationUseCase( + repository = mainPasswordRepository, + outcome = LegacyMigrationOutcome.Failed(IllegalStateException("unreadable")), + onImport = { runs++ }, + ), + ) + + vm.executeCreateAccess(password = "correct-password") + vm.uiState.first { it is AuthState.MigrationFailed } + assertEquals(1, runs) + + // The retry passes through ImportingLegacyData and back to MigrationFailed without ever + // suspending, because the import is a fake. uiState is conflated, so a collector waiting on + // the intermediate state is only ever resumed after the final one has replaced it and would + // wait forever. Drain the scheduler instead and assert on what the retry left behind. + vm.onEvent(AuthUIEvent.RetryMigration) + runCurrent() + + assertEquals(2, runs) + assertIs(vm.uiState.value) + assertEquals("original-v1-hash", mainPasswordRepository.hash) + } + + @Test + fun `an import that skipped rows reports them before navigating`() = runTest(dispatcher) { + mainPasswordRepository.hash = "original-v1-hash" + val vm = viewModel( + runPendingMigration = runPendingMigrationUseCase( + repository = mainPasswordRepository, + outcome = LegacyMigrationOutcome.Migrated( + LegacyMigrationReport( + migratedItems = 14, + failures = listOf( + LegacyRowFailure(1, "an account", LegacyFailureReason.Unreadable), + LegacyRowFailure(2, "another", LegacyFailureReason.Unreadable), + ), + ), + ), + ), + ) + + vm.executeCreateAccess(password = "correct-password") + val state = vm.uiState.first { it is AuthState.MigrationSummary } + + assertEquals(2, (state as AuthState.MigrationSummary).skippedItems) + assertEquals("", mainPasswordRepository.hash) + } } From d1bf8208ea1b66a0723fc6d4abf552b3e54f68b2 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 16 Aug 2026 12:09:51 +0200 Subject: [PATCH 06/20] fix(auth): keep the password error and guard against a double retry The loading tail writes the scope's state back when the block returns, so the Migrating submit path's direct write to _uiState was overwritten and a rejected v1 main password stopped the spinner without marking the field. Route it through the scope, the way the Login path already does. Retry is a button the user reaches after a failure, so it can be tapped twice before the first run publishes anything. Two concurrent imports would read and write the same v1 rows, so hold the job and drop the second tap. --- feature/auth/build.gradle.kts | 3 +++ .../auth/presentation/AuthViewModel.kt | 27 ++++++++++++++++--- .../auth/presentation/AuthViewModelTest.kt | 11 +++++++- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index 77b76cca0..b46e00ffb 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -6,6 +6,9 @@ plugins { android { namespace = "de.davis.keygo.feature.auth" + // AuthViewModel.onSessionEstablished logs a failed v1 import through android.util.Log, which is + // the only record of why it failed. Unit tests cover that branch, and an unmocked android.jar + // throws rather than returning, so the log call would fail the test instead of the assertion. testOptions { unitTests.isReturnDefaultValues = true } diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt index 4b10dbe61..caacb3d81 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt @@ -22,6 +22,7 @@ import de.davis.keygo.legacy_migration.domain.model.MigrationResult import de.davis.keygo.legacy_migration.domain.usecase.HasMainPasswordUseCase import de.davis.keygo.legacy_migration.domain.usecase.RunPendingMigrationUseCase import de.davis.keygo.legacy_migration.domain.usecase.ValidateMainPasswordUseCase +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -93,6 +94,8 @@ internal class AuthViewModel( private val navigationEventChannel = Channel(Channel.BUFFERED) val navigationEvent = navigationEventChannel.receiveAsFlow() + private var migrationJob: Job? = null + fun onEvent(event: AuthUIEvent) { when (event) { is AuthUIEvent.RequestBiometricAuthentication -> if (uiState.value is AuthState.Login) requestBiometricLogin() @@ -115,9 +118,12 @@ internal class AuthViewModel( loading { validateMainPassword(password).asResult(Unit) .onFailure { - _uiState.update { - if (it !is AuthState.Interactable) return@update it - it.copyDefaultState(passwordError = UiFieldError.Incorrect) + // Through the scope rather than straight to _uiState: loading + // writes the scope's state back when the block returns, so a + // direct write here would be overwritten and the user would see + // the spinner stop with no error against the field. + updateState { + copyDefaultState(passwordError = UiFieldError.Incorrect) } }.onSuccess { createPasswordOrBiometricAccess(state, password) @@ -197,7 +203,12 @@ internal class AuthViewModel( * v1 migration pending, never flips the screen into an import it is not going to run. */ fun onSessionEstablished() { - viewModelScope.launch { + // Retry is a button on a screen the user reaches after a failure, so it can be tapped twice + // before the first run has published anything. Two concurrent imports would both read the + // same v1 rows and both write them, so the second tap joins the run in flight instead. + if (migrationJob?.isActive == true) return + + migrationJob = viewModelScope.launch { if (!hasV1MainPassword()) { navigationEventChannel.trySend(Unit) return@launch @@ -242,6 +253,14 @@ private class LoadingScope( var updatedState: State = state private set + /** + * Records a state change without claiming a session was established, for the paths that have + * something to say about the screen but have not authenticated anything. + */ + fun updateState(transform: State.() -> State) { + updatedState = updatedState.transform() + } + fun Result.handleAuthenticationResult(onFailure: State.(E) -> State = { this }) { onSuccess { onSuccess() } .onFailure { updatedState = updatedState.onFailure(it) } diff --git a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt index c015f5104..071a3efea 100644 --- a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt +++ b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt @@ -207,17 +207,26 @@ class AuthViewModelTest { fun `retrying a failed import runs it again`() = runTest(dispatcher) { mainPasswordRepository.hash = "original-v1-hash" var runs = 0 + var stateDuringImport: AuthState? = null + // Sampled from inside the import because uiState is conflated: ImportingLegacyData is + // replaced before any collector is resumed, so this is the only place it can be observed. + var underTest: AuthViewModel? = null val vm = viewModel( runPendingMigration = runPendingMigrationUseCase( repository = mainPasswordRepository, outcome = LegacyMigrationOutcome.Failed(IllegalStateException("unreadable")), - onImport = { runs++ }, + onImport = { + runs++ + stateDuringImport = underTest?.uiState?.value + }, ), ) + underTest = vm vm.executeCreateAccess(password = "correct-password") vm.uiState.first { it is AuthState.MigrationFailed } assertEquals(1, runs) + assertEquals(AuthState.ImportingLegacyData, stateDuringImport) // The retry passes through ImportingLegacyData and back to MigrationFailed without ever // suspending, because the import is a fake. uiState is conflated, so a collector waiting on From fcaab7ed7f9f03149acae3e5d05e5124504c9f00 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 16 Aug 2026 12:19:04 +0200 Subject: [PATCH 07/20] test(auth): cover the rejected v1 password keeping its field error Drives the real Submit event, which needs a hash ValidateMainPasswordUseCase can hex-decode. The other tests seed a placeholder and enter past validation. --- .../auth/presentation/AuthViewModelTest.kt | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt index 071a3efea..86cbca67e 100644 --- a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt +++ b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt @@ -1,6 +1,8 @@ package de.davis.keygo.feature.auth.presentation +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.lifecycle.SavedStateHandle +import de.davis.keygo.core.ui.model.UiFieldError import de.davis.keygo.core.identity.FakeAccountRepository import de.davis.keygo.core.identity.domain.usecase.CreateAccessUseCase import de.davis.keygo.core.identity.domain.usecase.UnlockWithPasswordUseCase @@ -182,6 +184,36 @@ class AuthViewModelTest { assertEquals("", mainPasswordRepository.hash) } + /** + * The Migrating submit path reports a rejected password through the loading scope rather than + * writing to `uiState` itself. `loading` writes the scope's state back once the block returns, + * so a direct write lands first and is then overwritten, stopping the spinner and leaving the + * field with nothing on it. + * + * The only test that drives the real `Submit` event, so it is also the only one that needs a + * hash `ValidateMainPasswordUseCase` can actually decode. The other tests seed a placeholder + * and reach the ViewModel past validation. + */ + @Test + fun `a rejected v1 main password leaves an error on the field`() = runTest(dispatcher) { + // Hex of a real bcrypt 2a hash of "password". The use case hex-decodes the stored hash + // before handing it to bcrypt, so a non-hex placeholder throws instead of returning false. + mainPasswordRepository.hash = "243261243130244e39716f38754c4f69636b6778325a4d525a6f4d7965" + + "496a5a416763666c377039326c644778616436384c4a5a644c31376c685779" + val vm = viewModel() + + val migrating = assertIs(vm.uiState.value) + migrating.passwordTextFieldState.setTextAndPlaceCursorAtEnd("the-wrong-password") + + vm.onEvent(AuthUIEvent.Submit) + // Bcrypt runs on Dispatchers.Default, which the scheduler cannot see, so wait on the + // loading flag for the same reason awaitIdle does. + vm.awaitIdle() + + val after = assertIs(vm.uiState.value) + assertEquals(UiFieldError.Incorrect, after.passwordError) + } + /** * The descendant of the 97b15f3c regression test. There the v1 password was dropped before the * account existed. Here it must not be dropped before the user's rows are across. From 0b509a2a4462a70a5d6b726acc33c518b97f5d38 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 16 Aug 2026 12:21:33 +0200 Subject: [PATCH 08/20] refactor(migration): confine the sequenced use cases to the module --- .../legacy_migration/domain/usecase/ClearMainPasswordUseCase.kt | 2 +- .../de/davis/keygo/legacy_migration/MigrationTestUseCases.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/ClearMainPasswordUseCase.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/ClearMainPasswordUseCase.kt index 90fe84bc1..c4199a86d 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/ClearMainPasswordUseCase.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/ClearMainPasswordUseCase.kt @@ -4,7 +4,7 @@ import de.davis.keygo.legacy_migration.domain.repository.MainPasswordRepository import org.koin.core.annotation.Single @Single -class ClearMainPasswordUseCase internal constructor( +internal class ClearMainPasswordUseCase internal constructor( private val mainPasswordRepository: MainPasswordRepository ) { suspend operator fun invoke() = mainPasswordRepository.clearMainPassword() diff --git a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt index 3cafca909..f422c04a6 100644 --- a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt @@ -8,7 +8,7 @@ import de.davis.keygo.legacy_migration.domain.usecase.LegacyDataImporter import de.davis.keygo.legacy_migration.domain.usecase.RunPendingMigrationUseCase import de.davis.keygo.legacy_migration.domain.usecase.ValidateMainPasswordUseCase -fun clearMainPasswordUseCase(repository: FakeMainPasswordRepository): ClearMainPasswordUseCase = +internal fun clearMainPasswordUseCase(repository: FakeMainPasswordRepository): ClearMainPasswordUseCase = ClearMainPasswordUseCase(repository.asMainPasswordRepository()) fun hasMainPasswordUseCase(repository: FakeMainPasswordRepository): HasMainPasswordUseCase = From 255bf647f582192370691eab4910260f44c66463 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 16 Aug 2026 12:22:57 +0200 Subject: [PATCH 09/20] refactor(migration): drop the session-driven v1 import --- .../domain/usecase/LegacyDataImportStarter.kt | 55 ---- .../domain/usecase/LegacyImportRunner.kt | 109 ------- .../domain/usecase/LegacyImportRunnerTest.kt | 286 ------------------ 3 files changed, 450 deletions(-) delete mode 100644 legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyDataImportStarter.kt delete mode 100644 legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunner.kt delete mode 100644 legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunnerTest.kt diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyDataImportStarter.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyDataImportStarter.kt deleted file mode 100644 index 567583fe4..000000000 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyDataImportStarter.kt +++ /dev/null @@ -1,55 +0,0 @@ -package de.davis.keygo.legacy_migration.domain.usecase - -import android.util.Log -import de.davis.keygo.core.security.domain.Session -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import org.koin.core.annotation.Single - -private const val TAG = "LegacyDataImport" - -/** - * Runs the v1 import in the background on every unlock. - * - * Driven by [Session.sessionStarts] rather than called from the auth screen, because the auth - * screen is only one of the doors: the autofill service and both passkey activities start sessions - * of their own. An import wired to some of them would leave a partial migration sitting until the - * user happened to come back through the right one, and would go quietly stale again the next time - * a door is added. - * - * The import is not work that can sit in front of the user: it opens the inherited file, decrypts - * every row through the Keystore and writes them all back under new keys, and how long that takes - * is a property of the user's old database rather than anything we can bound. It runs on its own - * application scope on [Dispatchers.IO], off the main thread and outside the lifetime of whatever - * unlocked, which for the auth screen is a ViewModel cleared moments later. - * - * There is no in-process lock today for a run to be interrupted by: the session lives for the - * process, and the app's only current "lock" is a process or activity restart, which ends the run - * along with everything else. A run interrupted by a future lock feature would fail instead, leave - * the legacy file exactly as it found it, and be retried on the next unlock. - * - * Created at Koin start so the collector is already listening when the first session begins. - */ -@Single(createdAtStart = true) -internal class LegacyDataImportStarter( - session: Session, - migrateLegacyData: MigrateLegacyDataUseCase, -) { - - private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) - - private val runner = LegacyImportRunner( - scope = scope, - report = { message, cause -> - if (cause != null) Log.e(TAG, message, cause) else Log.e(TAG, message) - }, - import = { migrateLegacyData() }, - ) - - init { - session.sessionStarts.onEach { runner.start() }.launchIn(scope) - } -} diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunner.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunner.kt deleted file mode 100644 index 2e4032540..000000000 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunner.kt +++ /dev/null @@ -1,109 +0,0 @@ -package de.davis.keygo.legacy_migration.domain.usecase - -import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome -import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationReport -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import java.util.concurrent.atomic.AtomicBoolean -import kotlin.coroutines.cancellation.CancellationException - -/** - * Runs [import] on [scope], one run at a time, with nothing escaping into [scope]. - * - * One run at a time is what stops two unlocks from copying the same rows twice. The import mints a - * fresh item id for every row it takes across and has no key to recognise a row it already - * imported, so two runs over one legacy file would leave the user holding two of everything. A call - * made while a run is in flight is dropped rather than queued, and a call made after one has - * finished starts a new run, which is what makes the import retry on every unlock. - * - * That retry stops for good once a run reports [LegacyMigrationOutcome.nothingLeftToImport], which - * is the answer for the overwhelming majority of installs: no v1 file was ever there. Without the - * latch every unlock for the rest of the process would open the file, count it and sweep the - * filesystem to conclude the same nothing, and unlocks are not rare, since the autofill service and - * both passkey activities each start a session of their own. - * - * [import] returns its outcome rather than throwing for anything expected, and every ending that is - * not a clean success is turned into one call to [report]. - * - * Nothing thrown may reach [scope], where an uncaught throwable would take the process down. That - * covers [import] itself and also [report]: a throwing reporting implementation must not be able to - * do what a throwing import already cannot. [Throwable] and not [Exception] for [import]: - * [MigrateLegacyDataUseCase] catches [Exception] around its whole run, which leaves everything that - * is not one uncaught, and a module reaching Room, a native SQLite driver and the Keystore can raise - * a [LinkageError] or a [NoClassDefFoundError] on a device missing something it expected. - * - * Cancellation is rethrown rather than reported. See LegacyItemRepositoryImpl.withDao. - */ -internal class LegacyImportRunner( - private val scope: CoroutineScope, - private val report: (message: String, cause: Throwable?) -> Unit, - private val import: suspend () -> LegacyMigrationOutcome, -) { - - private val inFlight = AtomicBoolean(false) - - private val finished = AtomicBoolean(false) - - fun start() { - if (finished.get()) return - if (!inFlight.compareAndSet(false, true)) return - - // The flag is released on completion rather than in a finally, so a run whose scope died - // before its body ever ran still gives the next unlock its turn. - scope.launch { - try { - val outcome = import() - // Latched before the in-flight flag is released, so no start can slip between the - // two and win a run the verdict has already ruled out. - if (outcome.nothingLeftToImport) finished.set(true) - reportOutcome(outcome) - } catch (e: CancellationException) { - throw e - } catch (e: Throwable) { - reportSafely("v1 import threw", e) - } - }.invokeOnCompletion { inFlight.set(false) } - } - - private fun reportOutcome(outcome: LegacyMigrationOutcome) { - when (outcome) { - is LegacyMigrationOutcome.Failed -> - reportSafely("v1 import failed, retrying on the next unlock", outcome.cause) - - is LegacyMigrationOutcome.Migrated -> if ( - outcome.report.hasFailures || outcome.report.fileRetained - ) - reportSafely(migrationSummary(outcome.report), null) - - LegacyMigrationOutcome.NothingToMigrate -> Unit - } - } - - private fun migrationSummary(migrationReport: LegacyMigrationReport): String { - val parts = mutableListOf() - - if (migrationReport.hasFailures) { - // Grouped by reason rather than by row: a row's title is the user's own account name, - // and logcat is not the place for it. Counts and reasons are enough to work out what - // happened. - val byReason = migrationReport.failures.groupingBy { it.reason }.eachCount().entries - .joinToString { (reason, count) -> "$reason=$count" } - val total = migrationReport.migratedItems + migrationReport.failures.size - parts += "${migrationReport.failures.size} of $total row(s) skipped: $byReason" - } - - if (migrationReport.fileRetained) - parts += "the legacy file could not be cleared and will be retried on the next unlock" - - return "v1 import finished: ${parts.joinToString("; ")}" - } - - /** Calls [report] without letting a throw out of it reach [scope]. */ - private fun reportSafely(message: String, cause: Throwable?) { - try { - report(message, cause) - } catch (_: Throwable) { - // Nothing to escalate to: this is already the containment boundary. - } - } -} diff --git a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunnerTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunnerTest.kt deleted file mode 100644 index 0e2481b26..000000000 --- a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/LegacyImportRunnerTest.kt +++ /dev/null @@ -1,286 +0,0 @@ -package de.davis.keygo.legacy_migration.domain.usecase - -import de.davis.keygo.legacy_migration.domain.model.LegacyFailureReason -import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome -import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationReport -import de.davis.keygo.legacy_migration.domain.model.LegacyRowFailure -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest -import kotlin.coroutines.cancellation.CancellationException -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertIs -import kotlin.test.assertTrue - -/** - * Stands in for the import. Counts its calls and takes the call number, so a test can make the - * first run behave differently from the one that follows it. - */ -private class RecordingImport( - private val body: suspend (call: Int) -> LegacyMigrationOutcome = { - LegacyMigrationOutcome.NothingToMigrate - }, -) { - var invocations = 0 - private set - - suspend operator fun invoke(): LegacyMigrationOutcome { - invocations++ - return body(invocations) - } -} - -/** - * The runner is the only thing standing between a second unlock and a second copy of the user's - * vault, and the only thing standing between a throw in the import and a process the user watches - * die on the way in. - * - * Two runs over one legacy file would import every row twice under ids neither run can recognise as - * the other's, which is why a call during a run is dropped. A run that finished with rows still on - * disk must release, because retrying on the next unlock is what turns a partial import into a - * complete one; a run that finished with nothing left must not, because every later unlock would - * pay to conclude the same nothing. - * - * `runCurrent` and not `advanceUntilIdle` throughout. The runner launches into a background scope, - * which is what production does too, and `advanceUntilIdle` returns as soon as the foreground has - * nothing left rather than running the background work. It would leave every assertion below - * looking at an import that never started. - */ -@OptIn(ExperimentalCoroutinesApi::class) -class LegacyImportRunnerTest { - - private val diagnostics = mutableListOf>() - - private val clearedFile = LegacyMigrationOutcome.Migrated( - LegacyMigrationReport(migratedItems = 3, failures = emptyList()), - ) - - private val retainedFile = LegacyMigrationOutcome.Migrated( - LegacyMigrationReport(migratedItems = 3, failures = emptyList(), fileRetained = true), - ) - - private fun TestScope.runnerFor(import: RecordingImport) = LegacyImportRunner( - scope = backgroundScope, - report = { message, cause -> diagnostics += message to cause }, - import = { import() }, - ) - - @Test - fun `a call made while a run is in flight is dropped`() = runTest { - val gate = CompletableDeferred() - val import = RecordingImport { - gate.await() - LegacyMigrationOutcome.NothingToMigrate - } - val runner = runnerFor(import) - - runner.start() - runCurrent() // the first run gets going and parks on the gate - - runner.start() - runCurrent() - - assertEquals(1, import.invocations, "a second unlock during a run must be dropped") - - gate.complete(Unit) - runCurrent() - - assertEquals(1, import.invocations, "the dropped call must not be queued behind the run") - } - - @Test - fun `a call made after a run that left rows behind starts a new run`() = runTest { - val import = RecordingImport { retainedFile } - val runner = runnerFor(import) - - runner.start() - runCurrent() - - runner.start() - runCurrent() - - assertEquals(2, import.invocations, "a finished run must leave the next unlock free") - } - - /** - * The verdict that ends the retry. Almost every install never had a v1 file, and without this - * each later unlock would re-open it, count it and sweep the filesystem to reach the same - * nothing. Unlocks are not rare either: the autofill service and both passkey activities start - * sessions of their own. - */ - @Test - fun `a run with nothing left to import is not repeated`() = runTest { - val import = RecordingImport { LegacyMigrationOutcome.NothingToMigrate } - val runner = runnerFor(import) - - runner.start() - runCurrent() - - runner.start() - runCurrent() - - assertEquals(1, import.invocations, "a verdict of nothing to import holds for the process") - } - - @Test - fun `a run that cleared the legacy file is not repeated either`() = runTest { - val import = RecordingImport { clearedFile } - val runner = runnerFor(import) - - runner.start() - runCurrent() - - runner.start() - runCurrent() - - assertEquals(1, import.invocations, "the file is gone; a retry has nothing left to find") - } - - @Test - fun `an import that throws is reported and leaves the next unlock free to retry`() = runTest { - val boom = IllegalStateException("probing the legacy file blew up") - val import = RecordingImport { throw boom } - val runner = runnerFor(import) - - runner.start() - runCurrent() - - assertEquals(listOf(boom), diagnostics.map { it.second }, "a throw must be reported") - - runner.start() - runCurrent() - - assertEquals(2, import.invocations, "a failed run must release the runner") - } - - @Test - fun `an import that fails with an Error is contained too`() = runTest { - val import = RecordingImport { throw NoClassDefFoundError("a driver this device lacks") } - val runner = runnerFor(import) - - runner.start() - runCurrent() - - // Not Exception. This runs on an application scope, where anything that gets out takes the - // process down, and a device missing a native library is not a reason to lose the vault. - assertIs(diagnostics.single().second) - } - - @Test - fun `cancellation is passed through rather than reported as a failure`() = runTest { - val import = RecordingImport { call -> - if (call == 1) throw CancellationException("the run was cancelled") - LegacyMigrationOutcome.NothingToMigrate - } - val runner = runnerFor(import) - - runner.start() - runCurrent() - - assertTrue( - diagnostics.isEmpty(), - "a cancelled run has learned nothing about the user's file and must not answer for it", - ) - - runner.start() - runCurrent() - - assertEquals(2, import.invocations, "a cancelled run must release the runner as well") - } - - @Test - fun `a Failed outcome reports its cause through the seam`() = runTest { - val cause = IllegalStateException("the legacy database exists but could not be opened") - val import = RecordingImport { LegacyMigrationOutcome.Failed(cause) } - val runner = runnerFor(import) - - runner.start() - runCurrent() - - assertEquals( - cause, - diagnostics.singleOrNull()?.second, - "Failed is this module's channel for an expected failure and must reach the seam", - ) - } - - @Test - fun `a Migrated outcome with row failures produces a diagnostic`() = runTest { - val report = LegacyMigrationReport( - migratedItems = 2, - failures = listOf( - LegacyRowFailure( - legacyId = 1L, - title = "Gmail", - reason = LegacyFailureReason.Unreadable, - ), - ), - ) - val import = RecordingImport { LegacyMigrationOutcome.Migrated(report) } - val runner = runnerFor(import) - - runner.start() - runCurrent() - - assertEquals(1, diagnostics.size, "a run that drops entries must leave a trace") - assertTrue( - diagnostics.single().first.contains("Unreadable"), - "the diagnostic must carry the reason, not just that something was skipped", - ) - assertTrue( - !diagnostics.single().first.contains("Gmail"), - "a row's title is the user's own account name; it must not end up in logcat", - ) - } - - @Test - fun `a Migrated outcome with no row failures but a retained file still reports`() = runTest { - val runner = runnerFor(RecordingImport { retainedFile }) - - runner.start() - runCurrent() - - // The ending that duplicates the whole vault on the next unlock, and `hasFailures` alone - // cannot see it: there were none. - assertEquals(1, diagnostics.size, "a retained file must report even with no row failures") - assertTrue(diagnostics.single().first.contains("retried")) - } - - /** A runner each, because both of these endings latch and so cannot follow one another. */ - @Test - fun `NothingToMigrate and a clean Migrated produce no diagnostic`() = runTest { - runnerFor(RecordingImport { LegacyMigrationOutcome.NothingToMigrate }).start() - runCurrent() - - runnerFor(RecordingImport { clearedFile }).start() - runCurrent() - - assertTrue(diagnostics.isEmpty(), "the normal endings must produce no diagnostic line") - } - - @Test - fun `a reporter that throws is contained, and the runner still releases`() = runTest { - val cause = IllegalStateException("the legacy database exists but could not be opened") - val import = RecordingImport { LegacyMigrationOutcome.Failed(cause) } - val runner = LegacyImportRunner( - scope = backgroundScope, - report = { _, _ -> throw RuntimeException("the reporter itself is broken") }, - import = { import() }, - ) - - runner.start() - runCurrent() - - runner.start() - runCurrent() - - assertEquals( - 2, - import.invocations, - "a throwing reporter must not take the application scope down", - ) - } -} From e8f039bd540703aa53b40eee385c49fdfc2509da Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 16 Aug 2026 12:31:10 +0200 Subject: [PATCH 10/20] refactor(security): remove Session.sessionStarts --- .../keygo/core/security/data/SessionImpl.kt | 13 ------- .../keygo/core/security/domain/Session.kt | 12 ------- .../core/security/data/SessionImplTest.kt | 34 ------------------- .../keygo/core/security/crypto/FakeSession.kt | 16 +-------- .../feature/backup/data/BackupSession.kt | 4 --- 5 files changed, 1 insertion(+), 78 deletions(-) diff --git a/core/security/src/main/kotlin/de/davis/keygo/core/security/data/SessionImpl.kt b/core/security/src/main/kotlin/de/davis/keygo/core/security/data/SessionImpl.kt index f2b5c0fdd..75b2d742c 100644 --- a/core/security/src/main/kotlin/de/davis/keygo/core/security/data/SessionImpl.kt +++ b/core/security/src/main/kotlin/de/davis/keygo/core/security/data/SessionImpl.kt @@ -1,9 +1,6 @@ package de.davis.keygo.core.security.data import de.davis.keygo.core.security.domain.Session -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import org.koin.core.annotation.Single import javax.security.auth.DestroyFailedException @@ -12,22 +9,12 @@ internal class SessionImpl : Session { private var _ark: ByteArray? = null - // Replayed so a collector wired up after an unlock still sees it, and buffered with - // DROP_OLDEST so emitting can never suspend or fail inside startSession. - private val _sessionStarts = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - override val sessionStarts: Flow = _sessionStarts - override val ark: ByteArray get() = _ark ?: throw IllegalStateException("No active session") override fun startSession(ark: ByteArray) { endSession() _ark = ark - _sessionStarts.tryEmit(Unit) } override fun endSession() { diff --git a/core/security/src/main/kotlin/de/davis/keygo/core/security/domain/Session.kt b/core/security/src/main/kotlin/de/davis/keygo/core/security/domain/Session.kt index 338732356..49126e789 100644 --- a/core/security/src/main/kotlin/de/davis/keygo/core/security/domain/Session.kt +++ b/core/security/src/main/kotlin/de/davis/keygo/core/security/domain/Session.kt @@ -1,21 +1,9 @@ package de.davis.keygo.core.security.domain -import kotlinx.coroutines.flow.Flow - interface Session { val ark: ByteArray - /** - * Emits once per [startSession], for work that has to happen on every unlock however the user - * got there. - * - * A session is started from the auth screen, from the autofill service and from both passkey - * activities. A caller that enumerated those instead would go stale the next time a fifth way - * in is added, and would do so silently. - */ - val sessionStarts: Flow - fun startSession(ark: ByteArray) fun endSession() } diff --git a/core/security/src/test/kotlin/de/davis/keygo/core/security/data/SessionImplTest.kt b/core/security/src/test/kotlin/de/davis/keygo/core/security/data/SessionImplTest.kt index bed79086b..b0e3d106e 100644 --- a/core/security/src/test/kotlin/de/davis/keygo/core/security/data/SessionImplTest.kt +++ b/core/security/src/test/kotlin/de/davis/keygo/core/security/data/SessionImplTest.kt @@ -1,10 +1,5 @@ package de.davis.keygo.core.security.data -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -63,33 +58,4 @@ class SessionImplTest { session.endSession() session.endSession() // should not throw } - - /** - * What the v1 import's retry rests on: it is not enough for the first unlock to be announced, - * because the run it starts is the one that may have left work behind. - */ - @Test - fun `a later start reaches a collector that already handled an earlier one`() = runTest { - val starts = mutableListOf() - session.sessionStarts.onEach { starts += it }.launchIn(backgroundScope) - - session.startSession(generateArk()) - runCurrent() - session.startSession(generateArk()) - runCurrent() - - assertEquals(2, starts.size) - } - - /** - * Replayed so the work bound to an unlock still runs when its collector attaches after the - * fact. Nothing collecting must not be able to swallow the one signal that a v1 import, or - * anything wired here later, gets to hear. - */ - @Test - fun `a start is replayed to a collector that arrives afterwards`() = runTest { - session.startSession(generateArk()) - - session.sessionStarts.first() - } } diff --git a/core/security/src/testFixtures/kotlin/de/davis/keygo/core/security/crypto/FakeSession.kt b/core/security/src/testFixtures/kotlin/de/davis/keygo/core/security/crypto/FakeSession.kt index 84c831d87..49ff11216 100644 --- a/core/security/src/testFixtures/kotlin/de/davis/keygo/core/security/crypto/FakeSession.kt +++ b/core/security/src/testFixtures/kotlin/de/davis/keygo/core/security/crypto/FakeSession.kt @@ -1,9 +1,6 @@ package de.davis.keygo.core.security.crypto import de.davis.keygo.core.security.domain.Session -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow /** * A fake implementation of [Session] that provides a fixed DEK for testing purposes. @@ -18,24 +15,13 @@ class FakeSession( override val ark: ByteArray get() = _ark ?: throw IllegalStateException("FakeSession not started") - private val _sessionStarts = MutableSharedFlow( - replay = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - - override val sessionStarts: Flow = _sessionStarts - init { - if (startOnConstruct) { - _ark = ByteArray(32) { it.toByte() } - _sessionStarts.tryEmit(Unit) - } + if (startOnConstruct) _ark = ByteArray(32) { it.toByte() } } override fun startSession(ark: ByteArray) { _ark = ark startSessionCalled = true - _sessionStarts.tryEmit(Unit) } override fun endSession() { diff --git a/feature/backup/src/main/kotlin/de/davis/keygo/feature/backup/data/BackupSession.kt b/feature/backup/src/main/kotlin/de/davis/keygo/feature/backup/data/BackupSession.kt index b109b8db8..9c98fd429 100644 --- a/feature/backup/src/main/kotlin/de/davis/keygo/feature/backup/data/BackupSession.kt +++ b/feature/backup/src/main/kotlin/de/davis/keygo/feature/backup/data/BackupSession.kt @@ -1,8 +1,6 @@ package de.davis.keygo.feature.backup.data import de.davis.keygo.core.security.domain.Session -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.emptyFlow /** * A read-only [Session] holding a recovered ARK for the duration of a single backup. It never @@ -12,8 +10,6 @@ internal class BackupSession(private val backupArk: ByteArray) : Session { override val ark: ByteArray get() = backupArk - override val sessionStarts: Flow = emptyFlow() - override fun startSession(ark: ByteArray) = error("BackupSession is read-only") From c5b515c00a567f30f648a98f1bd5623a6ac432c5 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 16 Aug 2026 13:58:32 +0200 Subject: [PATCH 11/20] docs: describe the consolidated :legacy-migration module --- CLAUDE.md | 11 +++++++---- .../keygo/feature/auth/presentation/AuthViewModel.kt | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1744087ba..8f607ac49 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,8 +52,7 @@ Android password manager using Clean Architecture per module: | `:feature:*` (remaining) | `list_screen`, `item:{core,create,view}`, `credentials`, `totp`, `vault` | | `:automation` | `@VaultItem` annotation + automation support | | `:automation-processor` | KSP processor generating code from `@VaultItem` | -| `:migration:create-access` | Post-migration main-password/account setup (high risk) | -| `:migration:legacy-data` | Reads/decrypts the legacy v1 database for import (high risk) | +| `:legacy-migration` | The whole v1 to v2 migration: main password, item import (high risk) | | `:rust` | Rust crypto/passkey ops via UniFFI-generated Kotlin bindings | ## Key Patterns @@ -127,8 +126,12 @@ carries its own rules: ## Sensitive Areas -- **Migration** (`migration:create-access`, `migration:legacy-data`) — preserve backward compat, - smallest safe change +- **Migration** (`legacy-migration`) — preserve backward compat, smallest safe change. The v1 main + password record is the marker for the whole migration and is cleared only by + `RunPendingMigrationUseCase`, only after the item import has returned a verdict. Four names are + the on-disk identity of shipped v1 data and must never change: `main-password.db`, + `secure_element_database`, `password_manager_skey`, and the `ProtoMainPassword` field numbers. + `OnDiskIdentityTest` pins all four. - **Autofill** (`feature/autofill/`) — constrained by Android framework, keep conservative - **UniFFI** — preserve memory and type safety across the FFI boundary. - **Room schema** — check migration implications before changing entities diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt index caacb3d81..ec740e877 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt @@ -205,7 +205,7 @@ internal class AuthViewModel( fun onSessionEstablished() { // Retry is a button on a screen the user reaches after a failure, so it can be tapped twice // before the first run has published anything. Two concurrent imports would both read the - // same v1 rows and both write them, so the second tap joins the run in flight instead. + // same v1 rows and both write them, so a tap that lands while one is running is dropped. if (migrationJob?.isActive == true) return migrationJob = viewModelScope.launch { From 25b00eba3361e61cd843bed3614f6b01916f4cf9 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 16 Aug 2026 14:48:28 +0200 Subject: [PATCH 12/20] fix(auth): stop a second account creation from starting mid-flight The migrate path nested one loading inside another: the outer block returned as soon as executeCreateAccess had launched, so loading went back to false while key derivation was still running and Submit re-enabled for the whole of it. onEvent gates on the state being interactable rather than on the loading flag, so nothing below the button stopped a second run either. Two runs mint two accounts, two ARKs and two vaults, and the second overwrites the registry, leaving the first vault wrapped under an ARK nothing persists. Flatten the migrate path onto the caller's loading scope, and hold the job so a run that lands while another is in flight is dropped. --- .../core/identity/FakeAccountRepository.kt | 9 ++++++ .../auth/presentation/AuthViewModel.kt | 32 +++++++++++++++---- .../auth/presentation/AuthViewModelTest.kt | 25 +++++++++++++++ 3 files changed, 60 insertions(+), 6 deletions(-) diff --git a/core/identity/src/testFixtures/kotlin/de/davis/keygo/core/identity/FakeAccountRepository.kt b/core/identity/src/testFixtures/kotlin/de/davis/keygo/core/identity/FakeAccountRepository.kt index 20d84af6d..f5f246ba1 100644 --- a/core/identity/src/testFixtures/kotlin/de/davis/keygo/core/identity/FakeAccountRepository.kt +++ b/core/identity/src/testFixtures/kotlin/de/davis/keygo/core/identity/FakeAccountRepository.kt @@ -14,6 +14,14 @@ class FakeAccountRepository : AccountRepository { var setFails: Boolean = false + /** + * How many accounts were actually persisted. A caller that mints a second account overwrites + * the first here exactly as the real registry does, so the stored value alone cannot tell the + * two apart. + */ + var setCount: Int = 0 + private set + fun seed(account: Account) { this.account.update { account } } @@ -24,6 +32,7 @@ class FakeAccountRepository : AccountRepository { override suspend fun set(account: Account): Result { if (setFails) return Result.Failure(Unit) + setCount++ this.account.update { account } return Result.Success(Unit) } diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt index ec740e877..6cedc0632 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt @@ -96,6 +96,8 @@ internal class AuthViewModel( private var migrationJob: Job? = null + private var authJob: Job? = null + fun onEvent(event: AuthUIEvent) { when (event) { is AuthUIEvent.RequestBiometricAuthentication -> if (uiState.value is AuthState.Login) requestBiometricLogin() @@ -153,16 +155,27 @@ internal class AuthViewModel( } } - private fun createPasswordOrBiometricAccess( + /** + * Runs inside the caller's [loading] rather than starting a second one, so the screen stays + * loading until the account actually exists. A nested [loading] returned as soon as it had + * launched, which wrote `loading = false` back while key derivation was still running and + * re-enabled Submit for the whole of it. + */ + private suspend fun LoadingScope.createPasswordOrBiometricAccess( authState: AuthState.Migrating, - password: String + password: String, ) { - if (!authState.biometricsAvailable || !authState.useBiometrics) { - executeCreateAccess(password = password) + if (authState.biometricsAvailable && authState.useBiometrics) { + // Handed to the prompt, so this run is done. AuthScreen starts a fresh one with the + // cipher once the user has answered it. + biometricChannel.trySend(BiometricRequest.CreateAccess(password)) return } - biometricChannel.trySend(BiometricRequest.CreateAccess(password)) + createAllAccesses( + password = password, + biometricCipher = null, + ).handleAuthenticationResult() } private fun requestBiometricLogin() { @@ -173,13 +186,20 @@ internal class AuthViewModel( setLoading: Boolean = true, block: suspend LoadingScope.() -> Unit, ) { + // One auth run at a time. Submit and the biometric callback both arrive here, and `onEvent` + // gates on the state being interactable rather than on the loading flag, so nothing else + // stops a second run. Two runs of account creation mint two accounts, two ARKs and two + // vaults and the second overwrites the registry, which leaves the first vault wrapped under + // an ARK that is no longer persisted anywhere. + if (authJob?.isActive == true) return + if (setLoading) _uiState.update { if (it !is AuthState.Interactable) return@update it it.copyDefaultState(loading = true) } - viewModelScope.launch { + authJob = viewModelScope.launch { val current = _uiState.value as? AuthState.Interactable ?: return@launch var sessionEstablished = false diff --git a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt index 86cbca67e..34766dfda 100644 --- a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt +++ b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt @@ -184,6 +184,31 @@ class AuthViewModelTest { assertEquals("", mainPasswordRepository.hash) } + /** + * Key derivation takes long enough for a second tap to land inside it. `onEvent` gates on the + * state being interactable rather than on the loading flag, so before the guard the only thing + * stopping a second run was the button's own enabled state, and the migrate path stopped the + * spinner while derivation was still going. Two runs mint two accounts, two ARKs and two + * vaults, and the second overwrites the registry, leaving the first vault wrapped under an ARK + * nothing persists. + */ + @Test + fun `a second account creation started while one is in flight is dropped`() = + runTest(dispatcher) { + mainPasswordRepository.hash = "original-v1-hash" + val vm = viewModel() + + vm.executeCreateAccess(password = "correct-password") + // Leaves the first run suspended inside key derivation, which hops to a dispatcher the + // scheduler cannot see, so it cannot complete until something pumps the test one. + runCurrent() + vm.executeCreateAccess(password = "correct-password") + + vm.navigationEvent.first() + + assertEquals(1, accountRepository.setCount) + } + /** * The Migrating submit path reports a rejected password through the loading scope rather than * writing to `uiState` itself. `loading` writes the scope's state back once the block returns, From c1af9fe19d720612d4100bc98f8ce6a599793f95 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 16 Aug 2026 15:40:31 +0200 Subject: [PATCH 13/20] test(auth): pin the migrate path staying loading until the account exists The flattening had no coverage: the existing test drives executeCreateAccess, whose only production caller is the biometric callback, not the Submit path the defect was on. The two halves of the fix are coupled in a non-obvious direction. With the guard in place but the nesting still there, the inner loading is the run the guard drops, so no account is created at all. Verified by restoring the nesting: this test then fails. Also names the real reason the guard cannot eat the biometric hand-off. The collector resumes inline inside trySend, so AuthScreen runs while the job is still active; what saves it is requestCipher suspending. --- .../auth/presentation/AuthViewModel.kt | 12 +++++-- .../auth/presentation/AuthViewModelTest.kt | 33 +++++++++++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt index 6cedc0632..8190551c8 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt @@ -166,8 +166,16 @@ internal class AuthViewModel( password: String, ) { if (authState.biometricsAvailable && authState.useBiometrics) { - // Handed to the prompt, so this run is done. AuthScreen starts a fresh one with the - // cipher once the user has answered it. + // Handed to the prompt. AuthScreen starts a fresh run with the cipher once the user has + // answered, and by then this one has finished, so the guard in loading does not eat it. + // + // That ordering is worth stating, because it is not obvious and it is not local. The + // collector observing this channel runs on Dispatchers.Main.immediate, so it resumes + // inline inside trySend and AuthScreen's handler begins running while this job is still + // active. What saves it is that requestCipher suspends until the user answers, and its + // one synchronous return is a failure that never reaches executeCreateAccess. A fast + // path added there that returned a cipher without suspending would be dropped by the + // guard, and the user would sit on the migrate screen with no account. biometricChannel.trySend(BiometricRequest.CreateAccess(password)) return } diff --git a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt index 34766dfda..985a99a85 100644 --- a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt +++ b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt @@ -184,6 +184,39 @@ class AuthViewModelTest { assertEquals("", mainPasswordRepository.hash) } + /** + * The migrate path used to start a second [AuthViewModel.loading] for account creation, so the + * first one wrote `loading = false` back as soon as it had launched, while key derivation was + * still running. The screen dropped its spinner and re-enabled Submit for the whole of it. + * + * Account creation now runs inside the caller's block. This is load bearing rather than + * cosmetic: the guard below only makes a second run a silent no-op, and with the guard in place + * but the nesting still there, the inner call would be the run that got dropped and no account + * would be created at all. + */ + @Test + fun `the migrate submit stays loading until the account exists`() = runTest(dispatcher) { + // Hex of a real bcrypt 2a hash of "password". The use case hex-decodes before verifying. + mainPasswordRepository.hash = "2432612431302471776e45776767315a6c5176435a58336450614a7a2e" + + "31494351504a334e6d4a64566b4251686577564655745363646665366d4847" + val vm = viewModel() + vm.onEvent(AuthUIEvent.ToggleUseBiometrics(checked = false)) + + val migrating = assertIs(vm.uiState.value) + migrating.passwordTextFieldState.setTextAndPlaceCursorAtEnd("password") + + vm.onEvent(AuthUIEvent.Submit) + runCurrent() + + // Key derivation is still running on a dispatcher the scheduler cannot see. The screen must + // not have handed control back yet. + assertEquals(true, assertIs(vm.uiState.value).loading) + + vm.navigationEvent.first() + + assertEquals(1, accountRepository.setCount) + } + /** * Key derivation takes long enough for a second tap to land inside it. `onEvent` gates on the * state being interactable rather than on the loading flag, so before the guard the only thing From c47b9bce0bca643d3a2f9e390848f27d86050e33 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sun, 16 Aug 2026 16:46:32 +0200 Subject: [PATCH 14/20] refactor: cleanup --- .../feature/auth/presentation/AuthContent.kt | 3 ++- .../auth/presentation/AuthViewModel.kt | 1 - .../auth/presentation/AuthViewModelTest.kt | 26 +------------------ 3 files changed, 3 insertions(+), 27 deletions(-) diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthContent.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthContent.kt index a5a501c65..7e52e36a8 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthContent.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthContent.kt @@ -31,6 +31,7 @@ import androidx.compose.material3.OutlinedSecureTextField import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -140,7 +141,7 @@ fun AuthContent( } }, dismissButton = { - FilledTonalButton(onClick = { onEvent(AuthUIEvent.ContinueAfterMigration) }) { + TextButton(onClick = { onEvent(AuthUIEvent.ContinueAfterMigration) }) { Text(text = stringResource(R.string.continue_anyway)) } }, diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt index 8190551c8..13b68f92d 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt @@ -95,7 +95,6 @@ internal class AuthViewModel( val navigationEvent = navigationEventChannel.receiveAsFlow() private var migrationJob: Job? = null - private var authJob: Job? = null fun onEvent(event: AuthUIEvent) { diff --git a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt index 985a99a85..d4ae512f8 100644 --- a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt +++ b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt @@ -2,7 +2,6 @@ package de.davis.keygo.feature.auth.presentation import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.lifecycle.SavedStateHandle -import de.davis.keygo.core.ui.model.UiFieldError import de.davis.keygo.core.identity.FakeAccountRepository import de.davis.keygo.core.identity.domain.usecase.CreateAccessUseCase import de.davis.keygo.core.identity.domain.usecase.UnlockWithPasswordUseCase @@ -10,6 +9,7 @@ import de.davis.keygo.core.item.FakeVaultContextRepository import de.davis.keygo.core.item.FakeVaultRepository import de.davis.keygo.core.security.crypto.FakeBiometricAvailabilityRepository import de.davis.keygo.core.security.crypto.FakeSession +import de.davis.keygo.core.ui.model.UiFieldError import de.davis.keygo.feature.auth.presentation.model.AuthState import de.davis.keygo.feature.auth.presentation.model.AuthUIEvent import de.davis.keygo.legacy_migration.FakeMainPasswordRepository @@ -184,16 +184,6 @@ class AuthViewModelTest { assertEquals("", mainPasswordRepository.hash) } - /** - * The migrate path used to start a second [AuthViewModel.loading] for account creation, so the - * first one wrote `loading = false` back as soon as it had launched, while key derivation was - * still running. The screen dropped its spinner and re-enabled Submit for the whole of it. - * - * Account creation now runs inside the caller's block. This is load bearing rather than - * cosmetic: the guard below only makes a second run a silent no-op, and with the guard in place - * but the nesting still there, the inner call would be the run that got dropped and no account - * would be created at all. - */ @Test fun `the migrate submit stays loading until the account exists`() = runTest(dispatcher) { // Hex of a real bcrypt 2a hash of "password". The use case hex-decodes before verifying. @@ -242,16 +232,6 @@ class AuthViewModelTest { assertEquals(1, accountRepository.setCount) } - /** - * The Migrating submit path reports a rejected password through the loading scope rather than - * writing to `uiState` itself. `loading` writes the scope's state back once the block returns, - * so a direct write lands first and is then overwritten, stopping the spinner and leaving the - * field with nothing on it. - * - * The only test that drives the real `Submit` event, so it is also the only one that needs a - * hash `ValidateMainPasswordUseCase` can actually decode. The other tests seed a placeholder - * and reach the ViewModel past validation. - */ @Test fun `a rejected v1 main password leaves an error on the field`() = runTest(dispatcher) { // Hex of a real bcrypt 2a hash of "password". The use case hex-decodes the stored hash @@ -272,10 +252,6 @@ class AuthViewModelTest { assertEquals(UiFieldError.Incorrect, after.passwordError) } - /** - * The descendant of the 97b15f3c regression test. There the v1 password was dropped before the - * account existed. Here it must not be dropped before the user's rows are across. - */ @Test fun `a failed import leaves the v1 password in place and offers a retry`() = runTest(dispatcher) { From a1d904c614eaa5be74cd4cac21b87e89880b8f3a Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 17 Aug 2026 12:42:00 +0200 Subject: [PATCH 15/20] fix(migration): keep the main-password DataStore a structural singleton --- .../keygo/legacy_migration/di/MainPasswordDataStoreModule.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/MainPasswordDataStoreModule.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/MainPasswordDataStoreModule.kt index 6b409e7b3..f892201be 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/MainPasswordDataStoreModule.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/MainPasswordDataStoreModule.kt @@ -14,7 +14,7 @@ import org.koin.core.annotation.Single internal const val MAIN_PASSWORD_DATA_STORE_NAME = "main-password.db" @Module -internal class MainPasswordDataStoreModule { +internal object MainPasswordDataStoreModule { private val Context.protoMainPasswordDataStore by dataStore( fileName = MAIN_PASSWORD_DATA_STORE_NAME, From ae5e01045f426309f8c1f2b7f417c52f972c86df Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 17 Aug 2026 12:43:22 +0200 Subject: [PATCH 16/20] fix(migration): keep the v1 marker while the legacy file has rows left The Migrated branch cleared the marker unconditionally, but the import returns Migrated with fileRetained = true whenever the prune failed or the recount came back non-zero. On that path the marker went and Completed(skippedItems = 0) sent the user straight into the app. hasMainPassword() is false from then on, so nothing ever deletes secure_element_database or the password_manager_skey alias again: the v1 database stays on disk, still decryptable by a key still in the Keystore, with no path that retries. Gate the clear on LegacyMigrationOutcome.nothingLeftToImport, which is what it was added for and until now was read nowhere. A retained file keeps the marker and the next unlock tries again. The trade this reverses is a prune that keeps failing over rows already in v2, which reimports them each unlock. A duplicate is visible and undoable; a v1 database nothing can reach is neither. Co-Authored-By: Claude Opus 5 --- .../domain/model/MigrationResult.kt | 10 ++++-- .../usecase/RunPendingMigrationUseCase.kt | 12 +++++-- .../usecase/RunPendingMigrationUseCaseTest.kt | 36 +++++++++++++++---- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/MigrationResult.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/MigrationResult.kt index 0bb789172..0292c2db1 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/MigrationResult.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/model/MigrationResult.kt @@ -12,9 +12,13 @@ sealed interface MigrationResult { data object NotPending : MigrationResult /** - * The import reached a verdict about the v1 file and the marker is gone. [skippedItems] counts - * rows that could not be read; those rows are still in the legacy file, which - * `MigrateLegacyDataUseCase` retains whenever anything failed. + * The import reached a verdict about the v1 file. [skippedItems] counts rows that could not be + * read; those rows are still in the legacy file, which `MigrateLegacyDataUseCase` retains + * whenever anything failed. + * + * Says nothing about the marker. It is gone only if the run left nothing behind, and a run that + * did leave something is still a run that reached a verdict: the caller has the same nothing to + * act on either way, and the retry belongs to the next unlock rather than to this screen. */ data class Completed(val skippedItems: Int) : MigrationResult diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt index 5e0d1cc0e..a55632f9c 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt @@ -10,7 +10,7 @@ import kotlin.coroutines.cancellation.CancellationException * * The v1 main password record is the marker for the whole migration, not just for the access half. * It is read first, so an install that never ran v1 never opens the legacy path at all, and it is - * cleared last, and only once the import has said something definite about the v1 file. Between + * cleared last, and only once the import has said a later run would find nothing left. Between * those two the user's credential is the only way back to a migration that did not finish, so * dropping it early would strand them with rows still on disk and nothing to act on. * @@ -54,7 +54,15 @@ class RunPendingMigrationUseCase internal constructor( } is LegacyMigrationOutcome.Migrated -> { - clearMainPassword() + // A retained file is a file with v1 rows still in it, and the marker is the only + // thing that brings anything back to them: clearing it here would leave + // secure_element_database on disk forever, decryptable by an alias no later run + // can reach to delete either. So it goes only once a later run would find nothing. + // + // The cost is a prune that keeps failing while the rows are already in v2, which + // reimports them on every unlock. That is a duplicate the user can see and undo; + // the alternative is a v1 database they cannot. + if (outcome.nothingLeftToImport) clearMainPassword() MigrationResult.Completed(skippedItems = outcome.report.failures.size) } } diff --git a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt index 9eb6622fd..83be7feed 100644 --- a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt @@ -62,6 +62,11 @@ class RunPendingMigrationUseCaseTest { assertEquals("", mainPasswordRepository.hash) } + /** + * The marker follows the file, not the row count. A report that names failures and still says + * the file is gone has nothing left to come back to, so counting the skipped rows for the + * screen and dropping the credential are not in conflict. + */ @Test fun `reports the rows that were skipped and still clears the marker`() = runTest { val result = useCase { migrated(failures = listOf(rowFailure(1), rowFailure(2))) }() @@ -71,17 +76,36 @@ class RunPendingMigrationUseCaseTest { } /** - * A file left behind by a failed prune or a failed delete, rather than by a failed row. The - * rows are already in v2, so a retry would duplicate them; the marker goes and the stale file - * is accepted. This is the one place the design retries less than the code it replaced, and it - * is pinned here so the trade is deliberate rather than discovered. + * A file left behind by a failed prune or a failed delete, rather than by a failed row. Nothing + * on screen distinguishes it - the run reached a verdict and the user is let through - but the + * marker has to survive it: it is the only thing that brings a later run back to a + * secure_element_database that is still on disk and still decryptable by an alias only that run + * can delete. Clearing it here made that file permanent. + * + * The trade is a prune that keeps failing over rows already in v2, which reimports them on the + * next unlock. A duplicate is visible and undoable; a retained v1 database is neither. */ @Test - fun `clears the marker even when the legacy file could not be deleted`() = runTest { + fun `keeps the marker when the legacy file could not be deleted`() = runTest { val result = useCase { migrated(fileRetained = true) }() assertEquals(MigrationResult.Completed(skippedItems = 0), result) - assertEquals("", mainPasswordRepository.hash) + assertEquals("a-v1-bcrypt-hash", mainPasswordRepository.hash) + } + + /** + * The realistic shape of a skipped row: `MigrateLegacyDataUseCase` never deletes a file it + * could not empty, so failures and a retained file arrive together. The rows it could not read + * are still there, and the marker is what lets a later run try them again. + */ + @Test + fun `keeps the marker when rows were skipped and the file stayed behind`() = runTest { + val result = useCase { + migrated(failures = listOf(rowFailure(1), rowFailure(2)), fileRetained = true) + }() + + assertEquals(MigrationResult.Completed(skippedItems = 2), result) + assertEquals("a-v1-bcrypt-hash", mainPasswordRepository.hash) } /** From f5547d915694a8388ed9c4b7c4eb442b583c85e3 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 17 Aug 2026 12:48:08 +0200 Subject: [PATCH 17/20] fix(auth): run the v1 import in an application scope, not viewModelScope Moving the import onto viewModelScope changed two things beyond where the job is held. It put the work on the main thread. viewModelScope is Dispatchers.Main.immediate and nothing below it switches: LegacyKeyRepositoryImpl reaches the Keystore, LegacyAesGcmCipher decrypts per row, converter.convert re-encrypts every field per item, and deleteDatabaseAndKey is not even suspending. On a vault of a few hundred items that is seconds of StrictMode-violating work on the auth screen, and an ANR at the top end. It also opened a cancellation window that did not exist before. The batch write commits before the prune runs, so a back press between the two - or a popped nav entry, or a destroyed activity - cancelled the run. Cancellation is rethrown all the way up on purpose, so the marker stayed set over rows already in v2 and the next unlock imported them again under fresh ids: two copies of every v1 item. On the old application-scoped job only process death could do that. RunPendingMigrationUseCase owns the run instead. It is already the @Single that sequences the migration, so the scope and the one-run-at-a-time state have somewhere to live without a class in between: it starts the run on an application-lived IO scope and hands out a join on it. The ViewModel keeps the states it grew - ImportingLegacyData, the summary, the failure - but what its scope cancels is the waiting, not the import. Callers that overlap share one run and one verdict, which is also how a screen destroyed mid-import and rebuilt still reports the summary for a run it did not start. A run that has already finished is not replayed, so Retry after a failure really does retry. Co-Authored-By: Claude Opus 5 --- .../auth/presentation/AuthViewModelTest.kt | 5 +- .../di/LegacyMigrationModule.kt | 20 ++++- .../di/annotation/MigrationScopeQualifier.kt | 6 ++ .../usecase/RunPendingMigrationUseCase.kt | 25 +++++- .../usecase/RunPendingMigrationUseCaseTest.kt | 80 ++++++++++++++++++- .../legacy_migration/MigrationTestUseCases.kt | 3 + 6 files changed, 131 insertions(+), 8 deletions(-) create mode 100644 legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/annotation/MigrationScopeQualifier.kt diff --git a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt index d4ae512f8..b00b5d74e 100644 --- a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt +++ b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt @@ -104,7 +104,7 @@ class AuthViewModelTest { */ private fun TestScope.viewModel( runPendingMigration: RunPendingMigrationUseCase = - runPendingMigrationUseCase(mainPasswordRepository), + runPendingMigrationUseCase(backgroundScope, mainPasswordRepository), ): AuthViewModel { val vm = AuthViewModel( savedStateHandle = SavedStateHandle(), @@ -258,6 +258,7 @@ class AuthViewModelTest { mainPasswordRepository.hash = "original-v1-hash" val vm = viewModel( runPendingMigration = runPendingMigrationUseCase( + scope = backgroundScope, repository = mainPasswordRepository, outcome = LegacyMigrationOutcome.Failed(IllegalStateException("unreadable")), ), @@ -279,6 +280,7 @@ class AuthViewModelTest { var underTest: AuthViewModel? = null val vm = viewModel( runPendingMigration = runPendingMigrationUseCase( + scope = backgroundScope, repository = mainPasswordRepository, outcome = LegacyMigrationOutcome.Failed(IllegalStateException("unreadable")), onImport = { @@ -311,6 +313,7 @@ class AuthViewModelTest { mainPasswordRepository.hash = "original-v1-hash" val vm = viewModel( runPendingMigration = runPendingMigrationUseCase( + scope = backgroundScope, repository = mainPasswordRepository, outcome = LegacyMigrationOutcome.Migrated( LegacyMigrationReport( diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt index 542c46ea2..2c39350d8 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt @@ -1,8 +1,13 @@ package de.davis.keygo.legacy_migration.di +import de.davis.keygo.legacy_migration.di.annotation.MigrationScopeQualifier +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Configuration import org.koin.core.annotation.Module +import org.koin.core.annotation.Single @Module( includes = [ @@ -12,4 +17,17 @@ import org.koin.core.annotation.Module ) @Configuration @ComponentScan("de.davis.keygo.legacy_migration") -object LegacyMigrationModule +object LegacyMigrationModule { + + /** + * The scope the v1 import runs in. + * + * Application-lived, so no screen's lifetime can cut a run short, and on [Dispatchers.IO], + * because opening the inherited database, reaching the Keystore per row and writing every item + * back is blocking work throughout. A SupervisorJob so a run that fails takes nothing else with + * it. + */ + @Single + @MigrationScopeQualifier + fun provideMigrationScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) +} diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/annotation/MigrationScopeQualifier.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/annotation/MigrationScopeQualifier.kt new file mode 100644 index 000000000..74e32b781 --- /dev/null +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/annotation/MigrationScopeQualifier.kt @@ -0,0 +1,6 @@ +package de.davis.keygo.legacy_migration.di.annotation + +import org.koin.core.annotation.Named + +@Named +internal annotation class MigrationScopeQualifier diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt index a55632f9c..3a8e03e0e 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt @@ -1,7 +1,13 @@ package de.davis.keygo.legacy_migration.domain.usecase import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome +import de.davis.keygo.legacy_migration.di.annotation.MigrationScopeQualifier import de.davis.keygo.legacy_migration.domain.model.MigrationResult +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.koin.core.annotation.Single import kotlin.coroutines.cancellation.CancellationException @@ -23,9 +29,22 @@ class RunPendingMigrationUseCase internal constructor( private val hasMainPassword: HasMainPasswordUseCase, private val importLegacyData: LegacyDataImporter, private val clearMainPassword: ClearMainPasswordUseCase, + + @param:MigrationScopeQualifier + private val scope: CoroutineScope, ) { - suspend operator fun invoke(): MigrationResult { + private val lock = Mutex() + private var inFlight: Deferred? = null + + suspend operator fun invoke(): MigrationResult = currentRun().await() + + private suspend fun currentRun(): Deferred = lock.withLock { + inFlight?.takeIf { it.isActive } + ?: scope.async { runPending() }.also { inFlight = it } + } + + private suspend fun runPending(): MigrationResult { if (!hasMainPassword()) return MigrationResult.NotPending val outcome = try { @@ -38,8 +57,8 @@ class RunPendingMigrationUseCase internal constructor( // Throwable and not Exception: MigrateLegacyDataUseCase catches Exception around its // whole run, which leaves everything that is not one uncaught, and a module reaching // Room, a native SQLite driver and the Keystore can raise a LinkageError or a - // NoClassDefFoundError on a device missing something it expected. This now runs inside - // viewModelScope, where that would end the process rather than the migration. + // NoClassDefFoundError on a device missing something it expected. Uncaught, that would + // surface as a crash at whoever joined the run rather than as a migration that failed. return MigrationResult.Incomplete(e) } diff --git a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt index 83be7feed..8b6a6c8b5 100644 --- a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt @@ -8,6 +8,12 @@ import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationReport import de.davis.keygo.legacy_migration.domain.model.LegacyRowFailure import de.davis.keygo.legacy_migration.domain.model.MigrationResult import de.davis.keygo.legacy_migration.hasMainPasswordUseCase +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.async +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlin.coroutines.cancellation.CancellationException import kotlin.test.Test @@ -22,13 +28,15 @@ class RunPendingMigrationUseCaseTest { private var importsRun = 0 - private fun useCase(importer: LegacyDataImporter) = RunPendingMigrationUseCase( + private fun TestScope.useCase(importer: LegacyDataImporter) = RunPendingMigrationUseCase( hasMainPassword = hasMainPasswordUseCase(mainPasswordRepository), importLegacyData = LegacyDataImporter { importsRun++ importer() }, clearMainPassword = clearMainPasswordUseCase(mainPasswordRepository), + // One the scheduler can see, so a run still starts outside the caller's coroutine. + scope = backgroundScope, ) private fun migrated(failures: List = emptyList(), fileRetained: Boolean = false) = @@ -140,8 +148,8 @@ class RunPendingMigrationUseCaseTest { /** * MigrateLegacyDataUseCase catches Exception, which leaves everything that is not one uncaught. * A module reaching Room, a native SQLite driver and the Keystore can raise a LinkageError on a - * device missing something it expected, and this now runs in viewModelScope where that would - * take the process down rather than being logged. + * device missing something it expected, and uncaught it would surface as a crash at whoever + * joined the run rather than as a migration that failed. */ @Test fun `contains a throwable that is not an exception`() = runTest { @@ -160,4 +168,70 @@ class RunPendingMigrationUseCaseTest { assertEquals("a-v1-bcrypt-hash", mainPasswordRepository.hash) } + + /** + * The reason the run does not live in the caller's scope. MigrateLegacyDataUseCase commits the + * batch write before it prunes the rows it just wrote, and cancellation is rethrown all the way + * up on purpose, so a run cut short between the two leaves the marker set over rows already in + * v2. The next unlock imports them again under fresh ids: two copies of every v1 item, from a + * back press. + */ + @Test + fun `cancelling a joiner leaves the run to finish`() = runTest { + val started = CompletableDeferred() + val release = CompletableDeferred() + var reachedTheEnd = false + + val subject = useCase { + started.complete(Unit) + release.await() + reachedTheEnd = true + LegacyMigrationOutcome.NothingToMigrate + } + + val joiner = launch { subject() } + started.await() + joiner.cancelAndJoin() + + release.complete(Unit) + runCurrent() + + assertTrue(reachedTheEnd) + // The verdict still landed: the marker is gone and nothing is left for a retry to duplicate. + assertEquals("", mainPasswordRepository.hash) + } + + /** + * Two concurrent imports would read the same v1 rows and write them both. This is also how a + * ViewModel destroyed mid-import and rebuilt still reports the summary for a run it did not + * start: it joins the one already going rather than opening a second. + */ + @Test + fun `callers arriving together share one run and one verdict`() = runTest { + val release = CompletableDeferred() + val subject = useCase { + release.await() + migrated(failures = listOf(rowFailure(1)), fileRetained = true) + } + + val first = async { subject() } + val second = async { subject() } + runCurrent() + release.complete(Unit) + + assertEquals(MigrationResult.Completed(skippedItems = 1), first.await()) + assertEquals(MigrationResult.Completed(skippedItems = 1), second.await()) + assertEquals(1, importsRun) + } + + /** A run that has ended is not a run in flight, or Retry would hand back the failure again. */ + @Test + fun `a caller after a finished run starts a new one`() = runTest { + val subject = useCase { LegacyMigrationOutcome.Failed(IllegalStateException("unreadable")) } + + subject() + subject() + + assertEquals(2, importsRun) + } } diff --git a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt index f422c04a6..727b76fd2 100644 --- a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/MigrationTestUseCases.kt @@ -7,6 +7,7 @@ import de.davis.keygo.legacy_migration.domain.usecase.HasMainPasswordUseCase import de.davis.keygo.legacy_migration.domain.usecase.LegacyDataImporter import de.davis.keygo.legacy_migration.domain.usecase.RunPendingMigrationUseCase import de.davis.keygo.legacy_migration.domain.usecase.ValidateMainPasswordUseCase +import kotlinx.coroutines.CoroutineScope internal fun clearMainPasswordUseCase(repository: FakeMainPasswordRepository): ClearMainPasswordUseCase = ClearMainPasswordUseCase(repository.asMainPasswordRepository()) @@ -24,6 +25,7 @@ fun validateMainPasswordUseCase(repository: FakeMainPasswordRepository): Validat * need to see that a retry actually retried. */ fun runPendingMigrationUseCase( + scope: CoroutineScope, repository: FakeMainPasswordRepository, outcome: LegacyMigrationOutcome = LegacyMigrationOutcome.NothingToMigrate, onImport: () -> Unit = {}, @@ -34,4 +36,5 @@ fun runPendingMigrationUseCase( outcome }, clearMainPassword = clearMainPasswordUseCase(repository), + scope = scope, ) From e8b4652c99d1bd17b5bc718c2275b18b43aad0cd Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 17 Aug 2026 12:57:52 +0200 Subject: [PATCH 18/20] fix(auth): stop the end of a loading run from clobbering a newer state The update that ends a loading run writes back a snapshot taken before block() suspended, but lost the `if (it !is AuthState.Interactable) return@update it` guard that the update starting the run still has. onSessionEstablished is reachable from outside loading: AuthScreen calls it straight from the BiometricRequest.Login success handler, and nothing gates it on authJob. So the user submits the password form, the biometric prompt they had already triggered succeeds, the import starts and puts the screen in ImportingLegacyData, and this line then drops the stale Login snapshot back over it. The import is left running invisibly behind a live login form that can be submitted again. Guard it the same way, and pin it: the test fails against the unguarded write. FakeAccountRepository grows a held-open account read because it is the only place a test can stand inside an unlock - everything after it hops to Dispatchers.Default, which the test scheduler cannot see. Co-Authored-By: Claude Opus 5 --- .../core/identity/FakeAccountRepository.kt | 18 ++++++- .../auth/presentation/AuthViewModel.kt | 5 +- .../auth/presentation/AuthViewModelTest.kt | 52 +++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/core/identity/src/testFixtures/kotlin/de/davis/keygo/core/identity/FakeAccountRepository.kt b/core/identity/src/testFixtures/kotlin/de/davis/keygo/core/identity/FakeAccountRepository.kt index f5f246ba1..211beee49 100644 --- a/core/identity/src/testFixtures/kotlin/de/davis/keygo/core/identity/FakeAccountRepository.kt +++ b/core/identity/src/testFixtures/kotlin/de/davis/keygo/core/identity/FakeAccountRepository.kt @@ -3,6 +3,7 @@ package de.davis.keygo.core.identity import de.davis.keygo.core.identity.domain.model.Account import de.davis.keygo.core.identity.domain.repository.AccountRepository import de.davis.keygo.core.util.Result +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow @@ -14,6 +15,16 @@ class FakeAccountRepository : AccountRepository { var setFails: Boolean = false + /** + * When set, [getOrNull] suspends on it and answers with whatever it is completed to. + * + * The only place a test can stand inside an unlock. Reading the account is the first thing + * `UnlockWithPasswordUseCase` does and everything after it hops to `Dispatchers.Default`, which + * the test scheduler cannot see, so this is the one suspension a test can both hold open and + * release on demand. + */ + var pendingRead: CompletableDeferred? = null + /** * How many accounts were actually persisted. A caller that mints a second account overwrites * the first here exactly as the real registry does, so the stored value alone cannot tell the @@ -26,7 +37,12 @@ class FakeAccountRepository : AccountRepository { this.account.update { account } } - override suspend fun getOrNull(): Account? = account.value + // Not an elvis over await(): completing the read with null is the point of it, and an elvis + // would quietly hand back the seeded account instead. + override suspend fun getOrNull(): Account? = when (val pending = pendingRead) { + null -> account.value + else -> pending.await() + } override fun observe(): Flow = account.asStateFlow() diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt index 13b68f92d..0bff9adc9 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt @@ -216,7 +216,10 @@ internal class AuthViewModel( ) scope.block() - _uiState.update { scope.updatedState.copyDefaultState(loading = false) } + _uiState.update { + if (it !is AuthState.Interactable) return@update it + scope.updatedState.copyDefaultState(loading = false) + } if (sessionEstablished) onSessionEstablished() } diff --git a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt index b00b5d74e..08b397043 100644 --- a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt +++ b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt @@ -3,6 +3,7 @@ package de.davis.keygo.feature.auth.presentation import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.lifecycle.SavedStateHandle import de.davis.keygo.core.identity.FakeAccountRepository +import de.davis.keygo.core.identity.domain.model.Account import de.davis.keygo.core.identity.domain.usecase.CreateAccessUseCase import de.davis.keygo.core.identity.domain.usecase.UnlockWithPasswordUseCase import de.davis.keygo.core.item.FakeVaultContextRepository @@ -24,6 +25,7 @@ import de.davis.keygo.legacy_migration.validateMainPasswordUseCase import de.davis.keygo.rust.FakeAccountManager import de.davis.keygo.rust.FakeKeyDeriver import de.davis.keygo.rust.FakeKeyWrapper +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first @@ -308,6 +310,56 @@ class AuthViewModelTest { assertEquals("original-v1-hash", mainPasswordRepository.hash) } + /** + * The write that ends a loading run puts back a snapshot taken before `block()` suspended, so + * it has to be guarded the same way the one that starts the run is. + * + * `onSessionEstablished` is reachable from outside `loading`: AuthScreen calls it straight from + * the BiometricRequest.Login success handler and nothing gates it on `authJob`. So the user + * submits the password form, the biometric prompt they already triggered comes back, the import + * starts, and an unguarded write here would drop the live login form back on top of it - a form + * they can submit again while the import runs behind it. + */ + @Test + fun `a state set while the auth run was suspended is not written over`() = runTest(dispatcher) { + // An account plus a marker still on disk: what the user is left with the moment they tap + // Continue on MigrationFailed. Every unlock after that is a login form over a migration + // that is still pending. + val first = viewModel() + first.executeCreateAccess(password = "correct-password") + first.navigationEvent.first() + mainPasswordRepository.hash = "original-v1-hash" + + val vm = viewModel( + runPendingMigration = runPendingMigrationUseCase( + scope = backgroundScope, + repository = mainPasswordRepository, + outcome = LegacyMigrationOutcome.Failed(IllegalStateException("unreadable")), + ), + ) + val login = assertIs(vm.uiState.value) + login.passwordTextFieldState.setTextAndPlaceCursorAtEnd("correct-password") + + // Holds the unlock at its account read, which is the last point before it hops to a + // dispatcher the scheduler cannot see. + val read = CompletableDeferred() + accountRepository.pendingRead = read + vm.onEvent(AuthUIEvent.Submit) + runCurrent() + assertEquals(true, assertIs(vm.uiState.value).loading) + + vm.onSessionEstablished() + runCurrent() + assertIs(vm.uiState.value) + + // Answering with no account fails the unlock where it stands, so the only thing left to + // happen is loading writing its snapshot back. + read.complete(null) + runCurrent() + + assertIs(vm.uiState.value) + } + @Test fun `an import that skipped rows reports them before navigating`() = runTest(dispatcher) { mainPasswordRepository.hash = "original-v1-hash" From 554dddeebad4bf4b9ab4c5b5541d22a56a80b76a Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Mon, 17 Aug 2026 13:37:55 +0200 Subject: [PATCH 19/20] test(auth): drop returnDefaultValues by surfacing the import failure One Log.e call in AuthViewModel had turned returnDefaultValues on for every unit test in the module. The cost is module-wide and open-ended: any future test here that touches an unmocked android.jar method - TextUtils.isEmpty, Uri.parse, Base64 - silently gets 0/null/false and passes against behaviour that would crash on device, instead of failing loudly. MigrationResult.Incomplete already carries the cause, so AuthState.MigrationFailed carries it too and AuthScreen writes it out. The record survives, the branch is now assertable on state rather than on a logger, and the flag goes. Co-Authored-By: Claude Opus 5 --- feature/auth/build.gradle.kts | 8 +------- .../feature/auth/presentation/AuthViewModel.kt | 7 +------ .../auth/presentation/AuthViewModelTest.kt | 5 +++++ legacy-migration/build.gradle.kts | 1 + .../domain/usecase/RunPendingMigrationUseCase.kt | 16 +++++++++++++--- .../usecase/RunPendingMigrationUseCaseTest.kt | 11 ++++++++++- 6 files changed, 31 insertions(+), 17 deletions(-) diff --git a/feature/auth/build.gradle.kts b/feature/auth/build.gradle.kts index b46e00ffb..ab3dcdedb 100644 --- a/feature/auth/build.gradle.kts +++ b/feature/auth/build.gradle.kts @@ -5,13 +5,6 @@ plugins { android { namespace = "de.davis.keygo.feature.auth" - - // AuthViewModel.onSessionEstablished logs a failed v1 import through android.util.Log, which is - // the only record of why it failed. Unit tests cover that branch, and an unmocked android.jar - // throws rather than returning, so the log call would fail the test instead of the assertion. - testOptions { - unitTests.isReturnDefaultValues = true - } } dependencies { @@ -23,6 +16,7 @@ dependencies { implementation(libs.androidx.navigation.compose) testImplementation(projects.rust) + testImplementation(libs.robolectric) testImplementation(testFixtures(projects.core.identity)) testImplementation(testFixtures(projects.core.item)) testImplementation(testFixtures(projects.core.security)) diff --git a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt index 0bff9adc9..3b7a3e981 100644 --- a/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt +++ b/feature/auth/src/main/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModel.kt @@ -1,6 +1,5 @@ package de.davis.keygo.feature.auth.presentation -import android.util.Log import androidx.compose.foundation.text.input.TextFieldState import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel @@ -253,10 +252,8 @@ internal class AuthViewModel( if (result.skippedItems == 0) navigationEventChannel.trySend(Unit) else _uiState.update { AuthState.MigrationSummary(result.skippedItems) } - is MigrationResult.Incomplete -> { - Log.e(TAG, "v1 import did not finish", result.cause) + is MigrationResult.Incomplete -> _uiState.update { AuthState.MigrationFailed } - } } } } @@ -274,8 +271,6 @@ internal class AuthViewModel( } } -private const val TAG = "AuthViewModel" - private class LoadingScope( state: State, private val onSuccess: () -> Unit, diff --git a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt index 08b397043..f719e21d5 100644 --- a/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt +++ b/feature/auth/src/test/kotlin/de/davis/keygo/feature/auth/presentation/AuthViewModelTest.kt @@ -35,6 +35,9 @@ import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config import javax.crypto.Cipher import javax.crypto.KeyGenerator import kotlin.test.AfterTest @@ -53,6 +56,8 @@ import kotlin.test.assertIs * to retry. Clearing the marker now belongs to [RunPendingMigrationUseCase], which runs only after * a session exists and only once the import has said something definite about the v1 file. */ +@RunWith(RobolectricTestRunner::class) // Robolectric for android.util.Log alone. +@Config(sdk = [34]) @OptIn(ExperimentalCoroutinesApi::class) class AuthViewModelTest { diff --git a/legacy-migration/build.gradle.kts b/legacy-migration/build.gradle.kts index e0266dd55..e79b91cd6 100644 --- a/legacy-migration/build.gradle.kts +++ b/legacy-migration/build.gradle.kts @@ -28,6 +28,7 @@ dependencies { implementation(projects.core.util) testImplementation(libs.io.mockk) + testImplementation(libs.robolectric) testImplementation(libs.androidx.room3.testing) testImplementation(libs.androidx.sqlite.bundled) testImplementation(testFixtures(projects.core.item)) diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt index 3a8e03e0e..194d52134 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCase.kt @@ -1,7 +1,8 @@ package de.davis.keygo.legacy_migration.domain.usecase -import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome +import android.util.Log import de.davis.keygo.legacy_migration.di.annotation.MigrationScopeQualifier +import de.davis.keygo.legacy_migration.domain.model.LegacyMigrationOutcome import de.davis.keygo.legacy_migration.domain.model.MigrationResult import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred @@ -59,11 +60,11 @@ class RunPendingMigrationUseCase internal constructor( // Room, a native SQLite driver and the Keystore can raise a LinkageError or a // NoClassDefFoundError on a device missing something it expected. Uncaught, that would // surface as a crash at whoever joined the run rather than as a migration that failed. - return MigrationResult.Incomplete(e) + return incomplete(e) } return when (outcome) { - is LegacyMigrationOutcome.Failed -> MigrationResult.Incomplete(outcome.cause) + is LegacyMigrationOutcome.Failed -> incomplete(outcome.cause) // Exhaustive rather than an else, so an outcome added later cannot default into the // branch that drops the user's v1 credential. @@ -86,4 +87,13 @@ class RunPendingMigrationUseCase internal constructor( } } } + + private fun incomplete(cause: Throwable): MigrationResult.Incomplete { + Log.e(TAG, "v1 import did not finish", cause) + return MigrationResult.Incomplete(cause) + } + + private companion object { + const val TAG = "RunPendingMigration" + } } diff --git a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt index 8b6a6c8b5..59550176c 100644 --- a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/domain/usecase/RunPendingMigrationUseCaseTest.kt @@ -15,6 +15,9 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config import kotlin.coroutines.cancellation.CancellationException import kotlin.test.Test import kotlin.test.assertEquals @@ -22,6 +25,9 @@ import kotlin.test.assertFailsWith import kotlin.test.assertIs import kotlin.test.assertTrue +// Robolectric for android.util.Log alone +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) class RunPendingMigrationUseCaseTest { private val mainPasswordRepository = FakeMainPasswordRepository(hash = "a-v1-bcrypt-hash") @@ -39,7 +45,10 @@ class RunPendingMigrationUseCaseTest { scope = backgroundScope, ) - private fun migrated(failures: List = emptyList(), fileRetained: Boolean = false) = + private fun migrated( + failures: List = emptyList(), + fileRetained: Boolean = false + ) = LegacyMigrationOutcome.Migrated( LegacyMigrationReport( migratedItems = 3, From 489573fe4f0e096bd84ec2a3778f6c3ddb4e690c Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Tue, 18 Aug 2026 22:54:49 +0200 Subject: [PATCH 20/20] refactor(migration): switch dispatchers where the blocking is, not above it The import's scope carried Dispatchers.IO, which made every suspend function under it main-safe by accident rather than by contract. A caller that did not know to pick that scope - the autofill service, a future worker, a test - got whatever thread it came in on, and the module's own signatures gave it no reason to expect otherwise. Push the switch down to the calls that actually block: - LegacyKeyRepository.secretKey/deleteLegacyKey become suspend on IO. Loading the Keystore reaches the keymaster HAL. - LegacyItemRepository.deleteDatabase becomes suspend on IO, and withDao runs there too. Room moves its own suspending queries, but databaseProvider.get() stats the path and builds the database, and the first query through it runs v1's 1/2-to-3 recreate. The provider is @Synchronized and cannot itself be suspend, so the repository is as low as this goes. - readAll's row loop runs on Default. An AES-GCM open and a JSON parse per row is processor work, and it is the part that scales with the old vault. The re-encryption was already main-safe through CryptographicScopeImpl, so it needed nothing. provideMigrationScope now names no dispatcher and carries only the SupervisorJob and the application lifetime, which is the part no withContext can substitute for. Pinning a dispatcher there as well would only hide the next call that forgets to switch. Behaviour is unchanged. Pinned by a test that fails if any of the repository's switches is dropped, which nothing else would catch. Co-Authored-By: Claude Opus 5 --- .../data/crypto/LegacyAesGcmCipher.kt | 20 ++++++----- .../repository/LegacyItemRepositoryImpl.kt | 12 ++++--- .../repository/LegacyKeyRepositoryImpl.kt | 16 +++++---- .../di/LegacyMigrationModule.kt | 16 +++++---- .../domain/crypto/LegacyCipher.kt | 2 +- .../domain/repository/LegacyItemRepository.kt | 3 +- .../domain/repository/LegacyKeyRepository.kt | 4 +-- .../usecase/MigrateLegacyDataUseCase.kt | 2 +- .../data/crypto/LegacyAesGcmCipherTest.kt | 13 +++---- .../LegacyItemRepositoryImplTest.kt | 36 +++++++++++++++++++ .../legacy_migration/data/FakeLegacyCipher.kt | 2 +- .../data/FakeLegacyDatabaseProvider.kt | 14 +++++++- .../data/FakeLegacyItemRepository.kt | 2 +- .../data/FakeLegacyKeyRepository.kt | 4 +-- 14 files changed, 104 insertions(+), 42 deletions(-) diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipher.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipher.kt index dfbab3a7a..023abc98d 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipher.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipher.kt @@ -1,6 +1,8 @@ package de.davis.keygo.legacy_migration.data.crypto import de.davis.keygo.legacy_migration.domain.crypto.LegacyCipher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.koin.core.annotation.Single import javax.crypto.Cipher import javax.crypto.SecretKey @@ -13,16 +15,18 @@ import javax.crypto.spec.GCMParameterSpec @Single internal class LegacyAesGcmCipher : LegacyCipher { - override fun decrypt(blob: ByteArray, key: SecretKey): ByteArray? { + override suspend fun decrypt(blob: ByteArray, key: SecretKey): ByteArray? { if (blob.size <= IV_SIZE) return null - return runCatching { - Cipher.getInstance(TRANSFORMATION) - .apply { - init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(TAG_BITS, blob, 0, IV_SIZE)) - } - .doFinal(blob, IV_SIZE, blob.size - IV_SIZE) - }.getOrNull() + return withContext(Dispatchers.Default) { + runCatching { + Cipher.getInstance(TRANSFORMATION) + .apply { + init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(TAG_BITS, blob, 0, IV_SIZE)) + } + .doFinal(blob, IV_SIZE, blob.size - IV_SIZE) + }.getOrNull() + } } private companion object { diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImpl.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImpl.kt index b023aa614..5e1908c58 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImpl.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImpl.kt @@ -16,6 +16,8 @@ import de.davis.keygo.legacy_migration.domain.model.LegacyRowFailure import de.davis.keygo.legacy_migration.domain.repository.LegacyItemRepository import de.davis.keygo.legacy_migration.domain.repository.LegacyKeyRepository import de.davis.keygo.legacy_migration.domain.repository.LegacyReadResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.koin.core.annotation.Single import kotlin.coroutines.cancellation.CancellationException @@ -76,7 +78,9 @@ internal class LegacyItemRepositoryImpl( override suspend fun remainingCount(): Result = withDao { it.count() } - override fun deleteDatabase(): Boolean = databaseProvider.delete() + override suspend fun deleteDatabase(): Boolean = withContext(Dispatchers.IO) { + databaseProvider.delete() + } /** * Runs [block] against the legacy DAO, turning the ways the file can refuse to be read into a @@ -89,11 +93,11 @@ internal class LegacyItemRepositoryImpl( */ private suspend fun withDao( block: suspend (LegacyElementDao) -> T, - ): Result { + ): Result = withContext(Dispatchers.IO) { val dao = databaseProvider.get()?.legacyElementDao() - ?: return Result.Failure(LegacyReadFailure.DatabaseEmpty) + ?: return@withContext Result.Failure(LegacyReadFailure.DatabaseEmpty) - return try { + try { Result.Success(block(dao)) } catch (e: CancellationException) { // Not swallowed into a failure the way the other repositories do it. There a swallowed diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyKeyRepositoryImpl.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyKeyRepositoryImpl.kt index 7c8b63f06..ccf9050c8 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyKeyRepositoryImpl.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyKeyRepositoryImpl.kt @@ -1,6 +1,8 @@ package de.davis.keygo.legacy_migration.data.repository import de.davis.keygo.legacy_migration.domain.repository.LegacyKeyRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import org.koin.core.annotation.Single import java.security.KeyStore import javax.crypto.SecretKey @@ -13,10 +15,10 @@ internal const val LEGACY_KEY_ALIAS = "password_manager_skey" @Single internal class LegacyKeyRepositoryImpl : LegacyKeyRepository { - override fun secretKey(): SecretKey? = + override suspend fun secretKey(): SecretKey? = withAlias { getKey(LEGACY_KEY_ALIAS, null) as? SecretKey } - override fun deleteLegacyKey() { + override suspend fun deleteLegacyKey() { withAlias { deleteEntry(LEGACY_KEY_ALIAS) } } @@ -25,10 +27,12 @@ internal class LegacyKeyRepositoryImpl : LegacyKeyRepository { * A Keystore that will not load, or an alias that is already gone, is the same answer to both * callers: there is no legacy key. */ - private fun withAlias(block: KeyStore.() -> T): T? = runCatching { - val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) } - if (keyStore.containsAlias(LEGACY_KEY_ALIAS)) keyStore.block() else null - }.getOrNull() + private suspend fun withAlias(block: KeyStore.() -> T): T? = withContext(Dispatchers.IO) { + runCatching { + val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE).apply { load(null) } + if (keyStore.containsAlias(LEGACY_KEY_ALIAS)) keyStore.block() else null + }.getOrNull() + } private companion object { const val ANDROID_KEY_STORE = "AndroidKeyStore" diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt index 2c39350d8..9a9aaaaf7 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/di/LegacyMigrationModule.kt @@ -2,7 +2,6 @@ package de.davis.keygo.legacy_migration.di import de.davis.keygo.legacy_migration.di.annotation.MigrationScopeQualifier import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import org.koin.core.annotation.ComponentScan import org.koin.core.annotation.Configuration @@ -20,14 +19,17 @@ import org.koin.core.annotation.Single object LegacyMigrationModule { /** - * The scope the v1 import runs in. + * The scope the v1 import runs in. Application-lived, so no screen's lifetime can cut a run + * short, and a SupervisorJob so a run that fails takes nothing else with it. * - * Application-lived, so no screen's lifetime can cut a run short, and on [Dispatchers.IO], - * because opening the inherited database, reaching the Keystore per row and writing every item - * back is blocking work throughout. A SupervisorJob so a run that fails takes nothing else with - * it. + * Deliberately names no dispatcher. Every blocking call under the import switches for itself - + * `LegacyKeyRepository` for the Keystore, `LegacyItemRepositoryImpl.withDao` for the file, the + * row loop for the decrypt and parse, `CryptographicScopeImpl` for the re-encryption - so this + * scope has nothing left to correct. Pinning one here would only hide the next call that + * forgets to, and there would be no single place left that answers what thread a given + * operation runs on. */ @Single @MigrationScopeQualifier - fun provideMigrationScope(): CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) + fun provideMigrationScope(): CoroutineScope = CoroutineScope(SupervisorJob()) } diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/crypto/LegacyCipher.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/crypto/LegacyCipher.kt index a379bdd41..1aab3e5cb 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/crypto/LegacyCipher.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/crypto/LegacyCipher.kt @@ -11,5 +11,5 @@ internal fun interface LegacyCipher { * v1 alias, and a run resolves it once. Looking it up per blob costs a binder round trip to the * Keystore for every row, twice over, to arrive back at the same key. */ - fun decrypt(blob: ByteArray, key: SecretKey): ByteArray? + suspend fun decrypt(blob: ByteArray, key: SecretKey): ByteArray? } diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyItemRepository.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyItemRepository.kt index 506fa67d7..58dac4ffe 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyItemRepository.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyItemRepository.kt @@ -40,6 +40,5 @@ internal interface LegacyItemRepository { suspend fun remainingCount(): Result - /** Closes the database and deletes the file. */ - fun deleteDatabase(): Boolean + suspend fun deleteDatabase(): Boolean } diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyKeyRepository.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyKeyRepository.kt index a93fc6e90..c74782fa2 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyKeyRepository.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/repository/LegacyKeyRepository.kt @@ -6,11 +6,11 @@ import javax.crypto.SecretKey internal interface LegacyKeyRepository { /** Returns null when the alias is gone, which no blob in the file can survive. */ - fun secretKey(): SecretKey? + suspend fun secretKey(): SecretKey? /** * Removes v1's alias. Only ever called once the file it protected has provably gone, because a * key removed while encrypted rows are still on disk makes them unreadable for good. */ - fun deleteLegacyKey() + suspend fun deleteLegacyKey() } diff --git a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCase.kt b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCase.kt index cc676c948..1e5e9508d 100644 --- a/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCase.kt +++ b/legacy-migration/src/main/kotlin/de/davis/keygo/legacy_migration/domain/usecase/MigrateLegacyDataUseCase.kt @@ -181,7 +181,7 @@ internal class MigrateLegacyDataUseCase internal constructor( * A clean install reaches this too, by way of a provider with no file to hand out. There is * nothing to delete, [LegacyItemRepository.deleteDatabase] says so, and the alias stays. */ - private fun deleteDatabaseAndKey(): Boolean { + private suspend fun deleteDatabaseAndKey(): Boolean { if (!legacyItemRepository.deleteDatabase()) return false legacyKeyRepository.deleteLegacyKey() return true diff --git a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipherTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipherTest.kt index 91ef17159..ba6847414 100644 --- a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipherTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/crypto/LegacyAesGcmCipherTest.kt @@ -1,6 +1,7 @@ package de.davis.keygo.legacy_migration.data.crypto import de.davis.keygo.legacy_migration.data.encryptLikeV1 +import kotlinx.coroutines.test.runTest import java.security.SecureRandom import javax.crypto.Cipher import javax.crypto.KeyGenerator @@ -21,7 +22,7 @@ class LegacyAesGcmCipherTest { private val key: SecretKey = newKey() @Test - fun `decrypts a blob written by v1`() { + fun `decrypts a blob written by v1`() = runTest { val plaintext = "{\"type\":1,\"username\":\"ada\"}".encodeToByteArray() val decrypted = cipher.decrypt(encryptLikeV1(plaintext, key), key) @@ -30,7 +31,7 @@ class LegacyAesGcmCipherTest { } @Test - fun `decrypts an empty plaintext`() { + fun `decrypts an empty plaintext`() = runTest { assertContentEquals( byteArrayOf(), cipher.decrypt(encryptLikeV1(byteArrayOf(), key), key), @@ -38,19 +39,19 @@ class LegacyAesGcmCipherTest { } @Test - fun `returns null for a blob encrypted under a different key`() { + fun `returns null for a blob encrypted under a different key`() = runTest { val otherKey = newKey() assertNull(cipher.decrypt(encryptLikeV1(byteArrayOf(1, 2, 3), otherKey), key)) } @Test - fun `returns null for a blob shorter than the iv`() { + fun `returns null for a blob shorter than the iv`() = runTest { assertNull(cipher.decrypt(byteArrayOf(1, 2, 3), key)) } @Test - fun `returns null for a tampered blob`() { + fun `returns null for a tampered blob`() = runTest { val blob = encryptLikeV1("secret".encodeToByteArray(), key) blob[blob.size - 1] = (blob[blob.size - 1] + 1).toByte() @@ -58,7 +59,7 @@ class LegacyAesGcmCipherTest { } @Test - fun `uses a twelve byte iv prefix`() { + fun `uses a twelve byte iv prefix`() = runTest { val blob = encryptLikeV1("x".encodeToByteArray(), key) val manual = Cipher.getInstance("AES/GCM/NoPadding").apply { init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, blob, 0, 12)) diff --git a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImplTest.kt b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImplTest.kt index 36c88ee52..0df9d276d 100644 --- a/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImplTest.kt +++ b/legacy-migration/src/test/kotlin/de/davis/keygo/legacy_migration/data/repository/LegacyItemRepositoryImplTest.kt @@ -35,6 +35,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertIs +import kotlin.test.assertNotEquals import kotlin.test.assertNull import kotlin.test.assertTrue @@ -346,4 +347,39 @@ class LegacyItemRepositoryImplTest { assertTrue(repository.deleteDatabase(), "the file was there and had to go") assertTrue(databaseProvider.closed, "the file cannot be deleted from under an open handle") } + + /** + * Main-safety, pinned rather than argued. + * + * The scope the import runs in names no dispatcher on purpose, so each blocking call has to + * move off the caller's thread itself. Nothing else would catch a `withContext` dropped from + * one of them: the work still happens, the results are still right, and every other test in + * this file still passes - it just happens on whatever thread called in, which for the auth + * screen is the main one. + * + * Covers this repository only. `LegacyKeyRepositoryImpl` switches for the same reason, but a + * JVM test cannot reach the Keystore, and asserting it through the fake would only pin the + * fake's own threading. + */ + @Test + fun `the legacy file is opened and deleted off the calling thread`() = runTest { + insert(title = "Example", json = """{"type":1,"username":"ada"}""") + val caller = Thread.currentThread() + + repository.readAll().assertSuccess() + + assertNotEquals( + caller, + databaseProvider.lastAccessThread, + "the legacy database was opened on the caller's thread", + ) + + repository.deleteDatabase() + + assertNotEquals( + caller, + databaseProvider.lastAccessThread, + "the legacy file was deleted on the caller's thread", + ) + } } diff --git a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyCipher.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyCipher.kt index 3b1a157b7..54ec4a0aa 100644 --- a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyCipher.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyCipher.kt @@ -15,7 +15,7 @@ import javax.crypto.SecretKey */ internal class FakeLegacyCipher(private val failFor: ByteArray? = null) : LegacyCipher { - override fun decrypt(blob: ByteArray, key: SecretKey): ByteArray? = when { + override suspend fun decrypt(blob: ByteArray, key: SecretKey): ByteArray? = when { failFor != null && blob.contentEquals(failFor) -> null blob.decodeToString().startsWith(FAIL) -> null else -> blob diff --git a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyDatabaseProvider.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyDatabaseProvider.kt index 3a1f34c61..5e078b563 100644 --- a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyDatabaseProvider.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyDatabaseProvider.kt @@ -16,9 +16,20 @@ internal class FakeLegacyDatabaseProvider( var closed: Boolean = false private set + /** + * The thread the last [get] or [delete] ran on. Production's provider opens and unlinks the + * file on whichever thread calls it, so this is what a test asserts against to pin that the + * repository moved off the caller's. + */ + var lastAccessThread: Thread? = null + private set + private var fileDeleted: Boolean = false - override fun get(): LegacyDatabase? = database.takeUnless { fileDeleted } + override fun get(): LegacyDatabase? { + lastAccessThread = Thread.currentThread() + return database.takeUnless { fileDeleted } + } override fun close() { closed = true @@ -31,6 +42,7 @@ internal class FakeLegacyDatabaseProvider( * alias is gated on this answer. */ override fun delete(): Boolean { + lastAccessThread = Thread.currentThread() close() fileDeleted = true return SUFFIXES.count { File(file.absolutePath + it).delete() } > 0 diff --git a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyItemRepository.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyItemRepository.kt index fc3995dbe..3f7665590 100644 --- a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyItemRepository.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyItemRepository.kt @@ -41,7 +41,7 @@ internal class FakeLegacyItemRepository : LegacyItemRepository { override suspend fun remainingCount(): Result = countResult ?: Result.Success(rowsInFile()) - override fun deleteDatabase(): Boolean { + override suspend fun deleteDatabase(): Boolean { databaseDeleted = deleteSucceeds return deleteSucceeds } diff --git a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyKeyRepository.kt b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyKeyRepository.kt index d447ba931..7b944a263 100644 --- a/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyKeyRepository.kt +++ b/legacy-migration/src/testFixtures/kotlin/de/davis/keygo/legacy_migration/data/FakeLegacyKeyRepository.kt @@ -20,12 +20,12 @@ internal class FakeLegacyKeyRepository( var deleted: Boolean = false private set - override fun secretKey(): SecretKey? { + override suspend fun secretKey(): SecretKey? { probes++ return key } - override fun deleteLegacyKey() { + override suspend fun deleteLegacyKey() { deleted = true } }