From b92c4b50f3e7257559404c4905ce6b2a265fdf08 Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 1 Aug 2026 18:59:17 +0200 Subject: [PATCH 1/2] fix: obfuscation Closes: #63 --- .../view/creditcard/ViewCreditCardContent.kt | 3 +- .../creditcard/ViewCreditCardViewModel.kt | 1 + .../item/view/login/model/ObfuscatedString.kt | 16 ++++--- .../view/login/model/ObfuscatedStringTest.kt | 43 ++++++++++++++++++- 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt index 5482ecf28..2c292f32f 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt @@ -446,7 +446,8 @@ private fun ViewCreditCardContentPreview() { cardNumber = ObfuscatedString( "4111111111111111", formatted = "4111 1111 1111 1111", - visibleSuffixDigits = 4 + visibleSuffixDigits = 4, + preservedChars = setOf(' '), ), cvv = ObfuscatedString("123"), expirationDate = "12/26", diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardViewModel.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardViewModel.kt index d59145331..7c830ae18 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardViewModel.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardViewModel.kt @@ -85,6 +85,7 @@ internal class ViewCreditCardViewModel( raw = raw, formatted = cardFormatter.formatNumber(raw), visibleSuffixDigits = VISIBLE_CARD_NUMBER_SUFFIX, + preservedChars = setOf(' '), ) } } diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedString.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedString.kt index 14dc36193..b1cbb6411 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedString.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedString.kt @@ -4,17 +4,19 @@ data class ObfuscatedString( val raw: String, val formatted: String = raw, val visibleSuffixDigits: Int = 0, + val preservedChars: Set = emptySet(), ) { val hidden: String by lazy { - val totalDigits = formatted.count(Char::isDigit) - val reveal = if (totalDigits > visibleSuffixDigits) visibleSuffixDigits else 0 - var digitsSeen = 0 + val totalChars = formatted.count { it !in preservedChars } + val reveal = if (totalChars > visibleSuffixDigits) visibleSuffixDigits else 0 + var charsSeen = 0 buildString(formatted.length) { for (c in formatted) { - if (c.isDigit()) { - digitsSeen++ - append(if (totalDigits - digitsSeen < reveal) c else DEFAULT_OBFUSCATION_CHAR) - } else append(c) + if (c in preservedChars) append(c) + else { + charsSeen++ + append(if (totalChars - charsSeen < reveal) c else DEFAULT_OBFUSCATION_CHAR) + } } } } diff --git a/feature/item/view/src/test/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedStringTest.kt b/feature/item/view/src/test/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedStringTest.kt index 5fe10489d..afcffaaa4 100644 --- a/feature/item/view/src/test/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedStringTest.kt +++ b/feature/item/view/src/test/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedStringTest.kt @@ -18,11 +18,48 @@ class ObfuscatedStringTest { } @Test - fun `hidden preserves spaces from formatted string`() { - val obs = ObfuscatedString(raw = "4111111111111111", formatted = "4111 1111 1111 1111") + fun `hidden preserves spaces from formatted string when configured`() { + val obs = ObfuscatedString( + raw = "4111111111111111", + formatted = "4111 1111 1111 1111", + preservedChars = setOf(' '), + ) assertEquals(mask("**** **** **** ****"), obs.hidden) } + @Test + fun `hidden obfuscates spaces by default`() { + val obs = ObfuscatedString(raw = "4111111111111111", formatted = "4111 1111 1111 1111") + assertEquals(mask("****" + "*".repeat(15)), obs.hidden) + } + + @Test + fun `hidden obfuscates letters and symbols, not just digits`() { + val obs = ObfuscatedString("Sup3r\$ecret!") + assertEquals(mask("*".repeat(12)), obs.hidden) + } + + @Test + fun `hidden preserves only chars configured via preservedChars`() { + val obs = ObfuscatedString( + raw = "555-01-2345", + formatted = "555-01-2345", + preservedChars = setOf('-'), + ) + assertEquals(mask("***-**-****"), obs.hidden) + } + + @Test + fun `hidden reveals suffix among preserved chars for mixed content`() { + val obs = ObfuscatedString( + raw = "555-01-2345", + formatted = "555-01-2345", + visibleSuffixDigits = 3, + preservedChars = setOf('-'), + ) + assertEquals(mask("***-**-*345"), obs.hidden) + } + @Test fun `raw is unaffected by formatted`() { val obs = ObfuscatedString(raw = "4111111111111111", formatted = "4111 1111 1111 1111") @@ -35,6 +72,7 @@ class ObfuscatedStringTest { raw = "4111111111111234", formatted = "4111 1111 1111 1234", visibleSuffixDigits = 4, + preservedChars = setOf(' '), ) assertEquals(mask("**** **** **** 1234"), obs.hidden) } @@ -45,6 +83,7 @@ class ObfuscatedStringTest { raw = "378282246310005", formatted = "3782 822463 10005", visibleSuffixDigits = 4, + preservedChars = setOf(' '), ) assertEquals(mask("**** ****** *0005"), obs.hidden) } From 9dad6dbf475d19b353d7cc3cf834e79a2429bc0b Mon Sep 17 00:00:00 2001 From: Davis Wolfermann Date: Sat, 1 Aug 2026 19:23:29 +0200 Subject: [PATCH 2/2] refactor: rename parameter --- .../feature/item/view/creditcard/ViewCreditCardContent.kt | 2 +- .../item/view/creditcard/ViewCreditCardViewModel.kt | 2 +- .../feature/item/view/login/model/ObfuscatedString.kt | 4 ++-- .../feature/item/view/login/model/ObfuscatedStringTest.kt | 8 ++++---- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt index 2c292f32f..b5e40dde5 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardContent.kt @@ -446,7 +446,7 @@ private fun ViewCreditCardContentPreview() { cardNumber = ObfuscatedString( "4111111111111111", formatted = "4111 1111 1111 1111", - visibleSuffixDigits = 4, + visibleSuffixChars = 4, preservedChars = setOf(' '), ), cvv = ObfuscatedString("123"), diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardViewModel.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardViewModel.kt index 7c830ae18..e16cdea17 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardViewModel.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/creditcard/ViewCreditCardViewModel.kt @@ -84,7 +84,7 @@ internal class ViewCreditCardViewModel( ObfuscatedString( raw = raw, formatted = cardFormatter.formatNumber(raw), - visibleSuffixDigits = VISIBLE_CARD_NUMBER_SUFFIX, + visibleSuffixChars = VISIBLE_CARD_NUMBER_SUFFIX, preservedChars = setOf(' '), ) } diff --git a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedString.kt b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedString.kt index b1cbb6411..730998e06 100644 --- a/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedString.kt +++ b/feature/item/view/src/main/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedString.kt @@ -3,12 +3,12 @@ package de.davis.keygo.feature.item.view.login.model data class ObfuscatedString( val raw: String, val formatted: String = raw, - val visibleSuffixDigits: Int = 0, + val visibleSuffixChars: Int = 0, val preservedChars: Set = emptySet(), ) { val hidden: String by lazy { val totalChars = formatted.count { it !in preservedChars } - val reveal = if (totalChars > visibleSuffixDigits) visibleSuffixDigits else 0 + val reveal = if (totalChars > visibleSuffixChars) visibleSuffixChars else 0 var charsSeen = 0 buildString(formatted.length) { for (c in formatted) { diff --git a/feature/item/view/src/test/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedStringTest.kt b/feature/item/view/src/test/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedStringTest.kt index afcffaaa4..9fae53e8e 100644 --- a/feature/item/view/src/test/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedStringTest.kt +++ b/feature/item/view/src/test/kotlin/de/davis/keygo/feature/item/view/login/model/ObfuscatedStringTest.kt @@ -54,7 +54,7 @@ class ObfuscatedStringTest { val obs = ObfuscatedString( raw = "555-01-2345", formatted = "555-01-2345", - visibleSuffixDigits = 3, + visibleSuffixChars = 3, preservedChars = setOf('-'), ) assertEquals(mask("***-**-*345"), obs.hidden) @@ -71,7 +71,7 @@ class ObfuscatedStringTest { val obs = ObfuscatedString( raw = "4111111111111234", formatted = "4111 1111 1111 1234", - visibleSuffixDigits = 4, + visibleSuffixChars = 4, preservedChars = setOf(' '), ) assertEquals(mask("**** **** **** 1234"), obs.hidden) @@ -82,7 +82,7 @@ class ObfuscatedStringTest { val obs = ObfuscatedString( raw = "378282246310005", formatted = "3782 822463 10005", - visibleSuffixDigits = 4, + visibleSuffixChars = 4, preservedChars = setOf(' '), ) assertEquals(mask("**** ****** *0005"), obs.hidden) @@ -93,7 +93,7 @@ class ObfuscatedStringTest { val obs = ObfuscatedString( raw = "1234", formatted = "1234", - visibleSuffixDigits = 4, + visibleSuffixChars = 4, ) assertEquals(mask("****"), obs.hidden) }