Skip to content

Commit d0e693d

Browse files
committed
Introduce AuthorizePinUseCase
This breaks the dependency between PinRepository and AuthorizationRepository
1 parent 684cf88 commit d0e693d

9 files changed

Lines changed: 292 additions & 115 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ val appModule = module {
3838
single {
3939
AuthorizationRepository(
4040
preferences = get(),
41-
pinRepository = get(),
4241
encryptionScheme = get(),
4342
context = get(),
4443
clock = get()
@@ -77,6 +76,7 @@ val appModule = module {
7776
factoryOf(::SecurityResetUseCase)
7877
factoryOf(::PinStrengthCheckUseCase)
7978
factoryOf(::VerifyPinUseCase)
79+
factoryOf(::AuthorizePinUseCase)
8080
factoryOf(::CreatePinUseCase)
8181
factoryOf(::PinSizeUseCase)
8282
factoryOf(::RemovePoisonPillIUseCase)

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

Lines changed: 1 addition & 21 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,
@@ -115,29 +113,11 @@ class AuthorizationRepository(
115113
return true
116114
}
117115

118-
/**
119-
* Verifies the PIN and updates the authorization state if successful.
120-
* @param pin The PIN entered by the user
121-
* @return True if the PIN is correct, false otherwise
122-
*/
123-
suspend fun verifyPin(pin: String): HashedPin? {
124-
val hashedPin = pinRepository.getHashedPin()
125-
val isValid = pinRepository.verifySecurityPin(pin)
126-
return if (isValid && hashedPin != null) {
127-
authorizeSession()
128-
// Reset failed attempts counter on successful verification
129-
resetFailedAttempts()
130-
hashedPin
131-
} else {
132-
null
133-
}
134-
}
135-
136116
/**
137117
* Marks the current session as authorized and updates the last authentication time.
138118
* Also starts the SessionService to monitor session validity.
139119
*/
140-
private fun authorizeSession() {
120+
fun authorizeSession() {
141121
lastAuthTimeMs = clock.now()
142122
_isAuthorized.value = true
143123
startSessionService()
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+
}

app/src/main/kotlin/com/darkrockstudios/app/securecamera/usecases/CreatePinUseCase.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ class CreatePinUseCase(
1111
private val encryptionScheme: EncryptionScheme,
1212
private val pinRepository: PinRepository,
1313
private val preferencesDataSource: AppPreferencesDataSource,
14+
private val authorizePinUseCase: AuthorizePinUseCase
1415
) {
1516
suspend fun createPin(pin: String, schemeConfig: SchemeConfig): Boolean {
1617
pinRepository.setAppPin(pin, schemeConfig)
17-
val hashedPin = authorizationRepository.verifyPin(pin)
18+
val hashedPin = authorizePinUseCase.authorizePin(pin)
1819
return if (hashedPin != null) {
1920
authorizationRepository.createKey(pin, hashedPin)
2021
encryptionScheme.deriveAndCacheKey(pin, hashedPin)

app/src/main/kotlin/com/darkrockstudios/app/securecamera/usecases/VerifyPinUseCase.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class VerifyPinUseCase(
1111
private val pinRepository: PinRepository,
1212
private val encryptionScheme: EncryptionScheme,
1313
private val migratePinHash: MigratePinHash,
14+
private val authorizePinUseCase: AuthorizePinUseCase,
1415
) {
1516
suspend fun verifyPin(pin: String): Boolean {
1617
migratePinHash.runMigration(pin)
@@ -21,7 +22,7 @@ class VerifyPinUseCase(
2122
pinRepository.activatePoisonPill()
2223
}
2324

24-
val hashedPin = authManager.verifyPin(pin)
25+
val hashedPin = authorizePinUseCase.authorizePin(pin)
2526
return if (hashedPin != null) {
2627
encryptionScheme.deriveAndCacheKey(pin, hashedPin)
2728
authManager.resetFailedAttempts()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
package com.darkrockstudios.app.securecamera
22

33
import kotlin.time.Clock
4+
import kotlin.time.Duration
45
import kotlin.time.Instant
56

67
class TestClock(var fixedInstant: Instant) : Clock {
78
override fun now(): Instant = fixedInstant
9+
10+
fun advanceBy(duration: Duration) {
11+
fixedInstant += duration
12+
}
813
}

app/src/test/kotlin/com/darkrockstudios/app/securecamera/auth/AuthorizationManagerTest.kt

Lines changed: 16 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import com.darkrockstudios.app.securecamera.preferences.HashedPin
1010
import com.darkrockstudios.app.securecamera.security.SoftwareSchemeConfig
1111
import com.darkrockstudios.app.securecamera.security.pin.PinRepository
1212
import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
13+
import com.darkrockstudios.app.securecamera.usecases.AuthorizePinUseCase
1314
import io.mockk.coEvery
1415
import io.mockk.coVerify
1516
import io.mockk.mockk
@@ -24,6 +25,7 @@ import org.junit.Test
2425
import testutil.FakeDataStore
2526
import java.util.concurrent.TimeUnit
2627
import kotlin.time.Duration.Companion.milliseconds
28+
import kotlin.time.Duration.Companion.seconds
2729
import kotlin.time.Instant
2830

2931
@ExperimentalCoroutinesApi
@@ -32,9 +34,10 @@ class AuthorizationManagerTest {
3234
private lateinit var context: Context
3335
private lateinit var preferencesManager: AppPreferencesDataSource
3436
private lateinit var authManager: AuthorizationRepository
37+
private lateinit var authorizePinUseCase: AuthorizePinUseCase
38+
private lateinit var pinRepository: PinRepository
3539
private lateinit var dataStore: DataStore<Preferences>
3640
private lateinit var encryptionManager: EncryptionScheme
37-
private lateinit var pinRepository: PinRepository
3841
private lateinit var clock: TestClock
3942

4043
private val configJson = Json.encodeToString(SoftwareSchemeConfig)
@@ -45,49 +48,15 @@ class AuthorizationManagerTest {
4548
dataStore = FakeDataStore(emptyPreferences())
4649
preferencesManager = spyk(AppPreferencesDataSource(context, dataStore))
4750
encryptionManager = mockk(relaxed = true)
48-
pinRepository = mockk()
4951
clock = TestClock(Instant.fromEpochSeconds(1))
5052

51-
// Default mocks for PinRepository methods
53+
authManager = AuthorizationRepository(preferencesManager, encryptionManager, context, clock)
54+
55+
pinRepository = mockk()
5256
coEvery { pinRepository.getHashedPin() } returns HashedPin("hashed_pin", "salt")
5357
coEvery { pinRepository.verifySecurityPin(any()) } returns true
54-
coEvery { pinRepository.activatePoisonPill() } returns Unit
55-
coEvery { pinRepository.verifyPoisonPillPin(any()) } returns true
56-
coEvery { pinRepository.hasPoisonPillPin() } returns true
57-
58-
authManager = AuthorizationRepository(preferencesManager, pinRepository, encryptionManager, context, clock)
59-
}
6058

61-
@Test
62-
fun `verifyPin should update authorization state when PIN is valid`() = runTest {
63-
// Given
64-
val pin = "1234"
65-
preferencesManager.setAppPin(pin, configJson)
66-
67-
// When
68-
val result = authManager.verifyPin(pin)
69-
70-
// Then
71-
assertNotNull(result)
72-
assertTrue(authManager.isAuthorized.first())
73-
}
74-
75-
@Test
76-
fun `verifyPin should not update authorization state when PIN is invalid`() = runTest {
77-
// Given
78-
val correctPin = "1234"
79-
val incorrectPin = "5678"
80-
preferencesManager.setAppPin(correctPin, configJson)
81-
82-
// Mock verifySecurityPin to return false for incorrect PIN
83-
coEvery { pinRepository.verifySecurityPin(incorrectPin) } returns false
84-
85-
// When
86-
val result = authManager.verifyPin(incorrectPin)
87-
88-
// Then
89-
assertNull(result)
90-
assertFalse(authManager.isAuthorized.first())
59+
authorizePinUseCase = AuthorizePinUseCase(authManager, pinRepository)
9160
}
9261

9362
@Test
@@ -108,7 +77,7 @@ class AuthorizationManagerTest {
10877
// Given
10978
val pin = "1234"
11079
preferencesManager.setAppPin(pin, configJson)
111-
authManager.verifyPin(pin)
80+
authorizePinUseCase.authorizePin(pin)
11281

11382
// When
11483
val result = authManager.checkSessionValidity()
@@ -124,15 +93,12 @@ class AuthorizationManagerTest {
12493
val pin = "1234"
12594
preferencesManager.setAppPin(pin, configJson)
12695

127-
coEvery { pinRepository.getHashedPin() } returns null
128-
12996
// Set a very small session timeout (1 millisecond)
13097
preferencesManager.setSessionTimeout(1L)
13198

132-
authManager.verifyPin(pin)
99+
authorizePinUseCase.authorizePin(pin)
133100

134-
// Wait for the session to expire
135-
Thread.sleep(10)
101+
clock.advanceBy(1.seconds)
136102

137103
// When
138104
val result = authManager.checkSessionValidity()
@@ -147,7 +113,7 @@ class AuthorizationManagerTest {
147113
// Given
148114
val pin = "1234"
149115
preferencesManager.setAppPin(pin, configJson)
150-
authManager.verifyPin(pin)
116+
authorizePinUseCase.authorizePin(pin)
151117
assertTrue(authManager.isAuthorized.first())
152118

153119
// When
@@ -166,7 +132,7 @@ class AuthorizationManagerTest {
166132
preferencesManager.setSessionTimeout(customTimeout)
167133

168134
// When
169-
authManager.verifyPin(pin)
135+
authorizePinUseCase.authorizePin(pin)
170136

171137
// Then
172138
assertTrue(authManager.checkSessionValidity())
@@ -232,22 +198,6 @@ class AuthorizationManagerTest {
232198
assertEquals(0L, preferencesManager.getLastFailedAttemptTimestamp())
233199
}
234200

235-
@Test
236-
fun `verifyPin should reset failed attempts when PIN is valid`() = runTest {
237-
// Given
238-
val pin = "1234"
239-
preferencesManager.setAppPin(pin, configJson)
240-
preferencesManager.setFailedPinAttempts(3)
241-
preferencesManager.setLastFailedAttemptTimestamp(1000L)
242-
243-
// When
244-
val result = authManager.verifyPin(pin)
245-
246-
// Then
247-
assertNotNull(result)
248-
assertEquals(0, preferencesManager.getFailedPinAttempts())
249-
assertEquals(0L, preferencesManager.getLastFailedAttemptTimestamp())
250-
}
251201

252202
@Test
253203
fun `getLastFailedAttemptTimestamp should return the timestamp from preferences`() = runTest {
@@ -311,9 +261,9 @@ class AuthorizationManagerTest {
311261
// Given
312262
val pin = "1234"
313263

314-
// Create a new spy for preferencesManager for this test
264+
// Create a new AuthorizationRepository for this test
315265
val testAuthManager =
316-
AuthorizationRepository(preferencesManager, pinRepository, encryptionManager, context, clock)
266+
AuthorizationRepository(preferencesManager, encryptionManager, context, clock)
317267

318268
// Set up initial state
319269
preferencesManager.setAppPin(pin, configJson)
@@ -331,21 +281,6 @@ class AuthorizationManagerTest {
331281
coVerify { preferencesManager.securityFailureReset() }
332282
}
333283

334-
@Test
335-
fun `verifyPin should not update authorization state when PIN is valid but hashedPin is null`() = runTest {
336-
// Given
337-
val pin = "1234"
338-
coEvery { pinRepository.verifySecurityPin(pin) } returns true
339-
coEvery { pinRepository.getHashedPin() } returns null
340-
341-
// When
342-
val result = authManager.verifyPin(pin)
343-
344-
// Then
345-
assertNull(result)
346-
assertFalse(authManager.isAuthorized.first())
347-
coVerify { pinRepository.verifySecurityPin(pin) }
348-
}
349284

350285
@Test
351286
fun `calculateRemainingBackoffSeconds should return 0 when elapsed time exceeds backoff time`() = runTest {
@@ -379,7 +314,7 @@ class AuthorizationManagerTest {
379314
clock.fixedInstant = initialTime
380315

381316
// Authorize the session
382-
authManager.verifyPin(pin)
317+
authorizePinUseCase.authorizePin(pin)
383318
assertTrue(authManager.isAuthorized.first())
384319

385320
// Advance time by half the session timeout

0 commit comments

Comments
 (0)