pakstr 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. package/android-template/app/src/androidTest/java/com/pakstr/app/signer/SignerManagerTest.kt +266 -0
  2. package/android-template/app/src/androidTest/java/com/pakstr/app/signer/local/LocalAccountStoreTest.kt +272 -0
  3. package/android-template/app/src/androidTest/java/com/pakstr/app/utils/SignerPickerBottomSheetTest.kt +165 -0
  4. package/android-template/app/src/main/java/com/pakstr/app/MainActivity.kt +211 -11
  5. package/android-template/app/src/main/java/com/pakstr/app/crypto/Nip19Codec.kt +181 -0
  6. package/android-template/app/src/main/java/com/pakstr/app/signer/LocalSigner.kt +100 -0
  7. package/android-template/app/src/main/java/com/pakstr/app/signer/NostrBridge.kt +6 -6
  8. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerManager.kt +135 -3
  9. package/android-template/app/src/main/java/com/pakstr/app/signer/SignerType.kt +1 -0
  10. package/android-template/app/src/main/java/com/pakstr/app/signer/local/LocalAccountImportCandidate.kt +42 -0
  11. package/android-template/app/src/main/java/com/pakstr/app/signer/local/LocalAccountStore.kt +260 -0
  12. package/android-template/app/src/main/java/com/pakstr/app/utils/SignerPickerBottomSheet.kt +98 -12
  13. package/android-template/app/src/main/res/layout/activity_developer_tools.xml +15 -0
  14. package/android-template/app/src/main/res/layout/dialog_picker.xml +80 -0
  15. package/android-template/app/src/main/res/values/strings.xml +16 -1
  16. package/android-template/app/src/main/res/xml/backup_rules.xml +4 -11
  17. package/android-template/app/src/main/res/xml/data_extraction_rules.xml +7 -13
  18. package/android-template/app/src/test/java/com/pakstr/app/crypto/Nip19CodecTest.kt +178 -0
  19. package/android-template/app/src/test/java/com/pakstr/app/signer/LocalSignerTest.kt +110 -0
  20. package/package.json +3 -3
