Skip to content

Commit 1d6048d

Browse files
feat: Migrate PreferencesManager to DataStore with Encrypted Storage (#477)
* feat: Migrate PreferencesManager to DataStore with Encrypted Storage * fix: according to PR review
1 parent 4c59055 commit 1d6048d

File tree

9 files changed

+423
-181
lines changed

9 files changed

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

0 commit comments

Comments
 (0)