Skip to content

Commit d65fbc3

Browse files
feat: Migrate PreferencesManager to DataStore with Encrypted Storage
1 parent e7fc168 commit d65fbc3

File tree

5 files changed

+232
-175
lines changed

5 files changed

+232
-175
lines changed

app/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ dependencies {
133133
ksp "androidx.room:room-compiler:$room_version"
134134

135135
implementation "androidx.core:core-splashscreen:$core_splashscreen_version"
136+
implementation "androidx.datastore:datastore-preferences:1.2.0"
136137

137138
api platform("com.google.firebase:firebase-bom:$firebase_version")
138139
api "com.google.firebase:firebase-messaging"
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package org.openedx.app.data.storage
2+
3+
import android.security.keystore.KeyGenParameterSpec
4+
import android.security.keystore.KeyProperties
5+
import android.util.Base64
6+
import java.security.KeyStore
7+
import javax.crypto.Cipher
8+
import javax.crypto.KeyGenerator
9+
import javax.crypto.SecretKey
10+
import javax.crypto.spec.GCMParameterSpec
11+
12+
class DataStoreEncryption {
13+
14+
private val keyStore: KeyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
15+
16+
private fun getOrCreateSecretKey(): SecretKey {
17+
keyStore.getEntry(KEY_ALIAS, null)?.let { entry ->
18+
return (entry as KeyStore.SecretKeyEntry).secretKey
19+
}
20+
21+
val keyGenerator = KeyGenerator.getInstance(
22+
KeyProperties.KEY_ALGORITHM_AES,
23+
ANDROID_KEYSTORE
24+
)
25+
26+
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
27+
KEY_ALIAS,
28+
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
29+
)
30+
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
31+
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
32+
.setKeySize(256)
33+
.build()
34+
35+
keyGenerator.init(keyGenParameterSpec)
36+
return keyGenerator.generateKey()
37+
}
38+
39+
fun encrypt(plainText: String): String {
40+
if (plainText.isEmpty()) return ""
41+
42+
val cipher = Cipher.getInstance(TRANSFORMATION)
43+
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateSecretKey())
44+
45+
val iv = cipher.iv
46+
val encryptedBytes = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
47+
48+
val combined = ByteArray(iv.size + encryptedBytes.size)
49+
System.arraycopy(iv, 0, combined, 0, iv.size)
50+
System.arraycopy(encryptedBytes, 0, combined, iv.size, encryptedBytes.size)
51+
52+
return Base64.encodeToString(combined, Base64.NO_WRAP)
53+
}
54+
55+
fun decrypt(encryptedText: String): String {
56+
if (encryptedText.isEmpty()) return ""
57+
58+
return try {
59+
val combined = Base64.decode(encryptedText, Base64.NO_WRAP)
60+
61+
val iv = combined.copyOfRange(0, GCM_IV_LENGTH)
62+
val encryptedBytes = combined.copyOfRange(GCM_IV_LENGTH, combined.size)
63+
64+
val cipher = Cipher.getInstance(TRANSFORMATION)
65+
val spec = GCMParameterSpec(GCM_TAG_LENGTH * 8, iv)
66+
cipher.init(Cipher.DECRYPT_MODE, getOrCreateSecretKey(), spec)
67+
68+
String(cipher.doFinal(encryptedBytes), Charsets.UTF_8)
69+
} catch (e: Exception) {
70+
""
71+
}
72+
}
73+
74+
companion object {
75+
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
76+
private const val KEY_ALIAS = "openedx_datastore_key"
77+
private const val TRANSFORMATION = "AES/GCM/NoPadding"
78+
private const val GCM_IV_LENGTH = 12
79+
private const val GCM_TAG_LENGTH = 16
80+
}
81+
}

0 commit comments

Comments
 (0)