├── .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 | [![NPM](https://nodei.co/npm/cordova-plugin-secure-storage.png?downloads=true&downloadRank=true&stars=true)](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 | --------------------------------------------------------------------------------