@tokamakdev/plugin-secure-storage 0.1.0-beta.83 → 0.1.0-beta.85
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 +1 -0
- package/apple/SecureStoragePlugin.swift +61 -14
- package/package.json +2 -2
- package/src/index.ts +2 -2
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ the platform's default prompt is shown.
|
|
|
57
57
|
| `NotSupportedError` | The platform, build or device cannot enforce the options |
|
|
58
58
|
| `InvalidStateError` | The requested authentication is not set up on the device |
|
|
59
59
|
| `NotAllowedError` | The user cancelled or failed authentication, or the device is locked |
|
|
60
|
+
| `NeedsUIError` | Reading a value saved with `authentication` while the app is off screen on iOS or Android, where the system cannot show its prompt |
|
|
60
61
|
| `NotReadableError` | A stored value can no longer be decrypted |
|
|
61
62
|
| `TypeError` | An argument is missing or has an unsupported value |
|
|
62
63
|
| `OperationError` | The platform reported another failure |
|
|
@@ -2,14 +2,21 @@ import Foundation
|
|
|
2
2
|
import LocalAuthentication
|
|
3
3
|
import Security
|
|
4
4
|
|
|
5
|
+
#if os(iOS)
|
|
6
|
+
import UIKit
|
|
7
|
+
#endif
|
|
8
|
+
|
|
5
9
|
final class TokamakSecureStoragePlugin: TokamakPlugin {
|
|
6
10
|
let id = "secure-storage"
|
|
7
11
|
|
|
8
12
|
private let service = "tokamak.secure-storage"
|
|
9
13
|
/// Keychain calls block while the system authentication prompt is shown.
|
|
10
14
|
private let queue = DispatchQueue(label: "tokamak.secure-storage")
|
|
15
|
+
private let host: TokamakHost
|
|
11
16
|
|
|
12
|
-
init(host: TokamakHost) {
|
|
17
|
+
init(host: TokamakHost) {
|
|
18
|
+
self.host = host
|
|
19
|
+
}
|
|
13
20
|
|
|
14
21
|
private static let teamSigningRequired = TokamakPluginError.notSupported(
|
|
15
22
|
"Secure storage requires a team-signed build"
|
|
@@ -31,14 +38,33 @@ final class TokamakSecureStoragePlugin: TokamakPlugin {
|
|
|
31
38
|
reply: @escaping TokamakPluginReply
|
|
32
39
|
) {
|
|
33
40
|
let arguments = arguments as? [String: Any] ?? [:]
|
|
41
|
+
let prompting =
|
|
42
|
+
isUnlocked ? Result { () throws(TokamakPluginError) in try host.requireUI() } : nil
|
|
34
43
|
queue.async {
|
|
35
|
-
reply(
|
|
44
|
+
reply(
|
|
45
|
+
Result { () throws(TokamakPluginError) in
|
|
46
|
+
try self.perform(method, arguments, prompting: prompting)
|
|
47
|
+
})
|
|
36
48
|
}
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
/// Whether the device's protected data is available, which it is not while
|
|
52
|
+
/// an iPhone is locked. Read it on the main thread.
|
|
53
|
+
private var isUnlocked: Bool {
|
|
54
|
+
#if os(iOS)
|
|
55
|
+
UIApplication.shared.isProtectedDataAvailable
|
|
56
|
+
#else
|
|
57
|
+
true
|
|
58
|
+
#endif
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// `prompting` is whether a read that needs authentication may show the
|
|
62
|
+
/// system prompt, decided on the main thread as the call arrives: nil while
|
|
63
|
+
/// the device is locked, where such a read fails without UI anyway.
|
|
39
64
|
private func perform(
|
|
40
65
|
_ method: String,
|
|
41
|
-
_ arguments: [String: Any]
|
|
66
|
+
_ arguments: [String: Any],
|
|
67
|
+
prompting: Result<Void, TokamakPluginError>?
|
|
42
68
|
) throws(TokamakPluginError) -> Any? {
|
|
43
69
|
#if os(macOS)
|
|
44
70
|
// Unentitled reads report errSecItemNotFound rather than a missing entitlement.
|
|
@@ -59,7 +85,8 @@ final class TokamakSecureStoragePlugin: TokamakPlugin {
|
|
|
59
85
|
case "get":
|
|
60
86
|
return try get(
|
|
61
87
|
requiredString(arguments, "name"),
|
|
62
|
-
prompt: optionalString(arguments, "prompt")
|
|
88
|
+
prompt: optionalString(arguments, "prompt"),
|
|
89
|
+
prompting: prompting
|
|
63
90
|
)
|
|
64
91
|
case "delete":
|
|
65
92
|
try deleteItems(query(requiredString(arguments, "name")))
|
|
@@ -97,16 +124,28 @@ final class TokamakSecureStoragePlugin: TokamakPlugin {
|
|
|
97
124
|
try check(status)
|
|
98
125
|
}
|
|
99
126
|
|
|
100
|
-
|
|
127
|
+
/// Reads without UI first, so only a value saved with authentication needs
|
|
128
|
+
/// the app able to show the system prompt.
|
|
129
|
+
private func get(
|
|
130
|
+
_ name: String,
|
|
131
|
+
prompt: String?,
|
|
132
|
+
prompting: Result<Void, TokamakPluginError>?
|
|
133
|
+
) throws(TokamakPluginError) -> String? {
|
|
101
134
|
var item = query(name)
|
|
102
135
|
item[kSecReturnData] = true
|
|
103
|
-
|
|
104
|
-
let context = LAContext()
|
|
105
|
-
context.localizedReason = prompt
|
|
106
|
-
item[kSecUseAuthenticationContext] = context
|
|
107
|
-
}
|
|
136
|
+
item[kSecUseAuthenticationContext] = Self.contextWithoutUI()
|
|
108
137
|
var data: CFTypeRef?
|
|
109
|
-
|
|
138
|
+
var status = SecItemCopyMatching(item as CFDictionary, &data)
|
|
139
|
+
if status == errSecInteractionNotAllowed, let prompting {
|
|
140
|
+
try prompting.get()
|
|
141
|
+
item[kSecUseAuthenticationContext] = prompt.flatMap { prompt -> LAContext? in
|
|
142
|
+
guard !prompt.isEmpty else { return nil }
|
|
143
|
+
let context = LAContext()
|
|
144
|
+
context.localizedReason = prompt
|
|
145
|
+
return context
|
|
146
|
+
}
|
|
147
|
+
status = SecItemCopyMatching(item as CFDictionary, &data)
|
|
148
|
+
}
|
|
110
149
|
if status == errSecItemNotFound {
|
|
111
150
|
guard try exists(name) else { return nil }
|
|
112
151
|
throw TokamakPluginError(
|
|
@@ -154,13 +193,21 @@ final class TokamakSecureStoragePlugin: TokamakPlugin {
|
|
|
154
193
|
|
|
155
194
|
/// `query` reading attributes only, which needs no authentication.
|
|
156
195
|
private func attributesQuery(_ query: [CFString: Any]) -> [CFString: Any] {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
196
|
+
query.merging([
|
|
197
|
+
kSecReturnAttributes: true, kSecUseAuthenticationContext: Self.contextWithoutUI(),
|
|
198
|
+
]) {
|
|
160
199
|
_, attribute in attribute
|
|
161
200
|
}
|
|
162
201
|
}
|
|
163
202
|
|
|
203
|
+
/// A context in which reads that need authentication fail with
|
|
204
|
+
/// `errSecInteractionNotAllowed` rather than show the system prompt.
|
|
205
|
+
private static func contextWithoutUI() -> LAContext {
|
|
206
|
+
let context = LAContext()
|
|
207
|
+
context.interactionNotAllowed = true
|
|
208
|
+
return context
|
|
209
|
+
}
|
|
210
|
+
|
|
164
211
|
/// All of the plugin's items. The data protection keychain is the only
|
|
165
212
|
/// keychain on iOS; macOS needs a team-signed build to use it.
|
|
166
213
|
private func serviceQuery() -> [CFString: Any] {
|
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.85",
|
|
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.85"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"android",
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Plugin } from "@tokamakdev/plugin";
|
|
2
2
|
|
|
3
3
|
/** When a stored value can be read. */
|
|
4
4
|
export type Readable = "whenUnlocked" | "afterFirstUnlock";
|
|
@@ -22,7 +22,7 @@ export interface GetOptions {
|
|
|
22
22
|
readonly prompt?: string;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
class SecureStorage extends
|
|
25
|
+
class SecureStorage extends Plugin {
|
|
26
26
|
constructor() {
|
|
27
27
|
super("secure-storage");
|
|
28
28
|
}
|