Skip to content

Commit 4d484a4

Browse files
authored
Cleanup Architecture (#17)
Trying to re-architect PinRepository so it is not required by other Repositories Also finally fixed some tests! This breaks the dependency between PinRepository and AuthorizationRepository bu introducing new UseCases
1 parent 9a55fa1 commit 4d484a4

23 files changed

+965
-624
lines changed

app/src/main/kotlin/com/darkrockstudios/app/securecamera/AppModule.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import com.darkrockstudios.app.securecamera.preferences.AppPreferencesDataSource
1313
import com.darkrockstudios.app.securecamera.security.DeviceInfo
1414
import com.darkrockstudios.app.securecamera.security.SecurityLevel
1515
import com.darkrockstudios.app.securecamera.security.SecurityLevelDetector
16+
import com.darkrockstudios.app.securecamera.security.pin.PinCrypto
1617
import com.darkrockstudios.app.securecamera.security.pin.PinRepository
1718
import com.darkrockstudios.app.securecamera.security.pin.PinRepositoryHardware
1819
import com.darkrockstudios.app.securecamera.security.pin.PinRepositorySoftware
@@ -37,7 +38,6 @@ val appModule = module {
3738
single {
3839
AuthorizationRepository(
3940
preferences = get(),
40-
pinRepository = get(),
4141
encryptionScheme = get(),
4242
context = get(),
4343
clock = get()
@@ -58,14 +58,15 @@ val appModule = module {
5858
val detector = get<SecurityLevelDetector>()
5959
when (detector.detectSecurityLevel()) {
6060
SecurityLevel.SOFTWARE ->
61-
PinRepositorySoftware(get(), get())
61+
PinRepositorySoftware(get(), get(), get())
6262

6363
SecurityLevel.TEE, SecurityLevel.STRONGBOX -> {
64-
PinRepositoryHardware(get(), get(), get())
64+
PinRepositoryHardware(get(), get(), get(), get())
6565
}
6666
}
6767
} bind PinRepository::class
6868
singleOf(::SecurityLevelDetector)
69+
singleOf(::PinCrypto)
6970

7071
single { WorkManager.getInstance(get()) }
7172

@@ -75,11 +76,13 @@ val appModule = module {
7576
factoryOf(::SecurityResetUseCase)
7677
factoryOf(::PinStrengthCheckUseCase)
7778
factoryOf(::VerifyPinUseCase)
79+
factoryOf(::AuthorizePinUseCase)
7880
factoryOf(::CreatePinUseCase)
7981
factoryOf(::PinSizeUseCase)
8082
factoryOf(::RemovePoisonPillIUseCase)
8183
factoryOf(::MigratePinHash)
8284
factoryOf(::InvalidateSessionUseCase)
85+
factoryOf(::AddDecoyPhotoUseCase)
8386

8487
viewModelOf(::ObfuscatePhotoViewModel)
8588
viewModelOf(::ViewPhotoViewModel)

app/src/main/kotlin/com/darkrockstudios/app/securecamera/auth/AuthorizationRepository.kt

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package com.darkrockstudios.app.securecamera.auth
33
import android.content.Context
44
import com.darkrockstudios.app.securecamera.preferences.AppPreferencesDataSource
55
import com.darkrockstudios.app.securecamera.preferences.HashedPin
6-
import com.darkrockstudios.app.securecamera.security.pin.PinRepository
76
import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
87
import kotlinx.coroutines.flow.MutableStateFlow
98
import kotlinx.coroutines.flow.StateFlow
@@ -19,7 +18,6 @@ import kotlin.time.Instant
1918
*/
2019
class AuthorizationRepository(
2120
private val preferences: AppPreferencesDataSource,
22-
private val pinRepository: PinRepository,
2321
private val encryptionScheme: EncryptionScheme,
2422
private val context: Context,
2523
private val clock: Clock,
@@ -38,10 +36,6 @@ class AuthorizationRepository(
3836
preferences.securityFailureReset()
3937
}
4038

41-
suspend fun activatePoisonPill() {
42-
pinRepository.activatePoisonPill()
43-
}
44-
4539
/**
4640
* Gets the current number of failed PIN attempts
4741
* @return The number of failed attempts
@@ -119,29 +113,11 @@ class AuthorizationRepository(
119113
return true
120114
}
121115

122-
/**
123-
* Verifies the PIN and updates the authorization state if successful.
124-
* @param pin The PIN entered by the user
125-
* @return True if the PIN is correct, false otherwise
126-
*/
127-
suspend fun verifyPin(pin: String): HashedPin? {
128-
val hashedPin = pinRepository.getHashedPin()
129-
val isValid = pinRepository.verifySecurityPin(pin)
130-
return if (isValid && hashedPin != null) {
131-
authorizeSession()
132-
// Reset failed attempts counter on successful verification
133-
resetFailedAttempts()
134-
hashedPin
135-
} else {
136-
null
137-
}
138-
}
139-
140116
/**
141117
* Marks the current session as authorized and updates the last authentication time.
142118
* Also starts the SessionService to monitor session validity.
143119
*/
144-
private fun authorizeSession() {
120+
fun authorizeSession() {
145121
lastAuthTimeMs = clock.now()
146122
_isAuthorized.value = true
147123
startSessionService()

app/src/main/kotlin/com/darkrockstudios/app/securecamera/camera/SecureImageRepository.kt

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,17 @@ import com.ashampoo.kim.common.convertToPhotoMetadata
1010
import com.ashampoo.kim.model.GpsCoordinates
1111
import com.ashampoo.kim.model.MetadataUpdate
1212
import com.ashampoo.kim.model.TiffOrientation
13-
import com.darkrockstudios.app.securecamera.security.pin.PinRepository
1413
import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
1514
import timber.log.Timber
1615
import java.io.ByteArrayOutputStream
1716
import java.io.File
1817
import java.text.SimpleDateFormat
19-
import java.util.Date
20-
import java.util.Locale
18+
import java.util.*
2119
import kotlin.time.toJavaInstant
2220

2321

2422
class SecureImageRepository(
2523
private val appContext: Context,
26-
private val pinRepository: PinRepository,
2724
internal val thumbnailCache: ThumbnailCache,
2825
private val encryptionScheme: EncryptionScheme,
2926
) {
@@ -438,18 +435,15 @@ class SecureImageRepository(
438435

439436
fun numDecoys(): Int = getDecoyFiles().count()
440437

441-
suspend fun addDecoyPhoto(photoDef: PhotoDef): Boolean {
438+
suspend fun addDecoyPhotoWithKey(photoDef: PhotoDef, keyBytes: ByteArray): Boolean {
442439
return if (numDecoys() < MAX_DECOY_PHOTOS) {
443440
val jpgBytes = decryptJpg(photoDef)
444441
getDecoyDirectory().mkdirs()
445442
val decoyFile = getDecoyFile(photoDef)
446443

447-
val ppp = pinRepository.getHashedPoisonPillPin() ?: return false
448-
val pin = pinRepository.getPlainPoisonPillPin() ?: return false
449-
val ppk = encryptionScheme.deriveKey(plainPin = pin, hashedPin = ppp)
450444
encryptionScheme.encryptToFile(
451445
plain = jpgBytes,
452-
keyBytes = ppk,
446+
keyBytes = keyBytes,
453447
targetFile = decoyFile
454448
)
455449

app/src/main/kotlin/com/darkrockstudios/app/securecamera/preferences/AppPreferencesDataSource.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ class AppPreferencesDataSource(
7070
/**
7171
* Check if the user has completed the introduction
7272
*/
73-
val hasCompletedIntro: Flow<Boolean?> = context.dataStore.data
73+
val hasCompletedIntro: Flow<Boolean?> = dataStore.data
7474
.map { preferences ->
7575
preferences[HAS_COMPLETED_INTRO] ?: false
7676
}
7777

7878
// DELETE ME after beta migration is over
79-
val isProdReady: Flow<Boolean?> = context.dataStore.data
79+
val isProdReady: Flow<Boolean?> = dataStore.data
8080
.map { preferences ->
8181
preferences[IS_PROD_READY] ?: false
8282
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.darkrockstudios.app.securecamera.security.pin
2+
3+
import com.darkrockstudios.app.securecamera.preferences.HashedPin
4+
import com.darkrockstudios.app.securecamera.preferences.base64DecodeUrlSafe
5+
import com.darkrockstudios.app.securecamera.preferences.base64EncodeUrlSafe
6+
import com.lambdapioneer.argon2kt.Argon2Kt
7+
import com.lambdapioneer.argon2kt.Argon2KtResult
8+
import com.lambdapioneer.argon2kt.Argon2Mode
9+
import dev.whyoleg.cryptography.random.CryptographyRandom
10+
11+
/**
12+
* Pure hashing/verification helper for PINs. No I/O, KMP‑friendly.
13+
* Binds the hash to the provided deviceId bytes by concatenating to the PIN bytes.
14+
*/
15+
class PinCrypto(
16+
private val argon2: Argon2Kt = Argon2Kt(),
17+
private val iterations: Int = DEFAULT_ITERATIONS,
18+
private val costKiB: Int = DEFAULT_COST_KIB,
19+
) {
20+
fun hashPin(pin: String, deviceId: ByteArray): HashedPin {
21+
val salt = CryptographyRandom.nextBytes(16)
22+
val password = pin.toByteArray() + deviceId
23+
val result: Argon2KtResult = argon2.hash(
24+
mode = Argon2Mode.ARGON2_I,
25+
password = password,
26+
salt = salt,
27+
tCostInIterations = iterations,
28+
mCostInKibibyte = costKiB,
29+
)
30+
return HashedPin(
31+
result.encodedOutputAsString().toByteArray().base64EncodeUrlSafe(),
32+
salt.base64EncodeUrlSafe(),
33+
)
34+
}
35+
36+
fun verifyPin(pin: String, stored: HashedPin, deviceId: ByteArray): Boolean {
37+
val password = pin.toByteArray() + deviceId
38+
return argon2.verify(
39+
mode = Argon2Mode.ARGON2_I,
40+
encoded = String(stored.hash.base64DecodeUrlSafe()),
41+
password = password,
42+
)
43+
}
44+
45+
companion object {
46+
const val DEFAULT_ITERATIONS = 5
47+
const val DEFAULT_COST_KIB = 65536
48+
}
49+
}

app/src/main/kotlin/com/darkrockstudios/app/securecamera/security/pin/PinRepositoryHardware.kt

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,20 @@
11
package com.darkrockstudios.app.securecamera.security.pin
22

3-
import com.darkrockstudios.app.securecamera.preferences.*
3+
import com.darkrockstudios.app.securecamera.preferences.AppPreferencesDataSource
4+
import com.darkrockstudios.app.securecamera.preferences.HashedPin
5+
import com.darkrockstudios.app.securecamera.preferences.base64Decode
6+
import com.darkrockstudios.app.securecamera.preferences.base64Encode
47
import com.darkrockstudios.app.securecamera.security.DeviceInfo
58
import com.darkrockstudios.app.securecamera.security.SchemeConfig
6-
import com.darkrockstudios.app.securecamera.security.pin.PinRepository.Companion.ARGON_COST
7-
import com.darkrockstudios.app.securecamera.security.pin.PinRepository.Companion.ARGON_ITERATIONS
89
import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
9-
import com.lambdapioneer.argon2kt.Argon2Kt
10-
import com.lambdapioneer.argon2kt.Argon2KtResult
11-
import com.lambdapioneer.argon2kt.Argon2Mode
12-
import dev.whyoleg.cryptography.random.CryptographyRandom
13-
import kotlinx.serialization.encodeToString
1410
import kotlinx.serialization.json.Json
1511

1612
class PinRepositoryHardware(
1713
private val dataSource: AppPreferencesDataSource,
1814
private val encryptionScheme: EncryptionScheme,
1915
private val deviceInfo: DeviceInfo,
16+
private val pinCrypto: PinCrypto,
2017
) : PinRepository {
21-
private val argon2Kt = Argon2Kt()
2218

2319
override suspend fun setAppPin(
2420
pin: String,
@@ -41,32 +37,14 @@ class PinRepositoryHardware(
4137
}
4238

4339
override suspend fun hashPin(pin: String): HashedPin {
44-
val salt = CryptographyRandom.nextBytes(16)
45-
val password = pin.toByteArray() + deviceInfo.getDeviceIdentifier()
46-
val hashResult: Argon2KtResult = argon2Kt.hash(
47-
mode = Argon2Mode.ARGON2_I,
48-
password = password,
49-
salt = salt,
50-
tCostInIterations = ARGON_ITERATIONS,
51-
mCostInKibibyte = ARGON_COST,
52-
)
53-
54-
return HashedPin(
55-
hashResult.encodedOutputAsString().toByteArray().base64EncodeUrlSafe(),
56-
salt.base64EncodeUrlSafe()
57-
)
40+
return pinCrypto.hashPin(pin, deviceInfo.getDeviceIdentifier())
5841
}
5942

6043
override suspend fun verifyPin(
6144
inputPin: String,
6245
storedHash: HashedPin
6346
): Boolean {
64-
val password = inputPin.toByteArray() + deviceInfo.getDeviceIdentifier()
65-
return argon2Kt.verify(
66-
mode = Argon2Mode.ARGON2_I,
67-
encoded = String(storedHash.hash.base64DecodeUrlSafe()),
68-
password = password,
69-
)
47+
return pinCrypto.verifyPin(inputPin, storedHash, deviceInfo.getDeviceIdentifier())
7048
}
7149

7250
override suspend fun setPoisonPillPin(pin: String) {

app/src/main/kotlin/com/darkrockstudios/app/securecamera/security/pin/PinRepositorySoftware.kt

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,25 @@
11
package com.darkrockstudios.app.securecamera.security.pin
22

3-
import com.darkrockstudios.app.securecamera.preferences.*
3+
import com.darkrockstudios.app.securecamera.preferences.AppPreferencesDataSource
4+
import com.darkrockstudios.app.securecamera.preferences.HashedPin
5+
import com.darkrockstudios.app.securecamera.preferences.XorCipher
46
import com.darkrockstudios.app.securecamera.security.DeviceInfo
57
import com.darkrockstudios.app.securecamera.security.SchemeConfig
6-
import com.darkrockstudios.app.securecamera.security.pin.PinRepository.Companion.ARGON_COST
7-
import com.darkrockstudios.app.securecamera.security.pin.PinRepository.Companion.ARGON_ITERATIONS
8-
import com.lambdapioneer.argon2kt.Argon2Kt
9-
import com.lambdapioneer.argon2kt.Argon2KtResult
10-
import com.lambdapioneer.argon2kt.Argon2Mode
11-
import dev.whyoleg.cryptography.random.CryptographyRandom
12-
import kotlinx.serialization.encodeToString
138
import kotlinx.serialization.json.Json
149
import kotlin.io.encoding.ExperimentalEncodingApi
1510

1611
class PinRepositorySoftware(
1712
private val dataSource: AppPreferencesDataSource,
1813
private val deviceInfo: DeviceInfo,
14+
private val pinCrypto: PinCrypto,
1915
) : PinRepository {
20-
private val argon2Kt = Argon2Kt()
2116

2217
override suspend fun setAppPin(pin: String, schemeConfig: SchemeConfig) {
2318
val hashedPin: HashedPin = hashPin(pin)
2419
val key = dataSource.getCipherKey()
2520

26-
val cipheredHash = XorCipher.encrypt(Json.Default.encodeToString(hashedPin), key)
27-
val config = Json.Default.encodeToString(schemeConfig)
21+
val cipheredHash = XorCipher.encrypt(Json.encodeToString(hashedPin), key)
22+
val config = Json.encodeToString(schemeConfig)
2823
dataSource.setAppPin(cipheredHash, config)
2924
}
3025

@@ -36,30 +31,12 @@ class PinRepositorySoftware(
3631

3732
@OptIn(ExperimentalStdlibApi::class)
3833
override suspend fun hashPin(pin: String): HashedPin {
39-
val salt = CryptographyRandom.nextBytes(16)
40-
val password = pin.toByteArray() + deviceInfo.getDeviceIdentifier()
41-
val hashResult: Argon2KtResult = argon2Kt.hash(
42-
mode = Argon2Mode.ARGON2_I,
43-
password = password,
44-
salt = salt,
45-
tCostInIterations = ARGON_ITERATIONS,
46-
mCostInKibibyte = ARGON_COST,
47-
)
48-
49-
return HashedPin(
50-
hashResult.encodedOutputAsString().toByteArray().base64EncodeUrlSafe(),
51-
salt.base64EncodeUrlSafe()
52-
)
34+
return pinCrypto.hashPin(pin, deviceInfo.getDeviceIdentifier())
5335
}
5436

5537
@OptIn(ExperimentalStdlibApi::class)
5638
override suspend fun verifyPin(inputPin: String, storedHash: HashedPin): Boolean {
57-
val password = inputPin.toByteArray() + deviceInfo.getDeviceIdentifier()
58-
return argon2Kt.verify(
59-
mode = Argon2Mode.ARGON2_I,
60-
encoded = String(storedHash.hash.base64DecodeUrlSafe()),
61-
password = password,
62-
)
39+
return pinCrypto.verifyPin(inputPin, storedHash, deviceInfo.getDeviceIdentifier())
6340
}
6441

6542
/**
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.darkrockstudios.app.securecamera.usecases
2+
3+
import com.darkrockstudios.app.securecamera.camera.PhotoDef
4+
import com.darkrockstudios.app.securecamera.camera.SecureImageRepository
5+
import com.darkrockstudios.app.securecamera.security.pin.PinRepository
6+
import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
7+
8+
class AddDecoyPhotoUseCase(
9+
private val pinRepository: PinRepository,
10+
private val encryptionScheme: EncryptionScheme,
11+
private val imageRepository: SecureImageRepository,
12+
) {
13+
suspend fun addDecoyPhoto(photoDef: PhotoDef): Boolean {
14+
val ppp = pinRepository.getHashedPoisonPillPin() ?: return false
15+
val plain = pinRepository.getPlainPoisonPillPin() ?: return false
16+
val keyBytes = encryptionScheme.deriveKey(plainPin = plain, hashedPin = ppp)
17+
return imageRepository.addDecoyPhotoWithKey(photoDef, keyBytes)
18+
}
19+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package com.darkrockstudios.app.securecamera.usecases
2+
3+
import com.darkrockstudios.app.securecamera.auth.AuthorizationRepository
4+
import com.darkrockstudios.app.securecamera.preferences.HashedPin
5+
import com.darkrockstudios.app.securecamera.security.pin.PinRepository
6+
7+
class AuthorizePinUseCase(
8+
private val authManager: AuthorizationRepository,
9+
private val pinRepository: PinRepository,
10+
) {
11+
/**
12+
* Authorizes user by verifying the PIN and updates the authorization state if successful.
13+
* @param pin The PIN entered by the user
14+
* @return True if the PIN is correct, false otherwise
15+
*/
16+
suspend fun authorizePin(pin: String): HashedPin? {
17+
val hashedPin = pinRepository.getHashedPin()
18+
val isValid = pinRepository.verifySecurityPin(pin)
19+
return if (isValid && hashedPin != null) {
20+
authManager.authorizeSession()
21+
// Reset failed attempts counter on successful verification
22+
authManager.resetFailedAttempts()
23+
hashedPin
24+
} else {
25+
null
26+
}
27+
}
28+
}

0 commit comments

Comments
 (0)