@tokamakdev/plugin-secure-storage 0.1.0-beta.101 → 0.1.0-beta.102
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/README.md +2 -4
- package/android/SecureStoragePlugin.kt +11 -7
- package/apple/SecureStoragePlugin.swift +4 -13
- package/package.json +2 -2
- package/src/index.ts +4 -2
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ await secureStorage.replace("identity/encryption", base64Value, {
|
|
|
30
30
|
// Replaces the value, or creates it when the name has none.
|
|
31
31
|
await secureStorage.set("identity/encryption", base64Value, options);
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// get, delete and clear reject with ItemNotFoundError when there is no value.
|
|
34
34
|
const value = await secureStorage.get("identity/encryption", {
|
|
35
35
|
prompt: "Confirm it's you to link a new device",
|
|
36
36
|
});
|
|
@@ -75,7 +75,7 @@ screen. Android replaces values without asking.
|
|
|
75
75
|
|---|---|
|
|
76
76
|
| `NotSupportedError` | The platform, build or device cannot enforce the options |
|
|
77
77
|
| `DuplicateItemError` | `create` was given a name that has a value |
|
|
78
|
-
| `ItemNotFoundError` | `replace` was given a name that has no value |
|
|
78
|
+
| `ItemNotFoundError` | `replace`, `get` or `delete` was given a name that has no value, or `clear` found no values |
|
|
79
79
|
| `MissingEntitlementError` | `keychainGroup` is not one of the app's keychain access groups |
|
|
80
80
|
| `InvalidStateError` | The requested authentication is not set up on the device |
|
|
81
81
|
| `NotAllowedError` | The user cancelled or failed authentication, or the device is locked |
|
|
@@ -84,8 +84,6 @@ screen. Android replaces values without asking.
|
|
|
84
84
|
| `TypeError` | An argument is missing or has an unsupported value |
|
|
85
85
|
| `OperationError` | The platform reported another failure |
|
|
86
86
|
|
|
87
|
-
`get` resolves `null` when the name has no stored value.
|
|
88
|
-
|
|
89
87
|
## Keychain access groups
|
|
90
88
|
|
|
91
89
|
On Apple platforms an app can store values in each keychain access group its
|
|
@@ -134,7 +134,7 @@ class TokamakSecureStoragePlugin(
|
|
|
134
134
|
throw TokamakPluginError("DuplicateItemError", "A value is already stored under this name")
|
|
135
135
|
}
|
|
136
136
|
if (previous == null && mode == Write.REPLACE) {
|
|
137
|
-
throw
|
|
137
|
+
throw itemNotFound()
|
|
138
138
|
}
|
|
139
139
|
val previousAlias = previous?.let { alias(it.keyId) }
|
|
140
140
|
val publicKey = generatePublicKey(spec)
|
|
@@ -188,11 +188,7 @@ class TokamakSecureStoragePlugin(
|
|
|
188
188
|
private fun get(request: JSONObject, reply: TokamakPluginReply) {
|
|
189
189
|
val name = request.requireString("name")
|
|
190
190
|
val prompt = request.optionalString("prompt")
|
|
191
|
-
val stored = read(file(name))
|
|
192
|
-
if (stored == null) {
|
|
193
|
-
reply(Result.success(null))
|
|
194
|
-
return
|
|
195
|
-
}
|
|
191
|
+
val stored = read(file(name)) ?: throw itemNotFound()
|
|
196
192
|
val privateKey =
|
|
197
193
|
keyStore.getKey(alias(stored.keyId), null) as? PrivateKey
|
|
198
194
|
?: throw notReadable()
|
|
@@ -237,16 +233,22 @@ class TokamakSecureStoragePlugin(
|
|
|
237
233
|
|
|
238
234
|
private fun delete(name: String) {
|
|
239
235
|
val file = file(name)
|
|
240
|
-
val stored = read(file) ?:
|
|
236
|
+
val stored = read(file) ?: throw itemNotFound()
|
|
241
237
|
AtomicFile(file).delete()
|
|
242
238
|
keyStore.deleteEntry(alias(stored.keyId))
|
|
243
239
|
}
|
|
244
240
|
|
|
241
|
+
/** Deletes every value, and any key an interrupted write left behind. */
|
|
245
242
|
private fun clear() {
|
|
243
|
+
// An interrupted first write leaves only a `.new` file, which [read] ignores.
|
|
244
|
+
val hadValues = directory.list().orEmpty().any { !it.endsWith(".new") }
|
|
246
245
|
directory.deleteRecursively()
|
|
247
246
|
keyStore.aliases().toList()
|
|
248
247
|
.filter { it.startsWith(ALIAS_PREFIX) }
|
|
249
248
|
.forEach(keyStore::deleteEntry)
|
|
249
|
+
if (!hadValues) {
|
|
250
|
+
throw TokamakPluginError("ItemNotFoundError", "No values are stored")
|
|
251
|
+
}
|
|
250
252
|
}
|
|
251
253
|
|
|
252
254
|
private fun read(file: File): StoredValue? =
|
|
@@ -324,6 +326,8 @@ class TokamakSecureStoragePlugin(
|
|
|
324
326
|
fun notReadable() =
|
|
325
327
|
TokamakPluginError("NotReadableError", "The stored value can no longer be decrypted")
|
|
326
328
|
|
|
329
|
+
fun itemNotFound() = TokamakPluginError("ItemNotFoundError", "No value is stored under this name")
|
|
330
|
+
|
|
327
331
|
fun pluginError(error: Throwable): Throwable =
|
|
328
332
|
when (error) {
|
|
329
333
|
is KeyPermanentlyInvalidatedException,
|
|
@@ -103,10 +103,11 @@ final class TokamakSecureStoragePlugin: TokamakPlugin {
|
|
|
103
103
|
prompting: prompting
|
|
104
104
|
)
|
|
105
105
|
case "delete":
|
|
106
|
-
try
|
|
106
|
+
let name = try requiredString(arguments, "name")
|
|
107
|
+
try check(SecItemDelete(query(name, group: group) as CFDictionary))
|
|
107
108
|
return nil
|
|
108
109
|
case "clear":
|
|
109
|
-
try
|
|
110
|
+
try check(SecItemDelete(serviceQuery(group: group) as CFDictionary))
|
|
110
111
|
return nil
|
|
111
112
|
default:
|
|
112
113
|
throw .notSupported("\(id).\(method) is not supported")
|
|
@@ -158,15 +159,12 @@ final class TokamakSecureStoragePlugin: TokamakPlugin {
|
|
|
158
159
|
group: String?,
|
|
159
160
|
prompt: String?,
|
|
160
161
|
prompting: Result<Void, TokamakPluginError>?
|
|
161
|
-
) throws(TokamakPluginError) -> String
|
|
162
|
+
) throws(TokamakPluginError) -> String {
|
|
162
163
|
var item = query(name, group: group)
|
|
163
164
|
item[kSecReturnData] = true
|
|
164
165
|
item[kSecUseAuthenticationContext] = Self.contextWithoutUI()
|
|
165
166
|
var data: CFTypeRef?
|
|
166
167
|
var status = SecItemCopyMatching(item as CFDictionary, &data)
|
|
167
|
-
if status == errSecItemNotFound {
|
|
168
|
-
return nil
|
|
169
|
-
}
|
|
170
168
|
if status == errSecInteractionNotAllowed, let prompting {
|
|
171
169
|
try prompting.get()
|
|
172
170
|
item[kSecUseAuthenticationContext] = Self.context(prompt: prompt)
|
|
@@ -186,13 +184,6 @@ final class TokamakSecureStoragePlugin: TokamakPlugin {
|
|
|
186
184
|
return String(decoding: data, as: UTF8.self)
|
|
187
185
|
}
|
|
188
186
|
|
|
189
|
-
private func deleteItems(_ query: [CFString: Any]) throws(TokamakPluginError) {
|
|
190
|
-
let status = SecItemDelete(query as CFDictionary)
|
|
191
|
-
if status != errSecItemNotFound {
|
|
192
|
-
try check(status)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
187
|
/// A context in which keychain calls that need authentication fail with
|
|
197
188
|
/// `errSecInteractionNotAllowed` rather than show the system prompt.
|
|
198
189
|
private static func contextWithoutUI() -> LAContext {
|
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.102",
|
|
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
|
"peerDependencies": {
|
|
14
|
-
"@tokamakdev/plugin": "0.1.0-beta.
|
|
14
|
+
"@tokamakdev/plugin": "0.1.0-beta.102"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"android",
|
package/src/index.ts
CHANGED
|
@@ -64,15 +64,17 @@ class SecureStorage extends Plugin {
|
|
|
64
64
|
return this.call("set", { ...stored(name, value, options), prompt: options.prompt ?? null });
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
/** Reads the value; rejects with `ItemNotFoundError` when `name` has none. */
|
|
68
|
+
get(name: string, options: GetOptions = {}): Promise<string> {
|
|
68
69
|
return this.call("get", { name, prompt: options.prompt ?? null, keychainGroup: options.keychainGroup ?? null });
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/** Deletes the value; rejects with `ItemNotFoundError` when `name` has none. */
|
|
71
73
|
delete(name: string, options: DeleteOptions = {}): Promise<void> {
|
|
72
74
|
return this.call("delete", { name, keychainGroup: options.keychainGroup ?? null });
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
/** Deletes every stored value. */
|
|
77
|
+
/** Deletes every stored value; rejects with `ItemNotFoundError` when there are none. */
|
|
76
78
|
clear(options: DeleteOptions = {}): Promise<void> {
|
|
77
79
|
return this.call("clear", { keychainGroup: options.keychainGroup ?? null });
|
|
78
80
|
}
|