Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
5991402
refactor(db): rename db file
OffRange Jul 27, 2026
3a8748b
refactor(module): move create access migration
OffRange Jul 27, 2026
0ebf7d8
fix(script): resolve timeout issue
OffRange Jul 27, 2026
09cd929
feat(item): introduce timestamp
OffRange Jul 27, 2026
089d45e
feat(core-item): add ItemTransactionRunner and guard the database fil…
OffRange Jul 27, 2026
021f3bc
feat(migration): port the v1 room entities and open legacy databases …
OffRange Jul 27, 2026
86b8025
test(migration): cover null columns across the 2-to-3 recreate and co…
OffRange Jul 27, 2026
f0e5455
feat(migration): sanitize null legacy columns before Room opens the file
OffRange Jul 27, 2026
571d0a4
fix(migration): harden LegacyDatabaseSanitizer transaction and guard …
OffRange Jul 27, 2026
2bc95d3
feat(migration): decrypt v1 secure element blobs
OffRange Jul 28, 2026
a08a854
feat(migration): parse v1 element detail json
OffRange Jul 28, 2026
ed8f455
feat(migration): read and decode the legacy database into domain items
OffRange Jul 28, 2026
bc287e7
fix(migration): never create the legacy database file
OffRange Jul 28, 2026
2a6d5eb
docs(migration): fold the driver param into the class KDoc
OffRange Jul 28, 2026
5b83525
fix(migration): base state() on the probe's answer, not a query failure
OffRange Jul 28, 2026
675c805
feat(migration): convert legacy items into v2 logins and credit cards
OffRange Jul 28, 2026
c8a9dea
feat(migration): orchestrate the legacy import with per-row failure i…
OffRange Jul 28, 2026
954d15a
feat(auth): import v1 data after every successful session start
OffRange Jul 28, 2026
81f0a28
fix(migration): run the v1 import off the unlock path
OffRange Jul 28, 2026
992d893
fix(migration): report legacy import failures and row-skip diagnostics
OffRange Jul 28, 2026
4b8b36b
test(migration): prove the legacy import loses no data end to end
OffRange Jul 28, 2026
f8dd76f
test(migration): cover the null secure-element probe verdict in the e…
OffRange Jul 28, 2026
2ce3043
fix(migration): chunk the prune delete and report a file the import c…
OffRange Jul 29, 2026
23c48f5
refactor(legacy-data): cleanup
OffRange Jul 29, 2026
41f21ea
refactor: cleanup
OffRange Aug 1, 2026
7b07623
refactor: revert biometric unlock redirection
OffRange Aug 1, 2026
52cf30f
fix: review findings
OffRange Aug 1, 2026
692688a
fix: add function
OffRange Aug 1, 2026
96bf561
fix(legacy-data): restore encryptLikeV1 test helper
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 @@ -116,7 +116,8 @@ dependencies {
implementation(projects.feature.creditCard)
implementation(projects.feature.autofill)
implementation(projects.feature.settings)
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
@@ -0,0 +1,15 @@
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.repository.ItemTransactionRunner
import org.koin.core.annotation.Single

@Single
internal class ItemTransactionRunnerImpl(
private val database: ItemDatabase,
) : ItemTransactionRunner {

override suspend fun <R> inTransaction(block: suspend () -> R): R =
database.withTransaction { block() }
}
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,14 @@
package de.davis.keygo.core.item.domain.repository

/**
* Runs a block of item writes as a single database transaction.
*
* Exists so callers outside `:core:item` can commit a batch atomically without reaching for
* `ItemDatabase`, which is internal to this module. Nesting is safe: Room's transaction support
* is reentrant on the same connection, so repository methods that already open their own
* transaction join the outer one rather than starting a second.
*/
interface ItemTransactionRunner {

suspend fun <R> inTransaction(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