@tokamakdev/plugin-secure-storage 0.1.0-beta.82 → 0.1.0-beta.84
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 +58 -14
- package/package.json +3 -3
- 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,29 @@ final class TokamakSecureStoragePlugin: TokamakPlugin {
|
|
|
31
38
|
reply: @escaping TokamakPluginReply
|
|
32
39
|
) {
|
|
33
40
|
let arguments = arguments as? [String: Any] ?? [:]
|
|
41
|
+
let unlocked = Self.isUnlocked
|
|
34
42
|
queue.async {
|
|
35
|
-
reply(
|
|
43
|
+
reply(
|
|
44
|
+
Result { () throws(TokamakPluginError) in
|
|
45
|
+
try self.perform(method, arguments, unlocked: unlocked)
|
|
46
|
+
})
|
|
36
47
|
}
|
|
37
48
|
}
|
|
38
49
|
|
|
50
|
+
/// Whether the device's protected data is available, which it is not while
|
|
51
|
+
/// an iPhone is locked. Read it on the main thread.
|
|
52
|
+
private static var isUnlocked: Bool {
|
|
53
|
+
#if os(iOS)
|
|
54
|
+
UIApplication.shared.isProtectedDataAvailable
|
|
55
|
+
#else
|
|
56
|
+
true
|
|
57
|
+
#endif
|
|
58
|
+
}
|
|
59
|
+
|
|
39
60
|
private func perform(
|
|
40
61
|
_ method: String,
|
|
41
|
-
_ arguments: [String: Any]
|
|
62
|
+
_ arguments: [String: Any],
|
|
63
|
+
unlocked: Bool
|
|
42
64
|
) throws(TokamakPluginError) -> Any? {
|
|
43
65
|
#if os(macOS)
|
|
44
66
|
// Unentitled reads report errSecItemNotFound rather than a missing entitlement.
|
|
@@ -59,7 +81,8 @@ final class TokamakSecureStoragePlugin: TokamakPlugin {
|
|
|
59
81
|
case "get":
|
|
60
82
|
return try get(
|
|
61
83
|
requiredString(arguments, "name"),
|
|
62
|
-
prompt: optionalString(arguments, "prompt")
|
|
84
|
+
prompt: optionalString(arguments, "prompt"),
|
|
85
|
+
unlocked: unlocked
|
|
63
86
|
)
|
|
64
87
|
case "delete":
|
|
65
88
|
try deleteItems(query(requiredString(arguments, "name")))
|
|
@@ -97,16 +120,29 @@ final class TokamakSecureStoragePlugin: TokamakPlugin {
|
|
|
97
120
|
try check(status)
|
|
98
121
|
}
|
|
99
122
|
|
|
100
|
-
|
|
123
|
+
/// Reads without UI first, so only a value saved with authentication needs
|
|
124
|
+
/// the app able to show the system prompt. While the device is locked, the
|
|
125
|
+
/// read fails without UI whatever the value needs.
|
|
126
|
+
private func get(
|
|
127
|
+
_ name: String,
|
|
128
|
+
prompt: String?,
|
|
129
|
+
unlocked: Bool
|
|
130
|
+
) throws(TokamakPluginError) -> String? {
|
|
101
131
|
var item = query(name)
|
|
102
132
|
item[kSecReturnData] = true
|
|
103
|
-
|
|
104
|
-
let context = LAContext()
|
|
105
|
-
context.localizedReason = prompt
|
|
106
|
-
item[kSecUseAuthenticationContext] = context
|
|
107
|
-
}
|
|
133
|
+
item[kSecUseAuthenticationContext] = Self.contextWithoutUI()
|
|
108
134
|
var data: CFTypeRef?
|
|
109
|
-
|
|
135
|
+
var status = SecItemCopyMatching(item as CFDictionary, &data)
|
|
136
|
+
if status == errSecInteractionNotAllowed && unlocked {
|
|
137
|
+
try host.requireUI()
|
|
138
|
+
item[kSecUseAuthenticationContext] = prompt.flatMap { prompt -> LAContext? in
|
|
139
|
+
guard !prompt.isEmpty else { return nil }
|
|
140
|
+
let context = LAContext()
|
|
141
|
+
context.localizedReason = prompt
|
|
142
|
+
return context
|
|
143
|
+
}
|
|
144
|
+
status = SecItemCopyMatching(item as CFDictionary, &data)
|
|
145
|
+
}
|
|
110
146
|
if status == errSecItemNotFound {
|
|
111
147
|
guard try exists(name) else { return nil }
|
|
112
148
|
throw TokamakPluginError(
|
|
@@ -154,13 +190,21 @@ final class TokamakSecureStoragePlugin: TokamakPlugin {
|
|
|
154
190
|
|
|
155
191
|
/// `query` reading attributes only, which needs no authentication.
|
|
156
192
|
private func attributesQuery(_ query: [CFString: Any]) -> [CFString: Any] {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
193
|
+
query.merging([
|
|
194
|
+
kSecReturnAttributes: true, kSecUseAuthenticationContext: Self.contextWithoutUI(),
|
|
195
|
+
]) {
|
|
160
196
|
_, attribute in attribute
|
|
161
197
|
}
|
|
162
198
|
}
|
|
163
199
|
|
|
200
|
+
/// A context in which reads that need authentication fail with
|
|
201
|
+
/// `errSecInteractionNotAllowed` rather than show the system prompt.
|
|
202
|
+
private static func contextWithoutUI() -> LAContext {
|
|
203
|
+
let context = LAContext()
|
|
204
|
+
context.interactionNotAllowed = true
|
|
205
|
+
return context
|
|
206
|
+
}
|
|
207
|
+
|
|
164
208
|
/// All of the plugin's items. The data protection keychain is the only
|
|
165
209
|
/// keychain on iOS; macOS needs a team-signed build to use it.
|
|
166
210
|
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.84",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/mantty/tokamak.git",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"exports": {
|
|
11
11
|
".": "./src/index.ts"
|
|
12
12
|
},
|
|
13
|
-
"
|
|
14
|
-
"@tokamakdev/plugin": "0.1.0-beta.
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"@tokamakdev/plugin": "0.1.0-beta.84"
|
|
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
|
}
|