add: implement encrypted shared preferences

This commit is contained in:
Tobias 2025-11-27 14:07:12 +01:00
parent 8178ef2d83
commit 295da7963e
2 changed files with 40 additions and 9 deletions

View file

@ -129,6 +129,9 @@ dependencies {
// Better click handling for links
implementation 'me.saket:better-link-movement-method:2.2.0'
// Encrypted SharedPreferences for secure storage
implementation 'androidx.security:security-crypto:1.1.0'
// Markdown
implementation 'io.noties.markwon:core:4.6.2'
implementation 'io.noties.markwon:image-picasso:4.6.2'

View file

@ -7,6 +7,8 @@ import android.os.Build
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.*
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import io.heckel.ntfy.util.Log
import io.heckel.ntfy.util.validUrl
import java.util.concurrent.ConcurrentHashMap
@ -14,7 +16,11 @@ import java.util.concurrent.atomic.AtomicLong
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
class Repository(private val sharedPrefs: SharedPreferences, private val database: Database) {
class Repository(
private val sharedPrefs: SharedPreferences,
private val database: Database,
private val context: Context
) {
private val subscriptionDao = database.subscriptionDao()
private val notificationDao = database.notificationDao()
private val userDao = database.userDao()
@ -26,6 +32,26 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
val detailViewSubscriptionId = AtomicLong(0L) // Omg, what a hack ...
val mediaPlayer = MediaPlayer()
// Encrypted SharedPreferences for custom headers
private val encryptedPrefs: SharedPreferences by lazy {
try {
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
EncryptedSharedPreferences.create(
context,
ENCRYPTED_PREFS_FILE_NAME,
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
} catch (e: Exception) {
Log.e(TAG, "Failed to create encrypted preferences, falling back to regular SharedPreferences", e)
context.getSharedPreferences(ENCRYPTED_PREFS_FILE_NAME, Context.MODE_PRIVATE)
}
}
init {
Log.d(TAG, "Created $this")
}
@ -484,13 +510,13 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
}
fun getCustomHeaders(): Map<String, String> {
val json = sharedPrefs.getString(SHARED_PREFS_CUSTOM_HEADERS, null)
val json = encryptedPrefs.getString(ENCRYPTED_PREFS_CUSTOM_HEADERS_KEY, null)
return if (json != null) {
try {
val type = object : TypeToken<Map<String, String>>() {}.type
Gson().fromJson(json, type) ?: emptyMap()
} catch (e: Exception) {
Log.w("Repository", "Failed to parse custom headers", e)
Log.w(TAG, "Failed to parse custom headers", e)
emptyMap()
}
} else {
@ -506,9 +532,9 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
}
if (json == null) {
sharedPrefs.edit().remove(SHARED_PREFS_CUSTOM_HEADERS).apply()
encryptedPrefs.edit().remove(ENCRYPTED_PREFS_CUSTOM_HEADERS_KEY).apply()
} else {
sharedPrefs.edit().putString(SHARED_PREFS_CUSTOM_HEADERS, json).apply()
encryptedPrefs.edit().putString(ENCRYPTED_PREFS_CUSTOM_HEADERS_KEY, json).apply()
}
}
@ -537,7 +563,9 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
const val SHARED_PREFS_UNIFIED_PUSH_BASE_URL = "UnifiedPushBaseURL" // Legacy key required for migration to DefaultBaseURL
const val SHARED_PREFS_DEFAULT_BASE_URL = "DefaultBaseURL"
const val SHARED_PREFS_LAST_TOPICS = "LastTopics"
const val SHARED_PREFS_CUSTOM_HEADERS = "CustomHeaders"
const val ENCRYPTED_PREFS_FILE_NAME = "SecurePreferences"
const val ENCRYPTED_PREFS_CUSTOM_HEADERS_KEY = "CustomHeaders"
private const val LAST_TOPICS_COUNT = 3
@ -584,12 +612,12 @@ class Repository(private val sharedPrefs: SharedPreferences, private val databas
fun getInstance(context: Context): Repository {
val database = Database.getInstance(context.applicationContext)
val sharedPrefs = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
return getInstance(sharedPrefs, database)
return getInstance(sharedPrefs, database, context.applicationContext)
}
private fun getInstance(sharedPrefs: SharedPreferences, database: Database): Repository {
private fun getInstance(sharedPrefs: SharedPreferences, database: Database, context: Context): Repository {
return synchronized(Repository::class) {
val newInstance = instance ?: Repository(sharedPrefs, database)
val newInstance = instance ?: Repository(sharedPrefs, database, context)
instance = newInstance
newInstance
}