@tokamakdev/plugin-secure-storage 0.1.0-beta.64 → 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.
- package/android/AndroidManifest.xml +3 -0
- package/android/SecureStoragePlugin.kt +17 -93
- package/package.json +2 -2
- package/tokamak-plugin.json +2 -10
|
@@ -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(
|
|
67
|
+
set(requireObject(arguments))
|
|
65
68
|
reply(Result.success(null))
|
|
66
69
|
}
|
|
67
|
-
"get" -> execute(reply) { get(
|
|
70
|
+
"get" -> execute(reply) { get(requireObject(arguments), reply) }
|
|
68
71
|
"delete" -> execute(reply) {
|
|
69
|
-
delete(
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
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 ->
|
|
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.
|
|
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.
|
|
14
|
+
"@tokamakdev/plugin": "0.1.0-beta.69"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"android",
|
package/tokamak-plugin.json
CHANGED
|
@@ -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
|
-
"
|
|
17
|
+
"manifest": "android/AndroidManifest.xml"
|
|
26
18
|
}
|
|
27
19
|
}
|
|
28
20
|
}
|