@@ -0,0 +1,100 @@
1
+ package com.pakstr.app.signer
2
+
3
+ import com.pakstr.app.crypto.Bip340Hash
4
+ import com.pakstr.app.crypto.Nip46ClientKey
5
+ import com.pakstr.app.crypto.NostrEventVerifier
6
+ import com.pakstr.app.signer.interfaces.NostrSigner
7
+ import com.pakstr.app.signer.local.LocalAccountStore
8
+ import org.json.JSONObject
9
+
10
+ internal class LocalSigner(
11
+ private val keyLoader: () -> Nip46ClientKey
12
+ ) : NostrSigner {
13
+ constructor(store: LocalAccountStore) : this(
14
+ keyLoader = {
15
+ store.loadKey()
16
+ ?: throw IllegalStateException(
17
+ "Local account has not been created"
18
+ )
19
+ }
20
+ )
21
+ override suspend fun getPublicKey(): String {
22
+ return requireKey().use { key ->
23
+ key.publicKeyHex
24
+ }
25
+ }
26
+
27
+ override suspend fun signEvent(eventJson: String): String {
28
+ return requireKey().use { key ->
29
+ val event = JSONObject(eventJson)
30
+ val publicKey = key.publicKeyHex
31
+ val suppliedPublicKey = event.optString("pubkey")
32
+ require(
33
+ suppliedPublicKey.isBlank() ||
34
+ suppliedPublicKey == publicKey
35
+ ) {
36
+ "Event public key does not match local signer"
37
+ }
38
+ event.put("pubkey", publicKey)
39
+ NostrEventBuilder.addId(event)
40
+
41
+ val eventId =
42
+ Bip340Hash.hexToBytes(
43
+ event.getString("id")
44
+ )
45
+ val signature = key.sign(eventId)
46
+ try {
47
+ event.put(
48
+ "sig",
49
+ Bip340Hash.bytesToHex(signature)
50
+ )
51
+ } finally {
52
+ eventId.fill(0)
53
+ signature.fill(0)
54
+ }
55
+
56
+ check(NostrEventVerifier.verify(event)) {
57
+ "Generated signature failed verification"
58
+ }
59
+ event.toString()
60
+ }
61
+ }
62
+
63
+ override suspend fun nip04Encrypt(
64
+ pubkey: String,
65
+ plaintext: String
66
+ ): String {
67
+ throw UnsupportedOperationException(
68
+ "NIP-04 is not supported by the local signer"
69
+ )
70
+ }
71
+
72
+ override suspend fun nip04Decrypt(
73
+ pubkey: String,
74
+ ciphertext: String
75
+ ): String {
76
+ throw UnsupportedOperationException(
77
+ "NIP-04 is not supported by the local signer"
78
+ )
79
+ }
80
+
81
+ override suspend fun nip44Encrypt(
82
+ pubkey: String,
83
+ plaintext: String
84
+ ): String {
85
+ return requireKey().use { key ->
86
+ key.encryptNip44(plaintext, pubkey)
87
+ }
88
+ }
89
+
90
+ override suspend fun nip44Decrypt(
91
+ pubkey: String,
92
+ ciphertext: String
93
+ ): String {
94
+ return requireKey().use { key ->
95
+ key.decryptNip44(ciphertext, pubkey)
96
+ }
97
+ }
98
+
99
+ private fun requireKey() = keyLoader()
100
+ }
@@ -48,7 +48,7 @@ class NostrBridge(
48
48
  try {
49
49
  val pk =
50
50
  signerManager
51
- .getSigner()
51
+ .getConfiguredSigner()
52
52
  .getPublicKey()
53
53
  resolve(
54
54
  callbackId,
@@ -107,7 +107,7 @@ class NostrBridge(
107
107
 
108
108
  val result =
109
109
  signerManager
110
- .getSigner()
110
+ .getConfiguredSigner()
111
111
  .signEvent(
112
112
  payload
113
113
  )
@@ -180,7 +180,7 @@ class NostrBridge(
180
180
 
181
181
  val result =
182
182
  signerManager
183
- .getSigner()
183
+ .getConfiguredSigner()
184
184
  .nip04Encrypt(
185
185
  pubkey,
186
186
  plaintext
@@ -254,7 +254,7 @@ class NostrBridge(
254
254
 
255
255
  val result =
256
256
  signerManager
257
- .getSigner()
257
+ .getConfiguredSigner()
258
258
  .nip04Decrypt(
259
259
  pubkey,
260
260
  ciphertext
@@ -318,7 +318,7 @@ class NostrBridge(
318
318
 
319
319
  val result =
320
320
  signerManager
321
- .getSigner()
321
+ .getConfiguredSigner()
322
322
  .nip44Encrypt(
323
323
  json.getString("pubkey"),
324
324
  json.getString("plaintext")
@@ -373,7 +373,7 @@ class NostrBridge(
373
373
 
374
374
  val result =
375
375
  signerManager
376
- .getSigner()
376
+ .getConfiguredSigner()
377
377
  .nip44Decrypt(
378
378
  json.getString("pubkey"),
379
379
  json.getString("ciphertext")
@@ -14,12 +14,17 @@ import com.pakstr.app.signer.bunker.Nip46PairingUri
14
14
  import com.pakstr.app.signer.bunker.RelayNip46Client
15
15
  import com.pakstr.app.signer.exceptions.SignerUnavailableException
16
16
  import com.pakstr.app.signer.interfaces.NostrSigner
17
+ import com.pakstr.app.signer.local.LocalAccountAlreadyExistsException
18
+ import com.pakstr.app.signer.local.LocalAccountImportCandidate
19
+ import com.pakstr.app.signer.local.LocalAccountStore
20
+ import kotlinx.coroutines.CompletableDeferred
17
21
  import org.json.JSONObject
18
22
 
19
23
  class SignerManager(
20
24
  context: Context,
21
25
  private val amberLauncher: ActivityResultLauncher<Intent>,
22
- private val bunkerAuthChallengeHandler: ((String) -> Unit)? = null
26
+ private val bunkerAuthChallengeHandler: ((String) -> Unit)? = null,
27
+ private val signerPickerRequestHandler: (() -> Unit)? = null
23
28
  ) {
24
29
  private val context = context.applicationContext
25
30
  private val prefs =
@@ -30,10 +35,14 @@ class SignerManager(
30
35
  private val bunkerStore by lazy {
31
36
  BunkerConnectionStore(this.context)
32
37
  }
38
+ private val localAccountStore by lazy {
39
+ LocalAccountStore(this.context)
40
+ }
33
41
 
34
42
  private var activeSigner: NostrSigner? = null
35
43
  private var currentUserPubkey: String? = null
36
44
  private var pendingPairing: PendingPairing? = null
45
+ private var pendingSignerSelection: CompletableDeferred<Boolean>? = null
37
46
 
38
47
  init {
39
48
  AppDebugLogger.log(
@@ -44,7 +53,7 @@ class SignerManager(
44
53
  }
45
54
 
46
55
  fun getActiveType(): SignerType? {
47
- val value = prefs.getString("type", SignerType.AMBER.name)
56
+ val value = prefs.getString(KEY_SIGNER_TYPE, SignerType.AMBER.name)
48
57
  return try {
49
58
  SignerType.valueOf(value.toString())
50
59
  } catch (_: IllegalArgumentException) {
@@ -52,6 +61,20 @@ class SignerManager(
52
61
  }
53
62
  }
54
63
 
64
+ fun isSignerConfigured(): Boolean =
65
+ prefs.getBoolean(KEY_SIGNER_CONFIGURED, false)
66
+
67
+ fun migrateLegacySignerConfiguration(setupCompleted: Boolean) {
68
+ if (prefs.contains(KEY_SIGNER_CONFIGURED)) {
69
+ return
70
+ }
71
+ if (setupCompleted || prefs.contains(KEY_SIGNER_TYPE)) {
72
+ prefs.edit {
73
+ putBoolean(KEY_SIGNER_CONFIGURED, true)
74
+ }
75
+ }
76
+ }
77
+
55
78
  fun hasBunkerSession(): Boolean {
56
79
  return try {
57
80
  val connection = bunkerStore.restore() ?: return false
@@ -169,6 +192,35 @@ class SignerManager(
169
192
  setActiveTypeWithoutClosing(type)
170
193
  }
171
194
 
195
+ fun createLocalAccount() {
196
+ localAccountStore.createAccount()
197
+ closeActiveSigner()
198
+ setActiveTypeWithoutClosing(SignerType.LOCAL)
199
+ }
200
+
201
+ fun hasLocalAccount(): Boolean = localAccountStore.hasAccount()
202
+
203
+ internal fun prepareLocalAccountImport(
204
+ nsec: String
205
+ ): LocalAccountImportCandidate {
206
+ if (localAccountStore.hasAccount()) {
207
+ throw LocalAccountAlreadyExistsException()
208
+ }
209
+ return LocalAccountImportCandidate.parse(nsec)
210
+ }
211
+
212
+ internal fun importLocalAccount(
213
+ candidate: LocalAccountImportCandidate
214
+ ) {
215
+ try {
216
+ candidate.withPrivateKey(localAccountStore::importAccount)
217
+ closeActiveSigner()
218
+ setActiveTypeWithoutClosing(SignerType.LOCAL)
219
+ } finally {
220
+ candidate.close()
221
+ }
222
+ }
223
+
172
224
  fun logoutBunker() {
173
225
  try {
174
226
  (activeSigner as? BunkerSigner)?.disconnect()
@@ -182,6 +234,56 @@ class SignerManager(
182
234
  }
183
235
  }
184
236
 
237
+ suspend fun getConfiguredSigner(): NostrSigner {
238
+ if (isSignerConfigured()) {
239
+ return getSigner()
240
+ }
241
+
242
+ var shouldRequestPicker = false
243
+ val selection =
244
+ synchronized(this) {
245
+ if (isSignerConfigured()) {
246
+ null
247
+ } else {
248
+ pendingSignerSelection
249
+ ?: CompletableDeferred<Boolean>().also {
250
+ pendingSignerSelection = it
251
+ shouldRequestPicker = true
252
+ }
253
+ }
254
+ }
255
+
256
+ if (selection == null) {
257
+ return getSigner()
258
+ }
259
+ if (shouldRequestPicker) {
260
+ val handler = signerPickerRequestHandler
261
+ if (handler == null) {
262
+ cancelSignerSelection()
263
+ } else {
264
+ try {
265
+ handler()
266
+ } catch (_: Exception) {
267
+ cancelSignerSelection()
268
+ }
269
+ }
270
+ }
271
+ if (!selection.await()) {
272
+ throw SignerUnavailableException("Signer selection was dismissed")
273
+ }
274
+ return getSigner()
275
+ }
276
+
277
+ fun cancelSignerSelection() {
278
+ val selection =
279
+ synchronized(this) {
280
+ pendingSignerSelection.also {
281
+ pendingSignerSelection = null
282
+ }
283
+ }
284
+ selection?.complete(false)
285
+ }
286
+
185
287
  fun getSigner(): NostrSigner {
186
288
  activeSigner?.let { return it }
187
289
  val activeType = getActiveType()
@@ -195,6 +297,7 @@ class SignerManager(
195
297
  when (activeType) {
196
298
  SignerType.AMBER -> createAmberSigner()
197
299
  SignerType.BUNKER -> createBunkerSigner()
300
+ SignerType.LOCAL -> createLocalSigner()
198
301
  SignerType.FAKE -> FakeSigner()
199
302
  null -> throw IllegalStateException("No signer selected")
200
303
  }
@@ -204,6 +307,7 @@ class SignerManager(
204
307
 
205
308
  fun close() {
206
309
  cancelBunkerPairing()
310
+ cancelSignerSelection()
207
311
  closeActiveSigner()
208
312
  }
209
313
 
@@ -289,10 +393,36 @@ class SignerManager(
289
393
  return BunkerSigner(context, client)
290
394
  }
291
395
 
396
+ private fun createLocalSigner(): LocalSigner {
397
+ try {
398
+ localAccountStore.loadKey()?.use { key ->
399
+ currentUserPubkey = key.publicKeyHex
400
+ } ?: throw SignerUnavailableException(
401
+ "Local account has not been created"
402
+ )
403
+ } catch (error: SignerUnavailableException) {
404
+ throw error
405
+ } catch (error: Exception) {
406
+ throw IllegalStateException(
407
+ "Unable to restore local account",
408
+ error
409
+ )
410
+ }
411
+ return LocalSigner(localAccountStore)
412
+ }
413
+
292
414
  private fun setActiveTypeWithoutClosing(type: SignerType) {
293
415
  prefs.edit {
294
- putString("type", type.name)
416
+ putString(KEY_SIGNER_TYPE, type.name)
417
+ putBoolean(KEY_SIGNER_CONFIGURED, true)
295
418
  }
419
+ val selection =
420
+ synchronized(this) {
421
+ pendingSignerSelection.also {
422
+ pendingSignerSelection = null
423
+ }
424
+ }
425
+ selection?.complete(true)
296
426
  }
297
427
 
298
428
  private fun closeActiveSigner() {
@@ -319,6 +449,8 @@ class SignerManager(
319
449
  }
320
450
 
321
451
  private companion object {
452
+ const val KEY_SIGNER_TYPE = "type"
453
+ const val KEY_SIGNER_CONFIGURED = "signer_configured"
322
454
  const val BUNKER_PERMISSIONS =
323
455
  "sign_event,nip04_encrypt,nip04_decrypt," +
324
456
  "nip44_encrypt,nip44_decrypt"
@@ -3,6 +3,7 @@ package com.pakstr.app.signer
3
3
  enum class SignerType {
4
4
  AMBER,
5
5
  BUNKER,
6
+ LOCAL,
6
7
  FAKE
7
8
 
8
9
  }
@@ -0,0 +1,42 @@
1
+ package com.pakstr.app.signer.local
2
+
3
+ import com.pakstr.app.crypto.Nip19Codec
4
+ import com.pakstr.app.crypto.Nip46ClientKey
5
+
6
+ internal class LocalAccountImportCandidate private constructor(
7
+ private val key: Nip46ClientKey,
8
+ val npub: String
9
+ ) : AutoCloseable {
10
+
11
+ internal fun <T> withPrivateKey(
12
+ block: (ByteArray) -> T
13
+ ): T = key.withPrivateKey(block)
14
+
15
+ override fun close() {
16
+ key.close()
17
+ }
18
+
19
+ companion object {
20
+ fun parse(nsec: String): LocalAccountImportCandidate {
21
+ val privateKey = Nip19Codec.decodeNsec(nsec)
22
+ return try {
23
+ val key = Nip46ClientKey.fromPrivateKey(privateKey)
24
+ try {
25
+ val publicKey = key.publicKey
26
+ val npub =
27
+ try {
28
+ Nip19Codec.encodeNpub(publicKey)
29
+ } finally {
30
+ publicKey.fill(0)
31
+ }
32
+ LocalAccountImportCandidate(key, npub)
33
+ } catch (error: Exception) {
34
+ key.close()
35
+ throw error
36
+ }
37
+ } finally {
38
+ privateKey.fill(0)
39
+ }
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,260 @@
1
+ package com.pakstr.app.signer.local
2
+
3
+ import android.content.Context
4
+ import android.security.keystore.KeyGenParameterSpec
5
+ import android.security.keystore.KeyProperties
6
+ import android.util.Base64
7
+ import com.pakstr.app.crypto.Nip46ClientKey
8
+ import java.security.KeyStore
9
+ import javax.crypto.Cipher
10
+ import javax.crypto.KeyGenerator
11
+ import javax.crypto.SecretKey
12
+ import javax.crypto.spec.GCMParameterSpec
13
+
14
+ internal class LocalAccountStorageException(
15
+ message: String,
16
+ cause: Throwable? = null
17
+ ) : Exception(message, cause)
18
+
19
+ internal class LocalAccountAlreadyExistsException :
20
+ IllegalStateException("Local account already exists")
21
+
22
+ internal class LocalAccountStore(
23
+ context: Context,
24
+ private val preferencesName: String = PREFERENCES_NAME,
25
+ private val keyAlias: String = KEY_ALIAS
26
+ ) {
27
+ private val context = context.applicationContext
28
+ private val preferences =
29
+ this.context.getSharedPreferences(
30
+ preferencesName,
31
+ Context.MODE_PRIVATE
32
+ )
33
+
34
+ fun createAccount(): String =
35
+ synchronized(STORAGE_LOCK) {
36
+ if (hasAccountLocked()) {
37
+ return@synchronized requireNotNull(
38
+ loadKeyLocked()
39
+ ).use { key ->
40
+ key.publicKeyHex
41
+ }
42
+ }
43
+
44
+ val key = Nip46ClientKey.generate()
45
+ try {
46
+ key.withPrivateKey(::saveNewKey)
47
+ key.publicKeyHex
48
+ } finally {
49
+ key.close()
50
+ }
51
+ }
52
+
53
+ fun importAccount(privateKey: ByteArray): String =
54
+ synchronized(STORAGE_LOCK) {
55
+ if (hasAccountLocked()) {
56
+ throw LocalAccountAlreadyExistsException()
57
+ }
58
+
59
+ Nip46ClientKey.fromPrivateKey(privateKey).use { key ->
60
+ key.withPrivateKey(::saveNewKey)
61
+ key.publicKeyHex
62
+ }
63
+ }
64
+
65
+ fun hasAccount(): Boolean =
66
+ synchronized(STORAGE_LOCK) {
67
+ hasAccountLocked()
68
+ }
69
+
70
+ fun loadKey(): Nip46ClientKey? =
71
+ synchronized(STORAGE_LOCK) {
72
+ loadKeyLocked()
73
+ }
74
+
75
+ private fun hasAccountLocked(): Boolean {
76
+ val hasIv = preferences.contains(KEY_IV)
77
+ val hasCiphertext = preferences.contains(KEY_CIPHERTEXT)
78
+ if (hasIv != hasCiphertext) {
79
+ throw LocalAccountStorageException(
80
+ "Local account storage is incomplete"
81
+ )
82
+ }
83
+ return hasIv
84
+ }
85
+
86
+ private fun loadKeyLocked(): Nip46ClientKey? {
87
+ if (!hasAccountLocked()) {
88
+ return null
89
+ }
90
+
91
+ val encodedIv = preferences.getString(KEY_IV, null)
92
+ ?: throw LocalAccountStorageException(
93
+ "Local account storage is incomplete"
94
+ )
95
+ val encodedCiphertext =
96
+ preferences.getString(KEY_CIPHERTEXT, null)
97
+ ?: throw LocalAccountStorageException(
98
+ "Local account storage is incomplete"
99
+ )
100
+ val iv = decode(encodedIv)
101
+ val ciphertext = decode(encodedCiphertext)
102
+ var privateKey: ByteArray? = null
103
+
104
+ return try {
105
+ val wrappingKey = loadWrappingKey()
106
+ privateKey = decrypt(wrappingKey, iv, ciphertext)
107
+ Nip46ClientKey.fromPrivateKey(privateKey)
108
+ } catch (error: LocalAccountStorageException) {
109
+ throw error
110
+ } catch (error: Exception) {
111
+ throw LocalAccountStorageException(
112
+ "Unable to restore local account",
113
+ error
114
+ )
115
+ } finally {
116
+ iv.fill(0)
117
+ ciphertext.fill(0)
118
+ privateKey?.fill(0)
119
+ }
120
+ }
121
+
122
+ private fun saveNewKey(privateKey: ByteArray) {
123
+ if (hasAccountLocked()) {
124
+ throw LocalAccountAlreadyExistsException()
125
+ }
126
+
127
+ val wrappingKey = getOrCreateWrappingKey()
128
+ val cipher = Cipher.getInstance(TRANSFORMATION)
129
+ cipher.init(Cipher.ENCRYPT_MODE, wrappingKey)
130
+ cipher.updateAAD(AUTHENTICATED_DATA)
131
+ val ciphertext = cipher.doFinal(privateKey)
132
+ val iv = cipher.iv
133
+
134
+ try {
135
+ val saved =
136
+ preferences.edit()
137
+ .putString(
138
+ KEY_IV,
139
+ Base64.encodeToString(iv, Base64.NO_WRAP)
140
+ )
141
+ .putString(
142
+ KEY_CIPHERTEXT,
143
+ Base64.encodeToString(
144
+ ciphertext,
145
+ Base64.NO_WRAP
146
+ )
147
+ )
148
+ .commit()
149
+ if (!saved) {
150
+ throw LocalAccountStorageException(
151
+ "Unable to store local account"
152
+ )
153
+ }
154
+ } finally {
155
+ iv.fill(0)
156
+ ciphertext.fill(0)
157
+ }
158
+ }
159
+
160
+ private fun decrypt(
161
+ wrappingKey: SecretKey,
162
+ iv: ByteArray,
163
+ ciphertext: ByteArray
164
+ ): ByteArray {
165
+ val cipher = Cipher.getInstance(TRANSFORMATION)
166
+ cipher.init(
167
+ Cipher.DECRYPT_MODE,
168
+ wrappingKey,
169
+ GCMParameterSpec(GCM_TAG_SIZE_BITS, iv)
170
+ )
171
+ cipher.updateAAD(AUTHENTICATED_DATA)
172
+ return cipher.doFinal(ciphertext)
173
+ }
174
+
175
+ private fun getOrCreateWrappingKey(): SecretKey {
176
+ val keyStore = loadKeyStore()
177
+ val existing = keyStore.getKey(keyAlias, null)
178
+ if (existing != null) {
179
+ return existing as? SecretKey
180
+ ?: throw LocalAccountStorageException(
181
+ "Invalid local account wrapping key"
182
+ )
183
+ }
184
+
185
+ return try {
186
+ val generator =
187
+ KeyGenerator.getInstance(
188
+ KeyProperties.KEY_ALGORITHM_AES,
189
+ ANDROID_KEY_STORE
190
+ )
191
+ generator.init(
192
+ KeyGenParameterSpec.Builder(
193
+ keyAlias,
194
+ KeyProperties.PURPOSE_ENCRYPT or
195
+ KeyProperties.PURPOSE_DECRYPT
196
+ )
197
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
198
+ .setEncryptionPaddings(
199
+ KeyProperties.ENCRYPTION_PADDING_NONE
200
+ )
201
+ .setKeySize(WRAPPING_KEY_SIZE_BITS)
202
+ .build()
203
+ )
204
+ generator.generateKey()
205
+ } catch (error: Exception) {
206
+ throw LocalAccountStorageException(
207
+ "Unable to create local account wrapping key",
208
+ error
209
+ )
210
+ }
211
+ }
212
+
213
+ private fun loadWrappingKey(): SecretKey {
214
+ val key = loadKeyStore().getKey(keyAlias, null)
215
+ ?: throw LocalAccountStorageException(
216
+ "Local account wrapping key is unavailable"
217
+ )
218
+ return key as? SecretKey
219
+ ?: throw LocalAccountStorageException(
220
+ "Invalid local account wrapping key"
221
+ )
222
+ }
223
+
224
+ private fun loadKeyStore(): KeyStore {
225
+ return try {
226
+ KeyStore.getInstance(ANDROID_KEY_STORE)
227
+ .apply { load(null) }
228
+ } catch (error: Exception) {
229
+ throw LocalAccountStorageException(
230
+ "Unable to access local account storage",
231
+ error
232
+ )
233
+ }
234
+ }
235
+
236
+ private fun decode(value: String): ByteArray {
237
+ return try {
238
+ Base64.decode(value, Base64.NO_WRAP)
239
+ } catch (error: IllegalArgumentException) {
240
+ throw LocalAccountStorageException(
241
+ "Local account storage is invalid",
242
+ error
243
+ )
244
+ }
245
+ }
246
+
247
+ internal companion object {
248
+ const val PREFERENCES_NAME = "local_signer_account"
249
+ const val KEY_ALIAS = "pakstr_local_signer_wrapping_key_v1"
250
+ const val KEY_IV = "iv"
251
+ const val KEY_CIPHERTEXT = "ciphertext"
252
+ private val STORAGE_LOCK = Any()
253
+ private const val ANDROID_KEY_STORE = "AndroidKeyStore"
254
+ private const val TRANSFORMATION = "AES/GCM/NoPadding"
255
+ private const val GCM_TAG_SIZE_BITS = 128
256
+ private const val WRAPPING_KEY_SIZE_BITS = 256
257
+ private val AUTHENTICATED_DATA =
258
+ "pakstr/local-account/v1".toByteArray(Charsets.UTF_8)
259
+ }
260
+ }