Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
777b99b
refactor(db): rename db file
OffRange Jul 27, 2026
5f867b2
refactor(module): move create access migration
OffRange Jul 27, 2026
b8eb048
fix(script): resolve timeout issue
OffRange Jul 27, 2026
6969be6
feat(item): introduce timestamp
OffRange Jul 27, 2026
4e73af4
feat(core-item): add ItemTransactionRunner and guard the database fil…
OffRange Jul 27, 2026
6d597d0
feat(migration): port the v1 room entities and open legacy databases …
OffRange Jul 27, 2026
1a63e8f
test(migration): cover null columns across the 2-to-3 recreate and co…
OffRange Jul 27, 2026
96be967
feat(migration): sanitize null legacy columns before Room opens the file
OffRange Jul 27, 2026
7a2c0e1
fix(migration): harden LegacyDatabaseSanitizer transaction and guard …
OffRange Jul 27, 2026
f9d8c43
feat(migration): decrypt v1 secure element blobs
OffRange Jul 28, 2026
81cec02
feat(migration): parse v1 element detail json
OffRange Jul 28, 2026
aed1997
feat(migration): read and decode the legacy database into domain items
OffRange Jul 28, 2026
d759c03
fix(migration): never create the legacy database file
OffRange Jul 28, 2026
fb58ca9
docs(migration): fold the driver param into the class KDoc
OffRange Jul 28, 2026
0039da5
fix(migration): base state() on the probe's answer, not a query failure
OffRange Jul 28, 2026
c9a0bc0
feat(migration): convert legacy items into v2 logins and credit cards
OffRange Jul 28, 2026
3cd8065
feat(migration): orchestrate the legacy import with per-row failure i…
OffRange Jul 28, 2026
15e2b4e
feat(auth): import v1 data after every successful session start
OffRange Jul 28, 2026
f9d6b34
fix(migration): run the v1 import off the unlock path
OffRange Jul 28, 2026
964cb14
fix(migration): report legacy import failures and row-skip diagnostics
OffRange Jul 28, 2026
a5db590
test(migration): prove the legacy import loses no data end to end
OffRange Jul 28, 2026
01190e0
test(migration): cover the null secure-element probe verdict in the e…
OffRange Jul 28, 2026
4df0665
fix(migration): chunk the prune delete and report a file the import c…
OffRange Jul 29, 2026
3606ca6
refactor(legacy-data): cleanup
OffRange Jul 29, 2026
f2f5ac3
refactor: cleanup
OffRange Aug 1, 2026
daf9b1e
refactor: revert biometric unlock redirection
OffRange Aug 1, 2026
8472a46
fix: review findings
OffRange Aug 1, 2026
066e3be
fix: add function
OffRange Aug 1, 2026
9a0d257
fix(legacy-data): restore encryptLikeV1 test helper
OffRange Aug 1, 2026
7b5f2bb
refactor: dedup transaction runner
OffRange Aug 1, 2026
580b18e
fix: make CI pass
OffRange Aug 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ out/
build/
!gradle/wrapper/gradle-wrapper.jar

# Lock file the Room Gradle plugin drops in a module root when it copies schemas
*.lck

# Signing files
.signing/

