mirror of
https://github.com/greenart7c3/Amber.git
synced 2026-10-05 19:08:23 +00:00
438 lines
16 KiB
Kotlin
438 lines
16 KiB
Kotlin
package com.greenart7c3.nostrsigner
|
|
|
|
import android.annotation.SuppressLint
|
|
import android.content.Context
|
|
import android.content.SharedPreferences
|
|
import android.util.LruCache
|
|
import androidx.compose.runtime.Immutable
|
|
import androidx.core.content.edit
|
|
import com.greenart7c3.nostrsigner.models.Account
|
|
import com.greenart7c3.nostrsigner.models.AmberSettings
|
|
import com.greenart7c3.nostrsigner.okhttp.HttpClientManager
|
|
import com.greenart7c3.nostrsigner.ui.parseBiometricsTimeType
|
|
import com.vitorpamplona.ammolite.relays.COMMON_FEED_TYPES
|
|
import com.vitorpamplona.ammolite.relays.RelaySetupInfo
|
|
import com.vitorpamplona.quartz.crypto.CryptoUtils
|
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
|
import com.vitorpamplona.quartz.encoders.HexKey
|
|
import com.vitorpamplona.quartz.encoders.hexToByteArray
|
|
import com.vitorpamplona.quartz.encoders.toHexKey
|
|
import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
|
import fr.acinq.secp256k1.jni.BuildConfig
|
|
import java.io.File
|
|
|
|
// Release mode (!BuildConfig.DEBUG) always uses encrypted preferences
|
|
// To use plaintext SharedPreferences for debugging, set this to true
|
|
// It will only apply in Debug builds
|
|
private const val DEBUG_PLAINTEXT_PREFERENCES = false
|
|
private const val DEBUG_PREFERENCES_NAME = "debug_prefs"
|
|
|
|
private object PrefKeys {
|
|
const val PIN = "pin"
|
|
const val USE_PIN = "use_pin"
|
|
const val CURRENT_ACCOUNT = "currently_logged_in_account"
|
|
const val SAVED_ACCOUNTS = "all_saved_accounts"
|
|
const val NOSTR_PRIVKEY = "nostr_privkey"
|
|
const val NOSTR_PUBKEY = "nostr_pubkey"
|
|
const val REMEMBER_APPS = "remember_apps"
|
|
const val ACCOUNT_NAME = "account_name"
|
|
const val RATIONALE = "rationale"
|
|
const val USE_PROXY = "use_proxy"
|
|
const val PROXY_PORT = "proxy_port"
|
|
const val LANGUAGE_PREFS = "languagePreferences"
|
|
const val DEFAULT_RELAYS = "default_relays"
|
|
const val DEFAULT_PROFILE_RELAYS = "default_profile_relays"
|
|
const val ENDPOINT = "endpoint"
|
|
const val PUSH_SERVER_MESSAGE = "push_server_message"
|
|
const val ALLOW_NEW_CONNECTIONS = "allow_new_connections"
|
|
const val USE_AUTH = "use_auth"
|
|
const val BIOMETRICS_TYPE = "biometrics_type"
|
|
const val LAST_BIOMETRICS_TIME = "last_biometrics_time"
|
|
const val SIGN_POLICY = "default_sign_policy"
|
|
const val SEED_WORDS2 = "seed_words"
|
|
const val PROFILE_URL = "profile_url"
|
|
const val LAST_METADATA_UPDATE = "last_metadata_update"
|
|
const val LAST_CHECK = "last_check"
|
|
const val NCRYPT_SEC = "ncrypt_sec"
|
|
}
|
|
|
|
@Immutable
|
|
data class AccountInfo(
|
|
val npub: String,
|
|
val hasPrivKey: Boolean,
|
|
)
|
|
|
|
object LocalPreferences {
|
|
private const val COMMA = ","
|
|
private var currentAccount: String? = null
|
|
private var accountCache = LruCache<String, Account>(10)
|
|
|
|
fun allSavedAccounts(context: Context): List<AccountInfo> {
|
|
return savedAccounts(context).map { npub ->
|
|
AccountInfo(
|
|
npub,
|
|
true,
|
|
)
|
|
}.toSet().toList()
|
|
}
|
|
|
|
fun currentAccount(context: Context): String? {
|
|
if (currentAccount == null) {
|
|
currentAccount = encryptedPreferences(context).getString(PrefKeys.CURRENT_ACCOUNT, null)
|
|
}
|
|
return currentAccount
|
|
}
|
|
|
|
private fun updateCurrentAccount(context: Context, npub: String) {
|
|
if (currentAccount != npub) {
|
|
currentAccount = npub
|
|
|
|
encryptedPreferences(context).edit {
|
|
apply {
|
|
putString(PrefKeys.CURRENT_ACCOUNT, npub)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun shouldShowRationale(context: Context): Boolean? {
|
|
if (!encryptedPreferences(context).contains(PrefKeys.RATIONALE)) {
|
|
return null
|
|
}
|
|
return encryptedPreferences(context).getBoolean(PrefKeys.RATIONALE, true)
|
|
}
|
|
|
|
fun updateShouldShowRationale(context: Context, value: Boolean) {
|
|
encryptedPreferences(context).edit {
|
|
apply {
|
|
putBoolean(PrefKeys.RATIONALE, value)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun saveSettingsToEncryptedStorage(settings: AmberSettings) {
|
|
val context = NostrSigner.getInstance()
|
|
encryptedPreferences(context).edit {
|
|
apply {
|
|
remove(PrefKeys.ENDPOINT)
|
|
remove(PrefKeys.PUSH_SERVER_MESSAGE)
|
|
putStringSet(PrefKeys.DEFAULT_RELAYS, settings.defaultRelays.map { it.url }.toSet())
|
|
putStringSet(PrefKeys.DEFAULT_PROFILE_RELAYS, settings.defaultProfileRelays.map { it.url }.toSet())
|
|
putLong(PrefKeys.LAST_BIOMETRICS_TIME, settings.lastBiometricsTime)
|
|
putBoolean(PrefKeys.USE_AUTH, settings.useAuth)
|
|
putInt(PrefKeys.BIOMETRICS_TYPE, settings.biometricsTimeType.screenCode)
|
|
putBoolean(PrefKeys.USE_PIN, settings.usePin)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun getLastCheck(context: Context, npub: String): Long {
|
|
return encryptedPreferences(context, npub).getLong(PrefKeys.LAST_CHECK, 0)
|
|
}
|
|
|
|
fun setLastCheck(context: Context, npub: String, time: Long) {
|
|
encryptedPreferences(context, npub).edit {
|
|
apply {
|
|
putLong(PrefKeys.LAST_CHECK, time)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun getLastMetadataUpdate(context: Context, npub: String): Long {
|
|
return encryptedPreferences(context, npub).getLong(PrefKeys.LAST_METADATA_UPDATE, 0)
|
|
}
|
|
|
|
fun setLastMetadataUpdate(context: Context, npub: String, time: Long) {
|
|
encryptedPreferences(context, npub).edit {
|
|
apply {
|
|
putLong(PrefKeys.LAST_METADATA_UPDATE, time)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun loadPinFromEncryptedStorage(): String? {
|
|
val context = NostrSigner.getInstance()
|
|
return encryptedPreferences(context).getString(PrefKeys.PIN, null)
|
|
}
|
|
|
|
fun loadProfileUrlFromEncryptedStorage(npub: String): String? {
|
|
val context = NostrSigner.getInstance()
|
|
return encryptedPreferences(context, npub).getString(PrefKeys.PROFILE_URL, null)
|
|
}
|
|
|
|
fun saveProfileUrlToEncryptedStorage(profileUrl: String?, npub: String) {
|
|
val context = NostrSigner.getInstance()
|
|
encryptedPreferences(context, npub).edit {
|
|
apply {
|
|
if (profileUrl == null) {
|
|
remove(PrefKeys.PROFILE_URL)
|
|
} else {
|
|
putString(PrefKeys.PROFILE_URL, profileUrl)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fun savePinToEncryptedStorage(pin: String?) {
|
|
val context = NostrSigner.getInstance()
|
|
encryptedPreferences(context).edit {
|
|
apply {
|
|
if (pin == null) {
|
|
remove(PrefKeys.PIN)
|
|
} else {
|
|
putString(PrefKeys.PIN, pin)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun loadSettingsFromEncryptedStorage(): AmberSettings {
|
|
val context = NostrSigner.getInstance()
|
|
|
|
encryptedPreferences(context).apply {
|
|
return AmberSettings(
|
|
defaultRelays = getStringSet(PrefKeys.DEFAULT_RELAYS, null)?.map {
|
|
RelaySetupInfo(it, read = true, write = true, feedTypes = COMMON_FEED_TYPES)
|
|
} ?: listOf(RelaySetupInfo("wss://relay.nsec.app", read = true, write = true, feedTypes = COMMON_FEED_TYPES)),
|
|
defaultProfileRelays = getStringSet(PrefKeys.DEFAULT_PROFILE_RELAYS, null)?.map {
|
|
RelaySetupInfo(it, read = true, write = false, feedTypes = COMMON_FEED_TYPES)
|
|
} ?: listOf(
|
|
RelaySetupInfo("wss://relay.nostr.band", read = true, write = false, feedTypes = COMMON_FEED_TYPES),
|
|
RelaySetupInfo("wss://purplepag.es", read = true, write = false, feedTypes = COMMON_FEED_TYPES),
|
|
),
|
|
lastBiometricsTime = getLong(PrefKeys.LAST_BIOMETRICS_TIME, 0),
|
|
useAuth = getBoolean(PrefKeys.USE_AUTH, false),
|
|
biometricsTimeType = parseBiometricsTimeType(getInt(PrefKeys.BIOMETRICS_TYPE, 0)),
|
|
usePin = getBoolean(PrefKeys.USE_PIN, false),
|
|
)
|
|
}
|
|
}
|
|
|
|
private var savedAccounts: List<String>? = null
|
|
|
|
private fun savedAccounts(context: Context): List<String> {
|
|
if (savedAccounts == null) {
|
|
savedAccounts = encryptedPreferences(context)
|
|
.getString(PrefKeys.SAVED_ACCOUNTS, null)?.split(COMMA) ?: listOf()
|
|
}
|
|
return savedAccounts!!
|
|
}
|
|
|
|
private fun updateSavedAccounts(context: Context, accounts: List<String>) {
|
|
if (savedAccounts != accounts) {
|
|
savedAccounts = accounts
|
|
|
|
encryptedPreferences(context).edit {
|
|
apply {
|
|
putString(PrefKeys.SAVED_ACCOUNTS, accounts.joinToString(COMMA).ifBlank { null })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun getDirPath(context: Context): String {
|
|
return "${context.filesDir.parent}/shared_prefs/"
|
|
}
|
|
|
|
private fun addAccount(context: Context, npub: String) {
|
|
val accounts = savedAccounts(context).toMutableList()
|
|
if (npub !in accounts) {
|
|
accounts.add(npub)
|
|
updateSavedAccounts(context, accounts)
|
|
}
|
|
}
|
|
|
|
private fun setCurrentAccount(context: Context, account: Account) {
|
|
updateCurrentAccount(context, account.npub)
|
|
addAccount(context, account.npub)
|
|
}
|
|
|
|
fun switchToAccount(context: Context, npub: String) {
|
|
updateCurrentAccount(context, npub)
|
|
}
|
|
|
|
fun containsAccount(context: Context, npub: String): Boolean {
|
|
return savedAccounts(context).contains(npub)
|
|
}
|
|
|
|
/**
|
|
* Removes the account from the app level shared preferences
|
|
*/
|
|
private fun removeAccount(context: Context, npub: String) {
|
|
val accounts = savedAccounts(context).toMutableList()
|
|
if (accounts.remove(npub)) {
|
|
updateSavedAccounts(context, accounts)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Deletes the npub-specific shared preference file
|
|
*/
|
|
private fun deleteUserPreferenceFile(context: Context, npub: String) {
|
|
val prefsDir = File(getDirPath(context))
|
|
prefsDir.list()?.forEach {
|
|
if (it.contains(npub)) {
|
|
File(prefsDir, it).delete()
|
|
}
|
|
}
|
|
}
|
|
|
|
private fun encryptedPreferences(
|
|
context: Context,
|
|
npub: String? = null,
|
|
): SharedPreferences {
|
|
return if (BuildConfig.DEBUG && DEBUG_PLAINTEXT_PREFERENCES) {
|
|
val preferenceFile = if (npub == null) DEBUG_PREFERENCES_NAME else "${DEBUG_PREFERENCES_NAME}_$npub"
|
|
context.getSharedPreferences(preferenceFile, Context.MODE_PRIVATE)
|
|
} else {
|
|
return EncryptedStorage.preferences(npub, context)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Clears the preferences for a given npub, deletes the preferences xml file,
|
|
* and switches the user to the first account in the list if it exists
|
|
*
|
|
* We need to use `commit()` to write changes to disk and release the file
|
|
* lock so that it can be deleted. If we use `apply()` there is a race
|
|
* condition and the file will probably not be deleted
|
|
*/
|
|
@SuppressLint("ApplySharedPref")
|
|
fun updatePrefsForLogout(npub: String, context: Context): Boolean {
|
|
accountCache.remove(npub)
|
|
val userPrefs = encryptedPreferences(context, npub)
|
|
userPrefs.edit(commit = true) { clear() }
|
|
removeAccount(context, npub)
|
|
deleteUserPreferenceFile(context, npub)
|
|
|
|
if (savedAccounts(context).isEmpty()) {
|
|
val appPrefs = encryptedPreferences(context)
|
|
appPrefs.edit(commit = true) { clear() }
|
|
return true
|
|
} else if (currentAccount(context) == npub) {
|
|
updateCurrentAccount(context, savedAccounts(context).elementAt(0))
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
fun updatePrefsForLogin(context: Context, account: Account) {
|
|
setCurrentAccount(context, account)
|
|
saveToEncryptedStorage(context, account)
|
|
}
|
|
|
|
fun saveToEncryptedStorage(context: Context, account: Account) {
|
|
val prefs = encryptedPreferences(context = context, account.npub)
|
|
prefs.edit {
|
|
apply {
|
|
account.signer.keyPair.privKey.let { putString(PrefKeys.NOSTR_PRIVKEY, it?.toHexKey()) }
|
|
account.signer.keyPair.pubKey.let { putString(PrefKeys.NOSTR_PUBKEY, it.toHexKey()) }
|
|
putBoolean(PrefKeys.USE_PROXY, account.useProxy)
|
|
putInt(PrefKeys.PROXY_PORT, account.proxyPort)
|
|
putString(PrefKeys.ACCOUNT_NAME, account.name)
|
|
putString(PrefKeys.LANGUAGE_PREFS, account.language)
|
|
putBoolean(PrefKeys.ALLOW_NEW_CONNECTIONS, account.allowNewConnections)
|
|
putInt(PrefKeys.SIGN_POLICY, account.signPolicy)
|
|
putString(PrefKeys.SEED_WORDS2, account.seedWords.joinToString(separator = " ") { it })
|
|
}
|
|
}
|
|
}
|
|
|
|
fun loadFromEncryptedStorage(context: Context): Account? {
|
|
return loadFromEncryptedStorage(context, currentAccount(context))
|
|
}
|
|
|
|
fun setAccountName(
|
|
context: Context,
|
|
npub: String,
|
|
value: String,
|
|
) {
|
|
encryptedPreferences(context, npub).edit {
|
|
apply {
|
|
putString(PrefKeys.ACCOUNT_NAME, value)
|
|
}
|
|
}
|
|
accountCache.get(npub)?.let {
|
|
it.name = value
|
|
}
|
|
}
|
|
|
|
fun getAccountName(context: Context, npub: String): String {
|
|
encryptedPreferences(context, npub).apply {
|
|
return getString(PrefKeys.ACCOUNT_NAME, "") ?: ""
|
|
}
|
|
}
|
|
|
|
suspend fun saveNcryptsec(
|
|
npub: String,
|
|
privateKeyHex: HexKey,
|
|
password: String,
|
|
) {
|
|
val context = NostrSigner.getInstance()
|
|
val ncryptsec = CryptoUtils.nip49.encrypt(privateKeyHex, password)
|
|
encryptedPreferences(context, npub).edit {
|
|
apply {
|
|
putString(PrefKeys.NCRYPT_SEC, ncryptsec)
|
|
}
|
|
}
|
|
}
|
|
|
|
fun updateProxy(
|
|
context: Context,
|
|
useProxy: Boolean,
|
|
port: Int,
|
|
) {
|
|
val npub = currentAccount(context) ?: return
|
|
encryptedPreferences(context, npub).edit {
|
|
apply {
|
|
putBoolean(PrefKeys.USE_PROXY, useProxy)
|
|
putInt(PrefKeys.PROXY_PORT, port)
|
|
}
|
|
}
|
|
accountCache.get(npub)?.let {
|
|
it.useProxy = useProxy
|
|
it.proxyPort = port
|
|
}
|
|
HttpClientManager.setDefaultProxyOnPort(port)
|
|
}
|
|
|
|
fun loadFromEncryptedStorage(context: Context, npub: String?): Account? {
|
|
if (npub != null && accountCache.get(npub) != null) {
|
|
return accountCache.get(npub)
|
|
}
|
|
encryptedPreferences(context, npub).apply {
|
|
val pubKey = getString(PrefKeys.NOSTR_PUBKEY, null) ?: return null
|
|
val privKey = getString(PrefKeys.NOSTR_PRIVKEY, null)
|
|
|
|
this.edit {
|
|
apply {
|
|
remove(PrefKeys.REMEMBER_APPS)
|
|
}
|
|
}
|
|
val name = getString(PrefKeys.ACCOUNT_NAME, "") ?: ""
|
|
val useProxy = getBoolean(PrefKeys.USE_PROXY, false)
|
|
val proxyPort = getInt(PrefKeys.PROXY_PORT, 9050)
|
|
val language = getString(PrefKeys.LANGUAGE_PREFS, null)
|
|
val allowNewConnections = getBoolean(PrefKeys.ALLOW_NEW_CONNECTIONS, false)
|
|
val signPolicy = getInt(PrefKeys.SIGN_POLICY, 1)
|
|
val savedSeedWords = getString(PrefKeys.SEED_WORDS2, null)
|
|
val seedWords = savedSeedWords?.split(" ")?.toSet() ?: emptySet()
|
|
|
|
HttpClientManager.setDefaultProxyOnPort(proxyPort)
|
|
val account =
|
|
Account(
|
|
signer = NostrSignerInternal(KeyPair(privKey = privKey?.hexToByteArray(), pubKey = pubKey.hexToByteArray())),
|
|
name = name,
|
|
useProxy = useProxy,
|
|
proxyPort = proxyPort,
|
|
language = language,
|
|
allowNewConnections = allowNewConnections,
|
|
signPolicy = signPolicy,
|
|
seedWords = seedWords,
|
|
)
|
|
accountCache.put(npub, account)
|
|
return account
|
|
}
|
|
}
|
|
}
|