@tokamakdev/plugin-secure-storage 0.1.0-beta.65 → 0.1.0-beta.69

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.
@@ -0,0 +1,3 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-permission android:name="android.permission.USE_BIOMETRIC" />
3
+ </manifest>
@@ -1,9 +1,7 @@
1
1
  package com.tokamak.plugins.securestorage
2
2
 
3
- import android.hardware.biometrics.BiometricManager
4
3
  import android.hardware.biometrics.BiometricManager.Authenticators
5
4
  import android.hardware.biometrics.BiometricPrompt
6
- import android.os.CancellationSignal
7
5
  import android.security.keystore.KeyGenParameterSpec
8
6
  import android.security.keystore.KeyInfo
9
7
  import android.security.keystore.KeyPermanentlyInvalidatedException
@@ -15,6 +13,12 @@ import com.tokamak.runtime.TokamakHost
15
13
  import com.tokamak.runtime.TokamakPlugin
16
14
  import com.tokamak.runtime.TokamakPluginError
17
15
  import com.tokamak.runtime.TokamakPluginReply
16
+ import com.tokamak.runtime.authenticateOwner
17
+ import com.tokamak.runtime.optionalString
18
+ import com.tokamak.runtime.requireBoolean
19
+ import com.tokamak.runtime.requireObject
20
+ import com.tokamak.runtime.requireOwnerAuthentication
21
+ import com.tokamak.runtime.requireString
18
22
  import java.io.ByteArrayOutputStream
19
23
  import java.io.DataInputStream
20
24
  import java.io.DataOutputStream
