feat: migrate secret management to datastore proto and tink

This commit is contained in:
2026-06-02 18:15:22 +02:00
parent 2be68d38a5
commit e8b3f898e8
16 changed files with 328 additions and 128 deletions
+40 -1
View File
@@ -1,7 +1,17 @@
import com.google.protobuf.gradle.*
configurations.all {
resolutionStrategy {
force("com.google.protobuf:protobuf-javalite:${libs.versions.protobuf.get()}")
force("com.google.protobuf:protobuf-kotlin-lite:${libs.versions.protobuf.get()}")
}
}
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
alias(libs.plugins.ksp)
alias(libs.plugins.protobuf)
}
android {
@@ -39,6 +49,13 @@ android {
compose = true
buildConfig = true
}
sourceSets {
getByName("main") {
proto {
srcDir("src/main/proto")
}
}
}
}
dependencies {
@@ -72,6 +89,11 @@ dependencies {
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)
// DataStore + Proto + Tink
implementation(libs.datastore.proto)
implementation(libs.protobuf.kotlin.lite)
implementation(libs.tink.android)
// DataStore
implementation(libs.androidx.datastore.preferences)
@@ -99,5 +121,22 @@ dependencies {
// animations
implementation(libs.androidx.compose.animation)
}
}
protobuf {
protoc {
artifact = libs.protobuf.protoc.get().toString()
}
generateProtoTasks {
all().configureEach { // no "task ->" here
builtins {
id("java") {
option("lite")
}
id("kotlin") {
option("lite")
}
}
}
}
}
@@ -3,7 +3,6 @@ package com.bitcointxoko.gudariwallet
import android.Manifest
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.PowerManager
@@ -17,6 +16,9 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.*
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import com.bitcointxoko.gudariwallet.security.EncryptedSecretStore
import com.bitcointxoko.gudariwallet.service.WalletNotificationService
import com.bitcointxoko.gudariwallet.ui.OnboardingScreen
@@ -27,6 +29,8 @@ import com.bitcointxoko.gudariwallet.ui.theme.GudariWalletTheme
import com.bitcointxoko.gudariwallet.util.NotificationHelper
import com.bitcointxoko.gudariwallet.util.IntentUtils
import kotlinx.coroutines.flow.MutableSharedFlow
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
class MainActivity : AppCompatActivity() {
@@ -57,8 +61,13 @@ class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
secretStore = EncryptedSecretStore(applicationContext)
val credentialsState = secretStore.credentials
.stateIn(
scope = lifecycleScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null
)
// Must be first — before any notification is posted
NotificationHelper.createChannels(applicationContext)
@@ -72,7 +81,10 @@ class MainActivity : AppCompatActivity() {
setContent {
GudariWalletTheme {
var isOnboarded by remember { mutableStateOf(secretStore.isOnboarded) }
val credentials by credentialsState.collectAsState()
// Treat null (loading) the same as not-onboarded to avoid a flash
val isOnboarded = credentials?.isOnboarded == true
if (isOnboarded) {
LaunchedEffect(Unit) {
@@ -80,20 +92,20 @@ class MainActivity : AppCompatActivity() {
requestNotificationPermissionIfNeeded()
promptBatteryOptimizationIfNeeded()
}
// Pass pendingPaymentHash so WalletScreen can consume it
WalletScreen(
vm = vm,
pendingPaymentHash = pendingPaymentHash,
vm = vm,
pendingPaymentHash = pendingPaymentHash,
pendingPaymentUri = pendingPaymentUri
)
} else {
OnboardingScreen(
secretStore = secretStore,
onComplete = { isOnboarded = true }
onComplete = { /* no-op: Flow update triggers recomposition automatically */ }
)
}
}
}
}
// Called when the activity is already running and a notification is tapped
@@ -1,7 +1,11 @@
package com.bitcointxoko.gudariwallet.api
import com.bitcointxoko.gudariwallet.BuildConfig
import com.bitcointxoko.gudariwallet.security.SecretStore
import com.bitcointxoko.gudariwallet.util.WalletConstants
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
@@ -25,10 +29,14 @@ object LNbitsClient {
}
.build()
fun create(baseUrl: String): LNbitsApi {
val url = if (baseUrl.endsWith("/")) baseUrl else "$baseUrl/"
fun create(secrets: SecretStore): LNbitsApi {
val baseUrl = runBlocking(Dispatchers.IO) {
secrets.credentials.first().baseUrl
}.ifBlank { "https://placeholder.invalid" }
val normalizedUrl = if (baseUrl.endsWith("/")) baseUrl else "$baseUrl/"
return Retrofit.Builder()
.baseUrl(url)
.baseUrl(normalizedUrl)
.client(httpClient)
.addConverterFactory(GsonConverterFactory.create(GsonProvider.gson))
.build()
@@ -18,7 +18,7 @@ class LNbitsWalletRepository(
// ── Balance ───────────────────────────────────────────────────────────────
override suspend fun getBalance(): WalletBalance {
val response = api.getWallet(secrets.invoiceKey)
val response = api.getWallet(secrets.invoiceKey())
return WalletBalance(sats = response.balance / WalletConstants.MSAT_PER_SAT)
}
@@ -26,7 +26,7 @@ class LNbitsWalletRepository(
override suspend fun createInvoice(amountSats: Long, memo: String): CreatedInvoice {
val response = api.createInvoice(
secrets.invoiceKey,
secrets.invoiceKey(),
CreateInvoiceRequest(
out = false,
amount = amountSats,
@@ -41,13 +41,13 @@ class LNbitsWalletRepository(
}
override suspend fun isPaymentReceived(paymentHash: String): Boolean {
return api.checkPayment(secrets.invoiceKey, paymentHash).paid
return api.checkPayment(secrets.invoiceKey(), paymentHash).paid
}
// ── Send ──────────────────────────────────────────────────────────────────
override suspend fun decodeBolt11(bolt11: String): DecodedBolt11 {
val response = api.decodeInvoice(secrets.invoiceKey, DecodeInvoiceRequest(data = bolt11))
val response = api.decodeInvoice(secrets.invoiceKey(), DecodeInvoiceRequest(data = bolt11))
val amountSats = response.amountMsat
?.takeIf { it > 0L }
?.div(WalletConstants.MSAT_PER_SAT)
@@ -64,7 +64,7 @@ class LNbitsWalletRepository(
override suspend fun scanLnurl(code: String): ScannedLnurl {
val r = api.lnurlScan(secrets.invoiceKey, code)
val r = api.lnurlScan(secrets.invoiceKey(), code)
return ScannedLnurl(
tag = r.tag,
callback = r.callback,
@@ -169,7 +169,7 @@ class LNbitsWalletRepository(
override suspend fun payBolt11(bolt11: String): PaymentConfirmation {
val response = api.payInvoice(
secrets.adminKey,
secrets.adminKey(),
PayInvoiceRequest(out = true, bolt11 = bolt11)
)
return PaymentConfirmation(paymentHash = response.paymentHash)
@@ -182,7 +182,7 @@ class LNbitsWalletRepository(
comment: String?
): PaymentConfirmation {
val response = api.payLnurl(
secrets.adminKey,
secrets.adminKey(),
PayLnurlRequest(
res = LnurlPayRes(
tag = rawRes.tag,
@@ -220,10 +220,10 @@ class LNbitsWalletRepository(
}
override suspend fun getLightningAddress(): String? {
val links = api.getLnurlpLinks(secrets.invoiceKey, allWallets = false)
val links = api.getLnurlpLinks(secrets.invoiceKey(), allWallets = false)
val username = links.firstOrNull { !it.username.isNullOrBlank() }?.username
?: return null
val domain = secrets.baseUrl
val domain = secrets.baseUrl()
.removePrefix("https://")
.removePrefix("http://")
.trimEnd('/')
@@ -246,10 +246,10 @@ class LNbitsWalletRepository(
// ── History ───────────────────────────────────────────────────────────────
override suspend fun getPayments(offset: Int, limit: Int): List<PaymentRecord> {
return api.getPayments(secrets.invoiceKey, limit, offset)
return api.getPayments(secrets.invoiceKey(), limit, offset)
}
override suspend fun getPaymentDetail(checkingId: String): PaymentDetailResponse {
return api.getPaymentDetail(secrets.invoiceKey, checkingId)
return api.getPaymentDetail(secrets.invoiceKey(), checkingId)
}
}
@@ -0,0 +1,54 @@
package com.bitcointxoko.gudariwallet.security
import android.content.Context
import android.util.Log
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.migrations.SharedPreferencesMigration
import androidx.datastore.migrations.SharedPreferencesView
import com.bitcointxoko.gudariwallet.util.WalletConstants
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import java.io.File
import androidx.core.content.edit
/**
* Singleton DataStore for encrypted credentials.
*
* On first access after install/update, [SharedPreferencesMigration] will
* automatically read the old SharedPreferences values (decrypting them via
* [SecretManager]), write them into this DataStore, and mark migration done.
*
* The old SharedPreferences XML file is NOT deleted automatically — call
* [cleanUpLegacyPrefs] once you've confirmed migration succeeded.
*/
object CredentialsDataStore {
private const val TAG = "CredentialsDataStore"
@Volatile
private var instance: DataStore<Credentials>? = null
fun getInstance(context: Context): DataStore<Credentials> {
return instance ?: synchronized(this) {
instance ?: create(context.applicationContext).also { instance = it }
}
}
private fun create(appContext: Context): DataStore<Credentials> {
val streamingAead = CredentialsTinkManager.getOrCreateStreamingAead(appContext)
val serializer = CredentialsSerializer(streamingAead)
return DataStoreFactory.create(
serializer = serializer,
produceFile = {
File(appContext.filesDir, "datastore/${WalletConstants.CREDENTIALS_DATASTORE_FILE}")
},
corruptionHandler = ReplaceFileCorruptionHandler { Credentials.getDefaultInstance() },
scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
)
}
}
@@ -0,0 +1,53 @@
package com.bitcointxoko.gudariwallet.security
import android.util.Log
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.google.crypto.tink.StreamingAead
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.InputStream
import java.io.OutputStream
class CredentialsSerializer(
private val streamingAead: StreamingAead
) : Serializer<Credentials> {
private val TAG = "CredentialsSerializer"
override val defaultValue: Credentials = Credentials.getDefaultInstance()
override suspend fun readFrom(input: InputStream): Credentials =
withContext(Dispatchers.IO) {
try {
val result = Credentials.parseFrom(
streamingAead.newDecryptingStream(input, AAD)
)
Log.d(TAG, "Credentials decrypted successfully — isOnboarded: ${result.isOnboarded}")
result
} catch (e: Exception) {
Log.e(TAG, "Decryption failed — DataStore may be corrupt or key rotated", e)
throw CorruptionException("Cannot read/decrypt credentials DataStore", e)
}
}
override suspend fun writeTo(t: Credentials, output: OutputStream): Unit =
withContext(Dispatchers.IO) {
try {
val encryptingStream = streamingAead.newEncryptingStream(output, AAD)
t.writeTo(encryptingStream) // returns Int — discarded by Unit return type
encryptingStream.close()
Log.d(TAG, "Credentials encrypted and written — isOnboarded: ${t.isOnboarded}, " +
"hasInvoiceKey: ${t.invoiceKey.isNotBlank()}, " +
"hasAdminKey: ${t.adminKey.isNotBlank()}, " +
"hasBaseUrl: ${t.baseUrl.isNotBlank()}")
} catch (e: Exception) {
Log.e(TAG, "Encryption/write failed", e)
throw e
}
}
companion object {
private val AAD = "credentials_store".toByteArray(Charsets.UTF_8)
}
}
@@ -0,0 +1,45 @@
package com.bitcointxoko.gudariwallet.security
import android.content.Context
import android.util.Log
import com.bitcointxoko.gudariwallet.util.WalletConstants
import com.google.crypto.tink.StreamingAead
import com.google.crypto.tink.integration.android.AndroidKeysetManager
import com.google.crypto.tink.streamingaead.StreamingAeadConfig
import com.google.crypto.tink.KeyTemplates
/**
* Manages the Tink keyset used to encrypt the credentials DataStore file.
*
* The keyset itself is stored in SharedPreferences, encrypted under an
* Android Keystore master key — so the key material never leaves secure hardware.
*/
object CredentialsTinkManager {
private const val TAG = "CredentialsTinkManager"
init {
// Register all StreamingAead primitives once at class load time
StreamingAeadConfig.register()
Log.d(TAG, "StreamingAeadConfig registered")
}
fun getOrCreateStreamingAead(context: Context): StreamingAead {
return try {
val handle = AndroidKeysetManager.Builder()
.withSharedPref(context, WalletConstants.TINK_KEYSET_KEY, WalletConstants.TINK_KEYSET_PREF)
.withKeyTemplate(KeyTemplates.get("AES256_GCM_HKDF_4KB"))
.withMasterKeyUri(WalletConstants.TINK_MASTER_KEY_URI)
.build()
.keysetHandle
Log.i(TAG, "Keyset loaded — key count: ${handle.size()}, primary key ID: ${handle.primary.id}")
handle.getPrimitive(StreamingAead::class.java).also {
Log.i(TAG, "StreamingAead primitive acquired successfully")
}
} catch (e: Exception) {
Log.e(TAG, "Failed to initialise StreamingAead — Keystore may be unavailable", e)
throw e
}
}
}
@@ -1,58 +0,0 @@
package com.bitcointxoko.gudariwallet.security
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import android.util.Base64
object SecretManager {
private const val KEY_ALIAS = "lnbits_api_key"
private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private const val GCM_TAG_LENGTH = 128
/** Generate (or retrieve) the AES key in the Keystore */
private fun getOrCreateKey(): SecretKey {
val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) }
keyStore.getKey(KEY_ALIAS, null)?.let { return it as SecretKey }
val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER)
keyGen.init(
KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setKeySize(256)
// Require device lock screen to be set up
.setUserAuthenticationRequired(false)
.build()
)
return keyGen.generateKey()
}
/** Encrypt a plaintext string; returns Base64(IV + ciphertext) */
fun encrypt(plaintext: String): String {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
val iv = cipher.iv
val ciphertext = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))
val combined = iv + ciphertext
return Base64.encodeToString(combined, Base64.DEFAULT)
}
/** Decrypt a Base64(IV + ciphertext) string */
fun decrypt(encoded: String): String {
val combined = Base64.decode(encoded, Base64.DEFAULT)
val iv = combined.copyOfRange(0, 12)
val ciphertext = combined.copyOfRange(12, combined.size)
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), GCMParameterSpec(GCM_TAG_LENGTH, iv))
return String(cipher.doFinal(ciphertext), Charsets.UTF_8)
}
}
@@ -1,46 +1,61 @@
package com.bitcointxoko.gudariwallet.security
import android.content.Context
import com.bitcointxoko.gudariwallet.util.WalletConstants
import android.util.Log
import androidx.datastore.core.DataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
// Interface — lets you swap in a FakeSecretStore in tests
interface SecretStore {
val invoiceKey: String
val adminKey: String
val baseUrl: String
val isOnboarded: Boolean
fun saveCredentials(baseUrl: String, invoiceKey: String, adminKey: String)
/** Full credentials as a Flow — collect in ViewModels for reactive UI. */
val credentials: Flow<Credentials>
/** One-shot suspend accessors — safe to call from any suspend function. */
suspend fun invoiceKey(): String
suspend fun adminKey(): String
suspend fun baseUrl(): String
suspend fun isOnboarded(): Boolean
suspend fun saveCredentials(baseUrl: String, invoiceKey: String, adminKey: String)
}
class EncryptedSecretStore(context: Context) : SecretStore {
private val TAG = "EncryptedSecretStore"
private val prefs = context.getSharedPreferences(WalletConstants.PREFS_NAME, Context.MODE_PRIVATE)
private val dataStore: DataStore<Credentials> =
CredentialsDataStore.getInstance(context)
override val invoiceKey: String
get() = decryptOrEmpty(WalletConstants.PREFS_INVOICE_KEY_ENC)
override val credentials: Flow<Credentials>
get() = dataStore.data
override val adminKey: String
get() = decryptOrEmpty(WalletConstants.PREFS_ADMIN_KEY_ENC)
override suspend fun invoiceKey(): String =
dataStore.data.map { it.invoiceKey }.first()
override val baseUrl: String
get() = prefs.getString(WalletConstants.PREFS_BASE_URL, "").orEmpty()
override suspend fun adminKey(): String =
dataStore.data.map { it.adminKey }.first()
override val isOnboarded: Boolean
get() = prefs.getBoolean(WalletConstants.PREFS_ONBOARDED, false)
override suspend fun baseUrl(): String =
dataStore.data.map { it.baseUrl }.first()
override fun saveCredentials(baseUrl: String, invoiceKey: String, adminKey: String) {
prefs.edit()
.putString(WalletConstants.PREFS_BASE_URL, baseUrl)
.putString(WalletConstants.PREFS_INVOICE_KEY_ENC, SecretManager.encrypt(invoiceKey))
.putString(WalletConstants.PREFS_ADMIN_KEY_ENC, SecretManager.encrypt(adminKey))
.putBoolean(WalletConstants.PREFS_ONBOARDED, true)
.apply()
override suspend fun isOnboarded(): Boolean =
dataStore.data.map { it.isOnboarded }.first()
override suspend fun saveCredentials(
baseUrl: String,
invoiceKey: String,
adminKey: String
) {
Log.i(TAG, "Saving credentials — baseUrl length: ${baseUrl.length}, " +
"invoiceKey length: ${invoiceKey.length}, adminKey length: ${adminKey.length}")
dataStore.updateData { current ->
current.toBuilder()
.setBaseUrl(baseUrl)
.setInvoiceKey(invoiceKey)
.setAdminKey(adminKey)
.setIsOnboarded(true)
.build()
}
Log.i(TAG, "Credentials saved and DataStore updated")
}
private fun decryptOrEmpty(key: String): String {
val enc = prefs.getString(key, "").orEmpty()
if (enc.isBlank()) return ""
return runCatching { SecretManager.decrypt(enc) }.getOrDefault("")
}
}
}
@@ -118,7 +118,7 @@ class WalletNotificationService : Service() {
// Step 2: Open the WebSocket. If already open (e.g. service restarted
// by Android after a kill), close the old one first.
openWebSocket()
serviceScope.launch { openWebSocket() }
// START_REDELIVER_INTENT: if Android kills the service under memory
// pressure, it will restart it and re-deliver the last intent.
@@ -141,19 +141,21 @@ class WalletNotificationService : Service() {
// ── WebSocket ─────────────────────────────────────────────────────────────
private fun openWebSocket() {
private suspend fun openWebSocket() {
val secrets = EncryptedSecretStore(applicationContext)
val invoiceKey = secrets.invoiceKey()
val baseUrl = secrets.baseUrl()
if (secrets.invoiceKey.isBlank() || secrets.baseUrl.isBlank()) {
if (invoiceKey.isBlank() || baseUrl.isBlank()) {
Log.e(TAG, "Credentials not available — cannot open WebSocket")
stopSelf()
return
}
val wsUrl = secrets.baseUrl
val wsUrl = baseUrl
.replace("https://", "wss://")
.replace("http://", "ws://")
.trimEnd('/') + "/api/v1/ws/${secrets.invoiceKey}"
.trimEnd('/') + "/api/v1/ws/${invoiceKey}"
Log.d(TAG, "Opening WebSocket: $wsUrl")
@@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.*
import androidx.compose.ui.unit.dp
import com.bitcointxoko.gudariwallet.security.SecretStore
import kotlinx.coroutines.launch
@Composable
fun OnboardingScreen(
@@ -23,6 +24,8 @@ fun OnboardingScreen(
var adminKey by remember { mutableStateOf("") }
var adminVisible by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) }
val scope = rememberCoroutineScope()
Column(
modifier = Modifier
@@ -96,8 +99,10 @@ fun OnboardingScreen(
error = "All fields are required."
return@Button
}
secretStore.saveCredentials(baseUrl, invoiceKey, adminKey)
onComplete()
scope.launch {
secretStore.saveCredentials(baseUrl, invoiceKey, adminKey)
onComplete()
}
},
modifier = Modifier.fillMaxWidth()
) {
@@ -19,13 +19,11 @@ import com.bitcointxoko.gudariwallet.ui.clipboard.ClipboardViewModel
import com.bitcointxoko.gudariwallet.ui.fiat.FiatViewModel
import com.bitcointxoko.gudariwallet.ui.receive.ReceiveViewModel
import com.bitcointxoko.gudariwallet.ui.send.SendViewModel
import kotlinx.coroutines.flow.MutableStateFlow
class WalletViewModelFactory(private val app: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
val secrets = EncryptedSecretStore(app)
val baseUrl = secrets.baseUrl.ifBlank { "https://placeholder.invalid" }
val api = LNbitsClient.create(baseUrl)
val api = LNbitsClient.create(secrets)
val nodeAliasSvc = NodeAliasService(apiClient = NodeAliasClient(), context = app)
val lnurlCache = LnurlCacheRepository(app)
val paymentCache = PaymentCacheRepository(app)
@@ -2,6 +2,15 @@ package com.bitcointxoko.gudariwallet.util
object WalletConstants {
// ── Credentials DataStore (Tink-encrypted) ──────────────────────────────
const val CREDENTIALS_DATASTORE_FILE = "credentials.pb"
// Tink keyset — stored in its own SharedPreferences file, encrypted
// under the Android Keystore master key. Separate from wallet_prefs.
const val TINK_KEYSET_PREF = "credentials_tink_keyset_prefs"
const val TINK_KEYSET_KEY = "credentials_tink_keyset"
const val TINK_MASTER_KEY_URI = "android-keystore://credentials_master_key"
// ── Unit conversion ───────────────────────────────────────────────────────
const val MSAT_PER_SAT = 1_000L // millisatoshis per satoshi
@@ -10,13 +19,6 @@ object WalletConstants {
const val WS_MAX_BACKOFF_MS = 16_000L // cap — never wait longer than this
const val WS_MAX_ATTEMPTS = 5 // give up after 5 failed reconnects
// ── SharedPreferences ─────────────────────────────────────────────────────
const val PREFS_NAME = "lnbits_prefs"
const val PREFS_BASE_URL = "base_url"
const val PREFS_INVOICE_KEY_ENC = "invoice_key_enc"
const val PREFS_ADMIN_KEY_ENC = "admin_key_enc"
const val PREFS_ONBOARDED = "onboarded"
// ── Network ───────────────────────────────────────────────────────────────
const val CONNECT_TIMEOUT_S = 15L
const val READ_TIMEOUT_S = 30L
+12
View File
@@ -0,0 +1,12 @@
syntax = "proto3";
option java_package = "com.bitcointxoko.gudariwallet.security";
option java_multiple_files = true;
option java_outer_classname = "CredentialsProto";
message Credentials {
string base_url = 1;
string invoice_key = 2;
string admin_key = 3;
bool is_onboarded = 4;
}