Skip to content

Commit 54dfefe

Browse files
committed
Improved PIN security
- Hardware Scheme now encrypts the PIN hash - PINs are now hashed with Argon2 instead of bCrypt
1 parent 0be5b9b commit 54dfefe

23 files changed

+906
-435
lines changed

app/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ dependencies {
114114
implementation(libs.androidx.runtime.livedata)
115115
implementation(libs.bcrypt)
116116
implementation(libs.androidx.work.runtime.ktx)
117+
implementation(libs.argon2kt)
117118

118119
"fullImplementation"(libs.face.detection)
119120

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ 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.PinRepository
17+
import com.darkrockstudios.app.securecamera.security.pin.PinRepositoryHardware
18+
import com.darkrockstudios.app.securecamera.security.pin.PinRepositorySoftware
1619
import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
1720
import com.darkrockstudios.app.securecamera.security.schemes.HardwareBackedEncryptionScheme
1821
import com.darkrockstudios.app.securecamera.security.schemes.SoftwareEncryptionScheme
@@ -40,6 +43,17 @@ val appModule = module {
4043
}
4144
}
4245
} bind EncryptionScheme::class
46+
single<PinRepository> {
47+
val detector = get<SecurityLevelDetector>()
48+
when (detector.detectSecurityLevel()) {
49+
SecurityLevel.SOFTWARE ->
50+
PinRepositorySoftware(get())
51+
52+
SecurityLevel.TEE, SecurityLevel.STRONGBOX -> {
53+
PinRepositoryHardware(get(), get())
54+
}
55+
}
56+
} bind PinRepository::class
4357
singleOf(::SecurityLevelDetector)
4458