Expand Down
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,8 @@ dependencies {
implementation(projects.feature.autofill)
implementation(projects.feature.settings)
implementation(projects.feature.backup)
implementation(projects.migrationCreateAccess)
implementation(projects.migration.createAccess)
implementation(projects.migration.legacyData)

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.androidx.room) apply false
alias(libs.plugins.androidx.room3) apply false
alias(libs.plugins.kotlin.parcelize) apply false
alias(libs.plugins.kotlin.compose) apply false
alias(libs.plugins.kotlin.serialization) apply false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 1,
"identityHash": "d827ca95e51c22ef1dc56c2eb260f41c",
"identityHash": "4f0b0d6a7ba5ff447e745fb0f08d8be3",
"entities": [
{
"tableName": "vault",
Expand Down Expand Up @@ -54,7 +54,7 @@
},
{
"tableName": "item",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` BLOB NOT NULL, `vault_id` BLOB NOT NULL, `name` TEXT NOT NULL, `note` TEXT, `item_type` TEXT NOT NULL, `pinned` INTEGER NOT NULL, `wrapped_key` BLOB NOT NULL, `key_nonce` BLOB NOT NULL, PRIMARY KEY(`id`), FOREIGN KEY(`vault_id`) REFERENCES `vault`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` BLOB NOT NULL, `vault_id` BLOB NOT NULL, `name` TEXT NOT NULL, `note` TEXT, `item_type` TEXT NOT NULL, `pinned` INTEGER NOT NULL, `wrapped_key` BLOB NOT NULL, `key_nonce` BLOB NOT NULL, `created_at` INTEGER NOT NULL, `modified_at` INTEGER, PRIMARY KEY(`id`), FOREIGN KEY(`vault_id`) REFERENCES `vault`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
Expand Down Expand Up @@ -102,6 +102,17 @@
"columnName": "key_nonce",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "timestamp.createdAt",
"columnName": "created_at",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "timestamp.modifiedAt",
"columnName": "modified_at",
"affinity": "INTEGER"
}
],
"primaryKey": {
Expand Down Expand Up @@ -597,7 +608,7 @@
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'd827ca95e51c22ef1dc56c2eb260f41c')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4f0b0d6a7ba5ff447e745fb0f08d8be3')"
]
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ internal class DatabaseModule {
Room.databaseBuilder(
context,
ItemDatabase::class.java,
"secure_element_database",
"keygo_database",
).build()

@Single
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ internal data class ItemEntity(
val pinned: Boolean,

@Embedded val keyInformation: KeyInformation,
@Embedded val timestamp: Timestamp,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package de.davis.keygo.core.item.data.local.entity

import androidx.room.ColumnInfo

internal data class Timestamp(
@ColumnInfo("created_at")
val createdAt: Long,
@ColumnInfo("modified_at")
val modifiedAt: Long?,
)
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal fun CreditCardProjection.toDomain() = CreditCard(
vaultId = item.itemEntity.vaultId,
name = item.itemEntity.name,
keyInformation = item.itemEntity.keyInformation.toDomain(),
timestamp = item.itemEntity.timestamp.toDomain(),
tags = item.tags.map(TagEntity::toDomain).toSet(),
note = item.itemEntity.note,
pinned = item.itemEntity.pinned,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ internal fun Item.toData() = ItemEntity(
pinned = pinned,

keyInformation = keyInformation.toEntity(),
timestamp = timestamp.toEntity(),
)

internal fun LightweightItem.toDomain() = LiteItem.Concrete(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ internal fun LoginProjection.toDomain(): Login = Login(
name = item.itemEntity.name,
note = item.itemEntity.note,
keyInformation = item.itemEntity.keyInformation.toDomain(),
timestamp = item.itemEntity.timestamp.toDomain(),
tags = item.tags.map(TagEntity::toDomain).toSet(),
pinned = item.itemEntity.pinned,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package de.davis.keygo.core.item.data.mapper

import de.davis.keygo.core.item.data.local.entity.Timestamp
import kotlin.time.Instant
import de.davis.keygo.core.item.domain.model.Timestamp as DomainTimestamp

internal fun DomainTimestamp.toEntity(): Timestamp = Timestamp(
createdAt = createdAt.toEpochMilliseconds(),
modifiedAt = modifiedAt?.toEpochMilliseconds(),
)

internal fun Timestamp.toDomain(): DomainTimestamp = DomainTimestamp(
createdAt = Instant.fromEpochMilliseconds(createdAt),
modifiedAt = modifiedAt?.let { Instant.fromEpochMilliseconds(it) },
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package de.davis.keygo.core.item.data.repository

import androidx.room.withTransaction
import de.davis.keygo.core.item.data.local.datasource.ItemDatabase
import de.davis.keygo.core.item.domain.TransactionRunner
import de.davis.keygo.core.item.domain.repository.TransactionRunner
import org.koin.core.annotation.Single

@Single
internal class RoomTransactionRunner(
internal class TransactionRunnerImpl(
private val database: ItemDatabase,
) : TransactionRunner {
override suspend fun <R> runInTransaction(block: suspend () -> R): R =
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ data class CreditCard(
override val vaultId: VaultId,
override val name: String,
override val keyInformation: KeyInformation,
override val timestamp: Timestamp,
override val tags: Set<Tag>,
override val note: String?,
override val pinned: Boolean,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ sealed interface Item : LiteItem {
val keyInformation: KeyInformation
val tags: Set<Tag>
val note: String?
val timestamp: Timestamp
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ data class Login(
override val vaultId: VaultId,
override val name: String,
override val keyInformation: KeyInformation,
override val timestamp: Timestamp,
override val tags: Set<Tag> = emptySet(),
override val note: String?,
override val pinned: Boolean,
Expand All @@ -27,7 +28,7 @@ data class Login(

val hasAnyContent: Boolean
get() = !username.isNullOrBlank()
|| passwordCredential != null
|| totp != null
|| passkeyRPs.isNotEmpty()
|| passwordCredential != null
|| totp != null
|| passkeyRPs.isNotEmpty()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package de.davis.keygo.core.item.domain.model

import kotlin.time.Clock
import kotlin.time.Instant

data class Timestamp(
val createdAt: Instant = Clock.System.now(),
val modifiedAt: Instant? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package de.davis.keygo.core.item.domain.model

import java.time.YearMonth
import java.time.format.DateTimeFormatter
import java.time.format.DateTimeParseException

// "yy" parses into the 2000-2099 range, which is correct for card expirations.
private val EXPIRATION_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("MM/yy")

fun String.toYearMonthOrNull(): YearMonth? = try {
YearMonth.parse(this, EXPIRATION_FORMATTER)
} catch (_: DateTimeParseException) {
null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package de.davis.keygo.core.item.domain.repository

interface TransactionRunner {
suspend fun <R> runInTransaction(block: suspend () -> R): R
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import de.davis.keygo.core.item.data.local.datasource.ItemDatabase
import de.davis.keygo.core.item.data.local.entity.ItemEntity
import de.davis.keygo.core.item.data.local.entity.TagEntity
import de.davis.keygo.core.item.data.local.entity.Timestamp
import de.davis.keygo.core.item.data.local.entity.VaultEntity
import de.davis.keygo.core.item.domain.alias.ItemId
import de.davis.keygo.core.item.domain.alias.VaultId
Expand Down Expand Up @@ -67,6 +68,7 @@ internal class ItemDaoSearchTest {
itemType = VaultItemType.Login,
pinned = false,
keyInformation = EntityKeyInformation(byteArrayOf(), byteArrayOf()),
timestamp = Timestamp(createdAt = 0L, modifiedAt = null),
)
)
if (tags.isNotEmpty())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import de.davis.keygo.core.item.data.local.datasource.ItemDatabase
import de.davis.keygo.core.item.data.local.entity.ItemEntity
import de.davis.keygo.core.item.data.local.entity.TagEntity
import de.davis.keygo.core.item.data.local.entity.Timestamp
import de.davis.keygo.core.item.data.local.entity.VaultEntity
import de.davis.keygo.core.item.domain.alias.ItemId
import de.davis.keygo.core.item.domain.alias.VaultId
Expand Down Expand Up @@ -64,6 +65,7 @@ internal class TagDaoTest {
itemType = VaultItemType.Login,
pinned = false,
keyInformation = EntityKeyInformation(byteArrayOf(), byteArrayOf()),
timestamp = Timestamp(createdAt = 0L, modifiedAt = null),
)
)
return id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import de.davis.keygo.core.item.domain.model.CreditCard
import de.davis.keygo.core.item.domain.model.EncryptedPayload
import de.davis.keygo.core.item.domain.model.KeyInformation
import de.davis.keygo.core.item.domain.model.Tag
import de.davis.keygo.core.item.domain.model.Timestamp
import de.davis.keygo.core.item.generated.domain.model.VaultItemType
import java.time.YearMonth
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
import de.davis.keygo.core.item.data.local.entity.KeyInformation as EntityKeyInformation
import de.davis.keygo.core.item.data.local.entity.Timestamp as EntityTimestamp

class CreditCardMapperTest {

Expand Down Expand Up @@ -128,6 +130,7 @@ class CreditCardMapperTest {
cardNumber = cardNumber,
cvv = cvv,
expirationDate = expirationDate,
timestamp = Timestamp(),
)

private fun baseProjection(
Expand Down Expand Up @@ -157,6 +160,7 @@ class CreditCardMapperTest {
wrappedKey = byteArrayOf(),
keyNonce = byteArrayOf(),
),
timestamp = EntityTimestamp(createdAt = 0L, modifiedAt = null),
),
tags = tags,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import de.davis.keygo.core.item.domain.model.Login
import de.davis.keygo.core.item.domain.model.PasswordCredential
import de.davis.keygo.core.item.domain.model.PasswordScore
import de.davis.keygo.core.item.domain.model.PasswordSecret
import de.davis.keygo.core.item.domain.model.Timestamp
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
Expand All @@ -34,6 +35,7 @@ class DomainMapperTest {
keyInformation = KeyInformation(wrappedKey = byteArrayOf(), keyNonce = byteArrayOf()),
note = null,
pinned = false,
timestamp = Timestamp(),
)

// toData
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import de.davis.keygo.core.item.domain.model.Login
import de.davis.keygo.core.item.domain.model.PasswordCredential
import de.davis.keygo.core.item.domain.model.PasswordScore
import de.davis.keygo.core.item.domain.model.PasswordSecret
import de.davis.keygo.core.item.domain.model.Timestamp
import de.davis.keygo.core.item.generated.domain.model.VaultItemType
import kotlin.test.Test
import kotlin.test.assertContentEquals
Expand Down Expand Up @@ -40,6 +41,7 @@ class ItemMapperTest {
pinned = pinned,
vaultId = newVaultId(),
keyInformation = KeyInformation(byteArrayOf(), byteArrayOf()),
timestamp = Timestamp(),
)

// Item.toData() -> ItemEntity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import de.davis.keygo.core.item.domain.model.PasswordCredential
import de.davis.keygo.core.item.domain.model.PasswordScore
import de.davis.keygo.core.item.domain.model.PasswordSecret
import de.davis.keygo.core.item.domain.model.Tag
import de.davis.keygo.core.item.domain.model.Timestamp
import de.davis.keygo.core.item.generated.domain.model.VaultItemType
import de.davis.keygo.core.item.data.local.entity.Timestamp as EntityTimestamp
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNull
Expand Down Expand Up @@ -103,6 +105,7 @@ class LoginMapperTest {
wrappedKey = byteArrayOf(),
keyNonce = byteArrayOf(),
),
timestamp = EntityTimestamp(createdAt = 0L, modifiedAt = null),
),
tags = tags,
),
Expand All @@ -125,5 +128,6 @@ class LoginMapperTest {
keyInformation = KeyInformation(byteArrayOf(), byteArrayOf()),
note = null,
pinned = false,
timestamp = Timestamp(),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import de.davis.keygo.core.item.domain.alias.newVaultId
import de.davis.keygo.core.item.domain.model.CreditCard
import de.davis.keygo.core.item.domain.model.EncryptedPayload
import de.davis.keygo.core.item.domain.model.KeyInformation
import de.davis.keygo.core.item.domain.model.Timestamp
import de.davis.keygo.core.item.generated.domain.model.VaultItemType
import de.davis.keygo.core.util.isFailure
import de.davis.keygo.core.util.isSuccess
Expand All @@ -34,6 +35,7 @@ import kotlin.test.assertEquals
import kotlin.test.assertNull
import kotlin.test.assertTrue
import de.davis.keygo.core.item.data.local.entity.KeyInformation as EntityKeyInformation
import de.davis.keygo.core.item.data.local.entity.Timestamp as EntityTimestamp

class CreditCardRepositoryImplTest {

Expand Down Expand Up @@ -146,6 +148,7 @@ class CreditCardRepositoryImplTest {
cardNumber = CreditCard.CardNumber(EncryptedPayload.EMPTY),
cvv = CreditCard.CVV(EncryptedPayload.EMPTY),
expirationDate = YearMonth.of(2030, 12),
timestamp = Timestamp(),
)

private fun projection(id: ItemId) = CreditCardProjection(
Expand All @@ -168,6 +171,7 @@ class CreditCardRepositoryImplTest {
wrappedKey = byteArrayOf(),
keyNonce = byteArrayOf(),
),
timestamp = EntityTimestamp(createdAt = 0L, modifiedAt = null),
),
tags = emptySet(),
),
Expand Down
Loading
Loading