Skip to content

Commit 182d833

Browse files
committed
Fix build: SecretKeyFactory.generateSecret() and Tink ProGuard rules
1 parent f53d4f2 commit 182d833

3 files changed

Lines changed: 99 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ app/.gradle/
33
app/local.properties
44
*.keystore
55
*.apk
6-
com/
6+
/com/

app/app/proguard-rules.pro

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,13 @@
9898
-keep class com.hostshield.service.ThreatIntelWorker { *; }
9999
-keep class com.hostshield.service.SafeSearchEnforcer { *; }
100100

101+
# ── v6.3: Google Tink / EncryptedSharedPreferences ────────
102+
-dontwarn com.google.errorprone.annotations.CanIgnoreReturnValue
103+
-dontwarn com.google.errorprone.annotations.CheckReturnValue
104+
-dontwarn com.google.errorprone.annotations.Immutable
105+
-dontwarn com.google.errorprone.annotations.RestrictedApi
106+
-keep class com.hostshield.data.preferences.SecureStore { *; }
107+
101108
# ── v5.0: MaxMind GeoIP2 ──────────────────────────────────
102109
-dontwarn com.maxmind.**
103110
-keep class com.maxmind.** { *; }
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package com.hostshield.data.preferences
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
import android.util.Base64
6+
import androidx.security.crypto.EncryptedSharedPreferences
7+
import androidx.security.crypto.MasterKeys
8+
import dagger.hilt.android.qualifiers.ApplicationContext
9+
import java.security.SecureRandom
10+
import javax.crypto.SecretKeyFactory
11+
import javax.crypto.spec.PBEKeySpec
12+
import javax.inject.Inject
13+
import javax.inject.Singleton
14+
15+
/**
16+
* Encrypted key-value store backed by [EncryptedSharedPreferences].
17+
*
18+
* Uses AES256_SIV for key encryption and AES256_GCM for value encryption
19+
* via the Android Keystore-backed master key.
20+
*/
21+
@Singleton
22+
class SecureStore @Inject constructor(
23+
@ApplicationContext private val context: Context
24+
) {
25+
private val prefs: SharedPreferences by lazy {
26+
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
27+
EncryptedSharedPreferences.create(
28+
"hostshield_secure_prefs",
29+
masterKeyAlias,
30+
context,
31+
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
32+
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
33+
)
34+
}
35+
36+
/** Retrieve a stored secret string, or [default] if absent. */
37+
fun getString(key: String, default: String = ""): String =
38+
prefs.getString(key, default) ?: default
39+
40+
/** Store a secret string value. */
41+
fun putString(key: String, value: String) {
42+
prefs.edit().putString(key, value).apply()
43+
}
44+
45+
/** Remove a key from the secure store. */
46+
fun remove(key: String) {
47+
prefs.edit().remove(key).apply()
48+
}
49+
50+
/** Check whether the store contains a given key. */
51+
fun contains(key: String): Boolean = prefs.contains(key)
52+
53+
// ── PBKDF2 PIN hashing ──────────────────────────────────────
54+
55+
companion object {
56+
private const val PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA256"
57+
private const val ITERATIONS = 210_000
58+
private const val KEY_LENGTH_BITS = 256
59+
private const val SALT_BYTES = 16
60+
61+
/**
62+
* Hash a raw PIN with PBKDF2-HMAC-SHA256.
63+
* Returns `"base64(salt):base64(hash)"`.
64+
*/
65+
fun hashPin(rawPin: String): String {
66+
val salt = ByteArray(SALT_BYTES).also { SecureRandom().nextBytes(it) }
67+
val spec = PBEKeySpec(rawPin.toCharArray(), salt, ITERATIONS, KEY_LENGTH_BITS)
68+
val hash = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM)
69+
.generateSecret(spec)
70+
.encoded
71+
val saltB64 = Base64.encodeToString(salt, Base64.NO_WRAP)
72+
val hashB64 = Base64.encodeToString(hash, Base64.NO_WRAP)
73+
return "$saltB64:$hashB64"
74+
}
75+
76+
/**
77+
* Verify a raw PIN against a stored `"salt:hash"` string produced by [hashPin].
78+
*/
79+
fun verifyPin(rawPin: String, stored: String): Boolean {
80+
if (!stored.contains(':')) return false
81+
val (saltB64, expectedB64) = stored.split(':', limit = 2)
82+
val salt = Base64.decode(saltB64, Base64.NO_WRAP)
83+
val spec = PBEKeySpec(rawPin.toCharArray(), salt, ITERATIONS, KEY_LENGTH_BITS)
84+
val hash = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM)
85+
.generateSecret(spec)
86+
.encoded
87+
val hashB64 = Base64.encodeToString(hash, Base64.NO_WRAP)
88+
return hashB64 == expectedB64
89+
}
90+
}
91+
}

0 commit comments

Comments
 (0)