capacitor-secure-storage-plugin 0.12.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.
@@ -11,8 +11,7 @@ Pod::Spec.new do |s|
11
11
  s.author = package['author']
12
12
  s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
13
13
  s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}'
14
- s.ios.deployment_target = '14.0'
14
+ s.ios.deployment_target = '15.0'
15
15
  s.dependency 'Capacitor'
16
- s.dependency 'SwiftKeychainWrapper'
17
16
  s.swift_version = '5.1'
18
17
  end
package/Package.swift CHANGED
@@ -3,28 +3,22 @@ import PackageDescription
3
3
 
4
4
  let package = Package(
5
5
  name: "CapacitorSecureStoragePlugin",
6
- platforms: [.iOS(.v14)],
6
+ platforms: [.iOS(.v15)],
7
7
  products: [
8
8
  .library(
9
9
  name: "CapacitorSecureStoragePlugin",
10
10
  targets: ["SecureStoragePlugin"])
11
11
  ],
12
12
  dependencies: [
13
- .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "7.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 v7
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,10 +188,29 @@ async getUsername(key: string) {
182
188
 
183
189
  ### iOS
184
190
 
185
- This plugin uses SwiftKeychainWrapper under the hood for iOS.
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`.
194
+
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.
196
+
197
+ ```swift
198
+ import Security
199
+
200
+
201
+ if !UserDefaults.standard.bool(forKey: "firstTimeLaunchOccurred") {
202
+ let query: [String: Any] = [
203
+ kSecClass as String: kSecClassGenericPassword,
204
+ kSecAttrService as String: "cap_sec"
205
+ ]
206
+ SecItemDelete(query as CFDictionary)
207
+
208
+ UserDefaults.standard.set(true, forKey: "firstTimeLaunchOccurred")
209
+ }
210
+ ```
186
211
 
187
212
  > **Warning**
188
- > Up to version v0.4.0 there was standard keychain used. Since v0.5.0 there is separate keychain wrapper, so keys() method returns only keys set in v0.5.0 or higher version.
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.
189
214
 
190
215
  ### Android
191
216
 
@@ -196,4 +221,4 @@ On Android it is implemented by AndroidKeyStore and SharedPreferences. Source: [
196
221
 
197
222
  ### Web
198
223
 
199
- There is no secure storage in browser (not because it is not implemented by this plugin, but it does not exist at all). Values are stored in LocalStorage, but they are at least base64 encoded. Plugin adds 'cap*sec*' prefix to keys to avoid conflicts with other data stored in LocalStorage.
224
+ There is no secure storage in browser (not because it is not implemented by this plugin, but it does not exist at all). Values are stored in LocalStorage, but they are at least base64 encoded. Plugin adds 'cap_sec' prefix to keys to avoid conflicts with other data stored in LocalStorage.
@@ -1,8 +1,8 @@
1
1
  ext {
2
2
  junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2'
3
- androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.0'
4
- androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.2.1'
5
- androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.6.1'
3
+ androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1'
4
+ androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0'
5
+ androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0'
6
6
  }
7
7
 
8
8
  buildscript {
@@ -11,18 +11,18 @@ buildscript {
11
11
  mavenCentral()
12
12
  }
13
13
  dependencies {
14
- classpath 'com.android.tools.build:gradle:8.7.2'
14
+ classpath 'com.android.tools.build:gradle:8.13.0'
15
15
  }
16
16
  }
17
17
 
18
18
  apply plugin: 'com.android.library'
19
19
 
20
20
  android {
21
- namespace "com.whitestein.securestorage"
22
- compileSdk project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 35
21
+ namespace = "com.whitestein.securestorage"
22
+ compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36
23
23
  defaultConfig {
24
- minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 23
25
- targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 35
24
+ minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24
25
+ targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36
26
26
  versionCode 1
27
27
  versionName "1.0"
28
28
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -34,7 +34,7 @@ android {
34
34
  }
35
35
  }
36
36
  lintOptions {
37
- abortOnError false
37
+ abortOnError = false
38
38
  }
39
39
  compileOptions {
40
40
  sourceCompatibility JavaVersion.VERSION_21
@@ -1 +1 @@
1
- {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,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;YACrD,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;SACzC;aAAM;YACL,OAAO,OAAO,CAAC,MAAM,CAAC,oCAAoC,CAAC,CAAC;SAC7D;IACH,CAAC;IACD,KAAK;QACH,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE;YAC9B,IAAI,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE;gBAClC,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;aAC9B;SACF;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';\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"]}
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"]}
@@ -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;AACA,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;AACA,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;AACA,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;AACA,aAAa;AACb,YAAY,OAAO,OAAO,CAAC,MAAM,CAAC,oCAAoC,CAAC;AACvE;AACA;AACA,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;AACA;AACA,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;AAC/C;AACA,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;AACA,IAAI,WAAW,GAAG;AAClB,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAChD;AACA;;;;;;;;;"}
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 +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;IACA,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;IACA,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;IACA,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;IACA,aAAa;IACb,YAAY,OAAO,OAAO,CAAC,MAAM,CAAC,oCAAoC,CAAC;IACvE;IACA;IACA,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;IACA;IACA,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC/C;IACA,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;IACA,IAAI,WAAW,GAAG;IAClB,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAChD;IACA;;;;;;;;;;;;;;;"}
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 keychainwrapper: KeychainWrapper = KeychainWrapper.init(serviceName: "cap_sec")
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
- let saveSuccessful: Bool = keychainwrapper.set(value, forKey: key, withAccessibility: .afterFirstUnlock)
27
- if(saveSuccessful) {
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
- let hasValueDedicated = keychainwrapper.hasValue(forKey: key)
40
- let hasValueStandard = KeychainWrapper.standard.hasValue(forKey: key)
41
-
42
- // copy standard value to dedicated and remove standard key
43
- if (hasValueStandard && !hasValueDedicated) {
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
- let keys = keychainwrapper.allKeys();
67
- call.resolve([
68
- "value": Array(keys)
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
- let hasValueDedicated = keychainwrapper.hasValue(forKey: key)
75
- let hasValueStandard = KeychainWrapper.standard.hasValue(forKey: key)
76
-
77
- if(hasValueDedicated || hasValueStandard) {
78
- KeychainWrapper.standard.removeObject(forKey: key);
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
- let keys = keychainwrapper.allKeys();
96
- // cleanup standard keychain wrapper keys
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
- "value": "ios"
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.12.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
  ],
@@ -31,12 +30,12 @@
31
30
  ],
32
31
  "scripts": {
33
32
  "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
34
- "verify:ios" : "xcodebuild -scheme CapacitorSecureStoragePlugin -destination generic/platform=iOS",
33
+ "verify:ios": "xcodebuild -scheme CapacitorSecureStoragePlugin -destination generic/platform=iOS",
35
34
  "verify:android": "cd android && ./gradlew clean build test && cd ..",
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 . --ext ts",
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": "^7.0.0",
50
- "@capacitor/cli": "^7.0.0",
51
- "@capacitor/core": "^7.0.0",
52
- "@capacitor/docgen": "^0.3.0",
53
- "@capacitor/ios": "^7.0.0",
54
- "@ionic/eslint-config": "^0.4.0",
48
+ "@capacitor/android": "^8.6.2",
49
+ "@capacitor/cli": "^8.6.2",
50
+ "@capacitor/core": "^8.6.2",
51
+ "@capacitor/docgen": "^0.3.1",
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": "^8.57.0",
58
- "prettier": "^3.4.2",
59
- "prettier-plugin-java": "^2.6.6",
60
- "release-it": "^17.2.1",
61
- "rimraf": "^6.0.1",
62
- "rollup": "^4.30.1",
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": "~4.0.3"
63
+ "typescript": "~6.0.3"
65
64
  },
66
65
  "peerDependencies": {
67
- "@capacitor/core": ">=7.0.0"
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
- }