├── .eslintrc.js
├── .gitignore
├── CHANGELOG
├── LICENSE
├── README.md
├── package.json
├── plugin.xml
├── src
├── android
│ ├── AES.java
│ ├── RSA.java
│ ├── SecureStorage.java
│ └── SharedPreferencesHandler.java
├── ios
│ ├── SAMKeychain
│ │ ├── LICENSE
│ │ ├── SAMKeychain.bundle
│ │ │ └── en.lproj
│ │ │ │ └── SAMKeychain.strings
│ │ ├── SAMKeychain.h
│ │ ├── SAMKeychain.m
│ │ ├── SAMKeychainQuery.h
│ │ └── SAMKeychainQuery.m
│ ├── SecureStorage.h
│ └── SecureStorage.m
└── windows
│ └── SecureStorage.js
├── tests
├── package.json
├── plugin.xml
└── tests.js
└── www
└── securestorage.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "jasmine": true
5 | },
6 | "globals": {
7 | "cordova": true,
8 | "module": true,
9 | "exports": true
10 | },
11 | "extends": "eslint:recommended",
12 | "rules": {
13 | "indent": [
14 | "error",
15 | 4
16 | ],
17 | "linebreak-style": [
18 | "error",
19 | "unix"
20 | ],
21 | "quotes": [
22 | "warn",
23 | "single"
24 | ],
25 | "semi": [
26 | "error",
27 | "always"
28 | ],
29 | "no-console": [
30 | "warn"
31 | ],
32 | "no-multi-spaces": [
33 | "error"
34 | ],
35 | "eqeqeq": [
36 | "error",
37 | "smart"
38 | ],
39 | "no-loop-func": [
40 | "error"
41 | ],
42 | "no-param-reassign": [
43 | "error"
44 | ],
45 | "vars-on-top": [
46 | "warn"
47 | ],
48 | "no-use-before-define": [
49 | "error"
50 | ],
51 | }
52 | };
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules/
2 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | Changelog
2 | =========
3 |
4 | 3.0.2 - 2019-04-15
5 | ------------------
6 |
7 | - Support data sharing between apps on android.
8 | [ajay-ambre]
9 |
10 | 3.0.1 - 2018-12-19
11 | ------------------
12 |
13 | - Properly access private keys on android.
14 | [demetris-manikas]
15 |
16 | 3.0.0 - 2018-11-01
17 | ------------------
18 |
19 | - Drop KitKat support, simplify android using only native AES.
20 | [demetris-manikas]
21 |
22 | 2.6.8 - 2017-05-29
23 | ------------------
24 |
25 | - Fix multiple namespaced storages for android.
26 | [demetris-manikas]
27 |
28 | 2.6.7 - 2017-05-24
29 | ------------------
30 |
31 | - Fixed windows bug introduced by 2.6.6.
32 | [digaus]
33 |
34 | 2.6.6 - 2017-05-19
35 | ------------------
36 |
37 | - Drop unsafe iOS options.
38 | [demetris-manikas]
39 |
40 | 2.6.5 - 2017-04-10
41 | ------------------
42 |
43 | - Delete saved values on new RSA key creation.
44 | [demetris-manikas]
45 |
46 | 2.6.4 - 2017-03-31
47 | ------------------
48 |
49 | - Graceful handling of unsupported Android versions (4.3 and below).
50 | [dpa99c]
51 |
52 | - Minor android fixes and tests.
53 | [demetris-manikas]
54 |
55 | - Documentation fixes.
56 | [jvjvjv]
57 |
58 | 2.6.3 - 2016-11-21
59 | ------------------
60 |
61 | - Android now thread-safe, fixes the issue with concurrent decryptions failing.
62 | [demetris-manikas]
63 |
64 | 2.6.2 - 2016-11-18
65 | ------------------
66 |
67 | - Introduce a queue for decryption on Android so that only one
68 | happens at a time. Fixes #75.
69 | [ggozad]
70 |
71 | 2.6.1 - 2016-11-09
72 | ------------------
73 |
74 | - Make sure only string values are stored.
75 | [demetris-manikas]
76 |
77 | 2.6.0 - 2016-10-26
78 | ------------------
79 |
80 | - Introduce keys(), clear() methods.
81 | [yyfearth, demetris-manikas, ggozad]
82 |
83 | 2.5.2 - 2016-10-11
84 | ------------------
85 |
86 | - Fix migration issues.
87 | [demetris-manikas]
88 |
89 | 2.5.1 - 2016-09-12
90 | ------------------
91 |
92 | - Fix migration to SharedPreferences error in certain android phones.
93 | [ggozad]
94 |
95 | 2.5.0 - 2016-09-12
96 | ------------------
97 |
98 | - Move from localStorage to SharedPreferences.
99 | [demetris-manikas]
100 |
101 | 2.4.2 - 2016-09-01
102 | ------------------
103 |
104 | - Update SSKeychain for iOS to SAMKeychain.
105 | [briananderson1222]
106 |
107 | - More tests.
108 | [ggozad]
109 |
110 | 2.4.1 - 2016-07-28
111 | ------------------
112 |
113 | - Make intitializing SecureStorage fail on android if screen-lock settings not appropriate. Introdudce secureDevice as an android-only function to bring up screen-lock settings with only appropriate options.
114 | [ggozad, jdard, demetris-manikas]
115 |
116 | 2.4.0 - 2016-07-04
117 | ------------------
118 |
119 | - Windows phone implementation.
120 | [sgrebnov]
121 |
122 | - Pass javascript Error to cordova failure callbacks, cleanup.
123 | [sgrebnov]
124 |
125 | 2.3.1 - 2016-06-22
126 | ------------------
127 |
128 | - Patch sjcl_ss.js to prevent cordova with browserify flag including a unnecessary huge crypto-browserify polyfill.
129 | [yyfearth]
130 |
131 | 2.3.0 - 2016-06-16
132 | ------------------
133 |
134 | - Native AES on android devices that support it.
135 | [demetris-manikas]
136 |
137 | 2.2.4 - 2016-05-25
138 | ------------------
139 |
140 | - Fix #30, allowing concurrent calls to securestarage in iOS,
141 | by not persisting callbackId's.
142 | [ggozad]
143 |
144 | 2.2.3 - 2016-05-18
145 | ------------------
146 |
147 | - Fix broken version number in plugin.xml.
148 | [ggozad]
149 |
150 | 2.2.2 - 2016-05-10
151 | ------------------
152 |
153 | - Execute iOS commands in separate threads instead of the main thread.
154 | [goshakkk / hellyeahllc]
155 |
156 | 2.2.1 - 2016-02-09
157 | ------------------
158 |
159 | - package.json meta.
160 | [ggozad]
161 |
162 | 2.2.0 - 2016-01-05
163 | ------------------
164 |
165 | - Supporting iOS 7 without 'WhenPasscodeSetThisDeviceOnly' option.
166 | [embedded-spirit]
167 |
168 |
169 | 2.1.0 - 2015-10-29
170 | ------------------
171 |
172 | - Keychain accessibility setting for iOS.
173 | [schaumiii]
174 |
175 |
176 | 2.0.0 - 2015-10-28
177 | ------------------
178 |
179 | - Update to use npm.
180 | [ggozad]
181 |
182 | 1.1.2 - 2015-04-16
183 | ------------------
184 |
185 | - Fix Android key creation conflict.
186 | [demetris-manikas]
187 |
188 | 1.1.1 - 2015-04-13
189 | ------------------
190 |
191 | - Android cleanup.
192 | [demetris-manikas]
193 |
194 | 1.1.0 - 2015-04-01
195 | ------------------
196 |
197 | - Support the browser platform.
198 | [ggozad]
199 |
200 | 1.0.0 - 2015-03-30
201 | ------------------
202 |
203 | - Initial release.
204 | [ggozad]
205 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2015 Crypho AS
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Project no longer mantained.
2 |
3 | As Crypho does not use Cordova for a long time now, it has become clear that we cannot keep maintaining this project any longer, or give it the attention it deserves.
4 | A big thanks to all the contributors.
5 |
6 | # SecureStorage plugin for Apache Cordova
7 |
8 | [](https://nodei.co/npm/cordova-plugin-secure-storage/)
9 |
10 | ## Introduction
11 |
12 | This plugin is for use with [Apache Cordova](http://cordova.apache.org/) and allows your application to securely store secrets
13 | such as usernames, passwords, tokens, certificates or other sensitive information (strings) on iOS & Android phones and Windows devices.
14 |
15 | ### Supported platforms
16 |
17 | - Android
18 | - iOS
19 | - Windows (Windows 8.x/Store, Windows 10/UWP and Windows Phone 8.1+)
20 |
21 | ### Contents
22 |
23 | - [Installation](#installation)
24 | - [Plugin API](#plugin-api)
25 | - [LICENSE](#license)
26 |
27 | ## Installation
28 |
29 | Below are the methods for installing this plugin automatically using command line tools. For additional info, take a look at the [Plugman Documentation](https://cordova.apache.org/docs/en/latest/plugin_ref/plugman.html), [`cordova plugin` command](https://cordova.apache.org/docs/en/latest/reference/cordova-cli/index.html#cordova-plugin-command) and [Cordova Plugin Specification](https://cordova.apache.org/docs/en/latest/plugin_ref/spec.html).
30 |
31 | ### Cordova
32 |
33 | The plugin can be installed via the Cordova command line interface:
34 |
35 | - Navigate to the root folder for your phonegap project.
36 | - Run the command:
37 |
38 | ```sh
39 | cordova plugin add cordova-plugin-secure-storage
40 | ```
41 |
42 | or if you want to be running the development version,
43 |
44 | ```sh
45 | cordova plugin add https://github.com/crypho/cordova-plugin-secure-storage.git
46 | ```
47 |
48 | ## Plugin API
49 |
50 | #### Create a namespaced storage.
51 |
52 | ```js
53 | var ss = new cordova.plugins.SecureStorage(
54 | function() {
55 | console.log("Success");
56 | },
57 | function(error) {
58 | console.log("Error " + error);
59 | },
60 | "my_app"
61 | );
62 | ```
63 |
64 | #### Set a key/value in the storage.
65 |
66 | ```js
67 | ss.set(
68 | function(key) {
69 | console.log("Set " + key);
70 | },
71 | function(error) {
72 | console.log("Error " + error);
73 | },
74 | "mykey",
75 | "myvalue"
76 | );
77 | ```
78 |
79 | where `key` and `value` are both strings.
80 |
81 | #### Get a key's value from the storage.
82 |
83 | ```js
84 | ss.get(
85 | function(value) {
86 | console.log("Success, got " + value);
87 | },
88 | function(error) {
89 | console.log("Error " + error);
90 | },
91 | "mykey"
92 | );
93 | ```
94 |
95 | #### Remove a key from the storage.
96 |
97 | ```js
98 | ss.remove(
99 | function(key) {
100 | console.log("Removed " + key);
101 | },
102 | function(error) {
103 | console.log("Error, " + error);
104 | },
105 | "mykey"
106 | );
107 | ```
108 |
109 | #### Get all keys from the storage.
110 |
111 | ```js
112 | ss.keys(
113 | function(keys) {
114 | console.log("Got keys " + keys.join(", "));
115 | },
116 | function(error) {
117 | console.log("Error, " + error);
118 | }
119 | );
120 | ```
121 |
122 | #### Clear all keys from the storage.
123 |
124 | ```js
125 | ss.clear(
126 | function() {
127 | console.log("Cleared");
128 | },
129 | function(error) {
130 | console.log("Error, " + error);
131 | }
132 | );
133 | ```
134 |
135 | ## Platform details
136 |
137 | #### iOS
138 |
139 | On iOS secrets are stored directly in the KeyChain through the [SAMKeychain](https://github.com/soffes/samkeychain) library.
140 |
141 | ##### Configuration
142 |
143 | On iOS it is possible to configure the accessibility of the keychain by setting the `KeychainAccessibility` preference in the `config.xml` to one of the following strings:
144 |
145 | - AfterFirstUnlock
146 | - AfterFirstUnlockThisDeviceOnly
147 | - WhenUnlocked (default)
148 | - WhenUnlockedThisDeviceOnly
149 | - WhenPasscodeSetThisDeviceOnly (this option is available only on iOS8 and later)
150 |
151 | For reference what these settings mean, see [Keychain Item Accessibility Constants](https://developer.apple.com/reference/security/keychain_services/keychain_item_accessibility_constants).
152 |
153 | For example, include in your `config.xml` the following:
154 |
155 | ```xml
156 |
157 |
158 |
159 | ```
160 |
161 | #### iOS 7 Support
162 |
163 | iOS 7 is supported without `WhenPasscodeSetThisDeviceOnly` option.
164 |
165 | How to test the plugin using iOS 7 simulator:
166 |
167 | - Download and install Xcode 6 into a separate folder, e.g. /Application/Xcode 6/
168 | - Run `$ xcode-select --switch /Contents/Developer`
169 | - Build Cordova app with the plugin and run it in iOS 7 simulator
170 |
171 | #### Android
172 |
173 | On Android there does not exist an equivalent of the iOS KeyChain. The `SecureStorage` API is implemented as follows:
174 |
175 | - A random 256-bit AES key is generated.
176 | - The AES key encrypts the value.
177 | - The AES key is encrypted with a device-generated RSA (RSA/ECB/PKCS1Padding) from the Android KeyStore.
178 | - The combination of the encrypted AES key and value are stored in `SharedPreferences`.
179 |
180 | The inverse process is followed on `get`.
181 |
182 | Native AES is used.
183 | Minimum android supported version is 5.0 Lollipop. If you need to support earlier Android versions use version 2.6.8.
184 |
185 | ##### Users must have a secure screen-lock set.
186 |
187 | The plugin will only work correctly if the user has sufficiently secure settings on the lock screen. If not, the plugin will fail to initialize and the failure callback will be called on `init()`. This is because in order to use the Android Credential Storage and create RSA keys the device needs to be somewhat secure.
188 |
189 | In case of failure to initialize, the app developer should inform the user about the security requirements of her app and initialize again after the user has changed the screen-lock settings. To facilitate this, we provide `secureDevice` which will bring up the screen-lock settings and will call the success or failure callbacks depending on whether the user locked the screen appropriately.
190 |
191 | For example, this would keep asking the user to enable screen lock forever. Obviously adapt to your needs :)
192 |
193 | ```js
194 | var ss;
195 | var _init = function() {
196 | ss = new cordova.plugins.SecureStorage(
197 | function() {
198 | console.log("OK");
199 | },
200 | function() {
201 | navigator.notification.alert(
202 | "Please enable the screen lock on your device. This app cannot operate securely without it.",
203 | function() {
204 | ss.secureDevice(
205 | function() {
206 | _init();
207 | },
208 | function() {
209 | _init();
210 | }
211 | );
212 | },
213 | "Screen lock is disabled"
214 | );
215 | },
216 | "my_app"
217 | );
218 | };
219 | _init();
220 | ```
221 |
222 | ##### Sharing data between 2 apps on Android.
223 |
224 | The plugin can be used to share data securely between 2 Android apps.
225 |
226 | This can be done by updating the `config.xml` for both the Android apps with below changes:
227 |
228 | 1. Add `xmlns:android="http://schemas.android.com/apk/res/android"` in the initial `widget` tag.
229 | 2. Add the below tag in ``
230 |
231 | ```xml
232 |
233 |
234 |
235 | ```
236 |
237 | This is required as both the apps should have the same `android:sharedUserId`. For e.g. `android:sharedUserId="com.example.myUser"`.
238 |
239 | Consider `App1` with `packageName` as `com.test.app1`.
240 | Data can be set from the `App1` using the below code.
241 |
242 | ```js
243 | var ss = new cordova.plugins.SecureStorage(
244 | function() {
245 | ss.set(
246 | function(res) {
247 | console.log("Shared key set: " + res);
248 | },
249 | function(err) {
250 | console.log("Error setting shared key: " + err);
251 | },
252 | "sharedKey",
253 | "sharedValue"
254 | );
255 | },
256 | function(err) {
257 | console.log("Error creating SecureStorage: " + err);
258 | },
259 | "my_shared_data"
260 | );
261 | ```
262 |
263 | Consider `App2` with `packageName` as `com.test.app2`.
264 | Data can be accessed from the `App2` using the `packageName` of `App1` as shown in below code.
265 |
266 | ```js
267 | var ss = new cordova.plugins.SecureStorage(
268 | function() {
269 | ss.get(
270 | function(res) {
271 | console.log("Got Shared key: " + res);
272 | },
273 | function(err) {
274 | console.log("Error getting shared key: " + err);
275 | },
276 | "sharedKey"
277 | );
278 | },
279 | function(err) {
280 | console.log("Error creating SecureStorage: " + err);
281 | },
282 | "my_shared_data",
283 | {
284 | android: {
285 | packageName: "com.test.app1"
286 | }
287 | }
288 | );
289 | ```
290 |
291 | Please note that if the 2 apps use different `android:sharedUserId`, the `App2` will fail with an error `Error: Key [sharedKey] not found`.
292 |
293 | If `App1` is uninstalled and `App2` tries to access the `sharedKey` from `App1`, `App2` will fail with an error `Error: Application package com.test.app1 not found`.
294 |
295 | ##### Android keystore deletion on security setting change
296 |
297 | Changing the lock screen type on Android erases the keystore (issues [61989](https://code.google.com/p/android/issues/detail?id=61989) and [210402](https://code.google.com/p/android/issues/detail?id=210402)). This is also described in the [Android Security: The Forgetful Keystore](https://doridori.github.io/android-security-the-forgetful-keystore/) blog post.
298 |
299 | This means that any values saved using the plugin could be lost if the user changes security settings. The plugin should therefore be used as a secure credential cache and not persistent storage on Android.
300 |
301 | #### Windows
302 |
303 | Windows implementation is based on [PasswordVault](https://msdn.microsoft.com/en-us/library/windows/apps/windows.security.credentials.passwordvault.aspx) object from the [Windows.Security.Credentials](https://msdn.microsoft.com/en-us/library/windows/apps/windows.security.credentials.aspx) namespace.
304 | The contents of the locker are specific to the app so different apps and services don't have access to credentials associated with other apps or services.
305 |
306 | **Limitations:** you can only store up to ten credentials per app. If you try to store more than ten credentials, you will
307 | encounter an error. Read [documentation](https://msdn.microsoft.com/en-us/library/windows/apps/hh701231.aspx) for more details.
308 |
309 | #### Browser
310 |
311 | The browser platform is supported as a mock only. Key/values are stored unencrypted in localStorage.
312 |
313 | ## FAQ
314 |
315 | - I get the error `cordova.plugins.SecureStorage is not a function`, what gives?
316 |
317 | You can instantiate the plugin only after the `deviceready` event is fired. The plugin is not available before that. Also make sure you use the plugin after its success callback has fired.
318 |
319 | - Do my users really need to set a PIN code on their android devices to use the plugin?
320 |
321 | Yes, sorry. Android will not allow the creation of cryptographic keys unless the user has enabled at least PIN locking on the device.
322 |
323 | ## Testing
324 |
325 | ### Setup
326 |
327 | 1. Create a cordova app.
328 | 2. Change the start page in config.xml with ``
329 | 3. Add your platforms.
330 | 4. Add the `cordova-plugin-test-framework` plugin:
331 |
332 | ```
333 | cordova plugin add cordova-plugin-test-framework
334 | ```
335 |
336 | 5. Finally add the secure storage plugin as well as the tests from its location
337 |
338 | ```
339 | cordova plugin add PATH_TO_SECURE_STORAGE_PLUGIN
340 | cordova plugin add PATH_TO_SECURE_STORAGE_PLUGIN/tests
341 | ```
342 |
343 | ### Running the tests
344 |
345 | Just run the app for all platforms. Remember, if you have changes to test you will need to remove the secure storage plugin and add it again for the changes to be seen by the app.
346 |
347 | ## LICENSE
348 |
349 | The MIT License
350 |
351 | Copyright (c) 2015 Crypho AS.
352 |
353 | Permission is hereby granted, free of charge, to any person obtaining a copy
354 | of this software and associated documentation files (the "Software"), to deal
355 | in the Software without restriction, including without limitation the rights
356 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
357 | copies of the Software, and to permit persons to whom the Software is
358 | furnished to do so, subject to the following conditions:
359 |
360 | The above copyright notice and this permission notice shall be included in
361 | all copies or substantial portions of the Software.
362 |
363 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
364 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
365 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
366 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
367 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
368 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
369 | THE SOFTWARE.
370 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cordova-plugin-secure-storage",
3 | "version": "3.0.2",
4 | "description": "Secure storage plugin for iOS & Android",
5 | "author": "Yiorgis Gozadinos ",
6 | "contributors": [
7 | {
8 | "name": "Demetris Manikas",
9 | "email": "demetris.manikas@gmail.com"
10 | },
11 | {
12 | "name": "Sergey Grebnov",
13 | "email": "sergei.grebnov@gmail.com"
14 | },
15 | {
16 | "name": "Ajay Ambre",
17 | "email": "ajayambre88@gmail.com"
18 | }
19 | ],
20 | "cordova": {
21 | "id": "cordova-plugin-secure-storage",
22 | "platforms": ["android", "ios", "wp8", "windows"]
23 | },
24 | "keywords": [
25 | "cordova",
26 | "security",
27 | "encryption",
28 | "storage",
29 | "ecosystem:cordova",
30 | "cordova-android",
31 | "cordova-ios",
32 | "cordova-wp8",
33 | "cordova-windows",
34 | "cordova-browser"
35 | ],
36 | "main": "www/securestorage.js",
37 | "repository": {
38 | "type": "git",
39 | "url": "git+https://github.com/crypho/cordova-plugin-secure-storage.git"
40 | },
41 | "scripts": {
42 | "eslint": "./node_modules/.bin/eslint www/securestorage.js tests/tests.js"
43 | },
44 | "devDependencies": {
45 | "cordova-plugin-test-framework": ">=1.0.0",
46 | "eslint": ""
47 | },
48 | "license": "MIT",
49 | "bugs": {
50 | "url": "https://github.com/crypho/cordova-plugin-secure-storage/issues"
51 | },
52 | "homepage": "https://github.com/crypho/cordova-plugin-secure-storage#readme"
53 | }
54 |
--------------------------------------------------------------------------------
/plugin.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | SecureStorage
8 | Crypho AS
9 |
10 |
11 | Secure, encrypted storage for cordova apps in iOS and Android.
12 |
13 |
14 | MIT
15 |
16 | keychain, encryption, security
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/src/android/AES.java:
--------------------------------------------------------------------------------
1 | package com.crypho.plugins;
2 |
3 | import android.util.Log;
4 | import android.util.Base64;
5 |
6 | import org.json.JSONException;
7 | import org.json.JSONObject;
8 |
9 | import java.security.Key;
10 | import java.security.SecureRandom;
11 |
12 | import javax.crypto.Cipher;
13 | import javax.crypto.SecretKey;
14 | import javax.crypto.spec.SecretKeySpec;
15 | import javax.crypto.spec.IvParameterSpec;
16 | import javax.crypto.KeyGenerator;
17 |
18 | public class AES {
19 | private static final String CIPHER_MODE = "CCM";
20 | private static final int KEY_SIZE = 256;
21 | private static final int VERSION = 1;
22 | private static final Cipher CIPHER = getCipher();
23 |
24 | public static JSONObject encrypt(byte[] msg, byte[] adata) throws Exception {
25 | byte[] iv, ct, secretKeySpec_enc;
26 | synchronized (CIPHER) {
27 | SecretKeySpec secretKeySpec = generateKeySpec();
28 | secretKeySpec_enc = secretKeySpec.getEncoded();
29 | initCipher(Cipher.ENCRYPT_MODE, secretKeySpec, null, adata);
30 | iv = CIPHER.getIV();
31 | ct = CIPHER.doFinal(msg);
32 | }
33 |
34 | JSONObject value = new JSONObject();
35 | value.put("iv", Base64.encodeToString(iv, Base64.DEFAULT));
36 | value.put("v", Integer.toString(VERSION));
37 | value.put("ks", Integer.toString(KEY_SIZE));
38 | value.put("cipher", "AES");
39 | value.put("mode", CIPHER_MODE);
40 | value.put("adata", Base64.encodeToString(adata, Base64.DEFAULT));
41 | value.put("ct", Base64.encodeToString(ct, Base64.DEFAULT));
42 |
43 | JSONObject result = new JSONObject();
44 | result.put("key", Base64.encodeToString(secretKeySpec_enc, Base64.DEFAULT));
45 | result.put("value", value);
46 | result.put("native", true);
47 |
48 | return result;
49 | }
50 |
51 | public static String decrypt(byte[] buf, byte[] key, byte[] iv, byte[] adata) throws Exception {
52 | SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
53 | synchronized (CIPHER) {
54 | initCipher(Cipher.DECRYPT_MODE, secretKeySpec, iv, adata);
55 | return new String(CIPHER.doFinal(buf));
56 | }
57 | }
58 |
59 | private static SecretKeySpec generateKeySpec() throws Exception {
60 | KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
61 | keyGenerator.init(KEY_SIZE, new SecureRandom());
62 | SecretKey sc = keyGenerator.generateKey();
63 | return new SecretKeySpec(sc.getEncoded(), "AES");
64 | }
65 |
66 | private static void initCipher(int cipherMode, Key key, byte[] iv, byte[] adata) throws Exception {
67 | if (iv != null) {
68 | CIPHER.init(cipherMode, key, new IvParameterSpec(iv));
69 | } else {
70 | CIPHER.init(cipherMode, key);
71 | }
72 | CIPHER.updateAAD(adata);
73 | }
74 |
75 | private static Cipher getCipher() {
76 | try {
77 | return Cipher.getInstance("AES/" + CIPHER_MODE + "/NoPadding");
78 | } catch (Exception e) {
79 | return null;
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/android/RSA.java:
--------------------------------------------------------------------------------
1 | package com.crypho.plugins;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 |
6 | import android.security.KeyPairGeneratorSpec;
7 |
8 | import java.math.BigInteger;
9 | import java.security.Key;
10 | import java.security.KeyPairGenerator;
11 | import java.security.KeyStore;
12 | import java.util.Calendar;
13 |
14 | import javax.crypto.Cipher;
15 | import javax.security.auth.x500.X500Principal;
16 |
17 | public class RSA {
18 | private static final String KEYSTORE_PROVIDER = "AndroidKeyStore";
19 | private static final Cipher CIPHER = getCipher();
20 |
21 | public static byte[] encrypt(byte[] buf, String alias) throws Exception {
22 | return runCipher(Cipher.ENCRYPT_MODE, alias, buf);
23 | }
24 |
25 | public static byte[] decrypt(byte[] buf, String alias) throws Exception {
26 | return runCipher(Cipher.DECRYPT_MODE, alias, buf);
27 | }
28 |
29 | public static void createKeyPair(Context ctx, String alias) throws Exception {
30 | Calendar notBefore = Calendar.getInstance();
31 | Calendar notAfter = Calendar.getInstance();
32 | notAfter.add(Calendar.YEAR, 100);
33 | String principalString = String.format("CN=%s, OU=%s", alias, ctx.getPackageName());
34 | KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(ctx)
35 | .setAlias(alias)
36 | .setSubject(new X500Principal(principalString))
37 | .setSerialNumber(BigInteger.ONE)
38 | .setStartDate(notBefore.getTime())
39 | .setEndDate(notAfter.getTime())
40 | .setEncryptionRequired()
41 | .setKeySize(2048)
42 | .setKeyType("RSA")
43 | .build();
44 | KeyPairGenerator kpGenerator = KeyPairGenerator.getInstance("RSA", KEYSTORE_PROVIDER);
45 | kpGenerator.initialize(spec);
46 | kpGenerator.generateKeyPair();
47 | }
48 |
49 | public static boolean isEntryAvailable(String alias) {
50 | try {
51 | return loadKey(Cipher.ENCRYPT_MODE, alias) != null;
52 | } catch (Exception e) {
53 | return false;
54 | }
55 | }
56 |
57 | private static byte[] runCipher(int cipherMode, String alias, byte[] buf) throws Exception {
58 | Key key = loadKey(cipherMode, alias);
59 | synchronized (CIPHER) {
60 | CIPHER.init(cipherMode, key);
61 | return CIPHER.doFinal(buf);
62 | }
63 | }
64 |
65 | private static Key loadKey(int cipherMode, String alias) throws Exception {
66 | KeyStore keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER);
67 | keyStore.load(null, null);
68 | Key key;
69 | switch (cipherMode) {
70 | case Cipher.ENCRYPT_MODE:
71 | key = keyStore.getCertificate(alias).getPublicKey();
72 | if (key == null) {
73 | throw new Exception("Failed to load the public key for " + alias);
74 | }
75 | break;
76 | case Cipher.DECRYPT_MODE:
77 | key = keyStore.getKey(alias, null);
78 | if (key == null) {
79 | throw new Exception("Failed to load the private key for " + alias);
80 | }
81 | break;
82 | default : throw new Exception("Invalid cipher mode parameter");
83 | }
84 | return key;
85 | }
86 |
87 | private static Cipher getCipher() {
88 | try {
89 | return Cipher.getInstance("RSA/ECB/PKCS1Padding");
90 | } catch (Exception e) {
91 | return null;
92 | }
93 | }
94 | }
--------------------------------------------------------------------------------
/src/android/SecureStorage.java:
--------------------------------------------------------------------------------
1 | package com.crypho.plugins;
2 |
3 | import java.lang.reflect.Method;
4 | import java.util.Hashtable;
5 |
6 | import android.util.Log;
7 | import android.util.Base64;
8 | import android.os.Build;
9 | import android.app.KeyguardManager;
10 | import android.content.Context;
11 | import android.content.Intent;
12 |
13 | import org.apache.cordova.CallbackContext;
14 | import org.apache.cordova.CordovaArgs;
15 | import org.apache.cordova.CordovaPlugin;
16 | import org.json.JSONException;
17 | import org.json.JSONObject;
18 | import org.json.JSONArray;
19 | import javax.crypto.Cipher;
20 |
21 | public class SecureStorage extends CordovaPlugin {
22 | private static final String TAG = "SecureStorage";
23 |
24 | private static final boolean SUPPORTED = Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
25 |
26 | private static final String MSG_NOT_SUPPORTED = "API 21 (Android 5.0 Lollipop) is required. This device is running API " + Build.VERSION.SDK_INT;
27 | private static final String MSG_DEVICE_NOT_SECURE = "Device is not secure";
28 |
29 | private Hashtable SERVICE_STORAGE = new Hashtable();
30 | private String INIT_SERVICE;
31 | private String INIT_PACKAGENAME;
32 | private volatile CallbackContext initContext, secureDeviceContext;
33 | private volatile boolean initContextRunning = false;
34 |
35 | @Override
36 | public void onResume(boolean multitasking) {
37 | if (secureDeviceContext != null) {
38 | if (isDeviceSecure()) {
39 | secureDeviceContext.success();
40 | } else {
41 | secureDeviceContext.error(MSG_DEVICE_NOT_SECURE);
42 | }
43 | secureDeviceContext = null;
44 | }
45 |
46 | if (initContext != null && !initContextRunning) {
47 | cordova.getThreadPool().execute(new Runnable() {
48 | public void run() {
49 | initContextRunning = true;
50 | try {
51 | String alias = service2alias(INIT_SERVICE);
52 | if (!RSA.isEntryAvailable(alias)) {
53 | //Solves Issue #96. The RSA key may have been deleted by changing the lock type.
54 | getStorage(INIT_SERVICE).clear();
55 | RSA.createKeyPair(getContext(), alias);
56 | }
57 | initSuccess(initContext);
58 | } catch (Exception e) {
59 | Log.e(TAG, "Init failed :", e);
60 | initContext.error(e.getMessage());
61 | } finally {
62 | initContext = null;
63 | initContextRunning = false;
64 | }
65 | }
66 | });
67 | }
68 | }
69 |
70 | @Override
71 | public boolean execute(String action, CordovaArgs args, final CallbackContext callbackContext) throws JSONException {
72 | if(!SUPPORTED){
73 | Log.w(TAG, MSG_NOT_SUPPORTED);
74 | callbackContext.error(MSG_NOT_SUPPORTED);
75 | return false;
76 | }
77 | if ("init".equals(action)) {
78 | String service = args.getString(0);
79 | JSONObject options = args.getJSONObject(1);
80 | String packageName = options.optString("packageName", getContext().getPackageName());
81 |
82 | Context ctx = null;
83 |
84 | // Solves #151. By default, we use our own ApplicationContext
85 | // If packageName is provided, we try to get the Context of another Application with that packageName
86 | try {
87 | ctx = getPackageContext(packageName);
88 | } catch (Exception e) {
89 | // This will fail if the application with given packageName is not installed
90 | // OR if we do not have required permissions and cause a security violation
91 | Log.e(TAG, "Init failed :", e);
92 | callbackContext.error(e.getMessage());
93 | }
94 |
95 | INIT_PACKAGENAME = ctx.getPackageName();
96 | String alias = service2alias(service);
97 | INIT_SERVICE = service;
98 |
99 | SharedPreferencesHandler PREFS = new SharedPreferencesHandler(alias, ctx);
100 | SERVICE_STORAGE.put(service, PREFS);
101 |
102 | if (!isDeviceSecure()) {
103 | Log.e(TAG, MSG_DEVICE_NOT_SECURE);
104 | callbackContext.error(MSG_DEVICE_NOT_SECURE);
105 | } else if (!RSA.isEntryAvailable(alias)) {
106 | initContext = callbackContext;
107 | unlockCredentials();
108 | } else {
109 | initSuccess(callbackContext);
110 | }
111 | return true;
112 | }
113 | if ("set".equals(action)) {
114 | final String service = args.getString(0);
115 | final String key = args.getString(1);
116 | final String value = args.getString(2);
117 | final String adata = service;
118 | cordova.getThreadPool().execute(new Runnable() {
119 | public void run() {
120 | try {
121 | JSONObject result = AES.encrypt(value.getBytes(), adata.getBytes());
122 | byte[] aes_key = Base64.decode(result.getString("key"), Base64.DEFAULT);
123 | byte[] aes_key_enc = RSA.encrypt(aes_key, service2alias(service));
124 | result.put("key", Base64.encodeToString(aes_key_enc, Base64.DEFAULT));
125 | getStorage(service).store(key, result.toString());
126 | callbackContext.success(key);
127 | } catch (Exception e) {
128 | Log.e(TAG, "Encrypt failed :", e);
129 | callbackContext.error(e.getMessage());
130 | }
131 | }
132 | });
133 | return true;
134 | }
135 | if ("get".equals(action)) {
136 | final String service = args.getString(0);
137 | final String key = args.getString(1);
138 | String value = getStorage(service).fetch(key);
139 | if (value != null) {
140 | JSONObject json = new JSONObject(value);
141 | final byte[] encKey = Base64.decode(json.getString("key"), Base64.DEFAULT);
142 | JSONObject data = json.getJSONObject("value");
143 | final byte[] ct = Base64.decode(data.getString("ct"), Base64.DEFAULT);
144 | final byte[] iv = Base64.decode(data.getString("iv"), Base64.DEFAULT);
145 | final byte[] adata = Base64.decode(data.getString("adata"), Base64.DEFAULT);
146 | cordova.getThreadPool().execute(new Runnable() {
147 | public void run() {
148 | try {
149 | byte[] decryptedKey = RSA.decrypt(encKey, service2alias(service));
150 | String decrypted = new String(AES.decrypt(ct, decryptedKey, iv, adata));
151 | callbackContext.success(decrypted);
152 | } catch (Exception e) {
153 | Log.e(TAG, "Decrypt failed :", e);
154 | callbackContext.error(e.getMessage());
155 | }
156 | }
157 | });
158 | } else {
159 | callbackContext.error("Key [" + key + "] not found.");
160 | }
161 | return true;
162 | }
163 | if ("secureDevice".equals(action)) {
164 | secureDeviceContext = callbackContext;
165 | unlockCredentials();
166 | return true;
167 | }
168 | if ("remove".equals(action)) {
169 | String service = args.getString(0);
170 | String key = args.getString(1);
171 | getStorage(service).remove(key);
172 | callbackContext.success(key);
173 | return true;
174 | }
175 | if ("keys".equals(action)) {
176 | String service = args.getString(0);
177 | callbackContext.success(new JSONArray(getStorage(service).keys()));
178 | return true;
179 | }
180 | if ("clear".equals(action)) {
181 | String service = args.getString(0);
182 | getStorage(service).clear();
183 | callbackContext.success();
184 | return true;
185 | }
186 | return false;
187 | }
188 |
189 | private boolean isDeviceSecure() {
190 | KeyguardManager keyguardManager = (KeyguardManager)(getContext().getSystemService(Context.KEYGUARD_SERVICE));
191 | try {
192 | Method isSecure = null;
193 | isSecure = keyguardManager.getClass().getMethod("isDeviceSecure");
194 | return ((Boolean) isSecure.invoke(keyguardManager)).booleanValue();
195 | } catch (Exception e) {
196 | return keyguardManager.isKeyguardSecure();
197 | }
198 | }
199 |
200 | private String service2alias(String service) {
201 | String res = INIT_PACKAGENAME + "." + service;
202 | return res;
203 | }
204 |
205 | private SharedPreferencesHandler getStorage(String service) {
206 | return SERVICE_STORAGE.get(service);
207 | }
208 |
209 | private void initSuccess(CallbackContext context) {
210 | context.success();
211 | }
212 |
213 | private void unlockCredentials() {
214 | cordova.getActivity().runOnUiThread(new Runnable() {
215 | public void run() {
216 | Intent intent = new Intent("com.android.credentials.UNLOCK");
217 | startActivity(intent);
218 | }
219 | });
220 | }
221 |
222 | private Context getContext() {
223 | return cordova.getActivity().getApplicationContext();
224 | }
225 |
226 | private Context getPackageContext(String packageName) throws Exception {
227 | Context pkgContext = null;
228 |
229 | Context context = getContext();
230 | if (context.getPackageName().equals(packageName)) {
231 | pkgContext = context;
232 | } else {
233 | pkgContext = context.createPackageContext(packageName, 0);
234 | }
235 |
236 | return pkgContext;
237 | }
238 |
239 | private void startActivity(Intent intent) {
240 | cordova.getActivity().startActivity(intent);
241 | }
242 |
243 | }
244 |
--------------------------------------------------------------------------------
/src/android/SharedPreferencesHandler.java:
--------------------------------------------------------------------------------
1 | package com.crypho.plugins;
2 |
3 | import java.util.Set;
4 | import java.util.HashSet;
5 | import java.util.Iterator;
6 |
7 | import android.content.SharedPreferences;
8 | import android.content.Context;
9 |
10 | public class SharedPreferencesHandler {
11 | private SharedPreferences prefs;
12 |
13 | public SharedPreferencesHandler (String prefsName, Context ctx){
14 | prefs = ctx.getSharedPreferences(prefsName + "_SS", 0);
15 | }
16 |
17 | void store(String key, String value){
18 | SharedPreferences.Editor editor = prefs.edit();
19 | editor.putString("_SS_" + key, value);
20 | editor.commit();
21 | }
22 |
23 | String fetch (String key){
24 | return prefs.getString("_SS_" + key, null);
25 | }
26 |
27 | void remove (String key){
28 | SharedPreferences.Editor editor = prefs.edit();
29 | editor.remove("_SS_" + key);
30 | editor.commit();
31 | }
32 |
33 | Set keys (){
34 | Set res = new HashSet();
35 | Iterator iter = prefs.getAll().keySet().iterator();
36 | while (iter.hasNext()) {
37 | String key = iter.next();
38 | if (key.startsWith("_SS_") && !key.startsWith("_SS_MIGRATED_")) {
39 | res.add(key.replaceFirst("^_SS_", ""));
40 | }
41 | }
42 | return res;
43 | }
44 |
45 | void clear (){
46 | SharedPreferences.Editor editor = prefs.edit();
47 | editor.clear();
48 | editor.commit();
49 | }
50 | }
--------------------------------------------------------------------------------
/src/ios/SAMKeychain/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2010-2016 Sam Soffes, http://soff.es
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining
4 | a copy of this software and associated documentation files (the
5 | "Software"), to deal in the Software without restriction, including
6 | without limitation the rights to use, copy, modify, merge, publish,
7 | distribute, sublicense, and/or sell copies of the Software, and to
8 | permit persons to whom the Software is furnished to do so, subject to
9 | the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be
12 | included in all copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/src/ios/SAMKeychain/SAMKeychain.bundle/en.lproj/SAMKeychain.strings:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Crypho/cordova-plugin-secure-storage/cf7a39c5c261a05e5dc7c1e56de7085dc50b7898/src/ios/SAMKeychain/SAMKeychain.bundle/en.lproj/SAMKeychain.strings
--------------------------------------------------------------------------------
/src/ios/SAMKeychain/SAMKeychain.h:
--------------------------------------------------------------------------------
1 | //
2 | // SAMKeychain.h
3 | // SAMKeychain
4 | //
5 | // Created by Sam Soffes on 5/19/10.
6 | // Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
7 | //
8 |
9 | #if __has_feature(modules)
10 | @import Foundation;
11 | #else
12 | #import
13 | #endif
14 |
15 | /**
16 | Error code specific to SAMKeychain that can be returned in NSError objects.
17 | For codes returned by the operating system, refer to SecBase.h for your
18 | platform.
19 | */
20 | typedef NS_ENUM(OSStatus, SAMKeychainErrorCode) {
21 | /** Some of the arguments were invalid. */
22 | SAMKeychainErrorBadArguments = -1001,
23 | };
24 |
25 | /** SAMKeychain error domain */
26 | extern NSString *const kSAMKeychainErrorDomain;
27 |
28 | /** Account name. */
29 | extern NSString *const kSAMKeychainAccountKey;
30 |
31 | /**
32 | Time the item was created.
33 |
34 | The value will be a string.
35 | */
36 | extern NSString *const kSAMKeychainCreatedAtKey;
37 |
38 | /** Item class. */
39 | extern NSString *const kSAMKeychainClassKey;
40 |
41 | /** Item description. */
42 | extern NSString *const kSAMKeychainDescriptionKey;
43 |
44 | /** Item label. */
45 | extern NSString *const kSAMKeychainLabelKey;
46 |
47 | /** Time the item was last modified.
48 |
49 | The value will be a string.
50 | */
51 | extern NSString *const kSAMKeychainLastModifiedKey;
52 |
53 | /** Where the item was created. */
54 | extern NSString *const kSAMKeychainWhereKey;
55 |
56 | /**
57 | Simple wrapper for accessing accounts, getting passwords, setting passwords, and deleting passwords using the system
58 | Keychain on Mac OS X and iOS.
59 |
60 | This was originally inspired by EMKeychain and SDKeychain (both of which are now gone). Thanks to the authors.
61 | SAMKeychain has since switched to a simpler implementation that was abstracted from [SSToolkit](http://sstoolk.it).
62 | */
63 | @interface SAMKeychain : NSObject
64 |
65 | #pragma mark - Classic methods
66 |
67 | /**
68 | Returns a string containing the password for a given account and service, or `nil` if the Keychain doesn't have a
69 | password for the given parameters.
70 |
71 | @param serviceName The service for which to return the corresponding password.
72 |
73 | @param account The account for which to return the corresponding password.
74 |
75 | @return Returns a string containing the password for a given account and service, or `nil` if the Keychain doesn't
76 | have a password for the given parameters.
77 | */
78 | + (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account;
79 | + (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error __attribute__((swift_error(none)));
80 |
81 | /**
82 | Returns a nsdata containing the password for a given account and service, or `nil` if the Keychain doesn't have a
83 | password for the given parameters.
84 |
85 | @param serviceName The service for which to return the corresponding password.
86 |
87 | @param account The account for which to return the corresponding password.
88 |
89 | @return Returns a nsdata containing the password for a given account and service, or `nil` if the Keychain doesn't
90 | have a password for the given parameters.
91 | */
92 | + (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account;
93 | + (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error __attribute__((swift_error(none)));
94 |
95 |
96 | /**
97 | Deletes a password from the Keychain.
98 |
99 | @param serviceName The service for which to delete the corresponding password.
100 |
101 | @param account The account for which to delete the corresponding password.
102 |
103 | @return Returns `YES` on success, or `NO` on failure.
104 | */
105 | + (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account;
106 | + (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error __attribute__((swift_error(none)));
107 |
108 |
109 | /**
110 | Sets a password in the Keychain.
111 |
112 | @param password The password to store in the Keychain.
113 |
114 | @param serviceName The service for which to set the corresponding password.
115 |
116 | @param account The account for which to set the corresponding password.
117 |
118 | @return Returns `YES` on success, or `NO` on failure.
119 | */
120 | + (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account;
121 | + (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error __attribute__((swift_error(none)));
122 |
123 | /**
124 | Sets a password in the Keychain.
125 |
126 | @param password The password to store in the Keychain.
127 |
128 | @param serviceName The service for which to set the corresponding password.
129 |
130 | @param account The account for which to set the corresponding password.
131 |
132 | @return Returns `YES` on success, or `NO` on failure.
133 | */
134 | + (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account;
135 | + (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error __attribute__((swift_error(none)));
136 |
137 | /**
138 | Returns an array containing the Keychain's accounts, or `nil` if the Keychain has no accounts.
139 |
140 | See the `NSString` constants declared in SAMKeychain.h for a list of keys that can be used when accessing the
141 | dictionaries returned by this method.
142 |
143 | @return An array of dictionaries containing the Keychain's accounts, or `nil` if the Keychain doesn't have any
144 | accounts. The order of the objects in the array isn't defined.
145 | */
146 | + (NSArray *> *)allAccounts;
147 | + (NSArray *> *)allAccounts:(NSError *__autoreleasing *)error __attribute__((swift_error(none)));
148 |
149 |
150 | /**
151 | Returns an array containing the Keychain's accounts for a given service, or `nil` if the Keychain doesn't have any
152 | accounts for the given service.
153 |
154 | See the `NSString` constants declared in SAMKeychain.h for a list of keys that can be used when accessing the
155 | dictionaries returned by this method.
156 |
157 | @param serviceName The service for which to return the corresponding accounts.
158 |
159 | @return An array of dictionaries containing the Keychain's accounts for a given `serviceName`, or `nil` if the Keychain
160 | doesn't have any accounts for the given `serviceName`. The order of the objects in the array isn't defined.
161 | */
162 | + (NSArray *> *)accountsForService:(NSString *)serviceName;
163 | + (NSArray *> *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error __attribute__((swift_error(none)));
164 |
165 |
166 | #pragma mark - Configuration
167 |
168 | #if __IPHONE_4_0 && TARGET_OS_IPHONE
169 | /**
170 | Returns the accessibility type for all future passwords saved to the Keychain.
171 |
172 | @return Returns the accessibility type.
173 |
174 | The return value will be `NULL` or one of the "Keychain Item Accessibility
175 | Constants" used for determining when a keychain item should be readable.
176 |
177 | @see setAccessibilityType
178 | */
179 | + (CFTypeRef)accessibilityType;
180 |
181 | /**
182 | Sets the accessibility type for all future passwords saved to the Keychain.
183 |
184 | @param accessibilityType One of the "Keychain Item Accessibility Constants"
185 | used for determining when a keychain item should be readable.
186 |
187 | If the value is `NULL` (the default), the Keychain default will be used which
188 | is highly insecure. You really should use at least `kSecAttrAccessibleAfterFirstUnlock`
189 | for background applications or `kSecAttrAccessibleWhenUnlocked` for all
190 | other applications.
191 |
192 | @see accessibilityType
193 | */
194 | + (void)setAccessibilityType:(CFTypeRef)accessibilityType;
195 | #endif
196 |
197 | @end
198 |
199 | #import "SAMKeychainQuery.h"
200 |
--------------------------------------------------------------------------------
/src/ios/SAMKeychain/SAMKeychain.m:
--------------------------------------------------------------------------------
1 | //
2 | // SAMKeychain.m
3 | // SAMKeychain
4 | //
5 | // Created by Sam Soffes on 5/19/10.
6 | // Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
7 | //
8 |
9 | #import "SAMKeychain.h"
10 | #import "SAMKeychainQuery.h"
11 |
12 | NSString *const kSAMKeychainErrorDomain = @"com.samsoffes.samkeychain";
13 | NSString *const kSAMKeychainAccountKey = @"acct";
14 | NSString *const kSAMKeychainCreatedAtKey = @"cdat";
15 | NSString *const kSAMKeychainClassKey = @"labl";
16 | NSString *const kSAMKeychainDescriptionKey = @"desc";
17 | NSString *const kSAMKeychainLabelKey = @"labl";
18 | NSString *const kSAMKeychainLastModifiedKey = @"mdat";
19 | NSString *const kSAMKeychainWhereKey = @"svce";
20 |
21 | #if __IPHONE_4_0 && TARGET_OS_IPHONE
22 | static CFTypeRef SAMKeychainAccessibilityType = NULL;
23 | #endif
24 |
25 | @implementation SAMKeychain
26 |
27 | + (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account {
28 | return [self passwordForService:serviceName account:account error:nil];
29 | }
30 |
31 |
32 | + (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
33 | SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
34 | query.service = serviceName;
35 | query.account = account;
36 | [query fetch:error];
37 | return query.password;
38 | }
39 |
40 | + (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account {
41 | return [self passwordDataForService:serviceName account:account error:nil];
42 | }
43 |
44 | + (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error {
45 | SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
46 | query.service = serviceName;
47 | query.account = account;
48 | [query fetch:error];
49 |
50 | return query.passwordData;
51 | }
52 |
53 |
54 | + (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account {
55 | return [self deletePasswordForService:serviceName account:account error:nil];
56 | }
57 |
58 |
59 | + (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
60 | SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
61 | query.service = serviceName;
62 | query.account = account;
63 | return [query deleteItem:error];
64 | }
65 |
66 |
67 | + (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account {
68 | return [self setPassword:password forService:serviceName account:account error:nil];
69 | }
70 |
71 |
72 | + (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
73 | SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
74 | query.service = serviceName;
75 | query.account = account;
76 | query.password = password;
77 | return [query save:error];
78 | }
79 |
80 | + (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account {
81 | return [self setPasswordData:password forService:serviceName account:account error:nil];
82 | }
83 |
84 |
85 | + (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error {
86 | SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
87 | query.service = serviceName;
88 | query.account = account;
89 | query.passwordData = password;
90 | return [query save:error];
91 | }
92 |
93 | + (NSArray *)allAccounts {
94 | return [self allAccounts:nil];
95 | }
96 |
97 |
98 | + (NSArray *)allAccounts:(NSError *__autoreleasing *)error {
99 | return [self accountsForService:nil error:error];
100 | }
101 |
102 |
103 | + (NSArray *)accountsForService:(NSString *)serviceName {
104 | return [self accountsForService:serviceName error:nil];
105 | }
106 |
107 |
108 | + (NSArray *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error {
109 | SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
110 | query.service = serviceName;
111 | return [query fetchAll:error];
112 | }
113 |
114 |
115 | #if __IPHONE_4_0 && TARGET_OS_IPHONE
116 | + (CFTypeRef)accessibilityType {
117 | return SAMKeychainAccessibilityType;
118 | }
119 |
120 |
121 | + (void)setAccessibilityType:(CFTypeRef)accessibilityType {
122 | CFRetain(accessibilityType);
123 | if (SAMKeychainAccessibilityType) {
124 | CFRelease(SAMKeychainAccessibilityType);
125 | }
126 | SAMKeychainAccessibilityType = accessibilityType;
127 | }
128 | #endif
129 |
130 | @end
131 |
--------------------------------------------------------------------------------
/src/ios/SAMKeychain/SAMKeychainQuery.h:
--------------------------------------------------------------------------------
1 | //
2 | // SAMKeychainQuery.h
3 | // SAMKeychain
4 | //
5 | // Created by Caleb Davenport on 3/19/13.
6 | // Copyright (c) 2013-2014 Sam Soffes. All rights reserved.
7 | //
8 |
9 | #if __has_feature(modules)
10 | @import Foundation;
11 | @import Security;
12 | #else
13 | #import
14 | #import
15 | #endif
16 |
17 | #if __IPHONE_7_0 || __MAC_10_9
18 | // Keychain synchronization available at compile time
19 | #define SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE 1
20 | #endif
21 |
22 | #if __IPHONE_3_0 || __MAC_10_9
23 | // Keychain access group available at compile time
24 | #define SAMKEYCHAIN_ACCESS_GROUP_AVAILABLE 1
25 | #endif
26 |
27 | #ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
28 | typedef NS_ENUM(NSUInteger, SAMKeychainQuerySynchronizationMode) {
29 | SAMKeychainQuerySynchronizationModeAny,
30 | SAMKeychainQuerySynchronizationModeNo,
31 | SAMKeychainQuerySynchronizationModeYes
32 | };
33 | #endif
34 |
35 | /**
36 | Simple interface for querying or modifying keychain items.
37 | */
38 | @interface SAMKeychainQuery : NSObject
39 |
40 | /** kSecAttrAccount */
41 | @property (nonatomic, copy) NSString *account;
42 |
43 | /** kSecAttrService */
44 | @property (nonatomic, copy) NSString *service;
45 |
46 | /** kSecAttrLabel */
47 | @property (nonatomic, copy) NSString *label;
48 |
49 | #ifdef SAMKEYCHAIN_ACCESS_GROUP_AVAILABLE
50 | /** kSecAttrAccessGroup (only used on iOS) */
51 | @property (nonatomic, copy) NSString *accessGroup;
52 | #endif
53 |
54 | #ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
55 | /** kSecAttrSynchronizable */
56 | @property (nonatomic) SAMKeychainQuerySynchronizationMode synchronizationMode;
57 | #endif
58 |
59 | /** Root storage for password information */
60 | @property (nonatomic, copy) NSData *passwordData;
61 |
62 | /**
63 | This property automatically transitions between an object and the value of
64 | `passwordData` using NSKeyedArchiver and NSKeyedUnarchiver.
65 | */
66 | @property (nonatomic, copy) id passwordObject;
67 |
68 | /**
69 | Convenience accessor for setting and getting a password string. Passes through
70 | to `passwordData` using UTF-8 string encoding.
71 | */
72 | @property (nonatomic, copy) NSString *password;
73 |
74 |
75 | ///------------------------
76 | /// @name Saving & Deleting
77 | ///------------------------
78 |
79 | /**
80 | Save the receiver's attributes as a keychain item. Existing items with the
81 | given account, service, and access group will first be deleted.
82 |
83 | @param error Populated should an error occur.
84 |
85 | @return `YES` if saving was successful, `NO` otherwise.
86 | */
87 | - (BOOL)save:(NSError **)error;
88 |
89 | /**
90 | Delete keychain items that match the given account, service, and access group.
91 |
92 | @param error Populated should an error occur.
93 |
94 | @return `YES` if saving was successful, `NO` otherwise.
95 | */
96 | - (BOOL)deleteItem:(NSError **)error;
97 |
98 |
99 | ///---------------
100 | /// @name Fetching
101 | ///---------------
102 |
103 | /**
104 | Fetch all keychain items that match the given account, service, and access
105 | group. The values of `password` and `passwordData` are ignored when fetching.
106 |
107 | @param error Populated should an error occur.
108 |
109 | @return An array of dictionaries that represent all matching keychain items or
110 | `nil` should an error occur.
111 | The order of the items is not determined.
112 | */
113 | - (NSArray *> *)fetchAll:(NSError **)error;
114 |
115 | /**
116 | Fetch the keychain item that matches the given account, service, and access
117 | group. The `password` and `passwordData` properties will be populated unless
118 | an error occurs. The values of `password` and `passwordData` are ignored when
119 | fetching.
120 |
121 | @param error Populated should an error occur.
122 |
123 | @return `YES` if fetching was successful, `NO` otherwise.
124 | */
125 | - (BOOL)fetch:(NSError **)error;
126 |
127 |
128 | ///-----------------------------
129 | /// @name Synchronization Status
130 | ///-----------------------------
131 |
132 | #ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
133 | /**
134 | Returns a boolean indicating if keychain synchronization is available on the device at runtime. The #define
135 | SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE is only for compile time. If you are checking for the presence of synchronization,
136 | you should use this method.
137 |
138 | @return A value indicating if keychain synchronization is available
139 | */
140 | + (BOOL)isSynchronizationAvailable;
141 | #endif
142 |
143 | @end
144 |
--------------------------------------------------------------------------------
/src/ios/SAMKeychain/SAMKeychainQuery.m:
--------------------------------------------------------------------------------
1 | //
2 | // SAMKeychainQuery.m
3 | // SAMKeychain
4 | //
5 | // Created by Caleb Davenport on 3/19/13.
6 | // Copyright (c) 2013-2014 Sam Soffes. All rights reserved.
7 | //
8 |
9 | #import "SAMKeychainQuery.h"
10 | #import "SAMKeychain.h"
11 |
12 | @implementation SAMKeychainQuery
13 |
14 | @synthesize account = _account;
15 | @synthesize service = _service;
16 | @synthesize label = _label;
17 | @synthesize passwordData = _passwordData;
18 |
19 | #ifdef SAMKEYCHAIN_ACCESS_GROUP_AVAILABLE
20 | @synthesize accessGroup = _accessGroup;
21 | #endif
22 |
23 | #ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
24 | @synthesize synchronizationMode = _synchronizationMode;
25 | #endif
26 |
27 | #pragma mark - Public
28 |
29 | - (BOOL)save:(NSError *__autoreleasing *)error {
30 | OSStatus status = SAMKeychainErrorBadArguments;
31 | if (!self.service || !self.account || !self.passwordData) {
32 | if (error) {
33 | *error = [[self class] errorWithCode:status];
34 | }
35 | return NO;
36 | }
37 | NSMutableDictionary *query = nil;
38 | NSMutableDictionary * searchQuery = [self query];
39 | status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, nil);
40 | if (status == errSecSuccess) {//item already exists, update it!
41 | query = [[NSMutableDictionary alloc]init];
42 | [query setObject:self.passwordData forKey:(__bridge id)kSecValueData];
43 | #if __IPHONE_4_0 && TARGET_OS_IPHONE
44 | CFTypeRef accessibilityType = [SAMKeychain accessibilityType];
45 | if (accessibilityType) {
46 | [query setObject:(__bridge id)accessibilityType forKey:(__bridge id)kSecAttrAccessible];
47 | }
48 | #endif
49 | status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
50 | }else if(status == errSecItemNotFound){//item not found, create it!
51 | query = [self query];
52 | if (self.label) {
53 | [query setObject:self.label forKey:(__bridge id)kSecAttrLabel];
54 | }
55 | [query setObject:self.passwordData forKey:(__bridge id)kSecValueData];
56 | #if __IPHONE_4_0 && TARGET_OS_IPHONE
57 | CFTypeRef accessibilityType = [SAMKeychain accessibilityType];
58 | if (accessibilityType) {
59 | [query setObject:(__bridge id)accessibilityType forKey:(__bridge id)kSecAttrAccessible];
60 | }
61 | #endif
62 | status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
63 | }
64 | if (status != errSecSuccess && error != NULL) {
65 | *error = [[self class] errorWithCode:status];
66 | }
67 | return (status == errSecSuccess);}
68 |
69 |
70 | - (BOOL)deleteItem:(NSError *__autoreleasing *)error {
71 | OSStatus status = SAMKeychainErrorBadArguments;
72 | if (!self.service || !self.account) {
73 | if (error) {
74 | *error = [[self class] errorWithCode:status];
75 | }
76 | return NO;
77 | }
78 |
79 | NSMutableDictionary *query = [self query];
80 | #if TARGET_OS_IPHONE
81 | status = SecItemDelete((__bridge CFDictionaryRef)query);
82 | #else
83 | // On Mac OS, SecItemDelete will not delete a key created in a different
84 | // app, nor in a different version of the same app.
85 | //
86 | // To replicate the issue, save a password, change to the code and
87 | // rebuild the app, and then attempt to delete that password.
88 | //
89 | // This was true in OS X 10.6 and probably later versions as well.
90 | //
91 | // Work around it by using SecItemCopyMatching and SecKeychainItemDelete.
92 | CFTypeRef result = NULL;
93 | [query setObject:@YES forKey:(__bridge id)kSecReturnRef];
94 | status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
95 | if (status == errSecSuccess) {
96 | status = SecKeychainItemDelete((SecKeychainItemRef)result);
97 | CFRelease(result);
98 | }
99 | #endif
100 |
101 | if (status != errSecSuccess && error != NULL) {
102 | *error = [[self class] errorWithCode:status];
103 | }
104 |
105 | return (status == errSecSuccess);
106 | }
107 |
108 |
109 | - (NSArray *)fetchAll:(NSError *__autoreleasing *)error {
110 | NSMutableDictionary *query = [self query];
111 | [query setObject:@YES forKey:(__bridge id)kSecReturnAttributes];
112 | [query setObject:(__bridge id)kSecMatchLimitAll forKey:(__bridge id)kSecMatchLimit];
113 | #if __IPHONE_4_0 && TARGET_OS_IPHONE
114 | CFTypeRef accessibilityType = [SAMKeychain accessibilityType];
115 | if (accessibilityType) {
116 | [query setObject:(__bridge id)accessibilityType forKey:(__bridge id)kSecAttrAccessible];
117 | }
118 | #endif
119 |
120 | CFTypeRef result = NULL;
121 | OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
122 | if (status != errSecSuccess && error != NULL) {
123 | *error = [[self class] errorWithCode:status];
124 | return nil;
125 | }
126 |
127 | return (__bridge_transfer NSArray *)result;
128 | }
129 |
130 |
131 | - (BOOL)fetch:(NSError *__autoreleasing *)error {
132 | OSStatus status = SAMKeychainErrorBadArguments;
133 | if (!self.service || !self.account) {
134 | if (error) {
135 | *error = [[self class] errorWithCode:status];
136 | }
137 | return NO;
138 | }
139 |
140 | CFTypeRef result = NULL;
141 | NSMutableDictionary *query = [self query];
142 | [query setObject:@YES forKey:(__bridge id)kSecReturnData];
143 | [query setObject:(__bridge id)kSecMatchLimitOne forKey:(__bridge id)kSecMatchLimit];
144 | status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
145 |
146 | if (status != errSecSuccess) {
147 | if (error) {
148 | *error = [[self class] errorWithCode:status];
149 | }
150 | return NO;
151 | }
152 |
153 | self.passwordData = (__bridge_transfer NSData *)result;
154 | return YES;
155 | }
156 |
157 |
158 | #pragma mark - Accessors
159 |
160 | - (void)setPasswordObject:(id)object {
161 | self.passwordData = [NSKeyedArchiver archivedDataWithRootObject:object];
162 | }
163 |
164 |
165 | - (id)passwordObject {
166 | if ([self.passwordData length]) {
167 | return [NSKeyedUnarchiver unarchiveObjectWithData:self.passwordData];
168 | }
169 | return nil;
170 | }
171 |
172 |
173 | - (void)setPassword:(NSString *)password {
174 | self.passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
175 | }
176 |
177 |
178 | - (NSString *)password {
179 | if ([self.passwordData length]) {
180 | return [[NSString alloc] initWithData:self.passwordData encoding:NSUTF8StringEncoding];
181 | }
182 | return nil;
183 | }
184 |
185 |
186 | #pragma mark - Synchronization Status
187 |
188 | #ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
189 | + (BOOL)isSynchronizationAvailable {
190 | #if TARGET_OS_IPHONE
191 | // Apple suggested way to check for 7.0 at runtime
192 | // https://developer.apple.com/library/ios/documentation/userexperience/conceptual/transitionguide/SupportingEarlieriOS.html
193 | return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1;
194 | #else
195 | return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4;
196 | #endif
197 | }
198 | #endif
199 |
200 |
201 | #pragma mark - Private
202 |
203 | - (NSMutableDictionary *)query {
204 | NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:3];
205 | [dictionary setObject:(__bridge id)kSecClassGenericPassword forKey:(__bridge id)kSecClass];
206 |
207 | if (self.service) {
208 | [dictionary setObject:self.service forKey:(__bridge id)kSecAttrService];
209 | }
210 |
211 | if (self.account) {
212 | [dictionary setObject:self.account forKey:(__bridge id)kSecAttrAccount];
213 | }
214 |
215 | #ifdef SAMKEYCHAIN_ACCESS_GROUP_AVAILABLE
216 | #if !TARGET_IPHONE_SIMULATOR
217 | if (self.accessGroup) {
218 | [dictionary setObject:self.accessGroup forKey:(__bridge id)kSecAttrAccessGroup];
219 | }
220 | #endif
221 | #endif
222 |
223 | #ifdef SAMKEYCHAIN_SYNCHRONIZATION_AVAILABLE
224 | if ([[self class] isSynchronizationAvailable]) {
225 | id value;
226 |
227 | switch (self.synchronizationMode) {
228 | case SAMKeychainQuerySynchronizationModeNo: {
229 | value = @NO;
230 | break;
231 | }
232 | case SAMKeychainQuerySynchronizationModeYes: {
233 | value = @YES;
234 | break;
235 | }
236 | case SAMKeychainQuerySynchronizationModeAny: {
237 | value = (__bridge id)(kSecAttrSynchronizableAny);
238 | break;
239 | }
240 | }
241 |
242 | [dictionary setObject:value forKey:(__bridge id)(kSecAttrSynchronizable)];
243 | }
244 | #endif
245 |
246 | return dictionary;
247 | }
248 |
249 |
250 | + (NSError *)errorWithCode:(OSStatus) code {
251 | static dispatch_once_t onceToken;
252 | static NSBundle *resourcesBundle = nil;
253 | dispatch_once(&onceToken, ^{
254 | NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"SAMKeychain" withExtension:@"bundle"];
255 | resourcesBundle = [NSBundle bundleWithURL:url];
256 | });
257 |
258 | NSString *message = nil;
259 | switch (code) {
260 | case errSecSuccess: return nil;
261 | case SAMKeychainErrorBadArguments: message = NSLocalizedStringFromTableInBundle(@"SAMKeychainErrorBadArguments", @"SAMKeychain", resourcesBundle, nil); break;
262 |
263 | #if TARGET_OS_IPHONE
264 | case errSecUnimplemented: {
265 | message = NSLocalizedStringFromTableInBundle(@"errSecUnimplemented", @"SAMKeychain", resourcesBundle, nil);
266 | break;
267 | }
268 | case errSecParam: {
269 | message = NSLocalizedStringFromTableInBundle(@"errSecParam", @"SAMKeychain", resourcesBundle, nil);
270 | break;
271 | }
272 | case errSecAllocate: {
273 | message = NSLocalizedStringFromTableInBundle(@"errSecAllocate", @"SAMKeychain", resourcesBundle, nil);
274 | break;
275 | }
276 | case errSecNotAvailable: {
277 | message = NSLocalizedStringFromTableInBundle(@"errSecNotAvailable", @"SAMKeychain", resourcesBundle, nil);
278 | break;
279 | }
280 | case errSecDuplicateItem: {
281 | message = NSLocalizedStringFromTableInBundle(@"errSecDuplicateItem", @"SAMKeychain", resourcesBundle, nil);
282 | break;
283 | }
284 | case errSecItemNotFound: {
285 | message = NSLocalizedStringFromTableInBundle(@"errSecItemNotFound", @"SAMKeychain", resourcesBundle, nil);
286 | break;
287 | }
288 | case errSecInteractionNotAllowed: {
289 | message = NSLocalizedStringFromTableInBundle(@"errSecInteractionNotAllowed", @"SAMKeychain", resourcesBundle, nil);
290 | break;
291 | }
292 | case errSecDecode: {
293 | message = NSLocalizedStringFromTableInBundle(@"errSecDecode", @"SAMKeychain", resourcesBundle, nil);
294 | break;
295 | }
296 | case errSecAuthFailed: {
297 | message = NSLocalizedStringFromTableInBundle(@"errSecAuthFailed", @"SAMKeychain", resourcesBundle, nil);
298 | break;
299 | }
300 | default: {
301 | message = NSLocalizedStringFromTableInBundle(@"errSecDefault", @"SAMKeychain", resourcesBundle, nil);
302 | }
303 | #else
304 | default:
305 | message = (__bridge_transfer NSString *)SecCopyErrorMessageString(code, NULL);
306 | #endif
307 | }
308 |
309 | NSDictionary *userInfo = nil;
310 | if (message) {
311 | userInfo = @{ NSLocalizedDescriptionKey : message };
312 | }
313 | return [NSError errorWithDomain:kSAMKeychainErrorDomain code:code userInfo:userInfo];
314 | }
315 |
316 | @end
317 |
--------------------------------------------------------------------------------
/src/ios/SecureStorage.h:
--------------------------------------------------------------------------------
1 | #import
2 |
3 | @interface SecureStorage : CDVPlugin
4 |
5 | - (void)init:(CDVInvokedUrlCommand*)command;
6 | - (void)get:(CDVInvokedUrlCommand*)command;
7 | - (void)set:(CDVInvokedUrlCommand*)command;
8 | - (void)remove:(CDVInvokedUrlCommand*)command;
9 | - (void)keys:(CDVInvokedUrlCommand*)command;
10 | - (void)clear:(CDVInvokedUrlCommand*)command;
11 |
12 | @property (nonatomic, copy) id keychainAccesssibilityMapping;
13 |
14 | @end
15 |
--------------------------------------------------------------------------------
/src/ios/SecureStorage.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 | #import "SecureStorage.h"
4 | #import
5 | #import "SAMKeychain.h"
6 |
7 | @implementation SecureStorage
8 |
9 | - (void)init:(CDVInvokedUrlCommand*)command
10 | {
11 | CFTypeRef accessibility;
12 | NSString *keychainAccessibility;
13 | NSDictionary *keychainAccesssibilityMapping;
14 |
15 | if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0){
16 | keychainAccesssibilityMapping = [NSDictionary dictionaryWithObjectsAndKeys:
17 | (__bridge id)(kSecAttrAccessibleAfterFirstUnlock), @"afterfirstunlock",
18 | (__bridge id)(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), @"afterfirstunlockthisdeviceonly",
19 | (__bridge id)(kSecAttrAccessibleWhenUnlocked), @"whenunlocked",
20 | (__bridge id)(kSecAttrAccessibleWhenUnlockedThisDeviceOnly), @"whenunlockedthisdeviceonly",
21 | (__bridge id)(kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), @"whenpasscodesetthisdeviceonly",
22 | nil];
23 | } else {
24 | keychainAccesssibilityMapping = [NSDictionary dictionaryWithObjectsAndKeys:
25 | (__bridge id)(kSecAttrAccessibleAfterFirstUnlock), @"afterfirstunlock",
26 | (__bridge id)(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), @"afterfirstunlockthisdeviceonly",
27 | (__bridge id)(kSecAttrAccessibleWhenUnlocked), @"whenunlocked",
28 | (__bridge id)(kSecAttrAccessibleWhenUnlockedThisDeviceOnly), @"whenunlockedthisdeviceonly",
29 | nil];
30 | }
31 | keychainAccessibility = [[self.commandDelegate.settings objectForKey:[@"KeychainAccessibility" lowercaseString]] lowercaseString];
32 | if (keychainAccessibility == nil) {
33 | [self successWithMessage: nil : command.callbackId];
34 | } else {
35 | if ([keychainAccesssibilityMapping objectForKey:(keychainAccessibility)] != nil) {
36 | accessibility = (__bridge CFTypeRef)([keychainAccesssibilityMapping objectForKey:(keychainAccessibility)]);
37 | [SAMKeychain setAccessibilityType:accessibility];
38 | [self successWithMessage: nil : command.callbackId];
39 | } else {
40 | [self failWithMessage: @"Unrecognized KeychainAccessibility value in config" : nil : command.callbackId];
41 | }
42 | }
43 | }
44 |
45 | - (void)get:(CDVInvokedUrlCommand*)command
46 | {
47 | NSString *service = [command argumentAtIndex:0];
48 | NSString *key = [command argumentAtIndex:1];
49 | [self.commandDelegate runInBackground:^{
50 | NSError *error;
51 |
52 | SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
53 | query.service = service;
54 | query.account = key;
55 |
56 | if ([query fetch:&error]) {
57 | [self successWithMessage: query.password : command.callbackId];
58 | } else {
59 | [self failWithMessage: @"Failure in SecureStorage.get()" : error : command.callbackId];
60 | }
61 | }];
62 | }
63 |
64 | - (void)set:(CDVInvokedUrlCommand*)command
65 | {
66 | NSString *service = [command argumentAtIndex:0];
67 | NSString *key = [command argumentAtIndex:1];
68 | NSString *value = [command argumentAtIndex:2];
69 | [self.commandDelegate runInBackground:^{
70 | NSError *error;
71 |
72 | SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
73 | query.service = service;
74 | query.account = key;
75 | query.password = value;
76 |
77 | if ([query save:&error]) {
78 | [self successWithMessage: key : command.callbackId];
79 | } else {
80 | [self failWithMessage: @"Failure in SecureStorage.set()" : error : command.callbackId];
81 | }
82 | }];
83 | }
84 |
85 | - (void)remove:(CDVInvokedUrlCommand*)command
86 | {
87 | NSString *service = [command argumentAtIndex:0];
88 | NSString *key = [command argumentAtIndex:1];
89 | [self.commandDelegate runInBackground:^{
90 | NSError *error;
91 |
92 | SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
93 | query.service = service;
94 | query.account = key;
95 |
96 | if ([query deleteItem:&error]) {
97 | [self successWithMessage: key : command.callbackId];
98 | } else {
99 | [self failWithMessage: @"Failure in SecureStorage.remove()" : error : command.callbackId];
100 | }
101 | }];
102 | }
103 |
104 | - (void)keys:(CDVInvokedUrlCommand*)command
105 | {
106 | NSString *service = [command argumentAtIndex:0];
107 | [self.commandDelegate runInBackground:^{
108 | NSError *error;
109 |
110 | SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
111 | query.service = service;
112 |
113 | NSArray *accounts = [query fetchAll:&error];
114 | if (accounts) {
115 | NSMutableArray *array = [NSMutableArray arrayWithCapacity:[accounts count]];
116 | for (id dict in accounts) {
117 | [array addObject:[dict valueForKeyPath:@"acct"]];
118 | }
119 |
120 | CDVPluginResult *commandResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:array];
121 | [self.commandDelegate sendPluginResult:commandResult callbackId:command.callbackId];
122 | } else if ([error code] == errSecItemNotFound) {
123 | CDVPluginResult *commandResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsArray:[NSArray array]];
124 | [self.commandDelegate sendPluginResult:commandResult callbackId:command.callbackId];
125 | } else {
126 | [self failWithMessage: @"Failure in SecureStorage.keys()" : error : command.callbackId];
127 | }
128 | }];
129 | }
130 |
131 | - (void)clear:(CDVInvokedUrlCommand*)command
132 | {
133 | NSString *service = [command argumentAtIndex:0];
134 | [self.commandDelegate runInBackground:^{
135 | NSError *error;
136 |
137 | SAMKeychainQuery *query = [[SAMKeychainQuery alloc] init];
138 | query.service = service;
139 |
140 | NSArray *accounts = [query fetchAll:&error];
141 | if (accounts) {
142 | for (id dict in accounts) {
143 | query.account = [dict valueForKeyPath:@"acct"];
144 | if (![query deleteItem:&error]) {
145 | break;
146 | }
147 | }
148 |
149 | if (!error) {
150 | [self successWithMessage: nil : command.callbackId];
151 | } else {
152 | [self failWithMessage: @"Failure in SecureStorage.clear()" : error : command.callbackId];
153 | }
154 |
155 | } else if ([error code] == errSecItemNotFound) {
156 | [self successWithMessage: nil : command.callbackId];
157 | } else {
158 | [self failWithMessage: @"Failure in SecureStorage.clear()" : error : command.callbackId];
159 | }
160 |
161 | }];
162 | }
163 |
164 | -(void)successWithMessage:(NSString *)message : (NSString *)callbackId
165 | {
166 | CDVPluginResult *commandResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:message];
167 | [self.commandDelegate sendPluginResult:commandResult callbackId:callbackId];
168 | }
169 |
170 | -(void)failWithMessage:(NSString *)message : (NSError *)error : (NSString *)callbackId
171 | {
172 | NSString *errorMessage = (error) ? [NSString stringWithFormat:@"%@ - %@", message, [error localizedDescription]] : message;
173 | CDVPluginResult *commandResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:errorMessage];
174 |
175 | [self.commandDelegate sendPluginResult:commandResult callbackId:callbackId];
176 | }
177 |
178 | @end
179 |
--------------------------------------------------------------------------------
/src/windows/SecureStorage.js:
--------------------------------------------------------------------------------
1 | var SecureStorageProxy = {
2 |
3 | init: function (win, fail, args) {
4 | setTimeout(win, 0);
5 | },
6 |
7 | get: function (win, fail, args) {
8 | try {
9 | var service = args[0];
10 | var key = args[1];
11 |
12 | var vault = new Windows.Security.Credentials.PasswordVault();
13 | var passwordCredential = vault.retrieve(service, key);
14 |
15 | win(passwordCredential.password);
16 | } catch (e) {
17 | fail('Failure in SecureStorage.get() - ' + e.message);
18 | }
19 | },
20 |
21 | set: function (win, fail, args) {
22 | try {
23 | var service = args[0];
24 | var key = args[1];
25 | var value = args[2];
26 |
27 | // Remarks: you can only store up to ten credentials per app in the Credential Locker.
28 | // If you try to store more than ten credentials, you will encounter an Exception.
29 | // https://msdn.microsoft.com/en-us/library/windows/apps/hh701231.aspx
30 |
31 | var vault = new Windows.Security.Credentials.PasswordVault();
32 | vault.add(new Windows.Security.Credentials.PasswordCredential(
33 | service, key, value));
34 |
35 | win(key);
36 | } catch (e) {
37 | fail('Failure in SecureStorage.set() - ' + e.message);
38 | }
39 | },
40 |
41 | remove: function (win, fail, args) {
42 | try {
43 | var service = args[0];
44 | var key = args[1];
45 |
46 | var vault = new Windows.Security.Credentials.PasswordVault();
47 | var passwordCredential = vault.retrieve(service, key);
48 |
49 | if (passwordCredential) {
50 | vault.remove(passwordCredential);
51 | }
52 |
53 | win(key);
54 | } catch (e) {
55 | fail('Failure in SecureStorage.remove() - ' + e.message);
56 | }
57 | },
58 |
59 | keys: function (win, fail, args) {
60 | try {
61 | var service = args[0];
62 | var vault = new Windows.Security.Credentials.PasswordVault();
63 | var passwordCredentials;
64 |
65 | try {
66 | passwordCredentials = vault.findAllByResource(service);
67 | } catch (e) {
68 | passwordCredentials = [];
69 | }
70 | passwordCredentials = passwordCredentials.map(function (passwordCredential) {
71 | return passwordCredential.userName;
72 | });
73 |
74 | win(passwordCredentials);
75 | } catch (e) {
76 | fail('Failure in SecureStorage.keys() - ' + e.message);
77 | }
78 | },
79 |
80 | clear: function (win, fail, args) {
81 | try {
82 | var service = args[0];
83 | var vault = new Windows.Security.Credentials.PasswordVault();
84 | var passwordCredentials;
85 |
86 | try {
87 | passwordCredentials = vault.findAllByResource(service);
88 | } catch (e) {
89 | passwordCredentials = [];
90 | }
91 | passwordCredentials.forEach(function (passwordCredential) {
92 | vault.remove(passwordCredential);
93 | });
94 |
95 | win();
96 | } catch (e) {
97 | fail('Failure in SecureStorage.clear() - ' + e.message);
98 | }
99 | },
100 | };
101 |
102 | require("cordova/exec/proxy").add("SecureStorage", SecureStorageProxy);
103 |
--------------------------------------------------------------------------------
/tests/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cordova-plugin-secure-storage-tests",
3 | "version": "1.0.0",
4 | "description": "Tests for cordova-plugin-secure-storage",
5 | "author": "Yiorgis Gozadinos ",
6 | "cordova": {
7 | "id": "cordova-plugin-secure-storage-tests",
8 | "platforms": [
9 | "android",
10 | "ios",
11 | "wp8",
12 | "windows"
13 | ]
14 | },
15 | "repository": {
16 | "type": "git",
17 | "url": "git+https://github.com/crypho/cordova-plugin-secure-storage.git"
18 | },
19 | "scripts": {
20 | "eslint": "./node_modules/.bin/eslint www/securestorage.js tests/tests.js"
21 | },
22 | "devDependencies": {
23 | "cordova-plugin-test-framework": ">=1.0.0",
24 | "eslint": ""
25 | },
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/crypho/cordova-plugin-secure-storage/issues"
29 | },
30 | "homepage": "https://github.com/crypho/cordova-plugin-secure-storage#readme"
31 | }
32 |
--------------------------------------------------------------------------------
/tests/plugin.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 | SecureStorage Tests
8 | Crypho AS
9 |
10 |
11 | Test for cordova-plugin-secure-storage
12 |
13 |
14 | MIT
15 |
16 | security, encryption, tests
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tests/tests.js:
--------------------------------------------------------------------------------
1 | var SERVICE = 'testing';
2 | var SERVICE_2 = 'testing_2';
3 |
4 | exports.defineAutoTests = function() {
5 | var ss, ss_2, handlers;
6 |
7 | if(cordova.platformId === 'android' && parseFloat(device.version) <= 4.3){
8 | describe('cordova-plugin-secure-storage-android-unsupported', function () {
9 | beforeEach(function () {
10 | handlers = {
11 | successHandler: function () {},
12 | errorHandler: function () {}
13 | };
14 | });
15 |
16 | it('should call the error handler when attempting to use the plugin on Android 4.3 or below', function (done) {
17 | spyOn(handlers, 'errorHandler').and.callFake(function (res) {
18 | expect(res).toEqual(jasmine.any(Error));
19 | expect(handlers.successHandler).not.toHaveBeenCalled();
20 | done();
21 | });
22 | spyOn(handlers, 'successHandler');
23 |
24 | ss = new cordova.plugins.SecureStorage(function () {
25 | ss.set(function () {
26 | ss.get(handlers.successHandler, handlers.errorHandler, 'foo');
27 | }, function () {}, 'foo', 'foo');
28 | }, handlers.errorHandler, SERVICE);
29 | });
30 | });
31 | return; // skip all other tests
32 | }
33 |
34 | describe('cordova-plugin-secure-storage', function () {
35 |
36 | beforeEach(function () {
37 | handlers = {
38 | successHandler: function () {},
39 | errorHandler: function () {},
40 | successHandler_2: function () {},
41 | errorHandler_2: function () {}
42 | };
43 | });
44 |
45 | afterEach(function (done) {
46 | ss = new cordova.plugins.SecureStorage(function () {
47 | ss.clear(function () {
48 | ss_2 = new cordova.plugins.SecureStorage(function () {
49 | ss_2.clear(done, handlers.errorHandler_2);
50 | }, handlers.errorHandler_2, SERVICE_2);
51 | }, handlers.errorHandler);
52 | }, handlers.errorHandler, SERVICE);
53 | });
54 |
55 | it('should be defined', function() {
56 | expect(cordova.plugins.SecureStorage).toBeDefined();
57 | });
58 |
59 | it('should be able to initialize', function (done) {
60 | spyOn(handlers, 'successHandler').and.callFake(function () {
61 | expect(ss.service).toEqual(SERVICE);
62 | expect(handlers.errorHandler).not.toHaveBeenCalled();
63 | done();
64 | });
65 | spyOn(handlers, 'errorHandler');
66 |
67 | ss = new cordova.plugins.SecureStorage(handlers.successHandler, handlers.errorHandler, SERVICE);
68 | });
69 |
70 | it('should be able to set a key/value', function (done) {
71 | spyOn(handlers, 'successHandler').and.callFake(function (res) {
72 | expect(res).toEqual('foo');
73 | expect(handlers.errorHandler).not.toHaveBeenCalled();
74 | done();
75 | });
76 | spyOn(handlers, 'errorHandler');
77 |
78 | ss = new cordova.plugins.SecureStorage(function () {
79 | ss.set(handlers.successHandler, handlers.errorHandler, 'foo', 'bar');
80 | }, handlers.errorHandler, SERVICE);
81 | });
82 |
83 | it('should be able to get a key/value', function (done) {
84 | spyOn(handlers, 'successHandler').and.callFake(function (res) {
85 | expect(res).toEqual('bar');
86 | expect(handlers.errorHandler).not.toHaveBeenCalled();
87 | done();
88 | });
89 | spyOn(handlers, 'errorHandler');
90 |
91 | ss = new cordova.plugins.SecureStorage(function () {
92 | ss.set(function () {
93 | ss.get(handlers.successHandler, handlers.errorHandler, 'foo');
94 | }, handlers.errorHandler, 'foo', 'bar');
95 | }, handlers.errorHandler, SERVICE);
96 | });
97 |
98 | it('should be able to get a key/value that was set from a previous instance of SecureStorage', function (done) {
99 | ss = new cordova.plugins.SecureStorage(function () {
100 | ss.set(function () {
101 | spyOn(handlers, 'successHandler').and.callFake(function (res) {
102 | expect(res).toEqual('bar');
103 | expect(handlers.errorHandler).not.toHaveBeenCalled();
104 | done();
105 | });
106 | spyOn(handlers, 'errorHandler');
107 |
108 | ss = new cordova.plugins.SecureStorage(function () {
109 | ss.get(handlers.successHandler, handlers.errorHandler, 'foo');
110 | }, handlers.errorHandler, SERVICE);
111 | }, handlers.errorHandler, 'foo', 'bar');
112 | }, handlers.errorHandler, SERVICE);
113 |
114 | });
115 |
116 | it('should call the error handler when getting a key that does not exist', function (done) {
117 | spyOn(handlers, 'errorHandler').and.callFake(function (res) {
118 | expect(res).toEqual(jasmine.any(Error));
119 | expect(handlers.successHandler).not.toHaveBeenCalled();
120 | done();
121 | });
122 | spyOn(handlers, 'successHandler');
123 |
124 | ss = new cordova.plugins.SecureStorage(function () {
125 | ss.get(handlers.successHandler, handlers.errorHandler, 'nofoo');
126 | }, handlers.errorHandler, SERVICE);
127 | });
128 |
129 | it('should call the error handler when getting a key that existed but got deleted', function (done) {
130 | spyOn(handlers, 'errorHandler').and.callFake(function (res) {
131 | expect(res).toEqual(jasmine.any(Error));
132 | expect(handlers.successHandler).not.toHaveBeenCalled();
133 | done();
134 | });
135 | spyOn(handlers, 'successHandler');
136 |
137 | ss = new cordova.plugins.SecureStorage(function () {
138 |
139 | ss.set(function () {
140 | ss.remove(function () {
141 | ss.get(handlers.successHandler, handlers.errorHandler, 'test');
142 | }, function () {}, 'test');
143 | }, function () {}, 'test', 'bar');
144 |
145 | }, handlers.errorHandler, SERVICE);
146 | });
147 |
148 | it('should call the error handler when setting a value that is not a string', function (done) {
149 | spyOn(handlers, 'errorHandler').and.callFake(function (res) {
150 | expect(res).toEqual(jasmine.any(Error));
151 | expect(handlers.successHandler).not.toHaveBeenCalled();
152 | done();
153 | });
154 | spyOn(handlers, 'successHandler');
155 |
156 | ss = new cordova.plugins.SecureStorage(function () {
157 | ss.get(handlers.successHandler, handlers.errorHandler, {'foo': 'bar'});
158 | }, handlers.errorHandler, SERVICE);
159 | });
160 |
161 |
162 | it('should be able to remove a key/value', function (done) {
163 | spyOn(handlers, 'successHandler').and.callFake(function (res) {
164 | expect(res).toEqual('foo');
165 | expect(handlers.errorHandler).not.toHaveBeenCalled();
166 | done();
167 | });
168 | spyOn(handlers, 'errorHandler');
169 |
170 | ss = new cordova.plugins.SecureStorage(function () {
171 | ss.set(function () {
172 | ss.remove(handlers.successHandler, handlers.errorHandler, 'foo');
173 | }, handlers.errorHandler, 'foo', 'bar');
174 | }, handlers.errorHandler, SERVICE);
175 | });
176 |
177 | it('should be able to set and retrieve multiple values in sequence', function (done) {
178 | var results = [];
179 | spyOn(handlers, 'successHandler').and.callFake(function (res) {
180 | results.push(res);
181 | expect(res === 'foo' || res === 'bar').toBe(true);
182 | if (results.indexOf('foo') !== -1 && results.indexOf('bar') !== -1) {
183 | done();
184 | }
185 | });
186 | ss = new cordova.plugins.SecureStorage(function () {
187 | ss.set(function () {
188 | ss.set(function () {
189 | ss.get(handlers.successHandler, handlers.errorHandler, 'foo');
190 | ss.get(handlers.successHandler, handlers.errorHandler, 'bar');
191 | }, function () {}, 'bar', 'bar');
192 | }, function () {}, 'foo', 'foo');
193 | }, handlers.errorHandler, SERVICE);
194 | });
195 |
196 | it('should be able to get the storage keys as an array', function (done){
197 | spyOn(handlers, 'successHandler').and.callFake(function (res) {
198 | expect(res).toEqual(jasmine.any(Array));
199 | expect(res.indexOf('foo') >= 0).toBeTruthy();
200 | expect(handlers.errorHandler).not.toHaveBeenCalled();
201 | done();
202 | });
203 | spyOn(handlers, 'errorHandler');
204 |
205 | ss = new cordova.plugins.SecureStorage(function () {
206 | ss.set(function () {
207 | ss.keys(handlers.successHandler, handlers.errorHandler);
208 | }, done.fail, 'foo', 'foo');
209 | }, handlers.errorHandler, SERVICE);
210 | });
211 |
212 | it('should be able to clear the storage', function (done){
213 | spyOn(handlers, 'successHandler').and.callFake(function () {
214 | expect(handlers.errorHandler).not.toHaveBeenCalled();
215 | ss.keys(function(key) {
216 | expect(key).toEqual(jasmine.any(Array));
217 | expect(key.length).toBe(0);
218 | done();
219 | }, done.fail);
220 | });
221 | spyOn(handlers, 'errorHandler');
222 |
223 | ss = new cordova.plugins.SecureStorage(function () {
224 | ss.set(function () {
225 | ss.clear(handlers.successHandler, handlers.errorHandler);
226 | }, done.fail, 'foo', 'foo');
227 | }, handlers.errorHandler, SERVICE);
228 | });
229 |
230 | it('should return [] when keys is called and the storage is empty', function (done){
231 | spyOn(handlers, 'successHandler').and.callFake(function (res) {
232 | expect(res).toEqual(jasmine.any(Array));
233 | expect(res.length).toBe(0);
234 | expect(handlers.errorHandler).not.toHaveBeenCalled();
235 | done();
236 | });
237 | spyOn(handlers, 'errorHandler');
238 |
239 | ss = new cordova.plugins.SecureStorage(function () {
240 | ss.clear(function () {
241 | ss.keys(handlers.successHandler, handlers.errorHandler);
242 | }, handlers.errorHandler);
243 | }, handlers.errorHandler, SERVICE);
244 | });
245 |
246 | it('should be able to clear without error when the storage is empty', function (done){
247 | spyOn(handlers, 'successHandler').and.callFake(function () {
248 | expect(handlers.errorHandler).not.toHaveBeenCalled();
249 | ss.keys(function(key) {
250 | expect(key).toEqual(jasmine.any(Array));
251 | expect(key.length).toBe(0);
252 | done();
253 | }, done.fail);
254 | });
255 | spyOn(handlers, 'errorHandler');
256 |
257 | ss = new cordova.plugins.SecureStorage(function () {
258 | ss.set(function () {
259 | ss.clear(handlers.successHandler, handlers.errorHandler);
260 | }, function () {}, 'foo', 'foo');
261 | }, handlers.errorHandler, SERVICE);
262 | });
263 |
264 | it('should be able to handle simultaneous sets and gets', function (done) {
265 | var count = 0,
266 | total = 5,
267 | decrypt_success, encrypt_success,
268 | i, values = {};
269 |
270 | spyOn(handlers, 'errorHandler');
271 |
272 | for (i=0 ; i < total ; i++) {
273 | values[i] = i.toString();
274 | }
275 |
276 | decrypt_success = function () {
277 | count ++;
278 | if (count === total) {
279 | expect(handlers.errorHandler).not.toHaveBeenCalled();
280 | done();
281 | }
282 | };
283 |
284 | function decrypt() {
285 | count = 0;
286 | for (i = 0 ; i < total; i++) {
287 | ss.get(
288 | decrypt_success,
289 | handlers.errorHandler,
290 | i.toString()
291 | );
292 | }
293 | }
294 |
295 | encrypt_success = function () {
296 | count ++;
297 | if (count === total) {
298 | decrypt();
299 | }
300 | };
301 |
302 | function encrypt() {
303 | count = 0;
304 | for (i = 0 ; i < total; i++) {
305 | ss.set(
306 | encrypt_success,
307 | handlers.errorHandler,
308 | i.toString(), values[i]
309 | );
310 | }
311 | }
312 |
313 | ss = new cordova.plugins.SecureStorage(function () {
314 | encrypt();
315 | }, handlers.errorHandler, SERVICE);
316 |
317 | });
318 |
319 | it('should not be able to get a key/value that was set from a another instance of SecureStorage', function (done) {
320 | ss = new cordova.plugins.SecureStorage(function () {
321 | ss.set(function () {
322 | ss_2 = new cordova.plugins.SecureStorage(function () {
323 | spyOn(handlers, 'errorHandler').and.callFake(function (res) {
324 | expect(handlers.successHandler).not.toHaveBeenCalled();
325 | done();
326 | });
327 | spyOn(handlers, 'successHandler');
328 | ss_2.get(handlers.successHandler, handlers.errorHandler, 'foo');
329 | },
330 | handlers.errorHandler,
331 | SERVICE_2);
332 | },
333 | handlers.errorHandler,
334 | 'foo',
335 | 'bar');
336 | },
337 | handlers.errorHandler,
338 | SERVICE);
339 | });
340 |
341 | it('different instances should be able to get different values for the same key', function (done) {
342 | ss = new cordova.plugins.SecureStorage(function () {
343 | ss.set(function () {
344 | ss_2 = new cordova.plugins.SecureStorage(function () {
345 | ss_2.set(function () {
346 | spyOn(handlers, 'successHandler').and.callFake(function (res) {
347 | expect(res).toEqual('bar');
348 | expect(handlers.errorHandler).not.toHaveBeenCalled();
349 |
350 | spyOn(handlers, 'successHandler_2').and.callFake(function (res) {
351 | expect(res).toEqual('bar_2');
352 | expect(handlers.errorHandler_2).not.toHaveBeenCalled();
353 | done();
354 | });
355 | spyOn(handlers, 'errorHandler_2');
356 | ss_2.get(handlers.successHandler_2, handlers.errorHandler_2, 'foo');
357 | });
358 | spyOn(handlers, 'errorHandler');
359 | ss.get(handlers.successHandler, handlers.errorHandler, 'foo');
360 | },
361 | handlers.errorHandler_2,
362 | 'foo',
363 | 'bar_2');
364 | },
365 | handlers.errorHandler_2,
366 | SERVICE_2);
367 | },
368 | handlers.errorHandler,
369 | 'foo',
370 | 'bar');
371 | },
372 | handlers.errorHandler,
373 | SERVICE);
374 | });
375 |
376 | });
377 | };
378 |
379 | exports.defineManualTests = function(contentEl, createActionButton) {
380 | var ss;
381 | if (cordova.platformId === 'android') {
382 | createActionButton('Init tests for android', function() {
383 | alert('You should run these tests twice. Once without screen locking, and once with screen locking set to PIN. When lock is disabled you should be prompted to set it.');
384 | ss = new cordova.plugins.SecureStorage(
385 | function () {
386 | alert('Init successfull.');
387 | },
388 | function () {
389 | alert('Init failed. The screen lock settings should now open. Set PIN or above.');
390 | ss.secureDevice(
391 | function () {
392 | alert('Device is secure.');
393 | },
394 | function () {
395 | alert('Device is not secure.');
396 | }
397 | );
398 | }, SERVICE);
399 | });
400 | }
401 | };
402 |
--------------------------------------------------------------------------------
/www/securestorage.js:
--------------------------------------------------------------------------------
1 | var SecureStorage;
2 |
3 | var SUPPORTED_PLATFORMS = ['android', 'ios', 'windows'];
4 |
5 | var _checkCallbacks = function (success, error) {
6 | if (typeof success != 'function') {
7 | throw new Error('SecureStorage failure: success callback parameter must be a function');
8 | }
9 | if (typeof error != 'function') {
10 | throw new Error('SecureStorage failure: error callback parameter must be a function');
11 | }
12 | };
13 |
14 | //Taken from undescore.js
15 | var _isString = function isString(x) {
16 | return Object.prototype.toString.call(x) === '[object String]';
17 | };
18 |
19 | /**
20 | * Helper method to execute Cordova native method
21 | *
22 | * @param {String} nativeMethodName Method to execute.
23 | * @param {Array} args Execution arguments.
24 | * @param {Function} success Called when returning successful result from an action.
25 | * @param {Function} error Called when returning error result from an action.
26 | *
27 | */
28 | var _executeNativeMethod = function (success, error, nativeMethodName, args) {
29 | var fail;
30 | // args checking
31 | _checkCallbacks(success, error);
32 |
33 | // By convention a failure callback should always receive an instance
34 | // of a JavaScript Error object.
35 | fail = function(err) {
36 | // provide default message if no details passed to callback
37 | if (typeof err === 'undefined') {
38 | error(new Error('Error occured while executing native method.'));
39 | } else {
40 | // wrap string to Error instance if necessary
41 | error(_isString(err) ? new Error(err) : err);
42 | }
43 | };
44 |
45 | cordova.exec(success, fail, 'SecureStorage', nativeMethodName, args);
46 | };
47 |
48 | SecureStorage = function (success, error, service, options) {
49 | var platformId = cordova.platformId;
50 | var opts = options && options[platformId] ? options[platformId] : {};
51 |
52 | this.service = service;
53 |
54 | try {
55 | _executeNativeMethod(success, error, 'init', [this.service, opts]);
56 | } catch (e) {
57 | error(e);
58 | }
59 | return this;
60 | };
61 |
62 | SecureStorage.prototype = {
63 | get: function (success, error, key) {
64 | try {
65 | if (!_isString(key)) {
66 | throw new Error('Key must be a string');
67 | }
68 | _executeNativeMethod(success, error, 'get', [this.service, key]);
69 | } catch (e) {
70 | error(e);
71 | }
72 | },
73 |
74 | set: function (success, error, key, value) {
75 | try {
76 | if (!_isString(value)) {
77 | throw new Error('Value must be a string');
78 | }
79 | _executeNativeMethod(success, error, 'set', [this.service, key, value]);
80 | } catch (e) {
81 | error(e);
82 | }
83 | },
84 |
85 | remove: function (success, error, key) {
86 | try {
87 | if (!_isString(key)) {
88 | throw new Error('Key must be a string');
89 | }
90 | _executeNativeMethod(success, error, 'remove', [this.service, key]);
91 | } catch (e) {
92 | error(e);
93 | }
94 | },
95 |
96 | keys: function (success, error) {
97 | try {
98 | _executeNativeMethod(success, error, 'keys', [this.service]);
99 | } catch (e) {
100 | error(e);
101 | }
102 | },
103 |
104 | clear: function (success, error) {
105 | try {
106 | _executeNativeMethod(success, error, 'clear', [this.service]);
107 | } catch (e) {
108 | error(e);
109 | }
110 | }
111 | };
112 |
113 | if (cordova.platformId === 'android') {
114 | SecureStorage.prototype.secureDevice = function (success, error) {
115 | try {
116 | _executeNativeMethod(success, error, 'secureDevice', []);
117 | } catch (e) {
118 | error(e);
119 | }
120 | }
121 | }
122 |
123 | if (!cordova.plugins) {
124 | cordova.plugins = {};
125 | }
126 |
127 | if (!cordova.plugins.SecureStorage) {
128 | cordova.plugins.SecureStorage = SecureStorage;
129 | }
130 |
131 | if (typeof module !== 'undefined' && module.exports) {
132 | module.exports = SecureStorage;
133 | }
134 |
--------------------------------------------------------------------------------