4559
single { WorkManager.getInstance(get()) }
@@ -52,6 +66,8 @@ val appModule = module {
5266
factoryOf(::VerifyPinUseCase)
5367
factoryOf(::CreatePinUseCase)
5468
factoryOf(::PinSizeUseCase)
69+
factoryOf(::RemovePoisonPillIUseCase)
70+
factoryOf(::MigratePinHash)
5571

5672
viewModelOf(::ObfuscatePhotoViewModel)
5773
viewModelOf(::ViewPhotoViewModel)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.darkrockstudios.app.securecamera
2+
3+
import kotlinx.coroutines.Job
4+
import kotlinx.coroutines.currentCoroutineContext
5+
import kotlinx.coroutines.sync.Mutex
6+
7+
class ReentrantMutex {
8+
private val mutex = Mutex()
9+
private var owner: Any? = null
10+
private var recursionCount = 0
11+
12+
suspend fun lock() {
13+
val current = currentCoroutineContext()[Job]
14+
if (owner == current) {
15+
recursionCount++
16+
return
17+
}
18+
mutex.lock()
19+
owner = current
20+
recursionCount = 1
21+
}
22+
23+
suspend fun unlock() {
24+
if (owner != currentCoroutineContext()[Job]) {
25+
throw IllegalStateException("Not the owner of the lock")
26+
}
27+
recursionCount--
28+
if (recursionCount == 0) {
29+
owner = null
30+
mutex.unlock()
31+
}
32+
}
33+
34+
suspend fun <T> withLock(action: suspend () -> T): T {
35+
lock()
36+
try {
37+
return action()
38+
} finally {
39+
unlock()
40+
}
41+
}
42+
}

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

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.darkrockstudios.app.securecamera.auth
22

33
import com.darkrockstudios.app.securecamera.preferences.AppPreferencesDataSource
44
import com.darkrockstudios.app.securecamera.preferences.HashedPin
5+
import com.darkrockstudios.app.securecamera.security.pin.PinRepository
56
import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
67
import kotlinx.coroutines.flow.MutableStateFlow
78
import kotlinx.coroutines.flow.StateFlow
@@ -13,7 +14,8 @@ import kotlin.math.pow
1314
* Manages user authorization state, including PIN verification and session expiration.
1415
*/
1516
class AuthorizationRepository(
16-
private val preferencesManager: AppPreferencesDataSource,
17+
private val preferences: AppPreferencesDataSource,
18+
private val pinRepository: PinRepository,
1719
private val encryptionScheme: EncryptionScheme,
1820
) {
1921
companion object {
@@ -26,27 +28,27 @@ class AuthorizationRepository(
2628
private var lastAuthTimeMs: Long = 0
2729

2830
suspend fun securityFailureReset() {
29-
preferencesManager.securityFailureReset()
31+
preferences.securityFailureReset()
3032
}
3133

3234
suspend fun activatePoisonPill() {
33-
preferencesManager.activatePoisonPill()
35+
pinRepository.activatePoisonPill()
3436
}
3537

3638
/**
3739
* Gets the current number of failed PIN attempts
3840
* @return The number of failed attempts
3941
*/
4042
suspend fun getFailedAttempts(): Int {
41-
return preferencesManager.getFailedPinAttempts()
43+
return preferences.getFailedPinAttempts()
4244
}
4345

4446
/**
4547
* Sets the number of failed PIN attempts
4648
* @param count The number of failed attempts to set
4749
*/
4850
suspend fun setFailedAttempts(count: Int) {
49-
preferencesManager.setFailedPinAttempts(count)
51+
preferences.setFailedPinAttempts(count)
5052
}
5153

5254
/**
@@ -59,7 +61,7 @@ class AuthorizationRepository(
5961
setFailedAttempts(newCount)
6062

6163
// Store the current timestamp as the last failed attempt time
62-
preferencesManager.setLastFailedAttemptTimestamp(System.currentTimeMillis())
64+
preferences.setLastFailedAttemptTimestamp(System.currentTimeMillis())
6365

6466
return newCount
6567
}
@@ -69,7 +71,7 @@ class AuthorizationRepository(
6971
* @return The timestamp of the last failed attempt
7072
*/
7173
suspend fun getLastFailedAttemptTimestamp(): Long {
72-
return preferencesManager.getLastFailedAttemptTimestamp()
74+
return preferences.getLastFailedAttemptTimestamp()
7375
}
7476

7577
/**
@@ -99,7 +101,7 @@ class AuthorizationRepository(
99101
*/
100102
suspend fun resetFailedAttempts() {
101103
setFailedAttempts(0)
102-
preferencesManager.setLastFailedAttemptTimestamp(0)
104+
preferences.setLastFailedAttemptTimestamp(0)
103105
}
104106

105107
/**
@@ -116,8 +118,8 @@ class AuthorizationRepository(
116118
* @return True if the PIN is correct, false otherwise
117119
*/
118120
suspend fun verifyPin(pin: String): HashedPin? {
119-
val hashedPin = preferencesManager.getHashedPin()
120-
val isValid = preferencesManager.verifySecurityPin(pin)
121+
val hashedPin = pinRepository.getHashedPin()
122+
val isValid = pinRepository.verifySecurityPin(pin)
121123
return if (isValid && hashedPin != null) {
122124
authorizeSession()
123125
// Reset failed attempts counter on successful verification
@@ -145,7 +147,7 @@ class AuthorizationRepository(
145147
return@runBlocking false
146148
}
147149

148-
val sessionTimeoutMs = preferencesManager.getSessionTimeout()
150+
val sessionTimeoutMs = preferences.getSessionTimeout()
149151
val currentTime = System.currentTimeMillis()
150152
val sessionValid = (currentTime - lastAuthTimeMs) < sessionTimeoutMs
151153

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ 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.preferences.AppPreferencesDataSource
13+
import com.darkrockstudios.app.securecamera.security.pin.PinRepository
1414
import com.darkrockstudios.app.securecamera.security.schemes.EncryptionScheme
1515
import java.io.ByteArrayOutputStream
1616
import java.io.File
@@ -22,7 +22,7 @@ import kotlin.time.toJavaInstant
2222

2323
class SecureImageRepository(
2424
private val appContext: Context,
25-
private val preferencesManager: AppPreferencesDataSource,
25+
private val pinRepository: PinRepository,
2626
internal val thumbnailCache: ThumbnailCache,
2727
private val encryptionScheme: EncryptionScheme,
2828
) {
@@ -441,8 +441,8 @@ class SecureImageRepository(
441441
getDecoyDirectory().mkdirs()
442442
val decoyFile = getDecoyFile(photoDef)
443443

444-
val ppp = preferencesManager.getHashedPoisonPillPin() ?: return false
445-
val pin = preferencesManager.getPlainPoisonPillPin() ?: return false
444+
val ppp = pinRepository.getHashedPoisonPillPin() ?: return false
445+
val pin = pinRepository.getPlainPoisonPillPin() ?: return false
446446
val ppk = encryptionScheme.deriveKey(plainPin = pin, hashedPin = ppp)
447447
encryptionScheme.encryptToFile(
448448
plain = jpgBytes,

0 commit comments

Comments
 (0)