@@ -55,18 +59,17 @@ class TokamakSecureStoragePlugin(
55
59
  private val worker = Executors.newSingleThreadExecutor()
56
60
  private val directory = File(context.noBackupFilesDir, "tokamak-secure-storage")
57
61
  private val keyStore = KeyStore.getInstance(KEYSTORE).apply { load(null) }
58
- private val biometrics = context.getSystemService(BiometricManager::class.java)
59
62
  private val random = SecureRandom()
60
63
 
61
64
  override fun call(method: String, arguments: Any?, reply: TokamakPluginReply) {
62
65
  when (method) {
63
66
  "set" -> execute(reply) {
64
- set(request(arguments))
67
+ set(requireObject(arguments))
65
68
  reply(Result.success(null))
66
69
  }
67
- "get" -> execute(reply) { get(request(arguments), reply) }
70
+ "get" -> execute(reply) { get(requireObject(arguments), reply) }
68
71
  "delete" -> execute(reply) {
69
- delete(request(arguments).requireString("name"))
72
+ delete(requireObject(arguments).requireString("name"))
70
73
  reply(Result.success(null))
71
74
  }
72
75
  "keys" -> execute(reply) { reply(Result.success(keys())) }
@@ -95,7 +98,7 @@ class TokamakSecureStoragePlugin(
95
98
  when (request.requireString("readable")) {
96
99
  "whenUnlocked" -> true
97
100
  "afterFirstUnlock" -> false
98
- else -> throw typeError("readable must be \"whenUnlocked\" or \"afterFirstUnlock\"")
101
+ else -> throw TokamakPluginError.typeError("readable must be \"whenUnlocked\" or \"afterFirstUnlock\"")
99
102
  }
100
103
  if (!request.requireBoolean("thisDeviceOnly")) {
101
104
  throw TokamakPluginError.notSupported("Android keeps secure storage values on this device")
@@ -123,21 +126,12 @@ class TokamakSecureStoragePlugin(
123
126
  private fun requireAuthentication(spec: KeyGenParameterSpec.Builder, wireName: String) {
124
127
  val authentication =
125
128
  Authentication.entries.firstOrNull { it.wireName == wireName } ?: throw unknownAuthentication()
126
- requireAvailable(authentication.authenticators)
129
+ host.requireOwnerAuthentication(authentication.authenticators)
127
130
  spec.setUserAuthenticationRequired(true)
128
131
  .setUserAuthenticationParameters(0, authentication.keyTypes)
129
132
  .setInvalidatedByBiometricEnrollment(authentication.invalidatedByEnrollment)
130
133
  }
131
134
 
132
- private fun requireAvailable(authenticators: Int) {
133
- when (biometrics.canAuthenticate(authenticators)) {
134
- BiometricManager.BIOMETRIC_SUCCESS -> Unit
135
- BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE ->
136
- throw TokamakPluginError.notSupported("This device cannot perform the requested authentication")
137
- else -> throw notSetUp()
138
- }
139
- }
140
-
141
135
  private fun generatePublicKey(spec: KeyGenParameterSpec.Builder): PublicKey {
142
136
  val generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, KEYSTORE)
143
137
  val keyPair =
@@ -201,26 +195,11 @@ class TokamakSecureStoragePlugin(
201
195
  stored: StoredValue,
202
196
  reply: TokamakPluginReply,
203
197
  ) {
204
- val activity = host.activity ?: throw notVisible()
205
- val callback = PromptCallback(reply) { execute(reply) { reply(Result.success(open(stored, unwrap))) } }
206
198
  val title = prompt?.takeIf { it.isNotEmpty() } ?: context.applicationInfo.loadLabel(context.packageManager)
207
- val dialog =
208
- BiometricPrompt.Builder(activity)
209
- .setTitle(title)
210
- .setAllowedAuthenticators(authenticators)
211
- if ((authenticators and Authenticators.DEVICE_CREDENTIAL) == 0) {
212
- dialog.setNegativeButton(
213
- activity.getString(android.R.string.cancel),
214
- activity.mainExecutor,
215
- ) { _, _ -> reply(Result.failure(cancelled())) }
216
- }
217
199
  onUiThread(reply) {
218
- dialog.build().authenticate(
219
- BiometricPrompt.CryptoObject(unwrap),
220
- CancellationSignal(),
221
- activity.mainExecutor,
222
- callback,
223
- )
200
+ host.authenticateOwner(title, authenticators, BiometricPrompt.CryptoObject(unwrap), reply) {
201
+ execute(reply) { reply(Result.success(open(stored, unwrap))) }
202
+ }
224
203
  }
225
204
  }
226
205
 
@@ -280,22 +259,6 @@ class TokamakSecureStoragePlugin(
280
259
 
281
260
  private fun ByteArray.toHex() = joinToString("") { "%02x".format(it) }
282
261
 
283
- private fun request(arguments: Any?): JSONObject =
284
- arguments as? JSONObject ?: throw typeError("$id arguments must be an object")
285
-
286
- private fun JSONObject.requireString(name: String): String =
287
- optionalString(name) ?: throw typeError("$name must be a string")
288
-
289
- private fun JSONObject.requireBoolean(name: String): Boolean =
290
- opt(name) as? Boolean ?: throw typeError("$name must be a boolean")
291
-
292
- private fun JSONObject.optionalString(name: String): String? =
293
- when (val value = opt(name)) {
294
- null, JSONObject.NULL -> null
295
- is String -> value
296
- else -> throw typeError("$name must be a string")
297
- }
298
-
299
262
  private enum class Authentication(
300
263
  val wireName: String,
301
264
  val keyTypes: Int,
@@ -333,33 +296,22 @@ class TokamakSecureStoragePlugin(
333
296
  // Keystore RSA keys default to SHA-1 for the OAEP MGF1 digest.
334
297
  val OAEP = OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT)
335
298
 
336
- fun typeError(message: String) = TokamakPluginError("TypeError", message)
337
-
338
299
  fun unknownAuthentication() =
339
- typeError("authentication must be \"biometricsOrPasscode\", \"biometrics\" or \"currentBiometrics\"")
300
+ TokamakPluginError.typeError("authentication must be \"biometricsOrPasscode\", \"biometrics\" or \"currentBiometrics\"")
340
301
 
341
302
  fun locked() = TokamakPluginError("NotAllowedError", "The device must be unlocked to read this value")
342
303
 
343
304
  fun notReadable() =
344
305
  TokamakPluginError("NotReadableError", "The stored value can no longer be decrypted")
345
306
 
346
- fun cancelled() = TokamakPluginError("NotAllowedError", "Authentication was cancelled")
347
-
348
- fun notSetUp() =
349
- TokamakPluginError("InvalidStateError", "The requested authentication is not set up on this device")
350
-
351
- fun notVisible() =
352
- TokamakPluginError("InvalidStateError", "The app has no activity to show the authentication prompt in")
353
-
354
- fun pluginError(error: Throwable): TokamakPluginError =
307
+ fun pluginError(error: Throwable): Throwable =
355
308
  when (error) {
356
- is TokamakPluginError -> error
357
309
  is KeyPermanentlyInvalidatedException,
358
310
  is BadPaddingException,
359
311
  is IllegalBlockSizeException,
360
312
  -> notReadable()
361
313
  is UserNotAuthenticatedException -> locked()
362
- else -> TokamakPluginError("OperationError", error.message ?: error.javaClass.name)
314
+ else -> error
363
315
  }
364
316
  }
365
317
  }
@@ -404,31 +356,3 @@ private class StoredValue(
404
356
  private fun DataInputStream.readExactly(size: Int) = ByteArray(size).also(::readFully)
405
357
  }
406
358
  }
407
-
408
- private class PromptCallback(
409
- private val reply: TokamakPluginReply,
410
- private val succeeded: () -> Unit,
411
- ) : BiometricPrompt.AuthenticationCallback() {
412
- override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) = succeeded()
413
-
414
- override fun onAuthenticationError(code: Int, message: CharSequence) =
415
- reply(Result.failure(authenticationError(code, message)))
416
- }
417
-
418
- private fun authenticationError(code: Int, message: CharSequence) =
419
- TokamakPluginError(
420
- when (code) {
421
- BiometricPrompt.BIOMETRIC_ERROR_CANCELED,
422
- BiometricPrompt.BIOMETRIC_ERROR_USER_CANCELED,
423
- BiometricPrompt.BIOMETRIC_ERROR_TIMEOUT,
424
- BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT,
425
- BiometricPrompt.BIOMETRIC_ERROR_LOCKOUT_PERMANENT,
426
- -> "NotAllowedError"
427
- BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS,
428
- BiometricPrompt.BIOMETRIC_ERROR_NO_DEVICE_CREDENTIAL,
429
- -> "InvalidStateError"
430
- BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT -> "NotSupportedError"
431
- else -> "OperationError"
432
- },
433
- message.toString(),
434
- )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamakdev/plugin-secure-storage",
3
- "version": "0.1.0-beta.65",
3
+ "version": "0.1.0-beta.69",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/mantty/tokamak.git",
@@ -11,7 +11,7 @@
11
11
  ".": "./src/index.ts"
12
12
  },
13
13
  "dependencies": {
14
- "@tokamakdev/plugin": "0.1.0-beta.65"
14
+ "@tokamakdev/plugin": "0.1.0-beta.69"
15
15
  },
16
16
  "files": [
17
17
  "android",
@@ -4,25 +4,17 @@
4
4
  "platforms": {
5
5
  "macos": {
6
6
  "class": "TokamakSecureStoragePlugin",
7
- "sources": ["apple/SecureStoragePlugin.swift"],
8
- "frameworks": ["LocalAuthentication", "Security"]
7
+ "sources": ["apple/SecureStoragePlugin.swift"]
9
8
  },
10
9
  "ios": {
11
10
  "class": "TokamakSecureStoragePlugin",
12
11
  "sources": ["apple/SecureStoragePlugin.swift"],
13
- "frameworks": ["LocalAuthentication", "Security"],
14
- "plist": "apple/Info.plist"
15
- },
16
- "ios-simulator": {
17
- "class": "TokamakSecureStoragePlugin",
18
- "sources": ["apple/SecureStoragePlugin.swift"],
19
- "frameworks": ["LocalAuthentication", "Security"],
20
12
  "plist": "apple/Info.plist"
21
13
  },
22
14
  "android": {
23
15
  "class": "com.tokamak.plugins.securestorage.TokamakSecureStoragePlugin",
24
16
  "sources": ["android/SecureStoragePlugin.kt"],
25
- "permissions": ["android.permission.USE_BIOMETRIC"]
17
+ "manifest": "android/AndroidManifest.xml"
26
18
  }
27
19
  }
28
20
  }