capacitor-secure-storage-plugin 0.13.0 → 0.14.0
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/CapacitorSecureStoragePlugin.podspec +0 -1
- package/Package.swift +3 -9
- package/README.md +17 -6
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/SecureStoragePlugin/Keychain.swift +113 -0
- package/ios/Sources/SecureStoragePlugin/SecureStoragePlugin.swift +46 -93
- package/package.json +14 -18
- package/ios/Tests/SecureStoragePluginTests/SecureStoragePluginTests.swift +0 -247
package/Package.swift
CHANGED
|
@@ -10,21 +10,15 @@ let package = Package(
|
|
|
10
10
|
targets: ["SecureStoragePlugin"])
|
|
11
11
|
],
|
|
12
12
|
dependencies: [
|
|
13
|
-
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
|
|
14
|
-
.package(url: "https://github.com/jrendel/SwiftKeychainWrapper.git", from: "4.0.0")
|
|
13
|
+
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0")
|
|
15
14
|
],
|
|
16
15
|
targets: [
|
|
17
16
|
.target(
|
|
18
17
|
name: "SecureStoragePlugin",
|
|
19
18
|
dependencies: [
|
|
20
19
|
.product(name: "Capacitor", package: "capacitor-swift-pm"),
|
|
21
|
-
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
22
|
-
.product(name: "SwiftKeychainWrapper", package: "SwiftKeychainWrapper")
|
|
20
|
+
.product(name: "Cordova", package: "capacitor-swift-pm")
|
|
23
21
|
],
|
|
24
|
-
path: "ios/Sources/SecureStoragePlugin")
|
|
25
|
-
.testTarget(
|
|
26
|
-
name: "SecureStoragePluginTests",
|
|
27
|
-
dependencies: ["SecureStoragePlugin"],
|
|
28
|
-
path: "ios/Tests/SecureStoragePluginTests")
|
|
22
|
+
path: "ios/Sources/SecureStoragePlugin")
|
|
29
23
|
]
|
|
30
24
|
)
|
package/README.md
CHANGED
|
@@ -6,12 +6,18 @@ Capacitor plugin for storing string values securly on iOS and Android.
|
|
|
6
6
|
|
|
7
7
|
## How to install
|
|
8
8
|
|
|
9
|
-
For Capacitor
|
|
9
|
+
For Capacitor v8
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
12
|
npm install capacitor-secure-storage-plugin
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
For Capacitor v7 - install with fixed version 0.12.0
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install capacitor-secure-storage-plugin@0.12.0
|
|
19
|
+
```
|
|
20
|
+
|
|
15
21
|
For Capacitor v6 - install with fixed version 0.10.0
|
|
16
22
|
|
|
17
23
|
```bash
|
|
@@ -182,24 +188,29 @@ async getUsername(key: string) {
|
|
|
182
188
|
|
|
183
189
|
### iOS
|
|
184
190
|
|
|
185
|
-
|
|
191
|
+
Values are stored in the iOS Keychain as generic passwords under the `cap_sec` service and are not synced to iCloud.
|
|
192
|
+
|
|
193
|
+
Values are accessible only after the device has been unlocked for the first time after a reboot. Any keychain failure other than a missing key (e.g. reading while the app runs in the background before the first unlock) rejects with message `Keychain error <OSStatus>: <description>` and `code` set to the OSStatus, e.g. `-25308` (`errSecInteractionNotAllowed`). Only a really missing key rejects with `Item with given key does not exist`.
|
|
186
194
|
|
|
187
195
|
After reinstalling an app, the data stored in the keychain are not deleted automatically. To clear the data the following code has to be added to AppDelegate.swift. This is just an example, there are multiple ways how it can be achieved.
|
|
188
196
|
|
|
189
197
|
```swift
|
|
190
|
-
import
|
|
198
|
+
import Security
|
|
191
199
|
|
|
192
200
|
|
|
193
201
|
if !UserDefaults.standard.bool(forKey: "firstTimeLaunchOccurred") {
|
|
194
|
-
let
|
|
195
|
-
|
|
202
|
+
let query: [String: Any] = [
|
|
203
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
204
|
+
kSecAttrService as String: "cap_sec"
|
|
205
|
+
]
|
|
206
|
+
SecItemDelete(query as CFDictionary)
|
|
196
207
|
|
|
197
208
|
UserDefaults.standard.set(true, forKey: "firstTimeLaunchOccurred")
|
|
198
209
|
}
|
|
199
210
|
```
|
|
200
211
|
|
|
201
212
|
> **Warning**
|
|
202
|
-
>
|
|
213
|
+
> Since v0.14.0 values saved by v0.4.0 or older (stored in the standard keychain) are no longer read. Upgrade through v0.13.0 first and read the values to migrate them.
|
|
203
214
|
|
|
204
215
|
### Android
|
|
205
216
|
|
package/dist/esm/web.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C,MAAM,OAAO,sBACX,SAAQ,SAAS;IADnB;;QAGE,WAAM,GAAG,UAAU,CAAC;QAyCZ,cAAS,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC;QAC/C,iBAAY,GAAG,CAAC,GAAW,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACvE,CAAC;IAzCC,GAAG,CAAC,OAAwB;QAC1B,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;QAChE,OAAO,KAAK,KAAK,IAAI;YACnB,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC;gBACd,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC;aACnB,CAAC;YACJ,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,oCAAoC,CAAC,CAAC;IAC3D,CAAC;IACD,GAAG,CAAC,OAAuC;QACzC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QACvE,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,MAAM,CAAC,OAAwB;QAC7B,IAAI,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;YACtD,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;YACrD,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1C,CAAC;aAAM,CAAC;YACN,OAAO,OAAO,CAAC,MAAM,CAAC,oCAAoC,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IACD,KAAK;QACH,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;YAC/B,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;gBACnC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YAC/B,CAAC;QACH,CAAC;QACD,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,IAAI;QACF,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC;aACnC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;aACzC,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC1B,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED,WAAW;QACT,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3C,CAAC;CAIF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type { SecureStoragePluginPlugin } from './definitions';\n\nexport class SecureStoragePluginWeb\n extends WebPlugin\n implements SecureStoragePluginPlugin {\n PREFIX = 'cap_sec_';\n\n get(options: { key: string }): Promise<{ value: string }> {\n const value = localStorage.getItem(this.addPrefix(options.key));\n return value !== null\n ? Promise.resolve({\n value: atob(value),\n })\n : Promise.reject('Item with given key does not exist');\n }\n set(options: { key: string; value: string }): Promise<{ value: boolean }> {\n localStorage.setItem(this.addPrefix(options.key), btoa(options.value));\n return Promise.resolve({ value: true });\n }\n remove(options: { key: string }): Promise<{ value: boolean }> {\n if (localStorage.getItem(this.addPrefix(options.key))) {\n localStorage.removeItem(this.addPrefix(options.key));\n return Promise.resolve({ value: true });\n } else {\n return Promise.reject('Item with given key does not exist');\n }\n }\n clear(): Promise<{ value: boolean }> {\n for (const key in localStorage) {\n if (key.indexOf(this.PREFIX) === 0) {\n localStorage.removeItem(key);\n }\n }\n return Promise.resolve({ value: true });\n }\n keys(): Promise<{ value: string[] }> {\n const keys = Object.keys(localStorage)\n .filter(k => k.indexOf(this.PREFIX) === 0)\n .map(this.removePrefix);\n return Promise.resolve({ value: keys });\n }\n\n getPlatform(): Promise<{ value: string }> {\n return Promise.resolve({ value: 'web' });\n }\n\n private addPrefix = (key: string) => this.PREFIX + key;\n private removePrefix = (key: string) => key.replace(this.PREFIX, '');\n}\n"]}
|
package/dist/plugin.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst SecureStoragePlugin = registerPlugin('SecureStoragePlugin', {\n web: () => import('./web').then(m => new m.SecureStoragePluginWeb()),\n});\nexport * from './definitions';\nexport { SecureStoragePlugin };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SecureStoragePluginWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.PREFIX = 'cap_sec_';\n this.addPrefix = (key) => this.PREFIX + key;\n this.removePrefix = (key) => key.replace(this.PREFIX, '');\n }\n get(options) {\n const value = localStorage.getItem(this.addPrefix(options.key));\n return value !== null\n ? Promise.resolve({\n value: atob(value),\n })\n : Promise.reject('Item with given key does not exist');\n }\n set(options) {\n localStorage.setItem(this.addPrefix(options.key), btoa(options.value));\n return Promise.resolve({ value: true });\n }\n remove(options) {\n if (localStorage.getItem(this.addPrefix(options.key))) {\n localStorage.removeItem(this.addPrefix(options.key));\n return Promise.resolve({ value: true });\n }\n else {\n return Promise.reject('Item with given key does not exist');\n }\n }\n clear() {\n for (const key in localStorage) {\n if (key.indexOf(this.PREFIX) === 0) {\n localStorage.removeItem(key);\n }\n }\n return Promise.resolve({ value: true });\n }\n keys() {\n const keys = Object.keys(localStorage)\n .filter(k => k.indexOf(this.PREFIX) === 0)\n .map(this.removePrefix);\n return Promise.resolve({ value: keys });\n }\n getPlatform() {\n return Promise.resolve({ value: 'web' });\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,mBAAmB,GAAGA,mBAAc,CAAC,qBAAqB,EAAE;AAClE,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,sBAAsB,EAAE,CAAC;AACxE,CAAC;;ACFM,MAAM,sBAAsB,SAASC,cAAS,CAAC;AACtD,IAAI,WAAW,GAAG;AAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;AAC3B,QAAQ,IAAI,CAAC,MAAM,GAAG,UAAU;AAChC,QAAQ,IAAI,CAAC,SAAS,GAAG,CAAC,GAAG,KAAK,IAAI,CAAC,MAAM,GAAG,GAAG;AACnD,QAAQ,IAAI,CAAC,YAAY,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AACjE,IAAI;AACJ,IAAI,GAAG,CAAC,OAAO,EAAE;AACjB,QAAQ,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AACvE,QAAQ,OAAO,KAAK,KAAK;AACzB,cAAc,OAAO,CAAC,OAAO,CAAC;AAC9B,gBAAgB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC;AAClC,aAAa;AACb,cAAc,OAAO,CAAC,MAAM,CAAC,oCAAoC,CAAC;AAClE,IAAI;AACJ,IAAI,GAAG,CAAC,OAAO,EAAE;AACjB,QAAQ,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC/C,IAAI;AACJ,IAAI,MAAM,CAAC,OAAO,EAAE;AACpB,QAAQ,IAAI,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE;AAC/D,YAAY,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AAChE,YAAY,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACnD,QAAQ;AACR,aAAa;AACb,YAAY,OAAO,OAAO,CAAC,MAAM,CAAC,oCAAoC,CAAC;AACvE,QAAQ;AACR,IAAI;AACJ,IAAI,KAAK,GAAG;AACZ,QAAQ,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE;AACxC,YAAY,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;AAChD,gBAAgB,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC;AAC5C,YAAY;AACZ,QAAQ;AACR,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC/C,IAAI;AACJ,IAAI,IAAI,GAAG;AACX,QAAQ,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY;AAC7C,aAAa,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;AACrD,aAAa,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC;AACnC,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC/C,IAAI;AACJ,IAAI,WAAW,GAAG;AAClB,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAChD,IAAI;AACJ
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst SecureStoragePlugin = registerPlugin('SecureStoragePlugin', {\n web: () => import('./web').then(m => new m.SecureStoragePluginWeb()),\n});\nexport * from './definitions';\nexport { SecureStoragePlugin };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SecureStoragePluginWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.PREFIX = 'cap_sec_';\n this.addPrefix = (key) => this.PREFIX + key;\n this.removePrefix = (key) => key.replace(this.PREFIX, '');\n }\n get(options) {\n const value = localStorage.getItem(this.addPrefix(options.key));\n return value !== null\n ? Promise.resolve({\n value: atob(value),\n })\n : Promise.reject('Item with given key does not exist');\n }\n set(options) {\n localStorage.setItem(this.addPrefix(options.key), btoa(options.value));\n return Promise.resolve({ value: true });\n }\n remove(options) {\n if (localStorage.getItem(this.addPrefix(options.key))) {\n localStorage.removeItem(this.addPrefix(options.key));\n return Promise.resolve({ value: true });\n }\n else {\n return Promise.reject('Item with given key does not exist');\n }\n }\n clear() {\n for (const key in localStorage) {\n if (key.indexOf(this.PREFIX) === 0) {\n localStorage.removeItem(key);\n }\n }\n return Promise.resolve({ value: true });\n }\n keys() {\n const keys = Object.keys(localStorage)\n .filter(k => k.indexOf(this.PREFIX) === 0)\n .map(this.removePrefix);\n return Promise.resolve({ value: keys });\n }\n getPlatform() {\n return Promise.resolve({ value: 'web' });\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,mBAAmB,GAAGA,mBAAc,CAAC,qBAAqB,EAAE;AAClE,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,sBAAsB,EAAE,CAAC;AACxE,CAAC;;ACFM,MAAM,sBAAsB,SAASC,cAAS,CAAC;AACtD,IAAI,WAAW,GAAG;AAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;AAC3B,QAAQ,IAAI,CAAC,MAAM,GAAG,UAAU;AAChC,QAAQ,IAAI,CAAC,SAAS,GAAG,CAAC,GAAG,KAAK,IAAI,CAAC,MAAM,GAAG,GAAG;AACnD,QAAQ,IAAI,CAAC,YAAY,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AACjE,IAAI;AACJ,IAAI,GAAG,CAAC,OAAO,EAAE;AACjB,QAAQ,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AACvE,QAAQ,OAAO,KAAK,KAAK;AACzB,cAAc,OAAO,CAAC,OAAO,CAAC;AAC9B,gBAAgB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC;AAClC,aAAa;AACb,cAAc,OAAO,CAAC,MAAM,CAAC,oCAAoC,CAAC;AAClE,IAAI;AACJ,IAAI,GAAG,CAAC,OAAO,EAAE;AACjB,QAAQ,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC/C,IAAI;AACJ,IAAI,MAAM,CAAC,OAAO,EAAE;AACpB,QAAQ,IAAI,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE;AAC/D,YAAY,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AAChE,YAAY,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AACnD,QAAQ;AACR,aAAa;AACb,YAAY,OAAO,OAAO,CAAC,MAAM,CAAC,oCAAoC,CAAC;AACvE,QAAQ;AACR,IAAI;AACJ,IAAI,KAAK,GAAG;AACZ,QAAQ,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE;AACxC,YAAY,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;AAChD,gBAAgB,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC;AAC5C,YAAY;AACZ,QAAQ;AACR,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC/C,IAAI;AACJ,IAAI,IAAI,GAAG;AACX,QAAQ,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY;AAC7C,aAAa,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;AACrD,aAAa,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC;AACnC,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC/C,IAAI;AACJ,IAAI,WAAW,GAAG;AAClB,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAChD,IAAI;AACJ;;;;;;;"}
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst SecureStoragePlugin = registerPlugin('SecureStoragePlugin', {\n web: () => import('./web').then(m => new m.SecureStoragePluginWeb()),\n});\nexport * from './definitions';\nexport { SecureStoragePlugin };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SecureStoragePluginWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.PREFIX = 'cap_sec_';\n this.addPrefix = (key) => this.PREFIX + key;\n this.removePrefix = (key) => key.replace(this.PREFIX, '');\n }\n get(options) {\n const value = localStorage.getItem(this.addPrefix(options.key));\n return value !== null\n ? Promise.resolve({\n value: atob(value),\n })\n : Promise.reject('Item with given key does not exist');\n }\n set(options) {\n localStorage.setItem(this.addPrefix(options.key), btoa(options.value));\n return Promise.resolve({ value: true });\n }\n remove(options) {\n if (localStorage.getItem(this.addPrefix(options.key))) {\n localStorage.removeItem(this.addPrefix(options.key));\n return Promise.resolve({ value: true });\n }\n else {\n return Promise.reject('Item with given key does not exist');\n }\n }\n clear() {\n for (const key in localStorage) {\n if (key.indexOf(this.PREFIX) === 0) {\n localStorage.removeItem(key);\n }\n }\n return Promise.resolve({ value: true });\n }\n keys() {\n const keys = Object.keys(localStorage)\n .filter(k => k.indexOf(this.PREFIX) === 0)\n .map(this.removePrefix);\n return Promise.resolve({ value: keys });\n }\n getPlatform() {\n return Promise.resolve({ value: 'web' });\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,mBAAmB,GAAGA,mBAAc,CAAC,qBAAqB,EAAE;IAClE,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,sBAAsB,EAAE,CAAC;IACxE,CAAC;;ICFM,MAAM,sBAAsB,SAASC,cAAS,CAAC;IACtD,IAAI,WAAW,GAAG;IAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;IAC3B,QAAQ,IAAI,CAAC,MAAM,GAAG,UAAU;IAChC,QAAQ,IAAI,CAAC,SAAS,GAAG,CAAC,GAAG,KAAK,IAAI,CAAC,MAAM,GAAG,GAAG;IACnD,QAAQ,IAAI,CAAC,YAAY,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;IACjE,IAAI;IACJ,IAAI,GAAG,CAAC,OAAO,EAAE;IACjB,QAAQ,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACvE,QAAQ,OAAO,KAAK,KAAK;IACzB,cAAc,OAAO,CAAC,OAAO,CAAC;IAC9B,gBAAgB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC;IAClC,aAAa;IACb,cAAc,OAAO,CAAC,MAAM,CAAC,oCAAoC,CAAC;IAClE,IAAI;IACJ,IAAI,GAAG,CAAC,OAAO,EAAE;IACjB,QAAQ,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9E,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAI;IACJ,IAAI,MAAM,CAAC,OAAO,EAAE;IACpB,QAAQ,IAAI,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE;IAC/D,YAAY,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChE,YAAY,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACnD,QAAQ;IACR,aAAa;IACb,YAAY,OAAO,OAAO,CAAC,MAAM,CAAC,oCAAoC,CAAC;IACvE,QAAQ;IACR,IAAI;IACJ,IAAI,KAAK,GAAG;IACZ,QAAQ,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE;IACxC,YAAY,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;IAChD,gBAAgB,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC;IAC5C,YAAY;IACZ,QAAQ;IACR,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAI;IACJ,IAAI,IAAI,GAAG;IACX,QAAQ,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY;IAC7C,aAAa,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;IACrD,aAAa,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC;IACnC,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAI;IACJ,IAAI,WAAW,GAAG;IAClB,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAChD,IAAI;IACJ
|
|
1
|
+
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst SecureStoragePlugin = registerPlugin('SecureStoragePlugin', {\n web: () => import('./web').then(m => new m.SecureStoragePluginWeb()),\n});\nexport * from './definitions';\nexport { SecureStoragePlugin };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SecureStoragePluginWeb extends WebPlugin {\n constructor() {\n super(...arguments);\n this.PREFIX = 'cap_sec_';\n this.addPrefix = (key) => this.PREFIX + key;\n this.removePrefix = (key) => key.replace(this.PREFIX, '');\n }\n get(options) {\n const value = localStorage.getItem(this.addPrefix(options.key));\n return value !== null\n ? Promise.resolve({\n value: atob(value),\n })\n : Promise.reject('Item with given key does not exist');\n }\n set(options) {\n localStorage.setItem(this.addPrefix(options.key), btoa(options.value));\n return Promise.resolve({ value: true });\n }\n remove(options) {\n if (localStorage.getItem(this.addPrefix(options.key))) {\n localStorage.removeItem(this.addPrefix(options.key));\n return Promise.resolve({ value: true });\n }\n else {\n return Promise.reject('Item with given key does not exist');\n }\n }\n clear() {\n for (const key in localStorage) {\n if (key.indexOf(this.PREFIX) === 0) {\n localStorage.removeItem(key);\n }\n }\n return Promise.resolve({ value: true });\n }\n keys() {\n const keys = Object.keys(localStorage)\n .filter(k => k.indexOf(this.PREFIX) === 0)\n .map(this.removePrefix);\n return Promise.resolve({ value: keys });\n }\n getPlatform() {\n return Promise.resolve({ value: 'web' });\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,mBAAmB,GAAGA,mBAAc,CAAC,qBAAqB,EAAE;IAClE,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,sBAAsB,EAAE,CAAC;IACxE,CAAC;;ICFM,MAAM,sBAAsB,SAASC,cAAS,CAAC;IACtD,IAAI,WAAW,GAAG;IAClB,QAAQ,KAAK,CAAC,GAAG,SAAS,CAAC;IAC3B,QAAQ,IAAI,CAAC,MAAM,GAAG,UAAU;IAChC,QAAQ,IAAI,CAAC,SAAS,GAAG,CAAC,GAAG,KAAK,IAAI,CAAC,MAAM,GAAG,GAAG;IACnD,QAAQ,IAAI,CAAC,YAAY,GAAG,CAAC,GAAG,KAAK,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;IACjE,IAAI;IACJ,IAAI,GAAG,CAAC,OAAO,EAAE;IACjB,QAAQ,MAAM,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACvE,QAAQ,OAAO,KAAK,KAAK;IACzB,cAAc,OAAO,CAAC,OAAO,CAAC;IAC9B,gBAAgB,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC;IAClC,aAAa;IACb,cAAc,OAAO,CAAC,MAAM,CAAC,oCAAoC,CAAC;IAClE,IAAI;IACJ,IAAI,GAAG,CAAC,OAAO,EAAE;IACjB,QAAQ,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;IAC9E,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAI;IACJ,IAAI,MAAM,CAAC,OAAO,EAAE;IACpB,QAAQ,IAAI,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE;IAC/D,YAAY,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChE,YAAY,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACnD,QAAQ;IACR,aAAa;IACb,YAAY,OAAO,OAAO,CAAC,MAAM,CAAC,oCAAoC,CAAC;IACvE,QAAQ;IACR,IAAI;IACJ,IAAI,KAAK,GAAG;IACZ,QAAQ,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE;IACxC,YAAY,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;IAChD,gBAAgB,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC;IAC5C,YAAY;IACZ,QAAQ;IACR,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAI;IACJ,IAAI,IAAI,GAAG;IACX,QAAQ,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY;IAC7C,aAAa,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;IACrD,aAAa,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC;IACnC,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC/C,IAAI;IACJ,IAAI,WAAW,GAAG;IAClB,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAChD,IAAI;IACJ;;;;;;;;;;;"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Security
|
|
3
|
+
|
|
4
|
+
/// Keychain operation failed for a reason other than a missing item,
|
|
5
|
+
/// e.g. `errSecInteractionNotAllowed` when the device was not unlocked since reboot.
|
|
6
|
+
struct KeychainError: Error {
|
|
7
|
+
let status: OSStatus
|
|
8
|
+
|
|
9
|
+
var message: String {
|
|
10
|
+
let description = SecCopyErrorMessageString(status, nil) as String? ?? "Unknown error"
|
|
11
|
+
return "Keychain error \(status): \(description)"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/// Minimal generic-password keychain store.
|
|
16
|
+
///
|
|
17
|
+
/// Items are stored in exactly the same shape SwiftKeychainWrapper used (account and generic
|
|
18
|
+
/// attributes as UTF-8 `Data`, value as UTF-8 bytes, non-synchronizable), so items written
|
|
19
|
+
/// by earlier plugin versions stay readable. Do not change the item shape without a migration.
|
|
20
|
+
///
|
|
21
|
+
/// Missing items are reported by `nil` / `false`; every other failure throws `KeychainError`.
|
|
22
|
+
struct Keychain {
|
|
23
|
+
let service: String
|
|
24
|
+
|
|
25
|
+
func set(_ value: String, forKey key: String) throws {
|
|
26
|
+
let attributes: [String: Any] = [
|
|
27
|
+
kSecValueData as String: Data(value.utf8),
|
|
28
|
+
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlock
|
|
29
|
+
]
|
|
30
|
+
let query = itemQuery(forKey: key)
|
|
31
|
+
|
|
32
|
+
var status = SecItemAdd(query.merging(attributes) { $1 } as CFDictionary, nil)
|
|
33
|
+
if status == errSecDuplicateItem {
|
|
34
|
+
// Accessibility must not be part of the match, otherwise items stored with
|
|
35
|
+
// a different accessibility by older versions could never be overwritten.
|
|
36
|
+
status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
|
|
37
|
+
}
|
|
38
|
+
try check(status)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func string(forKey key: String) throws -> String? {
|
|
42
|
+
var query = itemQuery(forKey: key)
|
|
43
|
+
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
44
|
+
query[kSecReturnData as String] = true
|
|
45
|
+
|
|
46
|
+
var result: AnyObject?
|
|
47
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
48
|
+
if status == errSecItemNotFound {
|
|
49
|
+
return nil
|
|
50
|
+
}
|
|
51
|
+
try check(status)
|
|
52
|
+
guard let data = result as? Data, let value = String(data: data, encoding: .utf8) else {
|
|
53
|
+
throw KeychainError(status: errSecDecode)
|
|
54
|
+
}
|
|
55
|
+
return value
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Returns `false` when there was nothing to remove.
|
|
59
|
+
func remove(forKey key: String) throws -> Bool {
|
|
60
|
+
let status = SecItemDelete(itemQuery(forKey: key) as CFDictionary)
|
|
61
|
+
if status == errSecItemNotFound {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
try check(status)
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func removeAll() throws {
|
|
69
|
+
let status = SecItemDelete(serviceQuery() as CFDictionary)
|
|
70
|
+
if status != errSecItemNotFound {
|
|
71
|
+
try check(status)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
func allKeys() throws -> [String] {
|
|
76
|
+
var query = serviceQuery()
|
|
77
|
+
query[kSecReturnAttributes as String] = true
|
|
78
|
+
query[kSecMatchLimit as String] = kSecMatchLimitAll
|
|
79
|
+
|
|
80
|
+
var result: AnyObject?
|
|
81
|
+
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
82
|
+
if status == errSecItemNotFound {
|
|
83
|
+
return []
|
|
84
|
+
}
|
|
85
|
+
try check(status)
|
|
86
|
+
let items = result as? [[String: Any]] ?? []
|
|
87
|
+
return items.compactMap { item in
|
|
88
|
+
(item[kSecAttrAccount as String] as? Data).flatMap { String(data: $0, encoding: .utf8) }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private func check(_ status: OSStatus) throws {
|
|
93
|
+
if status != errSecSuccess {
|
|
94
|
+
throw KeychainError(status: status)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private func serviceQuery() -> [String: Any] {
|
|
99
|
+
[
|
|
100
|
+
kSecClass as String: kSecClassGenericPassword,
|
|
101
|
+
kSecAttrService as String: service
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private func itemQuery(forKey key: String) -> [String: Any] {
|
|
106
|
+
let encodedKey = Data(key.utf8)
|
|
107
|
+
var query = serviceQuery()
|
|
108
|
+
query[kSecAttrAccount as String] = encodedKey
|
|
109
|
+
query[kSecAttrGeneric as String] = encodedKey
|
|
110
|
+
query[kSecAttrSynchronizable as String] = false
|
|
111
|
+
return query
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import Foundation
|
|
2
2
|
import Capacitor
|
|
3
|
-
import SwiftKeychainWrapper
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Please read the Capacitor iOS Plugin Development Guide
|
|
@@ -8,124 +7,78 @@ import SwiftKeychainWrapper
|
|
|
8
7
|
*/
|
|
9
8
|
@objc(SecureStoragePlugin)
|
|
10
9
|
public class SecureStoragePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
11
|
-
public let identifier = "SecureStoragePlugin"
|
|
12
|
-
public let jsName = "SecureStoragePlugin"
|
|
10
|
+
public let identifier = "SecureStoragePlugin"
|
|
11
|
+
public let jsName = "SecureStoragePlugin"
|
|
13
12
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
14
13
|
CAPPluginMethod(name: "set", returnType: CAPPluginReturnPromise),
|
|
15
14
|
CAPPluginMethod(name: "get", returnType: CAPPluginReturnPromise),
|
|
16
15
|
CAPPluginMethod(name: "keys", returnType: CAPPluginReturnPromise),
|
|
17
16
|
CAPPluginMethod(name: "remove", returnType: CAPPluginReturnPromise),
|
|
18
17
|
CAPPluginMethod(name: "clear", returnType: CAPPluginReturnPromise),
|
|
19
|
-
CAPPluginMethod(name: "getPlatform", returnType: CAPPluginReturnPromise)
|
|
20
|
-
]
|
|
21
|
-
var
|
|
22
|
-
|
|
18
|
+
CAPPluginMethod(name: "getPlatform", returnType: CAPPluginReturnPromise)
|
|
19
|
+
]
|
|
20
|
+
// `var` so tests can point the plugin at a test-only keychain service.
|
|
21
|
+
var keychain = Keychain(service: "cap_sec")
|
|
22
|
+
|
|
23
23
|
@objc func set(_ call: CAPPluginCall) {
|
|
24
24
|
let key = call.getString("key") ?? ""
|
|
25
25
|
let value = call.getString("value") ?? ""
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
call.resolve([
|
|
29
|
-
"value": saveSuccessful
|
|
30
|
-
])
|
|
31
|
-
}
|
|
32
|
-
else {
|
|
33
|
-
call.reject("error")
|
|
26
|
+
run(call) {
|
|
27
|
+
try keychain.set(value, forKey: key)
|
|
28
|
+
call.resolve(["value": true])
|
|
34
29
|
}
|
|
35
30
|
}
|
|
36
|
-
|
|
31
|
+
|
|
37
32
|
@objc func get(_ call: CAPPluginCall) {
|
|
38
33
|
let key = call.getString("key") ?? ""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
let syncValueSuccessful: Bool = keychainwrapper.set(
|
|
45
|
-
KeychainWrapper.standard.string(forKey: key) ?? "",
|
|
46
|
-
forKey: key,
|
|
47
|
-
withAccessibility: .afterFirstUnlock
|
|
48
|
-
)
|
|
49
|
-
let removeValueSuccessful: Bool = KeychainWrapper.standard.removeObject(forKey: key)
|
|
50
|
-
if (!syncValueSuccessful || !removeValueSuccessful) {
|
|
51
|
-
call.reject("error")
|
|
34
|
+
run(call) {
|
|
35
|
+
if let value = try keychain.string(forKey: key) {
|
|
36
|
+
call.resolve(["value": value])
|
|
37
|
+
} else {
|
|
38
|
+
call.reject(Self.notFoundMessage)
|
|
52
39
|
}
|
|
53
40
|
}
|
|
54
|
-
|
|
55
|
-
if(hasValueDedicated || hasValueStandard) {
|
|
56
|
-
call.resolve([
|
|
57
|
-
"value": keychainwrapper.string(forKey: key) ?? ""
|
|
58
|
-
])
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
call.reject("Item with given key does not exist")
|
|
62
|
-
}
|
|
63
41
|
}
|
|
64
|
-
|
|
42
|
+
|
|
65
43
|
@objc func keys(_ call: CAPPluginCall) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
])
|
|
44
|
+
run(call) {
|
|
45
|
+
call.resolve(["value": try keychain.allKeys()])
|
|
46
|
+
}
|
|
70
47
|
}
|
|
71
|
-
|
|
48
|
+
|
|
72
49
|
@objc func remove(_ call: CAPPluginCall) {
|
|
73
50
|
let key = call.getString("key") ?? ""
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let removeDedicatedSuccessful: Bool = keychainwrapper.removeObject(forKey: key)
|
|
80
|
-
if(removeDedicatedSuccessful) {
|
|
81
|
-
call.resolve([
|
|
82
|
-
"value": removeDedicatedSuccessful
|
|
83
|
-
])
|
|
51
|
+
run(call) {
|
|
52
|
+
if try keychain.remove(forKey: key) {
|
|
53
|
+
call.resolve(["value": true])
|
|
54
|
+
} else {
|
|
55
|
+
call.reject(Self.notFoundMessage)
|
|
84
56
|
}
|
|
85
|
-
else {
|
|
86
|
-
call.reject("Remove failed")
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
call.reject("Item with given key does not exist")
|
|
91
57
|
}
|
|
92
58
|
}
|
|
93
|
-
|
|
59
|
+
|
|
94
60
|
@objc func clear(_ call: CAPPluginCall) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if(keys.count == 0) {
|
|
99
|
-
call.resolve([
|
|
100
|
-
"value": true
|
|
101
|
-
])
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
for key in keys {
|
|
105
|
-
let hasValueStandard = KeychainWrapper.standard.hasValue(forKey: key)
|
|
106
|
-
if (hasValueStandard) {
|
|
107
|
-
let removeStandardSuccessful = KeychainWrapper.standard.removeObject(forKey: key)
|
|
108
|
-
if (!removeStandardSuccessful) {
|
|
109
|
-
call.reject("error")
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
let clearSuccessful: Bool = keychainwrapper.removeAllKeys()
|
|
115
|
-
if(clearSuccessful) {
|
|
116
|
-
call.resolve([
|
|
117
|
-
"value": clearSuccessful
|
|
118
|
-
])
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
call.reject("error")
|
|
122
|
-
}
|
|
61
|
+
run(call) {
|
|
62
|
+
try keychain.removeAll()
|
|
63
|
+
call.resolve(["value": true])
|
|
123
64
|
}
|
|
124
65
|
}
|
|
125
|
-
|
|
66
|
+
|
|
126
67
|
@objc func getPlatform(_ call: CAPPluginCall) {
|
|
127
|
-
call.resolve([
|
|
128
|
-
|
|
129
|
-
|
|
68
|
+
call.resolve(["value": "ios"])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
static let notFoundMessage = "Item with given key does not exist"
|
|
72
|
+
|
|
73
|
+
/// Rejects with the keychain's message and OSStatus as code, so JS can tell
|
|
74
|
+
/// e.g. a locked device (-25308) from a missing key.
|
|
75
|
+
private func run(_ call: CAPPluginCall, _ body: () throws -> Void) {
|
|
76
|
+
do {
|
|
77
|
+
try body()
|
|
78
|
+
} catch let error as KeychainError {
|
|
79
|
+
call.reject(error.message, String(error.status), error)
|
|
80
|
+
} catch {
|
|
81
|
+
call.reject(error.localizedDescription, nil, error)
|
|
82
|
+
}
|
|
130
83
|
}
|
|
131
84
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "capacitor-secure-storage-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Securely store secrets such as usernames, passwords, tokens, certificates or other sensitive information (strings) on iOS & Android",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
"android/build.gradle",
|
|
12
12
|
"dist/",
|
|
13
13
|
"ios/Sources",
|
|
14
|
-
"ios/Tests",
|
|
15
14
|
"Package.swift",
|
|
16
15
|
"CapacitorSecureStoragePlugin.podspec"
|
|
17
16
|
],
|
|
@@ -36,7 +35,7 @@
|
|
|
36
35
|
"verify:web": "npm run build",
|
|
37
36
|
"lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
|
|
38
37
|
"fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
|
|
39
|
-
"eslint": "eslint
|
|
38
|
+
"eslint": "eslint",
|
|
40
39
|
"prettier": "prettier \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
41
40
|
"swiftlint": "node-swiftlint",
|
|
42
41
|
"docgen": "docgen --api SecureStoragePluginPlugin --output-readme README.md --output-json dist/docs.json",
|
|
@@ -46,31 +45,28 @@
|
|
|
46
45
|
"prepublishOnly": "npm run build"
|
|
47
46
|
},
|
|
48
47
|
"devDependencies": {
|
|
49
|
-
"@capacitor/android": "^8.
|
|
50
|
-
"@capacitor/cli": "^8.
|
|
51
|
-
"@capacitor/core": "^8.
|
|
48
|
+
"@capacitor/android": "^8.6.2",
|
|
49
|
+
"@capacitor/cli": "^8.6.2",
|
|
50
|
+
"@capacitor/core": "^8.6.2",
|
|
52
51
|
"@capacitor/docgen": "^0.3.1",
|
|
53
|
-
"@capacitor/ios": "^8.
|
|
54
|
-
"@ionic/eslint-config": "^0.
|
|
52
|
+
"@capacitor/ios": "^8.6.2",
|
|
53
|
+
"@ionic/eslint-config": "^0.5.0",
|
|
55
54
|
"@ionic/prettier-config": "^4.0.0",
|
|
56
55
|
"@ionic/swiftlint-config": "^2.0.0",
|
|
57
|
-
"eslint": "^
|
|
58
|
-
"prettier": "^3.
|
|
59
|
-
"prettier-plugin-java": "^2.
|
|
60
|
-
"release-it": "^
|
|
61
|
-
"rimraf": "^6.1.
|
|
62
|
-
"rollup": "^4.
|
|
56
|
+
"eslint": "^10.14.0",
|
|
57
|
+
"prettier": "^3.10.1",
|
|
58
|
+
"prettier-plugin-java": "^2.11.0",
|
|
59
|
+
"release-it": "^21.1.2",
|
|
60
|
+
"rimraf": "^6.1.4",
|
|
61
|
+
"rollup": "^4.68.3",
|
|
63
62
|
"swiftlint": "^2.0.0",
|
|
64
|
-
"typescript": "
|
|
63
|
+
"typescript": "~6.0.3"
|
|
65
64
|
},
|
|
66
65
|
"peerDependencies": {
|
|
67
66
|
"@capacitor/core": ">=8.0.0"
|
|
68
67
|
},
|
|
69
68
|
"prettier": "@ionic/prettier-config",
|
|
70
69
|
"swiftlint": "@ionic/swiftlint-config",
|
|
71
|
-
"eslintConfig": {
|
|
72
|
-
"extends": "@ionic/eslint-config/recommended"
|
|
73
|
-
},
|
|
74
70
|
"capacitor": {
|
|
75
71
|
"ios": {
|
|
76
72
|
"src": "ios"
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
import Capacitor
|
|
3
|
-
import SwiftKeychainWrapper
|
|
4
|
-
|
|
5
|
-
@testable import Plugin
|
|
6
|
-
|
|
7
|
-
class PluginTests: XCTestCase {
|
|
8
|
-
|
|
9
|
-
func setupDedicatedWrapper() -> KeychainWrapper {
|
|
10
|
-
let wrapper = KeychainWrapper.init(serviceName: "cap_sec")
|
|
11
|
-
wrapper.removeAllKeys()
|
|
12
|
-
return wrapper
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
func testSet() {
|
|
16
|
-
let key = "key"
|
|
17
|
-
let value = "Hello, World!"
|
|
18
|
-
let valueModified = "Modified"
|
|
19
|
-
let keychainwrapper = setupDedicatedWrapper()
|
|
20
|
-
KeychainWrapper.standard.set(value, forKey: key)
|
|
21
|
-
|
|
22
|
-
let plugin = SecureStoragePlugin()
|
|
23
|
-
|
|
24
|
-
let call = CAPPluginCall(callbackId: "test", options: [
|
|
25
|
-
"key": key,
|
|
26
|
-
"value": valueModified
|
|
27
|
-
], success: { (result, call) in
|
|
28
|
-
let resultValue = result!.data?["value"] as? Bool
|
|
29
|
-
XCTAssertTrue(resultValue ?? false)
|
|
30
|
-
// dedicated keychain wrapper
|
|
31
|
-
let dedicatedValue = keychainwrapper.string(forKey: key)
|
|
32
|
-
XCTAssertEqual(valueModified, dedicatedValue)
|
|
33
|
-
// standard keychain wrapper should not be modified
|
|
34
|
-
let standardValue = KeychainWrapper.standard.string(forKey: key)
|
|
35
|
-
XCTAssertEqual(value, standardValue)
|
|
36
|
-
}, error: { (err) in
|
|
37
|
-
XCTFail("Error shouldn't have been called")
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
plugin.set(call!)
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
func testGet() {
|
|
44
|
-
let key = "key"
|
|
45
|
-
let value = "Hello, World!"
|
|
46
|
-
let keychainwrapper = setupDedicatedWrapper()
|
|
47
|
-
keychainwrapper.set(value, forKey: key)
|
|
48
|
-
|
|
49
|
-
let plugin = SecureStoragePlugin()
|
|
50
|
-
|
|
51
|
-
let call = CAPPluginCall(callbackId: "test", options: [
|
|
52
|
-
"key": key
|
|
53
|
-
], success: { (result, call) in
|
|
54
|
-
let resultValue = result!.data?["value"] as? String
|
|
55
|
-
XCTAssertEqual(value, resultValue)
|
|
56
|
-
}, error: { (err) in
|
|
57
|
-
XCTFail("Error shouldn't have been called")
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
plugin.get(call!)
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
func testGetCopy() {
|
|
64
|
-
let key = "key"
|
|
65
|
-
let value = "Hello, World!"
|
|
66
|
-
let keychainwrapper = setupDedicatedWrapper()
|
|
67
|
-
|
|
68
|
-
KeychainWrapper.standard.set(value, forKey: key)
|
|
69
|
-
|
|
70
|
-
let plugin = SecureStoragePlugin()
|
|
71
|
-
|
|
72
|
-
let call = CAPPluginCall(callbackId: "test", options: [
|
|
73
|
-
"key": key
|
|
74
|
-
], success: { (result, call) in
|
|
75
|
-
let resultValue = result!.data?["value"] as? String
|
|
76
|
-
XCTAssertEqual(value, resultValue)
|
|
77
|
-
let dedicatedValue = keychainwrapper.string(forKey: key)
|
|
78
|
-
XCTAssertEqual(value, dedicatedValue)
|
|
79
|
-
let standardValue = KeychainWrapper.standard.string(forKey: key)
|
|
80
|
-
XCTAssertNil(standardValue)
|
|
81
|
-
}, error: { (err) in
|
|
82
|
-
XCTFail("Error shouldn't have been called")
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
plugin.get(call!)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
func testKeys() {
|
|
89
|
-
let key = "key"
|
|
90
|
-
let key2 = "key2"
|
|
91
|
-
let value = "value"
|
|
92
|
-
|
|
93
|
-
let plugin = SecureStoragePlugin()
|
|
94
|
-
|
|
95
|
-
let keychainwrapper = setupDedicatedWrapper()
|
|
96
|
-
keychainwrapper.set(value, forKey: key)
|
|
97
|
-
keychainwrapper.set(value, forKey: key2)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
let callOne = CAPPluginCall(callbackId: "test", options: [:],
|
|
101
|
-
success: { (result, call) in
|
|
102
|
-
let resultValue = result!.data?["value"] as? Array<String>
|
|
103
|
-
XCTAssertEqual(2, resultValue!.count)
|
|
104
|
-
XCTAssertTrue(resultValue!.contains(key))
|
|
105
|
-
XCTAssertTrue(resultValue!.contains(key2))
|
|
106
|
-
}, error: { (err) in
|
|
107
|
-
XCTFail("Error shouldn't have been called")
|
|
108
|
-
})
|
|
109
|
-
plugin.keys(callOne!)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
func testNonExistingGet() {
|
|
113
|
-
let key = "keyNonExisting"
|
|
114
|
-
|
|
115
|
-
let plugin = SecureStoragePlugin()
|
|
116
|
-
|
|
117
|
-
let call = CAPPluginCall(callbackId: "test", options: [
|
|
118
|
-
"key": key
|
|
119
|
-
], success: { (result, call) in
|
|
120
|
-
XCTFail("Error shouldn't have been called")
|
|
121
|
-
}, error: { (err) in
|
|
122
|
-
XCTAssertNotNil(err)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
plugin.get(call!)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
func testNonExistingRemoveBoth() {
|
|
129
|
-
let key = "keyNonExisting"
|
|
130
|
-
|
|
131
|
-
let plugin = SecureStoragePlugin()
|
|
132
|
-
|
|
133
|
-
let call = CAPPluginCall(callbackId: "test", options: [
|
|
134
|
-
"key": key
|
|
135
|
-
], success: { (result, call) in
|
|
136
|
-
XCTFail("Error shouldn't have been called")
|
|
137
|
-
}, error: { (err) in
|
|
138
|
-
XCTAssertNotNil(err)
|
|
139
|
-
})
|
|
140
|
-
|
|
141
|
-
plugin.remove(call!)
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
func testRemoveBoth() {
|
|
145
|
-
let key = "key"
|
|
146
|
-
let value = "Hello, World!"
|
|
147
|
-
// prefill dedicated keychain wrapper
|
|
148
|
-
let keychainwrapper = setupDedicatedWrapper()
|
|
149
|
-
keychainwrapper.set(value, forKey: key)
|
|
150
|
-
// prefill standard keychain wrapper
|
|
151
|
-
KeychainWrapper.standard.set(value, forKey: key)
|
|
152
|
-
|
|
153
|
-
let plugin = SecureStoragePlugin()
|
|
154
|
-
|
|
155
|
-
let call = CAPPluginCall(callbackId: "test", options: [
|
|
156
|
-
"key": key
|
|
157
|
-
], success: { (result, call) in
|
|
158
|
-
let resultValue = result!.data?["value"] as? Bool
|
|
159
|
-
XCTAssertTrue(resultValue ?? false)
|
|
160
|
-
// dedicated keychain wrapper
|
|
161
|
-
let dedicatedValue = keychainwrapper.string(forKey: key)
|
|
162
|
-
XCTAssertNil(dedicatedValue)
|
|
163
|
-
// standard keychain wrapper
|
|
164
|
-
let standardValueRemoved = KeychainWrapper.standard.string(forKey: key)
|
|
165
|
-
XCTAssertNil(standardValueRemoved)
|
|
166
|
-
}, error: { (err) in
|
|
167
|
-
XCTFail("Error shouldn't have been called")
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
plugin.remove(call!)
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
// same as testRemoveBoth, but don't prefill standard wrapper
|
|
174
|
-
func testRemove() {
|
|
175
|
-
let key = "key"
|
|
176
|
-
let value = "Hello, World!"
|
|
177
|
-
// prefill dedicated keychain wrapper
|
|
178
|
-
let keychainwrapper = setupDedicatedWrapper()
|
|
179
|
-
keychainwrapper.set(value, forKey: key)
|
|
180
|
-
|
|
181
|
-
let plugin = SecureStoragePlugin()
|
|
182
|
-
|
|
183
|
-
let call = CAPPluginCall(callbackId: "test", options: [
|
|
184
|
-
"key": key
|
|
185
|
-
], success: { (result, call) in
|
|
186
|
-
let resultValue = result!.data?["value"] as? Bool
|
|
187
|
-
XCTAssertTrue(resultValue ?? false)
|
|
188
|
-
// dedicated keychain wrapper
|
|
189
|
-
let dedicatedValue = keychainwrapper.string(forKey: key)
|
|
190
|
-
XCTAssertNil(dedicatedValue)
|
|
191
|
-
}, error: { (err) in
|
|
192
|
-
XCTFail("Error shouldn't have been called")
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
plugin.remove(call!)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
func testClear() {
|
|
199
|
-
let key = "key"
|
|
200
|
-
let value = "Hello, World!"
|
|
201
|
-
let standardOnlyKey = "standard key"
|
|
202
|
-
let standardOnlyValue = "standard value"
|
|
203
|
-
// prefill dedicated keychain wrapper
|
|
204
|
-
let keychainwrapper = setupDedicatedWrapper()
|
|
205
|
-
keychainwrapper.set(value, forKey: key)
|
|
206
|
-
keychainwrapper.set(value + "2", forKey: key + "2")
|
|
207
|
-
// prefill standard keychain wrapper
|
|
208
|
-
KeychainWrapper.standard.set(value, forKey: key)
|
|
209
|
-
KeychainWrapper.standard.set(standardOnlyValue, forKey: standardOnlyKey)
|
|
210
|
-
|
|
211
|
-
let plugin = SecureStoragePlugin()
|
|
212
|
-
|
|
213
|
-
let call = CAPPluginCall(callbackId: "test", options: [
|
|
214
|
-
"key": key
|
|
215
|
-
], success: { (result, call) in
|
|
216
|
-
let resultValue = result!.data?["value"] as? Bool
|
|
217
|
-
XCTAssertTrue(resultValue ?? false)
|
|
218
|
-
// key present in dedicated and standard wrapper removed
|
|
219
|
-
let dedicatedValue = keychainwrapper.string(forKey: key)
|
|
220
|
-
XCTAssertNil(dedicatedValue)
|
|
221
|
-
let dedicatedValue2 = keychainwrapper.string(forKey: key + "2")
|
|
222
|
-
XCTAssertNil(dedicatedValue2)
|
|
223
|
-
let standardValue = KeychainWrapper.standard.string(forKey: key)
|
|
224
|
-
XCTAssertNil(standardValue)
|
|
225
|
-
// key only defined in standard wrapper still present
|
|
226
|
-
let standardValue2 = KeychainWrapper.standard.string(forKey: standardOnlyKey)
|
|
227
|
-
XCTAssertEqual(standardOnlyValue, standardValue2)
|
|
228
|
-
}, error: { (err) in
|
|
229
|
-
XCTFail("Error shouldn't have been called")
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
plugin.clear(call!)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
func testGetPlatform() {
|
|
236
|
-
let plugin = SecureStoragePlugin()
|
|
237
|
-
let call = CAPPluginCall(callbackId: "test",
|
|
238
|
-
success: { (result, call) in
|
|
239
|
-
let resultValue = result!.data?["value"] as? String
|
|
240
|
-
XCTAssertEqual("ios", resultValue)
|
|
241
|
-
}, error: { (err) in
|
|
242
|
-
XCTFail("Error shouldn't have been called")
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
plugin.getPlatform(call!)
|
|
246
|
-
}
|
|
247
|
-
}
|