├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── deploy.yaml │ ├── e2e_tests.yaml │ └── lint_and_types.yaml ├── .gitignore ├── .markdownlint.json ├── .nvmrc ├── .prettierrc ├── .yarnrc.yml ├── KeychainExample ├── .detoxrc.js ├── .watchmanconfig ├── App.tsx ├── Gemfile ├── Gemfile.lock ├── android │ ├── app │ │ └── src │ │ │ └── androidTest │ │ │ └── java │ │ │ └── com │ │ │ └── microsoft │ │ │ └── reacttestapp │ │ │ └── DetoxTest.java │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── app.json ├── babel.config.js ├── e2e │ ├── jest.config.js │ ├── testCases │ │ ├── biometricsAccessControlTest.spec.js │ │ └── noneAccessControTest.spec.js │ └── utils │ │ └── enrollFingerprintAndroid.sh ├── index.js ├── ios │ ├── .xcode.env │ ├── KeychainExample.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Podfile │ └── Podfile.lock ├── jest.config.js ├── metro.config.js ├── package.json ├── react-native.config.js └── tsconfig.json ├── LICENSE ├── README.md ├── RNKeychainManager.podspec ├── android ├── .npmignore ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── dorianmazur │ └── keychain │ ├── DataStorePrefsStorage.kt │ ├── DeviceAvailability.java │ ├── KeychainModule.java │ ├── KeychainModuleBuilder.java │ ├── KeychainPackage.java │ ├── PrefsStorage.java │ ├── SecurityLevel.java │ ├── cipherStorage │ ├── CipherStorage.java │ ├── CipherStorageBase.java │ ├── CipherStorageFacebookConceal.java │ ├── CipherStorageKeystoreAesCbc.java │ └── CipherStorageKeystoreRsaEcb.java │ ├── decryptionHandler │ ├── DecryptionResultHandler.java │ ├── DecryptionResultHandlerInteractiveBiometric.java │ ├── DecryptionResultHandlerInteractiveBiometricManualRetry.java │ ├── DecryptionResultHandlerNonInteractive.java │ └── DecryptionResultHandlerProvider.java │ └── exceptions │ ├── CryptoFailedException.java │ ├── EmptyParameterException.java │ └── KeyStoreAccessException.java ├── babel.config.js ├── ios ├── RNKeychainManager.xcodeproj │ └── project.pbxproj └── RNKeychainManager │ ├── RNKeychainManager.h │ └── RNKeychainManager.m ├── package.json ├── react-native.config.js ├── src └── index.ts ├── tsconfig.build.json ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | max_line_length = 100 15 | 16 | 17 | [*.java] 18 | max_line_length = 120 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /**/*.d.ts 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['plugin:@typescript-eslint/recommended', '@react-native'], 3 | root: true, 4 | parser: '@typescript-eslint/parser', 5 | plugins: ['@typescript-eslint'], 6 | rules: { 7 | 'prettier/prettier': 'error', 8 | '@typescript-eslint/no-var-requires': 'off', 9 | 'comma-dangle': 'off', // prettier already detects this 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | npm: 9 | name: NPM 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version-file: .nvmrc 18 | registry-url: 'https://registry.npmjs.org' 19 | - name: Enable corepack 20 | run: corepack enable 21 | - name: Install dependencies 22 | run: yarn install --immutable 23 | - name: Prepare for release 24 | run: yarn prepare 25 | - name: Publish to NPM 26 | run: npm publish 27 | env: 28 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 29 | -------------------------------------------------------------------------------- /.github/workflows/e2e_tests.yaml: -------------------------------------------------------------------------------- 1 | name: 'E2E Tests' 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - 'package.json' 9 | pull_request: 10 | branches: 11 | - main 12 | paths-ignore: 13 | - 'package.json' 14 | 15 | jobs: 16 | build-android: 17 | name: Build Android 18 | runs-on: ubuntu-latest 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v4 25 | with: 26 | node-version-file: .nvmrc 27 | 28 | - name: Enable corepack 29 | run: corepack enable 30 | 31 | - name: Install dependencies 32 | run: yarn install --immutable 33 | 34 | - name: Setup java 35 | uses: actions/setup-java@v4 36 | with: 37 | distribution: 'temurin' # See 'Supported distributions' for available options 38 | java-version: '19' 39 | 40 | - name: Cache Gradle 41 | uses: actions/cache@v4 42 | with: 43 | path: | 44 | ~/.gradle/wrapper 45 | ~/.gradle/caches 46 | key: ${{ runner.os }}-gradle-${{ hashFiles('KeychainExample/android/gradle/wrapper/gradle-wrapper.properties') }} 47 | restore-keys: | 48 | ${{ runner.os }}-gradle- 49 | 50 | - name: Downloading Gradle Dependencies 51 | run: | 52 | cd KeychainExample/android 53 | chmod +x gradlew 54 | ./gradlew --max-workers 2 dependencies 55 | 56 | - name: Build Android Release 57 | env: 58 | JAVA_OPTS: '-XX:MaxHeapSize=6g' 59 | working-directory: ./KeychainExample 60 | run: | 61 | yarn build:android 62 | yarn test:android:build 63 | 64 | - name: Upload Test APKs 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: test-apk 68 | path: KeychainExample/android/app/build/outputs/apk 69 | retention-days: 1 70 | 71 | test-android: 72 | runs-on: ubuntu-latest 73 | needs: build-android 74 | strategy: 75 | fail-fast: false 76 | matrix: 77 | api-level: 78 | - 31 79 | - 32 80 | - 33 81 | 82 | steps: 83 | - name: Enable KVM group perms 84 | run: | 85 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 86 | sudo udevadm control --reload-rules 87 | sudo udevadm trigger --name-match=kvm 88 | ls /dev/kvm 89 | 90 | - uses: actions/checkout@v4 91 | 92 | - name: Setup Node.js 93 | uses: actions/setup-node@v4 94 | with: 95 | node-version-file: .nvmrc 96 | 97 | - name: Enable corepack 98 | run: corepack enable 99 | 100 | - name: Install dependencies 101 | run: yarn install --immutable 102 | 103 | - name: Setup java 104 | uses: actions/setup-java@v4 105 | with: 106 | distribution: 'temurin' # See 'Supported distributions' for available options 107 | java-version: '19' 108 | 109 | - uses: actions/download-artifact@v4 110 | with: 111 | name: test-apk 112 | path: KeychainExample/android/app/build/outputs/apk 113 | 114 | - name: Display structure of downloaded files 115 | run: ls -R KeychainExample/android/app/build/outputs/apk 116 | 117 | - name: Run Tests 118 | uses: reactivecircus/android-emulator-runner@v2 119 | with: 120 | api-level: ${{ matrix.api-level }} 121 | arch: x86_64 122 | profile: pixel_6 123 | disable-animations: true 124 | avd-name: TestingAVD 125 | script: | 126 | cd KeychainExample 127 | yarn test:android:run 128 | env: 129 | API_LEVEL: ${{ matrix.api-level }} 130 | - name: Upload test artifacts 131 | if: ${{ failure() }} 132 | uses: actions/upload-artifact@v4 133 | with: 134 | name: android-test-artifacts 135 | path: KeychainExample/artifacts 136 | 137 | build-ios: 138 | name: Build and Test iOS 139 | runs-on: macos-13 140 | 141 | steps: 142 | - name: Checkout 143 | uses: actions/checkout@v4 144 | 145 | - name: Setup Node.js 146 | uses: actions/setup-node@v4 147 | with: 148 | node-version-file: .nvmrc 149 | 150 | - name: Enable corepack 151 | run: corepack enable 152 | - name: Install dependencies 153 | run: yarn install --immutable 154 | 155 | - name: setup-cocoapods 156 | uses: maxim-lobanov/setup-cocoapods@v1 157 | with: 158 | podfile-path: KeychainExample/ios/Podfile.lock 159 | 160 | - name: Cache cocoapods 161 | id: cocoapods-cache 162 | uses: actions/cache@v4 163 | with: 164 | path: | 165 | **/ios/Pods 166 | **/vendor/bundle 167 | key: ${{ runner.os }}-cocoapods-${{ hashFiles('KeychainExample/ios/Podfile.lock') }} 168 | restore-keys: | 169 | ${{ runner.os }}-cocoapods- 170 | 171 | - name: Install Bundle 172 | if: steps.cocoapods-cache.outputs.cache-hit != 'true' 173 | run: | 174 | cd KeychainExample 175 | bundle install 176 | 177 | - name: Install Detox dependencies 178 | run: | 179 | cd KeychainExample 180 | brew tap wix/brew 181 | brew install applesimutils 182 | yarn detox clean-framework-cache 183 | yarn detox build-framework-cache 184 | 185 | - name: Xcode Version 186 | run: | 187 | xcodebuild -version 188 | xcrun simctl list 189 | 190 | - name: Build iOS Release 191 | working-directory: ./KeychainExample 192 | run: | 193 | yarn build:ios 194 | cd ios && pod install && cd - 195 | yarn test:ios:build 196 | env: 197 | RCT_NEW_ARCH_ENABLED: 0 198 | USE_HERMES: 1 199 | 200 | - name: Test iOS Release 201 | run: | 202 | cd KeychainExample 203 | yarn test:ios:run 204 | 205 | - name: Upload test artifacts 206 | if: ${{ failure() }} 207 | uses: actions/upload-artifact@v4 208 | with: 209 | name: ios-test-artifacts 210 | path: KeychainExample/artifacts 211 | -------------------------------------------------------------------------------- /.github/workflows/lint_and_types.yaml: -------------------------------------------------------------------------------- 1 | name: Lint and types 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | name: Lint 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version-file: .nvmrc 22 | - name: Enable corepack 23 | run: corepack enable 24 | - name: Install dependencies 25 | run: yarn install --immutable 26 | - name: Lint files 27 | run: yarn lint 28 | typescript: 29 | name: Typescript 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - uses: actions/checkout@v4 34 | - name: Setup Node.js 35 | uses: actions/setup-node@v4 36 | with: 37 | node-version-file: .nvmrc 38 | - name: Enable corepack 39 | run: corepack enable 40 | - name: Install dependencies 41 | run: yarn install --immutable 42 | - name: Check flow types 43 | run: yarn typecheck 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IntelliJ 26 | # 27 | build/ 28 | .idea 29 | .gradle 30 | local.properties 31 | *.iml 32 | 33 | # Vscode / Eclipse 34 | .settings/ 35 | .project 36 | .classpath 37 | .vscode/ 38 | 39 | # node.js 40 | # 41 | node_modules/ 42 | npm-debug.log 43 | yarn-error.log 44 | 45 | # BUCK 46 | buck-out/ 47 | \.buckd/ 48 | *.keystore 49 | !debug.keystore 50 | 51 | 52 | # Bundle artifact 53 | *.jsbundle 54 | 55 | # CocoaPods 56 | ios/Pods/ 57 | KeychainExample/ios/Pods 58 | 59 | # Logs 60 | *.log 61 | 62 | # Merge backups kdiff3 63 | *.orig 64 | 65 | # Yarn 66 | .yarn/* 67 | !.yarn/patches 68 | !.yarn/plugins 69 | !.yarn/releases 70 | !.yarn/sdks 71 | !.yarn/versions 72 | 73 | # React Native 74 | .watchman-* 75 | KeychainExample/dist 76 | 77 | # Turborepo 78 | .turbo/ 79 | 80 | # Ruby 81 | KeychainExample/vendor/ 82 | 83 | # Tests 84 | KeychainExample/e2e/output 85 | KeychainExample/artifacts 86 | 87 | # React Native Bob Builder 88 | lib -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "no-inline-html": false, 4 | "line-length": false 5 | } 6 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "singleQuote": true, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nmHoistingLimits: workspaces 2 | 3 | nodeLinker: node-modules 4 | -------------------------------------------------------------------------------- /KeychainExample/.detoxrc.js: -------------------------------------------------------------------------------- 1 | /** @type {Detox.DetoxConfig} */ 2 | module.exports = { 3 | testRunner: { 4 | args: { 5 | $0: 'jest', 6 | config: 'e2e/jest.config.js', 7 | }, 8 | jest: { 9 | setupTimeout: 120000, 10 | }, 11 | }, 12 | apps: { 13 | 'ios.debug': { 14 | type: 'ios.app', 15 | binaryPath: 16 | 'ios/build/Build/Products/Debug-iphonesimulator/ReactTestApp.app', 17 | build: 18 | 'xcodebuild ONLY_ACTIVE_ARCH=YES -workspace ios/KeychainExample.xcworkspace -UseNewBuildSystem=YES -scheme KeychainExample -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build', 19 | }, 20 | 'ios.release': { 21 | type: 'ios.app', 22 | binaryPath: 23 | 'ios/build/Build/Products/Release-iphonesimulator/ReactTestApp.app', 24 | build: 25 | 'xcodebuild ONLY_ACTIVE_ARCH=YES -workspace ios/KeychainExample.xcworkspace -UseNewBuildSystem=YES -scheme KeychainExample -configuration Release -sdk iphonesimulator -derivedDataPath ios/build', 26 | }, 27 | 'android.debug': { 28 | type: 'android.apk', 29 | binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk', 30 | build: 31 | 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug', 32 | reversePorts: [8081], 33 | }, 34 | 'android.release': { 35 | type: 'android.apk', 36 | binaryPath: 'android/app/build/outputs/apk/release/app-release.apk', 37 | build: 38 | 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release', 39 | }, 40 | }, 41 | devices: { 42 | simulator: { 43 | type: 'ios.simulator', 44 | headless: Boolean(process.env.CI), 45 | device: { 46 | type: 'iPhone 15 Pro', 47 | }, 48 | }, 49 | attached: { 50 | type: 'android.attached', 51 | device: { 52 | adbName: '.*', 53 | }, 54 | }, 55 | emulator: { 56 | type: 'android.emulator', 57 | headless: Boolean(process.env.CI), 58 | device: { 59 | avdName: 'TestingAVD', 60 | }, 61 | }, 62 | }, 63 | artifacts: { 64 | plugins: { 65 | screenshot: { 66 | enabled: true, 67 | shouldTakeAutomaticSnapshots: true, 68 | keepOnlyFailedTestsArtifacts: true, 69 | takeWhen: { 70 | testStart: true, 71 | testDone: true, 72 | appNotReady: true, 73 | }, 74 | }, 75 | }, 76 | }, 77 | configurations: { 78 | 'ios.sim.debug': { 79 | device: 'simulator', 80 | app: 'ios.debug', 81 | }, 82 | 'ios.sim.release': { 83 | device: 'simulator', 84 | app: 'ios.release', 85 | }, 86 | 'android.att.debug': { 87 | device: 'attached', 88 | app: 'android.debug', 89 | }, 90 | 'android.att.release': { 91 | device: 'attached', 92 | app: 'android.release', 93 | }, 94 | 'android.emu.debug': { 95 | device: 'emulator', 96 | app: 'android.debug', 97 | }, 98 | 'android.emu.release': { 99 | device: 'emulator', 100 | app: 'android.release', 101 | }, 102 | }, 103 | }; 104 | -------------------------------------------------------------------------------- /KeychainExample/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /KeychainExample/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Alert, 4 | Keyboard, 5 | KeyboardAvoidingView, 6 | Platform, 7 | StyleSheet, 8 | Text, 9 | TextInput, 10 | TouchableHighlight, 11 | View, 12 | } from 'react-native'; 13 | import SegmentedControlTab from 'react-native-segmented-control-tab'; 14 | import * as Keychain from 'react-native-keychain-manager'; 15 | 16 | const ACCESS_CONTROL_OPTIONS = ['None', 'Passcode', 'Password']; 17 | const ACCESS_CONTROL_OPTIONS_ANDROID = ['None']; 18 | const ACCESS_CONTROL_MAP = [ 19 | null, 20 | Keychain.ACCESS_CONTROL.DEVICE_PASSCODE, 21 | Keychain.ACCESS_CONTROL.APPLICATION_PASSWORD, 22 | Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET, 23 | ]; 24 | const ACCESS_CONTROL_MAP_ANDROID = [ 25 | null, 26 | Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET, 27 | ]; 28 | const SECURITY_LEVEL_OPTIONS = ['Any', 'Software', 'Hardware']; 29 | const SECURITY_LEVEL_MAP = [ 30 | Keychain.SECURITY_LEVEL.ANY, 31 | Keychain.SECURITY_LEVEL.SECURE_SOFTWARE, 32 | Keychain.SECURITY_LEVEL.SECURE_HARDWARE, 33 | ]; 34 | 35 | const SECURITY_STORAGE_OPTIONS = ['Best', 'FB', 'AES', 'RSA']; 36 | const SECURITY_STORAGE_MAP = [ 37 | null, 38 | Keychain.STORAGE_TYPE.FB, 39 | Keychain.STORAGE_TYPE.AES, 40 | Keychain.STORAGE_TYPE.RSA, 41 | ]; 42 | 43 | export default class KeychainExample extends Component { 44 | state = { 45 | username: '', 46 | password: '', 47 | status: '', 48 | biometryType: undefined, 49 | accessControl: undefined as undefined | Keychain.ACCESS_CONTROL, 50 | securityLevel: undefined as undefined | Keychain.SECURITY_LEVEL, 51 | storage: undefined as undefined | Keychain.STORAGE_TYPE, 52 | selectedStorageIndex: 0, 53 | selectedSecurityIndex: 0, 54 | selectedAccessControlIndex: 0, 55 | hasGenericPassword: false, 56 | }; 57 | 58 | componentDidMount() { 59 | Keychain.getSupportedBiometryType().then((biometryType) => { 60 | this.setState({ biometryType }); 61 | }); 62 | Keychain.hasGenericPassword().then((hasGenericPassword) => { 63 | this.setState({ hasGenericPassword }); 64 | }); 65 | } 66 | 67 | async save() { 68 | try { 69 | const start = new Date(); 70 | await Keychain.setGenericPassword( 71 | this.state.username, 72 | this.state.password, 73 | { 74 | accessControl: this.state.accessControl, 75 | securityLevel: this.state.securityLevel, 76 | storage: this.state.storage, 77 | } 78 | ); 79 | 80 | const end = new Date(); 81 | 82 | this.setState({ 83 | username: '', 84 | password: '', 85 | status: `Credentials saved! takes: ${ 86 | end.getTime() - start.getTime() 87 | } millis`, 88 | }); 89 | } catch (err) { 90 | this.setState({ status: 'Could not save credentials, ' + err }); 91 | } 92 | } 93 | 94 | async load() { 95 | try { 96 | const options = { 97 | authenticationPrompt: { 98 | title: 'Authentication needed', 99 | subtitle: 'Subtitle', 100 | description: 'Some descriptive text', 101 | cancel: 'Cancel', 102 | }, 103 | }; 104 | const credentials = await Keychain.getGenericPassword(options); 105 | if (credentials) { 106 | this.setState({ ...credentials, status: 'Credentials loaded!' }); 107 | } else { 108 | this.setState({ status: 'No credentials stored.' }); 109 | } 110 | } catch (err) { 111 | this.setState({ status: 'Could not load credentials. ' + err }); 112 | } 113 | } 114 | 115 | async reset() { 116 | try { 117 | await Keychain.resetGenericPassword(); 118 | this.setState({ 119 | status: 'Credentials Reset!', 120 | username: '', 121 | password: '', 122 | }); 123 | } catch (err) { 124 | this.setState({ status: 'Could not reset credentials, ' + err }); 125 | } 126 | } 127 | 128 | async getAll() { 129 | try { 130 | const result = await Keychain.getAllGenericPasswordServices(); 131 | this.setState({ 132 | status: `All keys successfully fetched! Found: ${result.length} keys.`, 133 | }); 134 | } catch (err) { 135 | this.setState({ status: 'Could not get all keys. ' + err }); 136 | } 137 | } 138 | 139 | async ios_specifics() { 140 | try { 141 | const reply = await Keychain.setSharedWebCredentials( 142 | 'server', 143 | 'username', 144 | 'password' 145 | ); 146 | console.log(`setSharedWebCredentials: ${JSON.stringify(reply)}`); 147 | } catch (err) { 148 | Alert.alert('setSharedWebCredentials error', (err as Error).message); 149 | } 150 | 151 | try { 152 | const reply = await Keychain.requestSharedWebCredentials(); 153 | console.log(`requestSharedWebCredentials: ${JSON.stringify(reply)}`); 154 | } catch (err) { 155 | Alert.alert('requestSharedWebCredentials error', (err as Error).message); 156 | } 157 | } 158 | 159 | render() { 160 | const VALUES = 161 | Platform.OS === 'ios' 162 | ? ACCESS_CONTROL_OPTIONS 163 | : ACCESS_CONTROL_OPTIONS_ANDROID; 164 | const AC_MAP = 165 | Platform.OS === 'ios' ? ACCESS_CONTROL_MAP : ACCESS_CONTROL_MAP_ANDROID; 166 | const SL_MAP = Platform.OS === 'ios' ? [] : SECURITY_LEVEL_MAP; 167 | const ST_MAP = Platform.OS === 'ios' ? [] : SECURITY_STORAGE_MAP; 168 | 169 | return ( 170 | 174 | 175 | Keyboard.dismiss()}> 176 | Keychain Example 177 | 178 | 179 | Username 180 | 186 | this.setState({ username: event.nativeEvent.text }) 187 | } 188 | underlineColorAndroid="transparent" 189 | blurOnSubmit={false} 190 | returnKeyType="next" 191 | /> 192 | 193 | 194 | Password 195 | 202 | this.setState({ password: event.nativeEvent.text }) 203 | } 204 | underlineColorAndroid="transparent" 205 | /> 206 | 207 | 208 | Access Control 209 | 217 | this.setState({ 218 | ...this.state, 219 | accessControl: AC_MAP[index], 220 | selectedAccessControlIndex: index, 221 | }) 222 | } 223 | /> 224 | 225 | {Platform.OS === 'android' && ( 226 | 227 | Security Level 228 | 232 | this.setState({ 233 | ...this.state, 234 | securityLevel: SL_MAP[index], 235 | selectedSecurityIndex: index, 236 | }) 237 | } 238 | /> 239 | 240 | Storage 241 | 245 | this.setState({ 246 | ...this.state, 247 | storageSelection: ST_MAP[index], 248 | selectedStorageIndex: index, 249 | }) 250 | } 251 | /> 252 | 253 | )} 254 | {!!this.state.status && ( 255 | {this.state.status} 256 | )} 257 | 258 | 259 | this.save()} 261 | style={styles.button} 262 | > 263 | 264 | Save 265 | 266 | 267 | 268 | this.load()} 270 | style={styles.button} 271 | > 272 | 273 | Load 274 | 275 | 276 | 277 | this.reset()} 279 | style={styles.button} 280 | > 281 | 282 | Reset 283 | 284 | 285 | 286 | 287 | 288 | this.getAll()} 290 | style={styles.button} 291 | > 292 | 293 | Get Used Keys 294 | 295 | 296 | {Platform.OS === 'android' && ( 297 | { 299 | const level = await Keychain.getSecurityLevel(); 300 | if (level !== null) { 301 | Alert.alert('Security Level', JSON.stringify(level)); 302 | } 303 | }} 304 | style={styles.button} 305 | > 306 | 307 | Get security level 308 | 309 | 310 | )} 311 | {Platform.OS === 'ios' && ( 312 | this.ios_specifics()} 314 | style={styles.button} 315 | > 316 | 317 | Test Other APIs 318 | 319 | 320 | )} 321 | 322 | 323 | hasGenericPassword: {String(this.state.hasGenericPassword)} 324 | 325 | 326 | 327 | ); 328 | } 329 | } 330 | 331 | const styles = StyleSheet.create({ 332 | container: { 333 | flex: 1, 334 | justifyContent: 'center', 335 | backgroundColor: '#F5FCFF', 336 | }, 337 | content: { 338 | marginHorizontal: 20, 339 | }, 340 | title: { 341 | fontSize: 28, 342 | fontWeight: '200', 343 | textAlign: 'center', 344 | marginBottom: 20, 345 | }, 346 | field: { 347 | marginVertical: 5, 348 | }, 349 | label: { 350 | fontWeight: '500', 351 | fontSize: 15, 352 | marginBottom: 5, 353 | }, 354 | input: { 355 | color: '#000', 356 | borderWidth: StyleSheet.hairlineWidth, 357 | borderColor: '#ccc', 358 | backgroundColor: 'white', 359 | height: 32, 360 | fontSize: 14, 361 | padding: 8, 362 | }, 363 | status: { 364 | color: '#333', 365 | fontSize: 12, 366 | marginTop: 15, 367 | }, 368 | biometryType: { 369 | color: '#333', 370 | fontSize: 12, 371 | marginTop: 15, 372 | }, 373 | buttons: { 374 | flexDirection: 'row', 375 | justifyContent: 'space-between', 376 | marginTop: 20, 377 | }, 378 | button: { 379 | borderRadius: 3, 380 | padding: 2, 381 | overflow: 'hidden', 382 | }, 383 | save: { 384 | backgroundColor: '#0c0', 385 | }, 386 | load: { 387 | backgroundColor: '#333', 388 | }, 389 | reset: { 390 | backgroundColor: '#c00', 391 | }, 392 | buttonText: { 393 | color: 'white', 394 | fontSize: 14, 395 | paddingHorizontal: 16, 396 | paddingVertical: 8, 397 | }, 398 | }); 399 | -------------------------------------------------------------------------------- /KeychainExample/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures. 7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' 8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' -------------------------------------------------------------------------------- /KeychainExample/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | CFPropertyList (3.0.7) 5 | base64 6 | nkf 7 | rexml 8 | activesupport (7.2.1) 9 | base64 10 | bigdecimal 11 | concurrent-ruby (~> 1.0, >= 1.3.1) 12 | connection_pool (>= 2.2.5) 13 | drb 14 | i18n (>= 1.6, < 2) 15 | logger (>= 1.4.2) 16 | minitest (>= 5.1) 17 | securerandom (>= 0.3) 18 | tzinfo (~> 2.0, >= 2.0.5) 19 | addressable (2.8.7) 20 | public_suffix (>= 2.0.2, < 7.0) 21 | algoliasearch (1.27.5) 22 | httpclient (~> 2.8, >= 2.8.3) 23 | json (>= 1.5.1) 24 | atomos (0.1.3) 25 | base64 (0.2.0) 26 | bigdecimal (3.1.8) 27 | claide (1.1.0) 28 | cocoapods (1.15.2) 29 | addressable (~> 2.8) 30 | claide (>= 1.0.2, < 2.0) 31 | cocoapods-core (= 1.15.2) 32 | cocoapods-deintegrate (>= 1.0.3, < 2.0) 33 | cocoapods-downloader (>= 2.1, < 3.0) 34 | cocoapods-plugins (>= 1.0.0, < 2.0) 35 | cocoapods-search (>= 1.0.0, < 2.0) 36 | cocoapods-trunk (>= 1.6.0, < 2.0) 37 | cocoapods-try (>= 1.1.0, < 2.0) 38 | colored2 (~> 3.1) 39 | escape (~> 0.0.4) 40 | fourflusher (>= 2.3.0, < 3.0) 41 | gh_inspector (~> 1.0) 42 | molinillo (~> 0.8.0) 43 | nap (~> 1.0) 44 | ruby-macho (>= 2.3.0, < 3.0) 45 | xcodeproj (>= 1.23.0, < 2.0) 46 | cocoapods-core (1.15.2) 47 | activesupport (>= 5.0, < 8) 48 | addressable (~> 2.8) 49 | algoliasearch (~> 1.0) 50 | concurrent-ruby (~> 1.1) 51 | fuzzy_match (~> 2.0.4) 52 | nap (~> 1.0) 53 | netrc (~> 0.11) 54 | public_suffix (~> 4.0) 55 | typhoeus (~> 1.0) 56 | cocoapods-deintegrate (1.0.5) 57 | cocoapods-downloader (2.1) 58 | cocoapods-plugins (1.0.0) 59 | nap 60 | cocoapods-search (1.0.1) 61 | cocoapods-trunk (1.6.0) 62 | nap (>= 0.8, < 2.0) 63 | netrc (~> 0.11) 64 | cocoapods-try (1.2.0) 65 | colored2 (3.1.2) 66 | concurrent-ruby (1.3.4) 67 | connection_pool (2.4.1) 68 | drb (2.2.1) 69 | escape (0.0.4) 70 | ethon (0.16.0) 71 | ffi (>= 1.15.0) 72 | ffi (1.17.0) 73 | ffi (1.17.0-aarch64-linux-gnu) 74 | ffi (1.17.0-aarch64-linux-musl) 75 | ffi (1.17.0-arm-linux-gnu) 76 | ffi (1.17.0-arm-linux-musl) 77 | ffi (1.17.0-arm64-darwin) 78 | ffi (1.17.0-x86-linux-gnu) 79 | ffi (1.17.0-x86-linux-musl) 80 | ffi (1.17.0-x86_64-darwin) 81 | ffi (1.17.0-x86_64-linux-gnu) 82 | ffi (1.17.0-x86_64-linux-musl) 83 | fourflusher (2.3.1) 84 | fuzzy_match (2.0.4) 85 | gh_inspector (1.1.3) 86 | httpclient (2.8.3) 87 | i18n (1.14.5) 88 | concurrent-ruby (~> 1.0) 89 | json (2.7.2) 90 | logger (1.6.0) 91 | minitest (5.25.1) 92 | molinillo (0.8.0) 93 | nanaimo (0.3.0) 94 | nap (1.1.0) 95 | netrc (0.11.0) 96 | nkf (0.2.0) 97 | public_suffix (4.0.7) 98 | rexml (3.3.6) 99 | strscan 100 | ruby-macho (2.5.1) 101 | securerandom (0.3.1) 102 | strscan (3.1.0) 103 | typhoeus (1.4.1) 104 | ethon (>= 0.9.0) 105 | tzinfo (2.0.6) 106 | concurrent-ruby (~> 1.0) 107 | xcodeproj (1.25.0) 108 | CFPropertyList (>= 2.3.3, < 4.0) 109 | atomos (~> 0.1.3) 110 | claide (>= 1.0.2, < 2.0) 111 | colored2 (~> 3.1) 112 | nanaimo (~> 0.3.0) 113 | rexml (>= 3.3.2, < 4.0) 114 | 115 | PLATFORMS 116 | aarch64-linux-gnu 117 | aarch64-linux-musl 118 | arm-linux-gnu 119 | arm-linux-musl 120 | arm64-darwin 121 | ruby 122 | x86-linux-gnu 123 | x86-linux-musl 124 | x86_64-darwin 125 | x86_64-linux-gnu 126 | x86_64-linux-musl 127 | 128 | DEPENDENCIES 129 | activesupport (>= 6.1.7.5, != 7.1.0) 130 | cocoapods (>= 1.13, != 1.15.1, != 1.15.0) 131 | 132 | RUBY VERSION 133 | ruby 3.3.0p0 134 | 135 | BUNDLED WITH 136 | 2.5.17 137 | -------------------------------------------------------------------------------- /KeychainExample/android/app/src/androidTest/java/com/microsoft/reacttestapp/DetoxTest.java: -------------------------------------------------------------------------------- 1 | 2 | package com.microsoft.reacttestapp; 3 | 4 | import com.wix.detox.Detox; 5 | import com.wix.detox.config.DetoxConfig; 6 | 7 | import org.junit.Rule; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import androidx.test.ext.junit.runners.AndroidJUnit4; 12 | import androidx.test.filters.LargeTest; 13 | import androidx.test.rule.ActivityTestRule; 14 | 15 | @RunWith(AndroidJUnit4.class) 16 | @LargeTest 17 | public class DetoxTest { 18 | @Rule 19 | public ActivityTestRule mActivityRule = new ActivityTestRule<>(MainActivity.class, false, false); 20 | 21 | @Test 22 | public void runDetoxTests() { 23 | DetoxConfig detoxConfig = new DetoxConfig(); 24 | detoxConfig.idlePolicyConfig.masterTimeoutSec = 90; 25 | detoxConfig.idlePolicyConfig.idleResourceTimeoutSec = 60; 26 | detoxConfig.rnContextLoadTimeoutSec = (BuildConfig.DEBUG ? 180 : 60); 27 | 28 | Detox.runTests(mActivityRule, detoxConfig); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /KeychainExample/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | apply(from: { 3 | def searchDir = rootDir.toPath() 4 | do { 5 | def p = searchDir.resolve("node_modules/react-native-test-app/android/dependencies.gradle") 6 | if (p.toFile().exists()) { 7 | return p.toRealPath().toString() 8 | } 9 | } while (searchDir = searchDir.getParent()) 10 | throw new GradleException("Could not find `react-native-test-app`"); 11 | }()) 12 | 13 | repositories { 14 | mavenCentral() 15 | google() 16 | } 17 | 18 | dependencies { 19 | getReactNativeDependencies().each { dependency -> 20 | classpath(dependency) 21 | } 22 | } 23 | } 24 | 25 | allprojects { 26 | repositories { 27 | maven { 28 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 29 | url({ 30 | def searchDir = rootDir.toPath() 31 | do { 32 | def p = searchDir.resolve("node_modules/react-native/android") 33 | if (p.toFile().exists()) { 34 | return p.toRealPath().toString() 35 | } 36 | } while (searchDir = searchDir.getParent()) 37 | throw new GradleException("Could not find `react-native`"); 38 | }()) 39 | } 40 | mavenCentral() 41 | google() 42 | // START:detox 43 | maven { 44 | url("$rootDir/../node_modules/detox/Detox-android") 45 | } 46 | // END:detox 47 | } 48 | 49 | // START:detox 50 | afterEvaluate { project -> 51 | def androidExtension = project.extensions.findByName('android') 52 | if (androidExtension != null && project.name == 'app') { 53 | androidExtension.testBuildType System.getProperty('testBuildType', 'debug') 54 | androidExtension.defaultConfig { 55 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' 56 | 57 | ndk { 58 | abiFilters 'arm64-v8a', 'x86', 'x86_64' 59 | } 60 | } 61 | 62 | androidExtension.buildTypes { 63 | release { 64 | proguardFile "${rootProject.projectDir}/../node_modules/detox/android/detox/proguard-rules-app.pro" 65 | } 66 | } 67 | 68 | androidExtension.sourceSets.androidTest.java.srcDirs += "$rootDir/app/src/androidTest/java" 69 | 70 | project.dependencies { 71 | if (reactNativeVersion > 0 && reactNativeVersion < v(0, 73, 0)) { 72 | androidTestImplementation('com.wix:detox-legacy:+') 73 | } else { 74 | androidTestImplementation('com.wix:detox:+') 75 | } 76 | implementation 'androidx.appcompat:appcompat:1.1.0' 77 | } 78 | } 79 | } 80 | // END:detox 81 | } 82 | -------------------------------------------------------------------------------- /KeychainExample/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the Gradle Daemon. The setting is 11 | # particularly useful for configuring JVM memory settings for build performance. 12 | # This does not affect the JVM settings for the Gradle client VM. 13 | # The default is `-Xmx512m -XX:MaxMetaspaceSize=256m`. 14 | org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 15 | 16 | # When configured, Gradle will fork up to org.gradle.workers.max JVMs to execute 17 | # projects in parallel. To learn more about parallel task execution, see the 18 | # section on Gradle build performance: 19 | # https://docs.gradle.org/current/userguide/performance.html#parallel_execution. 20 | # Default is `false`. 21 | #org.gradle.parallel=true 22 | 23 | # AndroidX package structure to make it clearer which packages are bundled with the 24 | # Android operating system, and which are packaged with your app's APK 25 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 26 | android.useAndroidX=true 27 | # Automatically convert third-party libraries to use AndroidX 28 | android.enableJetifier=true 29 | # Jetifier randomly fails on these libraries 30 | android.jetifier.ignorelist=hermes-android 31 | 32 | # Use this property to specify which architecture you want to build. 33 | # You can also override it from the CLI using 34 | # ./gradlew -PreactNativeArchitectures=x86_64 35 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 36 | 37 | # Use this property to enable support to the new architecture. 38 | # This will allow you to use TurboModules and the Fabric render in 39 | # your application. You should enable this flag either if you want 40 | # to write custom TurboModules/Fabric components OR use libraries that 41 | # are providing them. 42 | # Note that this is incompatible with web debugging. 43 | #newArchEnabled=true 44 | #bridgelessEnabled=true 45 | 46 | # Uncomment the line below to build React Native from source. 47 | #react.buildFromSource=true 48 | 49 | # Version of Android NDK to build against. 50 | #ANDROID_NDK_VERSION=26.1.10909125 51 | 52 | # Version of Kotlin to build against. 53 | #KOTLIN_VERSION=1.8.22 54 | -------------------------------------------------------------------------------- /KeychainExample/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DorianMazur/react-native-keychain-manager/c0b092b26c9aa40ee16accec35a762fdf4dd6811/KeychainExample/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /KeychainExample/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /KeychainExample/android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /KeychainExample/android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /KeychainExample/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | mavenCentral() 5 | google() 6 | } 7 | } 8 | 9 | rootProject.name = "KeychainExample" 10 | 11 | apply(from: { 12 | def searchDir = rootDir.toPath() 13 | do { 14 | def p = searchDir.resolve("node_modules/react-native-test-app/test-app.gradle") 15 | if (p.toFile().exists()) { 16 | return p.toRealPath().toString() 17 | } 18 | } while (searchDir = searchDir.getParent()) 19 | throw new GradleException("Could not find `react-native-test-app`"); 20 | }()) 21 | applyTestAppSettings(settings) 22 | -------------------------------------------------------------------------------- /KeychainExample/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "KeychainExample", 3 | "displayName": "KeychainExample", 4 | "components": [ 5 | { 6 | "appKey": "KeychainExample", 7 | "displayName": "KeychainExample" 8 | } 9 | ], 10 | "resources": { 11 | "android": ["dist/res", "dist/main.android.jsbundle"], 12 | "ios": ["dist/assets", "dist/main.ios.jsbundle"], 13 | "macos": ["dist/assets", "dist/main.macos.jsbundle"], 14 | "visionos": ["dist/assets", "dist/main.visionos.jsbundle"], 15 | "windows": ["dist/assets", "dist/main.windows.bundle"] 16 | }, 17 | "android": { 18 | "package": "keychain.example" 19 | }, 20 | "ios": { 21 | "bundleIdentifier": "keychain.example" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /KeychainExample/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pak = require('../package.json'); 3 | 4 | module.exports = { 5 | presets: ['module:@react-native/babel-preset'], 6 | plugins: [ 7 | [require('@rnx-kit/polyfills')], 8 | [ 9 | 'module-resolver', 10 | { 11 | extensions: ['.tsx', '.ts', '.js', '.json'], 12 | alias: { 13 | [pak.name]: path.join(__dirname, '..', pak.source), 14 | }, 15 | }, 16 | ], 17 | ], 18 | }; 19 | -------------------------------------------------------------------------------- /KeychainExample/e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@jest/types').Config.InitialOptions} */ 2 | module.exports = { 3 | rootDir: '..', 4 | testMatch: ['/e2e/**/*.spec.js'], 5 | testTimeout: 120000, 6 | maxWorkers: 1, 7 | globalSetup: 'detox/runners/jest/globalSetup', 8 | globalTeardown: 'detox/runners/jest/globalTeardown', 9 | reporters: [ 10 | 'detox/runners/jest/reporter', 11 | ['jest-junit', { outputDirectory: 'e2e/output', outputName: 'report.xml' }], 12 | ], 13 | testEnvironment: 'detox/runners/jest/testEnvironment', 14 | verbose: true, 15 | }; 16 | -------------------------------------------------------------------------------- /KeychainExample/e2e/testCases/biometricsAccessControlTest.spec.js: -------------------------------------------------------------------------------- 1 | import { by, device, element, expect } from 'detox'; 2 | import path from 'path'; 3 | import cp from 'child_process'; 4 | 5 | const enrollBiometric = async () => { 6 | if (device.getPlatform() === 'android') { 7 | const script = path.resolve( 8 | __dirname, 9 | '../utils/enrollFingerprintAndroid.sh' 10 | ); 11 | const result = cp.spawnSync('sh', [script], { 12 | stdio: 'inherit', 13 | }); 14 | 15 | // Check for errors 16 | if (result.error) { 17 | console.error('Error executing script:', result.error); 18 | } 19 | } else { 20 | await device.setBiometricEnrollment(true); 21 | } 22 | }; 23 | 24 | describe('Biometrics Access Control', () => { 25 | beforeAll(async () => { 26 | await enrollBiometric(); 27 | }); 28 | 29 | beforeEach(async () => { 30 | await device.launchApp({ newInstance: true }); 31 | }); 32 | 33 | it('should save and retrieve username and password', async () => { 34 | await expect(element(by.text('Keychain Example'))).toExist(); 35 | await element(by.id('usernameInput')).typeText('testUsername'); 36 | await element(by.id('passwordInput')).typeText('testPassword'); 37 | // Hide keyboard 38 | await element(by.text('Keychain Example')).tap(); 39 | 40 | if (device.getPlatform() === 'android') { 41 | await element(by.text('Fingerprint')).tap(); 42 | await element(by.text('Software')).tap(); 43 | } else { 44 | await element(by.text('FaceID')).tap(); 45 | } 46 | 47 | await expect(element(by.text('Save'))).toBeVisible(); 48 | await element(by.text('Save')).tap(); 49 | await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); 50 | // Biometric prompt is not available in the IOS simulator 51 | if (device.getPlatform() === 'android') { 52 | setTimeout(() => { 53 | cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); 54 | }, 1000); 55 | } 56 | await element(by.text('Load')).tap(); 57 | await expect(element(by.text('Credentials loaded!'))).toBeVisible(); 58 | await expect(element(by.id('usernameInput'))).toHaveText('testUsername'); 59 | await expect(element(by.id('passwordInput'))).toHaveText('testPassword'); 60 | }); 61 | 62 | it('should retrieve username and password after app launch', async () => { 63 | await expect(element(by.text('Keychain Example'))).toExist(); 64 | await expect(element(by.text('hasGenericPassword: true'))).toBeVisible(); 65 | // Biometric prompt is not available in the IOS simulator 66 | if (device.getPlatform() === 'android') { 67 | setTimeout(() => { 68 | cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); 69 | }, 1000); 70 | } 71 | await element(by.text('Load')).tap(); 72 | await expect(element(by.text('Credentials loaded!'))).toBeVisible(); 73 | await expect(element(by.id('usernameInput'))).toHaveText('testUsername'); 74 | await expect(element(by.id('passwordInput'))).toHaveText('testPassword'); 75 | }); 76 | 77 | it(':android:should save and retrieve username and password for hardware security level', async () => { 78 | await expect(element(by.text('Keychain Example'))).toExist(); 79 | await element(by.id('usernameInput')).typeText('testUsernameHardware'); 80 | await element(by.id('passwordInput')).typeText('testPasswordHardware'); 81 | // Hide keyboard 82 | await element(by.text('Keychain Example')).tap(); 83 | await element(by.text('Fingerprint')).tap(); 84 | await element(by.text('Hardware')).tap(); 85 | 86 | await expect(element(by.text('Save'))).toBeVisible(); 87 | await element(by.text('Save')).tap(); 88 | await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); 89 | 90 | setTimeout(() => { 91 | cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); 92 | }, 1000); 93 | 94 | await element(by.text('Load')).tap(); 95 | await expect(element(by.text('Credentials loaded!'))).toBeVisible(); 96 | await expect(element(by.id('usernameInput'))).toHaveText( 97 | 'testUsernameHardware' 98 | ); 99 | await expect(element(by.id('passwordInput'))).toHaveText( 100 | 'testPasswordHardware' 101 | ); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /KeychainExample/e2e/testCases/noneAccessControTest.spec.js: -------------------------------------------------------------------------------- 1 | import { by, device, element, expect } from 'detox'; 2 | 3 | describe('None Access Control', () => { 4 | beforeEach(async () => { 5 | await device.launchApp({ newInstance: true }); 6 | }); 7 | 8 | it('should save and retrieve username and password', async () => { 9 | await expect(element(by.text('Keychain Example'))).toExist(); 10 | await element(by.id('usernameInput')).typeText('testUsername'); 11 | await element(by.id('passwordInput')).typeText('testPassword'); 12 | // Hide keyboard 13 | await element(by.text('Keychain Example')).tap(); 14 | await element(by.text('None')).tap(); 15 | 16 | if (device.getPlatform() === 'android') { 17 | await element(by.text('Software')).tap(); 18 | } 19 | 20 | await expect(element(by.text('Save'))).toBeVisible(); 21 | await element(by.text('Save')).tap(); 22 | await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); 23 | await element(by.text('Load')).tap(); 24 | await expect(element(by.text('Credentials loaded!'))).toBeVisible(); 25 | await expect(element(by.id('usernameInput'))).toHaveText('testUsername'); 26 | await expect(element(by.id('passwordInput'))).toHaveText('testPassword'); 27 | }); 28 | 29 | it('should retrieve username and password after app launch', async () => { 30 | await device.launchApp({ newInstance: true }); 31 | await expect(element(by.text('Keychain Example'))).toExist(); 32 | await expect(element(by.text('hasGenericPassword: true'))).toBeVisible(); 33 | await element(by.text('Load')).tap(); 34 | await expect(element(by.text('Credentials loaded!'))).toBeVisible(); 35 | await expect(element(by.id('usernameInput'))).toHaveText('testUsername'); 36 | await expect(element(by.id('passwordInput'))).toHaveText('testPassword'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /KeychainExample/e2e/utils/enrollFingerprintAndroid.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | DELAY=3 4 | adb shell locksettings set-pin 1111 5 | adb shell am start -a android.settings.SECURITY_SETTINGS 6 | sleep $DELAY 7 | adb shell input tap 274 1150 8 | sleep $DELAY 9 | adb shell input text 1111 10 | adb shell input keyevent 66 11 | sleep $DELAY 12 | adb shell input tap 900 2200 13 | sleep $DELAY 14 | adb shell input tap 900 2200 15 | sleep $DELAY 16 | adb -e emu finger touch 1 17 | sleep $DELAY 18 | adb -e emu finger touch 1 19 | sleep $DELAY 20 | adb -e emu finger touch 1 21 | sleep $DELAY 22 | adb shell input keyevent KEYCODE_HOME -------------------------------------------------------------------------------- /KeychainExample/index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native'; 2 | import App from './App'; 3 | import { name as appName } from './app.json'; 4 | 5 | AppRegistry.registerComponent(appName, () => App); 6 | -------------------------------------------------------------------------------- /KeychainExample/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | export NODE_BINARY=/Users/dorianmazur/.nvm/versions/node/v20.11.1/bin/node 2 | -------------------------------------------------------------------------------- /KeychainExample/ios/KeychainExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /KeychainExample/ios/KeychainExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /KeychainExample/ios/Podfile: -------------------------------------------------------------------------------- 1 | ws_dir = Pathname.new(__dir__) 2 | ws_dir = ws_dir.parent until 3 | File.exist?("#{ws_dir}/node_modules/react-native-test-app/test_app.rb") || 4 | ws_dir.expand_path.to_s == '/' 5 | require "#{ws_dir}/node_modules/react-native-test-app/test_app.rb" 6 | 7 | workspace 'KeychainExample.xcworkspace' 8 | 9 | use_test_app! 10 | -------------------------------------------------------------------------------- /KeychainExample/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | }; 4 | -------------------------------------------------------------------------------- /KeychainExample/metro.config.js: -------------------------------------------------------------------------------- 1 | const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); 2 | const path = require('path'); 3 | const escape = require('escape-string-regexp'); 4 | const exclusionList = require('metro-config/src/defaults/exclusionList'); 5 | const pak = require('../package.json'); 6 | 7 | const root = path.resolve(__dirname, '..'); 8 | const modules = Object.keys({ ...pak.peerDependencies }); 9 | 10 | /** 11 | * Metro configuration 12 | * https://facebook.github.io/metro/docs/configuration 13 | * 14 | * @type {import('metro-config').MetroConfig} 15 | */ 16 | const config = { 17 | watchFolders: [root], 18 | 19 | // We need to make sure that only one version is loaded for peerDependencies 20 | // So we block them at the root, and alias them to the versions in example's node_modules 21 | resolver: { 22 | blacklistRE: exclusionList( 23 | modules.map( 24 | (m) => 25 | new RegExp(`^${escape(path.join(root, 'node_modules', m))}\\/.*$`) 26 | ) 27 | ), 28 | 29 | extraNodeModules: modules.reduce((acc, name) => { 30 | acc[name] = path.join(__dirname, 'node_modules', name); 31 | return acc; 32 | }, {}), 33 | }, 34 | 35 | transformer: { 36 | getTransformOptions: async () => ({ 37 | transform: { 38 | experimentalImportSupport: false, 39 | inlineRequires: true, 40 | }, 41 | }), 42 | }, 43 | }; 44 | 45 | module.exports = mergeConfig(getDefaultConfig(__dirname), config); 46 | -------------------------------------------------------------------------------- /KeychainExample/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "KeychainExample", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios --simulator 'iPhone 15 Pro' --mode Release", 8 | "build:android": "npm run mkdist && react-native bundle --entry-file index.js --platform android --dev false --bundle-output dist/main.android.jsbundle --assets-dest dist/res", 9 | "build:ios": "npm run mkdist && react-native bundle --entry-file index.js --platform ios --dev false --bundle-output dist/main.ios.jsbundle --assets-dest dist/assets", 10 | "mkdist": "node -e \"require('node:fs').mkdirSync('dist', { recursive: true, mode: 0o755 })\"", 11 | "start": "react-native start", 12 | "test:android": "yarn test:android:build && yarn test:android:run", 13 | "test:android:build": "detox build --configuration android.emu.release", 14 | "test:android:run": "detox test --configuration android.emu.release", 15 | "test:ios": "yarn test:ios:build && yarn test:ios:run", 16 | "test:ios:build": "detox build --configuration ios.sim.release", 17 | "test:ios:run": "detox test --configuration ios.sim.release" 18 | }, 19 | "dependencies": { 20 | "react": "18.2.0", 21 | "react-native": "^0.74.5", 22 | "react-native-segmented-control-tab": "^4.0.0" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.20.0", 26 | "@babel/preset-env": "^7.20.0", 27 | "@babel/runtime": "^7.20.0", 28 | "@react-native/babel-preset": "^0.74.5", 29 | "@react-native/metro-config": "^0.74.5", 30 | "@react-native/typescript-config": "^0.74.5", 31 | "@rnx-kit/polyfills": "^0.1.1", 32 | "@types/react": "^18.2.0", 33 | "babel-plugin-module-resolver": "^5.0.2", 34 | "detox": "^20.25.6", 35 | "jest": "^29.7.0", 36 | "jest-junit": "^16.0.0", 37 | "react-native-test-app": "^3.9.7" 38 | }, 39 | "jest": { 40 | "preset": "react-native" 41 | }, 42 | "engines": { 43 | "node": ">=18" 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /KeychainExample/react-native.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pkg = require('../package.json'); 3 | const { configureProjects } = require('react-native-test-app'); 4 | 5 | module.exports = { 6 | assets: ['./assets/fonts/'], 7 | project: configureProjects({ 8 | android: { 9 | sourceDir: 'android', 10 | }, 11 | ios: { 12 | sourceDir: 'ios', 13 | automaticPodsInstallation: true, 14 | }, 15 | }), 16 | dependencies: { 17 | [pkg.name]: { 18 | root: path.join(__dirname, '..'), 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /KeychainExample/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@react-native/typescript-config/tsconfig.json", 3 | "compilerOptions": { 4 | "paths": { 5 | "react-native-keychain-manager": ["../src/index"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2024 Dorian Mazur 4 | 5 | Copyright (c) 2015 Joel Arvidsson 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | 25 | -------------------------------------------------------------------------------- /RNKeychainManager.podspec: -------------------------------------------------------------------------------- 1 | require 'json' 2 | 3 | version = JSON.parse(File.read('package.json'))["version"] 4 | folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32' 5 | 6 | Pod::Spec.new do |s| 7 | 8 | s.name = "RNKeychainManager" 9 | s.version = version 10 | s.summary = "Keychain Access for React Native." 11 | s.homepage = "https://github.com/DorianMazur/react-native-keychain-manager" 12 | s.license = "MIT" 13 | s.author = { "Dorian Mazur" => "mazur.dorian15@gmail.com" } 14 | s.ios.deployment_target = '9.0' 15 | s.tvos.deployment_target = '9.0' 16 | s.osx.deployment_target = '10.13' 17 | s.visionos.deployment_target = '1.0' 18 | s.source = { :git => "https://github.com/DorianMazur/react-native-keychain-manager.git", :tag => "v#{s.version}" } 19 | s.source_files = 'ios/RNKeychainManager/**/*.{h,m}' 20 | s.preserve_paths = "**/*.js" 21 | 22 | if respond_to?(:install_modules_dependencies, true) 23 | # React Native Core dependency 24 | install_modules_dependencies(s) 25 | else 26 | s.dependency "React-Core" 27 | 28 | # Don't install the dependencies when we run `pod install` in the old architecture. 29 | if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then 30 | s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1" 31 | s.pod_target_xcconfig = { 32 | "HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"", 33 | "OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1", 34 | "CLANG_CXX_LANGUAGE_STANDARD" => "c++17" 35 | } 36 | s.dependency "React-Codegen" 37 | s.dependency "RCT-Folly" 38 | s.dependency "RCTRequired" 39 | s.dependency "RCTTypeSafety" 40 | s.dependency "ReactCommon/turbomodule/core" 41 | end 42 | end 43 | 44 | end 45 | -------------------------------------------------------------------------------- /android/.npmignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .DS_Store 3 | .gradle/ 4 | .idea/ 5 | .npmignore 6 | build/ 7 | gradle/ 8 | gradlew 9 | gradlew.bat 10 | local.properties 11 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | // Buildscript is evaluated before everything else so we can't use safeExtGet 3 | def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : "1.8.0" 4 | 5 | repositories { 6 | maven { 7 | url "https://plugins.gradle.org/m2/" 8 | } 9 | mavenCentral() 10 | google() 11 | } 12 | 13 | dependencies { 14 | classpath "com.android.tools.build:gradle:8.3.1" 15 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 16 | } 17 | } 18 | 19 | def safeExtGet(prop, fallback) { 20 | rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback 21 | } 22 | 23 | def isNewArchitectureEnabled() { 24 | return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true" 25 | } 26 | 27 | if (isNewArchitectureEnabled()) { 28 | apply plugin: "com.facebook.react" 29 | } 30 | apply plugin: 'com.android.library' 31 | apply plugin: 'kotlin-android' 32 | 33 | repositories { 34 | google() 35 | mavenCentral() 36 | } 37 | 38 | android { 39 | // Conditional for compatibility with AGP <4.2. 40 | if (project.android.hasProperty("namespace")) { 41 | namespace = "com.dorianmazur.keychain" 42 | } 43 | compileSdkVersion safeExtGet('compileSdkVersion', 33) 44 | buildToolsVersion safeExtGet('buildToolsVersion', '33.0.0') 45 | 46 | defaultConfig { 47 | minSdkVersion safeExtGet('minSdkVersion', 23) 48 | compileSdkVersion safeExtGet('compileSdkVersion', 33) 49 | targetSdkVersion safeExtGet('targetSdkVersion', 33) 50 | buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() 51 | } 52 | 53 | compileOptions { 54 | sourceCompatibility JavaVersion.VERSION_1_8 55 | targetCompatibility JavaVersion.VERSION_1_8 56 | } 57 | 58 | buildFeatures { 59 | buildConfig true 60 | } 61 | 62 | lintOptions { 63 | disable "GradleCompatible" 64 | abortOnError false 65 | } 66 | 67 | testOptions { 68 | unitTests { 69 | includeAndroidResources = true 70 | } 71 | } 72 | } 73 | 74 | repositories { 75 | mavenCentral() 76 | } 77 | 78 | def kotlin_version = safeExtGet('kotlinVersion', '1.8.0') 79 | 80 | dependencies { 81 | //noinspection GradleDynamicVersion 82 | implementation "com.facebook.react:react-android:+" 83 | 84 | implementation 'androidx.appcompat:appcompat:1.6.1' 85 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 86 | 87 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 88 | 89 | // Used to store encrypted data 90 | implementation("androidx.datastore:datastore-preferences:1.0.0") 91 | 92 | // https://mvnrepository.com/artifact/androidx.biometric/biometric 93 | implementation 'androidx.biometric:biometric:1.1.0@aar' 94 | 95 | // https://mvnrepository.com/artifact/androidx.lifecycle/lifecycle-viewmodel 96 | // Needed for BiometricPrompt in androidx.biometric 97 | implementation "androidx.lifecycle:lifecycle-viewmodel:2.3.1@aar" 98 | 99 | // https://mvnrepository.com/artifact/androidx.fragment/fragment 100 | // Needed for BiometricPrompt in androidx.biometric 101 | implementation "androidx.fragment:fragment:1.3.2@aar" 102 | 103 | /* version higher 1.1.3 has problems with included soloader packages, 104 | https://github.com/facebook/conceal/releases */ 105 | implementation "com.facebook.conceal:conceal:1.1.3@aar" 106 | } 107 | 108 | if (isNewArchitectureEnabled()) { 109 | react { 110 | jsRootDir = file("../src/") 111 | libraryName = "RNKeychainManager" 112 | codegenJavaPackageName = "com.dorianmazur.keychain" 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx3g -Dkotlin.daemon.jvm.options\="-Xmx3g" -Dfile.encoding=UTF-8 2 | org.gradle.parallel=true 3 | org.gradle.daemon=true 4 | # AndroidX package structure to make it clearer which packages are bundled with the 5 | # Android operating system, and which are packaged with your app's APK 6 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 7 | android.useAndroidX=true 8 | # Automatically convert third-party libraries to use AndroidX 9 | android.enableJetifier=true 10 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DorianMazur/react-native-keychain-manager/c0b092b26c9aa40ee16accec35a762fdf4dd6811/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'react-native-keychain-manager' 2 | 3 | include(":library") 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/DataStorePrefsStorage.kt: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain 2 | 3 | import android.content.Context 4 | import android.util.Base64 5 | import androidx.datastore.core.DataMigration 6 | import androidx.datastore.core.DataStore 7 | import androidx.datastore.preferences.SharedPreferencesMigration 8 | import androidx.datastore.preferences.core.Preferences 9 | import androidx.datastore.preferences.core.edit 10 | import androidx.datastore.preferences.core.stringPreferencesKey 11 | import androidx.datastore.preferences.preferencesDataStore 12 | import com.facebook.react.bridge.ReactApplicationContext 13 | import com.dorianmazur.keychain.KeychainModule.KnownCiphers 14 | import com.dorianmazur.keychain.PrefsStorage.KEYCHAIN_DATA 15 | import com.dorianmazur.keychain.PrefsStorage.ResultSet 16 | import com.dorianmazur.keychain.PrefsStorage.getKeyForCipherStorage 17 | import com.dorianmazur.keychain.PrefsStorage.getKeyForPassword 18 | import com.dorianmazur.keychain.PrefsStorage.getKeyForUsername 19 | import com.dorianmazur.keychain.PrefsStorage.isKeyForCipherStorage 20 | import com.dorianmazur.keychain.cipherStorage.CipherStorage 21 | import kotlinx.coroutines.flow.first 22 | import kotlinx.coroutines.runBlocking 23 | 24 | @Suppress("unused") 25 | class DataStorePrefsStorage(reactContext: ReactApplicationContext) : PrefsStorage { 26 | 27 | private val Context.prefs: DataStore by preferencesDataStore( 28 | name = KEYCHAIN_DATA, 29 | produceMigrations = ::sharedPreferencesMigration 30 | ) 31 | private val prefs: DataStore = reactContext.prefs 32 | private val prefsData: Preferences get() = callSuspendable { prefs.data.first() } 33 | 34 | private fun sharedPreferencesMigration(context: Context): List> { 35 | return listOf(SharedPreferencesMigration(context, KEYCHAIN_DATA)) 36 | } 37 | 38 | override fun getEncryptedEntry(service: String): ResultSet? { 39 | val bytesForUsername = getBytesForUsername(service) 40 | val bytesForPassword = getBytesForPassword(service) 41 | var cipherStorageName = getCipherStorageName(service) 42 | 43 | // in case of wrong password or username 44 | if (bytesForUsername == null || bytesForPassword == null) return null 45 | if (cipherStorageName == null) { 46 | // If the CipherStorage name is not found, we assume it is because the entry was written by an older 47 | // version of this library. The older version used Facebook Conceal, so we default to that. 48 | cipherStorageName = KnownCiphers.FB 49 | } 50 | return ResultSet(cipherStorageName, bytesForUsername, bytesForPassword) 51 | } 52 | 53 | override fun removeEntry(service: String) { 54 | val keyForUsername = stringPreferencesKey(getKeyForUsername(service)) 55 | val keyForPassword = stringPreferencesKey(getKeyForPassword(service)) 56 | val keyForCipherStorage = stringPreferencesKey(getKeyForCipherStorage(service)) 57 | callSuspendable { 58 | prefs.edit { 59 | it.remove(keyForUsername) 60 | it.remove(keyForPassword) 61 | it.remove(keyForCipherStorage) 62 | } 63 | } 64 | } 65 | 66 | override fun storeEncryptedEntry( 67 | service: String, 68 | encryptionResult: CipherStorage.EncryptionResult, 69 | ) { 70 | val keyForUsername = stringPreferencesKey(getKeyForUsername(service)) 71 | val keyForPassword = stringPreferencesKey(getKeyForPassword(service)) 72 | val keyForCipherStorage = stringPreferencesKey(getKeyForCipherStorage(service)) 73 | callSuspendable { 74 | prefs.edit { 75 | it[keyForUsername] = Base64.encodeToString(encryptionResult.username, Base64.DEFAULT) 76 | it[keyForPassword] = Base64.encodeToString(encryptionResult.password, Base64.DEFAULT) 77 | it[keyForCipherStorage] = encryptionResult.cipherName 78 | } 79 | } 80 | } 81 | 82 | override fun getUsedCipherNames(): Set { 83 | val result: MutableSet = HashSet() 84 | val keys = prefsData.asMap().keys.map { it.name } 85 | for (key in keys) { 86 | if (isKeyForCipherStorage(key)) { 87 | val cipher = prefsData[stringPreferencesKey(key)] 88 | result.add(cipher) 89 | } 90 | } 91 | return result 92 | } 93 | 94 | private fun callSuspendable(block: suspend () -> T): T { 95 | return runBlocking { 96 | block() 97 | } 98 | } 99 | 100 | private fun getBytesForUsername(service: String): ByteArray? { 101 | val key = stringPreferencesKey(getKeyForUsername(service)) 102 | return getBytes(key) 103 | } 104 | 105 | private fun getBytesForPassword(service: String): ByteArray? { 106 | val key = stringPreferencesKey(getKeyForPassword(service)) 107 | return getBytes(key) 108 | } 109 | 110 | private fun getCipherStorageName(service: String): String? { 111 | val key = stringPreferencesKey(getKeyForCipherStorage(service)) 112 | return prefsData[key] 113 | } 114 | 115 | private fun getBytes(prefKey: Preferences.Key): ByteArray? { 116 | return prefsData[prefKey]?.let { Base64.decode(it, Base64.DEFAULT) } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/DeviceAvailability.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain; 2 | 3 | import android.Manifest; 4 | import android.annotation.TargetApi; 5 | import android.app.KeyguardManager; 6 | import android.content.Context; 7 | import android.content.pm.PackageManager; 8 | import android.os.Build; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.biometric.BiometricManager; 12 | 13 | import static android.content.pm.PackageManager.PERMISSION_GRANTED; 14 | import static androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS; 15 | 16 | /** 17 | * @see Biometric hradware 18 | */ 19 | @SuppressWarnings({"WeakerAccess", "deprecation"}) 20 | public class DeviceAvailability { 21 | public static boolean isStrongBiometricAuthAvailable(@NonNull final Context context) { 22 | return BiometricManager.from(context).canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BIOMETRIC_SUCCESS; 23 | } 24 | 25 | public static boolean isFingerprintAuthAvailable(@NonNull final Context context) { 26 | return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT); 27 | } 28 | 29 | public static boolean isFaceAuthAvailable(@NonNull final Context context) { 30 | return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE); 31 | } 32 | 33 | public static boolean isIrisAuthAvailable(@NonNull final Context context) { 34 | return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_IRIS); 35 | } 36 | 37 | /** Check is permissions granted for biometric things. */ 38 | public static boolean isPermissionsGranted(@NonNull final Context context) { 39 | // before api23 no permissions for biometric, no hardware == no permissions 40 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 41 | return false; 42 | } 43 | 44 | final KeyguardManager km = 45 | (KeyguardManager) context.getSystemService(Context.KEYGUARD_SERVICE); 46 | if( !km.isKeyguardSecure() ) return false; 47 | 48 | // api28+ 49 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 50 | return context.checkSelfPermission(Manifest.permission.USE_BIOMETRIC) == PERMISSION_GRANTED; 51 | } 52 | 53 | // before api28 54 | return context.checkSelfPermission(Manifest.permission.USE_FINGERPRINT) == PERMISSION_GRANTED; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/KeychainModuleBuilder.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain; 2 | 3 | import com.facebook.react.bridge.ReactApplicationContext; 4 | 5 | public class KeychainModuleBuilder { 6 | public static final boolean DEFAULT_USE_WARM_UP = true; 7 | 8 | private ReactApplicationContext reactContext; 9 | private boolean useWarmUp = DEFAULT_USE_WARM_UP; 10 | 11 | public KeychainModuleBuilder withReactContext(ReactApplicationContext reactContext) { 12 | this.reactContext = reactContext; 13 | return this; 14 | } 15 | 16 | public KeychainModuleBuilder usingWarmUp() { 17 | useWarmUp = true; 18 | return this; 19 | } 20 | 21 | public KeychainModuleBuilder withoutWarmUp() { 22 | useWarmUp = false; 23 | return this; 24 | } 25 | 26 | public KeychainModule build() { 27 | validate(); 28 | if (useWarmUp) { 29 | return KeychainModule.withWarming(reactContext); 30 | } else { 31 | return new KeychainModule(reactContext); 32 | } 33 | } 34 | 35 | private void validate() { 36 | if (reactContext == null) { 37 | throw new Error("React Context was not provided"); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/KeychainPackage.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.facebook.react.ReactPackage; 6 | import com.facebook.react.bridge.JavaScriptModule; 7 | import com.facebook.react.bridge.NativeModule; 8 | import com.facebook.react.bridge.ReactApplicationContext; 9 | import com.facebook.react.uimanager.ViewManager; 10 | 11 | import java.util.Collections; 12 | import java.util.List; 13 | 14 | @SuppressWarnings("unused") 15 | public class KeychainPackage implements ReactPackage { 16 | 17 | private final KeychainModuleBuilder builder; 18 | 19 | public KeychainPackage() { 20 | this(new KeychainModuleBuilder()); 21 | } 22 | 23 | public KeychainPackage(KeychainModuleBuilder builder) { 24 | this.builder = builder; 25 | } 26 | 27 | @Override 28 | @NonNull 29 | public List createNativeModules(@NonNull final ReactApplicationContext reactContext) { 30 | return Collections.singletonList(builder.withReactContext(reactContext).build()); 31 | } 32 | 33 | @NonNull 34 | public List> createJSModules() { 35 | return Collections.emptyList(); 36 | } 37 | 38 | @Override 39 | @NonNull 40 | public List createViewManagers(@NonNull final ReactApplicationContext reactContext) { 41 | return Collections.emptyList(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/PrefsStorage.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import com.dorianmazur.keychain.cipherStorage.CipherStorage; 7 | 8 | import java.util.Set; 9 | 10 | public interface PrefsStorage { 11 | String KEYCHAIN_DATA = "RN_KEYCHAIN"; 12 | 13 | class ResultSet extends CipherStorage.CipherResult { 14 | @KeychainModule.KnownCiphers 15 | public final String cipherStorageName; 16 | 17 | public ResultSet(@KeychainModule.KnownCiphers final String cipherStorageName, final byte[] usernameBytes, final byte[] passwordBytes) { 18 | super(usernameBytes, passwordBytes); 19 | 20 | this.cipherStorageName = cipherStorageName; 21 | } 22 | } 23 | 24 | @Nullable 25 | ResultSet getEncryptedEntry(@NonNull final String service); 26 | 27 | void removeEntry(@NonNull final String service); 28 | 29 | void storeEncryptedEntry(@NonNull final String service, @NonNull final CipherStorage.EncryptionResult encryptionResult); 30 | 31 | /** 32 | * List all types of cipher which are involved in en/decryption of the data stored herein. 33 | * A cipher type is stored together with the datum upon encryption so the datum can later be decrypted using correct 34 | * cipher. This way, a [PrefsStorageBase] can involve different ciphers for different data. This method returns all 35 | * ciphers involved with this storage. 36 | * 37 | * @return set of cipher names 38 | */ 39 | Set getUsedCipherNames(); 40 | 41 | @NonNull 42 | static String getKeyForUsername(@NonNull final String service) { 43 | return service + ":" + "u"; 44 | } 45 | 46 | @NonNull 47 | static String getKeyForPassword(@NonNull final String service) { 48 | return service + ":" + "p"; 49 | } 50 | 51 | @NonNull 52 | static String getKeyForCipherStorage(@NonNull final String service) { 53 | return service + ":" + "c"; 54 | } 55 | 56 | static boolean isKeyForCipherStorage(@NonNull final String key) { 57 | return key.endsWith(":c"); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/SecurityLevel.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | /** Minimal required level of the security implementation. */ 6 | public enum SecurityLevel { 7 | /** No security guarantees needed (default value); Credentials can be stored in FB Secure Storage */ 8 | ANY, 9 | /** Requires for the key to be stored in the Android Keystore, separate from the encrypted data. */ 10 | SECURE_SOFTWARE, 11 | /** Requires for the key to be stored on a secure hardware (Trusted Execution Environment or Secure Environment). */ 12 | SECURE_HARDWARE; 13 | 14 | /** Get JavaScript friendly name. */ 15 | @NonNull 16 | public String jsName() { 17 | return String.format("SECURITY_LEVEL_%s", this.name()); 18 | } 19 | 20 | public boolean satisfiesSafetyThreshold(@NonNull final SecurityLevel threshold) { 21 | return this.compareTo(threshold) >= 0; 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/cipherStorage/CipherStorage.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain.cipherStorage; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import com.dorianmazur.keychain.SecurityLevel; 7 | import com.dorianmazur.keychain.decryptionHandler.DecryptionResultHandler; 8 | import com.dorianmazur.keychain.exceptions.CryptoFailedException; 9 | import com.dorianmazur.keychain.exceptions.KeyStoreAccessException; 10 | 11 | import java.security.Key; 12 | import java.util.Set; 13 | 14 | @SuppressWarnings({"unused", "WeakerAccess"}) 15 | public interface CipherStorage { 16 | //region Helper classes 17 | 18 | /** basis for storing credentials in different data type formats. */ 19 | abstract class CipherResult { 20 | public final T username; 21 | public final T password; 22 | 23 | public CipherResult(final T username, final T password) { 24 | this.username = username; 25 | this.password = password; 26 | } 27 | } 28 | 29 | /** Credentials in bytes array, often a result of encryption. */ 30 | class EncryptionResult extends CipherResult { 31 | /** Name of used for encryption cipher storage. */ 32 | public final String cipherName; 33 | 34 | /** Main constructor. */ 35 | public EncryptionResult(final byte[] username, final byte[] password, final String cipherName) { 36 | super(username, password); 37 | this.cipherName = cipherName; 38 | } 39 | 40 | /** Helper constructor. Simplifies cipher name extraction. */ 41 | public EncryptionResult(final byte[] username, final byte[] password, @NonNull final CipherStorage cipherStorage) { 42 | this(username, password, cipherStorage.getCipherStorageName()); 43 | } 44 | } 45 | 46 | /** Credentials in string's, often a result of decryption. */ 47 | class DecryptionResult extends CipherResult { 48 | private final SecurityLevel securityLevel; 49 | 50 | public DecryptionResult(final String username, final String password) { 51 | this(username, password, SecurityLevel.ANY); 52 | } 53 | 54 | public DecryptionResult(final String username, final String password, final SecurityLevel level) { 55 | super(username, password); 56 | securityLevel = level; 57 | } 58 | 59 | public SecurityLevel getSecurityLevel() { 60 | return securityLevel; 61 | } 62 | } 63 | 64 | /** Ask access permission for decrypting credentials in provided context. */ 65 | class DecryptionContext extends CipherResult { 66 | public final Key key; 67 | public final String keyAlias; 68 | 69 | public DecryptionContext(@NonNull final String keyAlias, 70 | @NonNull final Key key, 71 | @NonNull final byte[] password, 72 | @NonNull final byte[] username) { 73 | super(username, password); 74 | this.keyAlias = keyAlias; 75 | this.key = key; 76 | } 77 | } 78 | 79 | //region API 80 | 81 | /** Encrypt credentials with provided key (by alias) and required security level. */ 82 | @NonNull 83 | EncryptionResult encrypt(@NonNull final String alias, 84 | @NonNull final String username, 85 | @NonNull final String password, 86 | @NonNull final SecurityLevel level) 87 | throws CryptoFailedException; 88 | 89 | /** 90 | * Decrypt credentials with provided key (by alias) and required security level. 91 | * In case of key stored in weaker security level than required will be raised exception. 92 | * That can happens during migration from one version of library to another. 93 | */ 94 | @NonNull 95 | DecryptionResult decrypt(@NonNull final String alias, 96 | @NonNull final byte[] username, 97 | @NonNull final byte[] password, 98 | @NonNull final SecurityLevel level) 99 | throws CryptoFailedException; 100 | 101 | /** Decrypt the credentials but redirect results of operation to handler. */ 102 | void decrypt(@NonNull final DecryptionResultHandler handler, 103 | @NonNull final String alias, 104 | @NonNull final byte[] username, 105 | @NonNull final byte[] password, 106 | @NonNull final SecurityLevel level) 107 | throws CryptoFailedException; 108 | 109 | /** Remove key (by alias) from storage. */ 110 | void removeKey(@NonNull final String alias) throws KeyStoreAccessException; 111 | 112 | /** 113 | * Return all keys present in this storage. 114 | * @return key aliases 115 | */ 116 | Set getAllKeys() throws KeyStoreAccessException; 117 | 118 | //endregion 119 | 120 | //region Configuration 121 | 122 | /** Storage name. */ 123 | String getCipherStorageName(); 124 | 125 | /** Minimal API level needed for using the storage. */ 126 | int getMinSupportedApiLevel(); 127 | 128 | /** Provided security level. */ 129 | SecurityLevel securityLevel(); 130 | 131 | /** True - based on secured hardware capabilities, otherwise False. */ 132 | boolean supportsSecureHardware(); 133 | 134 | /** True - based on biometric capabilities, otherwise false. */ 135 | boolean isBiometrySupported(); 136 | 137 | /** 138 | * The higher value means better capabilities. 139 | * Formula: 140 | * = 1000 * isBiometrySupported() + 141 | * 100 * isSecureHardware() + 142 | * minSupportedApiLevel() 143 | */ 144 | int getCapabilityLevel(); 145 | 146 | /** Get default name for alias/service. */ 147 | String getDefaultAliasServiceName(); 148 | //endregion 149 | } 150 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/cipherStorage/CipherStorageBase.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain.cipherStorage; 2 | 3 | import android.os.Build; 4 | import android.security.keystore.KeyGenParameterSpec; 5 | import android.security.keystore.KeyInfo; 6 | import android.text.TextUtils; 7 | import android.util.Log; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | import androidx.annotation.VisibleForTesting; 12 | 13 | import com.dorianmazur.keychain.SecurityLevel; 14 | import com.dorianmazur.keychain.exceptions.CryptoFailedException; 15 | import com.dorianmazur.keychain.exceptions.KeyStoreAccessException; 16 | 17 | import java.io.ByteArrayInputStream; 18 | import java.io.ByteArrayOutputStream; 19 | import java.io.Closeable; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.io.OutputStream; 23 | import java.nio.charset.Charset; 24 | import java.security.GeneralSecurityException; 25 | import java.security.Key; 26 | import java.security.KeyStore; 27 | import java.security.KeyStoreException; 28 | import java.security.NoSuchAlgorithmException; 29 | import java.security.ProviderException; 30 | import java.security.UnrecoverableKeyException; 31 | import java.util.Collections; 32 | import java.util.Enumeration; 33 | import java.util.HashSet; 34 | import java.util.Set; 35 | import java.util.concurrent.atomic.AtomicBoolean; 36 | import java.util.concurrent.atomic.AtomicInteger; 37 | 38 | import javax.crypto.Cipher; 39 | import javax.crypto.CipherInputStream; 40 | import javax.crypto.CipherOutputStream; 41 | import javax.crypto.NoSuchPaddingException; 42 | import javax.crypto.spec.IvParameterSpec; 43 | 44 | import static com.dorianmazur.keychain.SecurityLevel.SECURE_HARDWARE; 45 | 46 | @SuppressWarnings({"unused", "WeakerAccess", "CharsetObjectCanBeUsed", "UnusedReturnValue"}) 47 | abstract public class CipherStorageBase implements CipherStorage { 48 | //region Constants 49 | /** Logging tag. */ 50 | protected static final String LOG_TAG = CipherStorageBase.class.getSimpleName(); 51 | /** Default key storage type/name. */ 52 | public static final String KEYSTORE_TYPE = "AndroidKeyStore"; 53 | /** Key used for testing storage capabilities. */ 54 | public static final String TEST_KEY_ALIAS = KEYSTORE_TYPE + "#supportsSecureHardware"; 55 | /** Size of hash calculation buffer. Default: 4Kb. */ 56 | private static final int BUFFER_SIZE = 4 * 1024; 57 | /** Default size of read/write operation buffer. Default: 16Kb. */ 58 | private static final int BUFFER_READ_WRITE_SIZE = 4 * BUFFER_SIZE; 59 | /** Default charset encoding. */ 60 | public static final Charset UTF8 = Charset.forName("UTF-8"); 61 | //endregion 62 | 63 | //region Members 64 | /** Guard object for {@link #isSupportsSecureHardware} field. */ 65 | protected final Object _sync = new Object(); 66 | /** Try to resolve it only once and cache result for all future calls. */ 67 | protected transient AtomicBoolean isSupportsSecureHardware; 68 | /** Guard for {@link #isStrongboxAvailable} field assignment. */ 69 | protected final Object _syncStrongbox = new Object(); 70 | /** Try to resolve support of the strongbox and cache result for future calls. */ 71 | protected transient AtomicBoolean isStrongboxAvailable; 72 | /** Get cached instance of cipher. Get instance operation is slow. */ 73 | protected transient Cipher cachedCipher; 74 | /** Cached instance of the Keystore. */ 75 | protected transient KeyStore cachedKeyStore; 76 | //endregion 77 | 78 | //region Overrides 79 | 80 | /** Hardware supports keystore operations. */ 81 | @Override 82 | public SecurityLevel securityLevel() { 83 | return SecurityLevel.SECURE_HARDWARE; 84 | } 85 | 86 | /** 87 | * The higher value means better capabilities. Range: [19..1129]. 88 | * Formula: `1000 * isBiometrySupported() + 100 * isSecureHardware() + minSupportedApiLevel()` 89 | */ 90 | @Override 91 | public final int getCapabilityLevel() { 92 | // max: 1000 + 100 + 29 == 1129 93 | // min: 0000 + 000 + 19 == 0019 94 | 95 | return 96 | (1000 * (isBiometrySupported() ? 1 : 0)) + // 0..1000 97 | (getMinSupportedApiLevel()); // 19..29 98 | } 99 | 100 | /** Try device capabilities by creating temporary key in keystore. */ 101 | @Override 102 | public boolean supportsSecureHardware() { 103 | if (null != isSupportsSecureHardware) return isSupportsSecureHardware.get(); 104 | 105 | synchronized (_sync) { 106 | // double check pattern in use 107 | if (null != isSupportsSecureHardware) return isSupportsSecureHardware.get(); 108 | 109 | isSupportsSecureHardware = new AtomicBoolean(false); 110 | 111 | SelfDestroyKey sdk = null; 112 | 113 | // auto-closable supported from api18 only, our minimal is api16 114 | //noinspection TryFinallyCanBeTryWithResources 115 | try { 116 | sdk = new SelfDestroyKey(TEST_KEY_ALIAS); 117 | final boolean newValue = validateKeySecurityLevel(SECURE_HARDWARE, sdk.key); 118 | 119 | isSupportsSecureHardware.set(newValue); 120 | } catch (Throwable ignored) { 121 | } finally { 122 | if (null != sdk) sdk.close(); 123 | } 124 | } 125 | 126 | return isSupportsSecureHardware.get(); 127 | } 128 | 129 | /** {@inheritDoc} */ 130 | @Override 131 | public String getDefaultAliasServiceName() { 132 | return getCipherStorageName(); 133 | } 134 | 135 | /** Remove key with provided name from security storage. */ 136 | @Override 137 | public void removeKey(@NonNull final String alias) throws KeyStoreAccessException { 138 | final String safeAlias = getDefaultAliasIfEmpty(alias, getDefaultAliasServiceName()); 139 | final KeyStore ks = getKeyStoreAndLoad(); 140 | 141 | try { 142 | if (ks.containsAlias(safeAlias)) { 143 | ks.deleteEntry(safeAlias); 144 | } 145 | } catch (GeneralSecurityException ignored) { 146 | /* only one exception can be raised by code: 'KeyStore is not loaded' */ 147 | } 148 | } 149 | 150 | @Override 151 | public Set getAllKeys() throws KeyStoreAccessException { 152 | final KeyStore ks = getKeyStoreAndLoad(); 153 | try { 154 | Enumeration aliases = ks.aliases(); 155 | return new HashSet<>(Collections.list(aliases)); 156 | 157 | } catch (KeyStoreException e) { 158 | throw new KeyStoreAccessException("Error accessing aliases in keystore " + ks, e); 159 | } 160 | } 161 | 162 | //endregion 163 | 164 | //region Abstract methods 165 | 166 | /** Get encryption algorithm specification builder instance. */ 167 | @NonNull 168 | protected abstract KeyGenParameterSpec.Builder getKeyGenSpecBuilder(@NonNull final String alias) 169 | throws GeneralSecurityException; 170 | 171 | /** Get encryption algorithm specification builder instance. */ 172 | @NonNull 173 | protected abstract KeyGenParameterSpec.Builder getKeyGenSpecBuilder(@NonNull final String alias, @NonNull final boolean isforTesting) 174 | throws GeneralSecurityException; 175 | 176 | 177 | /** Get information about provided key. */ 178 | @NonNull 179 | protected abstract KeyInfo getKeyInfo(@NonNull final Key key) throws GeneralSecurityException; 180 | 181 | /** Try to generate key from provided specification. */ 182 | @NonNull 183 | protected abstract Key generateKey(@NonNull final KeyGenParameterSpec spec) 184 | throws GeneralSecurityException; 185 | 186 | /** Get name of the required encryption algorithm. */ 187 | @NonNull 188 | protected abstract String getEncryptionAlgorithm(); 189 | 190 | /** Get transformation algorithm for encrypt/decrypt operations. */ 191 | @NonNull 192 | protected abstract String getEncryptionTransformation(); 193 | //endregion 194 | 195 | //region Implementation 196 | 197 | /** Get cipher instance and cache it for any next call. */ 198 | @NonNull 199 | public Cipher getCachedInstance() throws NoSuchAlgorithmException, NoSuchPaddingException { 200 | if (null == cachedCipher) { 201 | synchronized (this) { 202 | if (null == cachedCipher) { 203 | cachedCipher = Cipher.getInstance(getEncryptionTransformation()); 204 | } 205 | } 206 | } 207 | 208 | return cachedCipher; 209 | } 210 | 211 | /** Check requirements to the security level. */ 212 | protected void throwIfInsufficientLevel(@NonNull final SecurityLevel level) 213 | throws CryptoFailedException { 214 | 215 | if (!securityLevel().satisfiesSafetyThreshold(level)) { 216 | throw new CryptoFailedException(String.format( 217 | "Insufficient security level (wants %s; got %s)", 218 | level, securityLevel())); 219 | } 220 | } 221 | 222 | /** Extract existing key or generate a new one. In case of problems raise exception. */ 223 | @NonNull 224 | protected Key extractGeneratedKey(@NonNull final String safeAlias, 225 | @NonNull final SecurityLevel level, 226 | @NonNull final AtomicInteger retries) 227 | throws GeneralSecurityException { 228 | Key key; 229 | 230 | do { 231 | final KeyStore keyStore = getKeyStoreAndLoad(); 232 | 233 | // if key is not available yet, try to generate the strongest possible 234 | if (!keyStore.containsAlias(safeAlias)) { 235 | generateKeyAndStoreUnderAlias(safeAlias, level); 236 | } 237 | 238 | // throw exception if cannot extract key in several retries 239 | key = extractKey(keyStore, safeAlias, retries); 240 | } while (null == key); 241 | 242 | return key; 243 | } 244 | 245 | /** Try to extract key by alias from keystore, in case of 'known android bug' reduce retry counter. */ 246 | @Nullable 247 | protected Key extractKey(@NonNull final KeyStore keyStore, 248 | @NonNull final String safeAlias, 249 | @NonNull final AtomicInteger retry) 250 | throws GeneralSecurityException { 251 | final Key key; 252 | 253 | // Fix for android.security.KeyStoreException: Invalid key blob 254 | // more info: https://stackoverflow.com/questions/36488219/android-security-keystoreexception-invalid-key-blob/36846085#36846085 255 | try { 256 | key = keyStore.getKey(safeAlias, null); 257 | } catch (final UnrecoverableKeyException ex) { 258 | // try one more time 259 | if (retry.getAndDecrement() > 0) { 260 | keyStore.deleteEntry(safeAlias); 261 | 262 | return null; 263 | } 264 | 265 | throw ex; 266 | } 267 | 268 | // null if the given alias does not exist or does not identify a key-related entry. 269 | if (null == key) { 270 | throw new KeyStoreAccessException("Empty key extracted!"); 271 | } 272 | 273 | return key; 274 | } 275 | 276 | /** Verify that provided key satisfy minimal needed level. */ 277 | protected boolean validateKeySecurityLevel(@NonNull final SecurityLevel level, 278 | @NonNull final Key key) 279 | throws GeneralSecurityException { 280 | 281 | return getSecurityLevel(key) 282 | .satisfiesSafetyThreshold(level); 283 | } 284 | 285 | /** Get the supported level of security for provided Key instance. */ 286 | @NonNull 287 | protected SecurityLevel getSecurityLevel(@NonNull final Key key) throws GeneralSecurityException { 288 | final KeyInfo keyInfo = getKeyInfo(key); 289 | 290 | // lower API23 we don't have any hardware support 291 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 292 | final boolean insideSecureHardware = keyInfo.isInsideSecureHardware(); 293 | 294 | if (insideSecureHardware) { 295 | return SECURE_HARDWARE; 296 | } 297 | } 298 | 299 | return SecurityLevel.SECURE_SOFTWARE; 300 | } 301 | 302 | /** Load key store. */ 303 | @NonNull 304 | public KeyStore getKeyStoreAndLoad() throws KeyStoreAccessException { 305 | if (null == cachedKeyStore) { 306 | synchronized (this) { 307 | if (null == cachedKeyStore) { 308 | // initialize instance 309 | try { 310 | final KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE); 311 | keyStore.load(null); 312 | 313 | cachedKeyStore = keyStore; 314 | } catch (final Throwable fail) { 315 | throw new KeyStoreAccessException("Could not access Keystore", fail); 316 | } 317 | } 318 | } 319 | } 320 | 321 | return cachedKeyStore; 322 | } 323 | 324 | /** Default encryption with cipher without initialization vector. */ 325 | @NonNull 326 | public byte[] encryptString(@NonNull final Key key, @NonNull final String value) 327 | throws IOException, GeneralSecurityException { 328 | 329 | return encryptString(key, value, Defaults.encrypt); 330 | } 331 | 332 | /** Default decryption with cipher without initialization vector. */ 333 | @NonNull 334 | public String decryptBytes(@NonNull final Key key, @NonNull final byte[] bytes) 335 | throws IOException, GeneralSecurityException { 336 | 337 | return decryptBytes(key, bytes, Defaults.decrypt); 338 | } 339 | 340 | /** Encrypt provided string value. */ 341 | @NonNull 342 | protected byte[] encryptString(@NonNull final Key key, @NonNull final String value, 343 | @Nullable final EncryptStringHandler handler) 344 | throws IOException, GeneralSecurityException { 345 | 346 | final Cipher cipher = getCachedInstance(); 347 | 348 | // encrypt the value using a CipherOutputStream 349 | try (final ByteArrayOutputStream output = new ByteArrayOutputStream()) { 350 | 351 | // write initialization vector to the beginning of the stream 352 | if (null != handler) { 353 | handler.initialize(cipher, key, output); 354 | output.flush(); 355 | } 356 | 357 | try (final CipherOutputStream encrypt = new CipherOutputStream(output, cipher)) { 358 | encrypt.write(value.getBytes(UTF8)); 359 | } 360 | 361 | return output.toByteArray(); 362 | } catch (Throwable fail) { 363 | Log.e(LOG_TAG, fail.getMessage(), fail); 364 | 365 | throw fail; 366 | } 367 | } 368 | 369 | /** Decrypt provided bytes to a string. */ 370 | @NonNull 371 | protected String decryptBytes(@NonNull final Key key, @NonNull final byte[] bytes, 372 | @Nullable final DecryptBytesHandler handler) 373 | throws GeneralSecurityException, IOException { 374 | final Cipher cipher = getCachedInstance(); 375 | 376 | // decrypt the bytes using a CipherInputStream 377 | try (ByteArrayInputStream in = new ByteArrayInputStream(bytes); 378 | ByteArrayOutputStream output = new ByteArrayOutputStream()) { 379 | 380 | // read the initialization vector from the beginning of the stream 381 | if (null != handler) { 382 | handler.initialize(cipher, key, in); 383 | } 384 | 385 | try (CipherInputStream decrypt = new CipherInputStream(in, cipher)) { 386 | copy(decrypt, output); 387 | } 388 | 389 | return new String(output.toByteArray(), UTF8); 390 | } catch (Throwable fail) { 391 | Log.w(LOG_TAG, fail.getMessage(), fail); 392 | 393 | throw fail; 394 | } 395 | } 396 | 397 | /** Get the most secured keystore */ 398 | public void generateKeyAndStoreUnderAlias(@NonNull final String alias, 399 | @NonNull final SecurityLevel requiredLevel) 400 | throws GeneralSecurityException { 401 | 402 | // Firstly, try to generate the key as safe as possible (strongbox). 403 | // see https://developer.android.com/training/articles/keystore#HardwareSecurityModule 404 | 405 | Key secretKey = null; 406 | 407 | // multi-threaded usage is possible 408 | synchronized (_syncStrongbox) { 409 | if (null == isStrongboxAvailable || isStrongboxAvailable.get()) { 410 | if (null == isStrongboxAvailable) isStrongboxAvailable = new AtomicBoolean(false); 411 | 412 | try { 413 | secretKey = tryGenerateStrongBoxSecurityKey(alias); 414 | 415 | isStrongboxAvailable.set(true); 416 | } catch (GeneralSecurityException | ProviderException ex) { 417 | Log.w(LOG_TAG, "StrongBox security storage is not available.", ex); 418 | } 419 | } 420 | } 421 | 422 | // If that is not possible, we generate the key in a regular way 423 | // (it still might be generated in hardware, but not in StrongBox) 424 | if (null == secretKey || !isStrongboxAvailable.get()) { 425 | try { 426 | secretKey = tryGenerateRegularSecurityKey(alias); 427 | } catch (GeneralSecurityException fail) { 428 | Log.e(LOG_TAG, "Regular security storage is not available.", fail); 429 | throw fail; 430 | } 431 | } 432 | 433 | if (!validateKeySecurityLevel(requiredLevel, secretKey)) { 434 | throw new CryptoFailedException("Cannot generate keys with required security guarantees"); 435 | } 436 | } 437 | 438 | /** Try to get secured keystore instance. */ 439 | @NonNull 440 | protected Key tryGenerateRegularSecurityKey(@NonNull final String alias) throws GeneralSecurityException { 441 | return tryGenerateRegularSecurityKey(alias, false); 442 | } 443 | @NonNull 444 | protected Key tryGenerateRegularSecurityKey(@NonNull final String alias, @NonNull final boolean isForTesting) 445 | throws GeneralSecurityException { 446 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 447 | throw new KeyStoreAccessException("Regular security keystore is not supported " + 448 | "for old API" + Build.VERSION.SDK_INT + "."); 449 | } 450 | 451 | final KeyGenParameterSpec specification = getKeyGenSpecBuilder(alias, isForTesting) 452 | .build(); 453 | 454 | return generateKey(specification); 455 | } 456 | 457 | /** Try to get strong secured keystore instance. (StrongBox security chip) */ 458 | @NonNull 459 | protected Key tryGenerateStrongBoxSecurityKey(@NonNull final String alias) throws GeneralSecurityException{ 460 | return tryGenerateStrongBoxSecurityKey(alias,false); 461 | } 462 | 463 | @NonNull 464 | protected Key tryGenerateStrongBoxSecurityKey(@NonNull final String alias, @NonNull final boolean isForTesting) 465 | throws GeneralSecurityException { 466 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { 467 | throw new KeyStoreAccessException("Strong box security keystore is not supported " + 468 | "for old API" + Build.VERSION.SDK_INT + "."); 469 | } 470 | 471 | final KeyGenParameterSpec specification = getKeyGenSpecBuilder(alias, isForTesting) 472 | .setIsStrongBoxBacked(true) 473 | .build(); 474 | 475 | return generateKey(specification); 476 | } 477 | 478 | //endregion 479 | 480 | //region Testing 481 | 482 | /** Override internal cipher instance cache. */ 483 | @VisibleForTesting 484 | public CipherStorageBase setCipher(final Cipher cipher) { 485 | cachedCipher = cipher; 486 | return this; 487 | } 488 | 489 | /** Override the keystore instance cache. */ 490 | @VisibleForTesting 491 | public CipherStorageBase setKeyStore(final KeyStore keystore) { 492 | cachedKeyStore = keystore; 493 | return this; 494 | } 495 | //endregion 496 | 497 | //region Static methods 498 | 499 | /** Convert provided service name to safe not-null/not-empty value. */ 500 | @NonNull 501 | public static String getDefaultAliasIfEmpty(@Nullable final String service, @NonNull final String fallback) { 502 | //noinspection ConstantConditions 503 | return TextUtils.isEmpty(service) ? fallback : service; 504 | } 505 | 506 | /** 507 | * Copy input stream to output. 508 | * 509 | * @param in instance of input stream. 510 | * @param out instance of output stream. 511 | * @throws IOException read/write operation failure. 512 | */ 513 | public static void copy(@NonNull final InputStream in, @NonNull final OutputStream out) throws IOException { 514 | // Transfer bytes from in to out 515 | final byte[] buf = new byte[BUFFER_READ_WRITE_SIZE]; 516 | int len; 517 | 518 | while ((len = in.read(buf)) > 0) { 519 | out.write(buf, 0, len); 520 | } 521 | } 522 | //endregion 523 | 524 | //region Nested declarations 525 | 526 | /** Generic cipher initialization. */ 527 | public static final class Defaults { 528 | public static final EncryptStringHandler encrypt = (cipher, key, output) -> { 529 | cipher.init(Cipher.ENCRYPT_MODE, key); 530 | }; 531 | 532 | public static final DecryptBytesHandler decrypt = (cipher, key, input) -> { 533 | cipher.init(Cipher.DECRYPT_MODE, key); 534 | }; 535 | } 536 | 537 | /** Initialization vector support. */ 538 | public static final class IV { 539 | /** Encryption/Decryption initialization vector length. */ 540 | public static final int IV_LENGTH = 16; 541 | 542 | /** Save Initialization vector to output stream. */ 543 | public static final EncryptStringHandler encrypt = (cipher, key, output) -> { 544 | cipher.init(Cipher.ENCRYPT_MODE, key); 545 | 546 | final byte[] iv = cipher.getIV(); 547 | output.write(iv, 0, iv.length); 548 | }; 549 | /** Read initialization vector from input stream and configure cipher by it. */ 550 | public static final DecryptBytesHandler decrypt = (cipher, key, input) -> { 551 | final IvParameterSpec iv = readIv(input); 552 | cipher.init(Cipher.DECRYPT_MODE, key, iv); 553 | }; 554 | 555 | /** Extract initialization vector from provided bytes array. */ 556 | @NonNull 557 | public static IvParameterSpec readIv(@NonNull final byte[] bytes) throws IOException { 558 | final byte[] iv = new byte[IV_LENGTH]; 559 | 560 | if (IV_LENGTH >= bytes.length) 561 | throw new IOException("Insufficient length of input data for IV extracting."); 562 | 563 | System.arraycopy(bytes, 0, iv, 0, IV_LENGTH); 564 | 565 | return new IvParameterSpec(iv); 566 | } 567 | 568 | /** Extract initialization vector from provided input stream. */ 569 | @NonNull 570 | public static IvParameterSpec readIv(@NonNull final InputStream inputStream) throws IOException { 571 | final byte[] iv = new byte[IV_LENGTH]; 572 | final int result = inputStream.read(iv, 0, IV_LENGTH); 573 | 574 | if (result != IV_LENGTH) 575 | throw new IOException("Input stream has insufficient data."); 576 | 577 | return new IvParameterSpec(iv); 578 | } 579 | } 580 | 581 | /** Handler for storing cipher configuration in output stream. */ 582 | public interface EncryptStringHandler { 583 | void initialize(@NonNull final Cipher cipher, @NonNull final Key key, @NonNull final OutputStream output) 584 | throws GeneralSecurityException, IOException; 585 | } 586 | 587 | /** Handler for configuring cipher by initialization data from input stream. */ 588 | public interface DecryptBytesHandler { 589 | void initialize(@NonNull final Cipher cipher, @NonNull final Key key, @NonNull final InputStream input) 590 | throws GeneralSecurityException, IOException; 591 | } 592 | 593 | /** Auto remove keystore key. */ 594 | public class SelfDestroyKey implements Closeable { 595 | public final String name; 596 | public final Key key; 597 | 598 | public SelfDestroyKey(@NonNull final String name) throws GeneralSecurityException { 599 | this(name, tryGenerateRegularSecurityKey(name, true)); 600 | } 601 | 602 | public SelfDestroyKey(@NonNull final String name, @NonNull final Key key) { 603 | this.name = name; 604 | this.key = key; 605 | } 606 | 607 | @Override 608 | public void close() { 609 | try { 610 | removeKey(name); 611 | } catch (KeyStoreAccessException ex) { 612 | Log.w(LOG_TAG, "AutoClose remove key failed. Error: " + ex.getMessage(), ex); 613 | } 614 | } 615 | } 616 | //endregion 617 | } 618 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/cipherStorage/CipherStorageFacebookConceal.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain.cipherStorage; 2 | 3 | import android.os.Build; 4 | import android.security.keystore.KeyGenParameterSpec; 5 | import android.security.keystore.KeyInfo; 6 | import android.util.Log; 7 | 8 | import androidx.annotation.NonNull; 9 | 10 | import com.facebook.android.crypto.keychain.AndroidConceal; 11 | import com.facebook.android.crypto.keychain.SharedPrefsBackedKeyChain; 12 | import com.facebook.crypto.Crypto; 13 | import com.facebook.crypto.CryptoConfig; 14 | import com.facebook.crypto.Entity; 15 | import com.facebook.crypto.keychain.KeyChain; 16 | import com.facebook.react.bridge.AssertionException; 17 | import com.facebook.react.bridge.ReactApplicationContext; 18 | import com.dorianmazur.keychain.KeychainModule.KnownCiphers; 19 | import com.dorianmazur.keychain.SecurityLevel; 20 | import com.dorianmazur.keychain.decryptionHandler.DecryptionResultHandler; 21 | import com.dorianmazur.keychain.exceptions.CryptoFailedException; 22 | 23 | import java.security.GeneralSecurityException; 24 | import java.security.Key; 25 | 26 | /** 27 | * @see Conceal Project 28 | * @see Fast Cryptographics 29 | */ 30 | @SuppressWarnings({"unused", "WeakerAccess"}) 31 | public class CipherStorageFacebookConceal extends CipherStorageBase { 32 | public static final String KEYCHAIN_DATA = "RN_KEYCHAIN"; 33 | 34 | private final Crypto crypto; 35 | 36 | public CipherStorageFacebookConceal(@NonNull final ReactApplicationContext reactContext) { 37 | KeyChain keyChain = new SharedPrefsBackedKeyChain(reactContext, CryptoConfig.KEY_256); 38 | 39 | this.crypto = AndroidConceal.get().createDefaultCrypto(keyChain); 40 | } 41 | 42 | //region Configuration 43 | @Override 44 | public String getCipherStorageName() { 45 | return KnownCiphers.FB; 46 | } 47 | 48 | @Override 49 | public int getMinSupportedApiLevel() { 50 | return Build.VERSION_CODES.JELLY_BEAN; 51 | } 52 | 53 | @Override 54 | public SecurityLevel securityLevel() { 55 | return SecurityLevel.ANY; 56 | } 57 | 58 | @Override 59 | public boolean supportsSecureHardware() { 60 | return false; 61 | } 62 | 63 | @Override 64 | public boolean isBiometrySupported() { 65 | return false; 66 | } 67 | //endregion 68 | 69 | //region Overrides 70 | @Override 71 | @NonNull 72 | public EncryptionResult encrypt(@NonNull final String alias, 73 | @NonNull final String username, 74 | @NonNull final String password, 75 | @NonNull final SecurityLevel level) 76 | throws CryptoFailedException { 77 | 78 | throwIfInsufficientLevel(level); 79 | throwIfNoCryptoAvailable(); 80 | 81 | final Entity usernameEntity = createUsernameEntity(alias); 82 | final Entity passwordEntity = createPasswordEntity(alias); 83 | 84 | try { 85 | final byte[] encryptedUsername = crypto.encrypt(username.getBytes(UTF8), usernameEntity); 86 | final byte[] encryptedPassword = crypto.encrypt(password.getBytes(UTF8), passwordEntity); 87 | 88 | return new EncryptionResult( 89 | encryptedUsername, 90 | encryptedPassword, 91 | this); 92 | } catch (Throwable fail) { 93 | throw new CryptoFailedException("Encryption failed for alias: " + alias, fail); 94 | } 95 | } 96 | 97 | @NonNull 98 | @Override 99 | public DecryptionResult decrypt(@NonNull final String alias, 100 | @NonNull final byte[] username, 101 | @NonNull final byte[] password, 102 | @NonNull final SecurityLevel level) 103 | throws CryptoFailedException { 104 | 105 | throwIfInsufficientLevel(level); 106 | throwIfNoCryptoAvailable(); 107 | 108 | final Entity usernameEntity = createUsernameEntity(alias); 109 | final Entity passwordEntity = createPasswordEntity(alias); 110 | 111 | try { 112 | final byte[] decryptedUsername = crypto.decrypt(username, usernameEntity); 113 | final byte[] decryptedPassword = crypto.decrypt(password, passwordEntity); 114 | 115 | return new DecryptionResult( 116 | new String(decryptedUsername, UTF8), 117 | new String(decryptedPassword, UTF8), 118 | SecurityLevel.ANY); 119 | } catch (Throwable fail) { 120 | throw new CryptoFailedException("Decryption failed for alias: " + alias, fail); 121 | } 122 | } 123 | 124 | /** redirect call to default {@link #decrypt(String, byte[], byte[], SecurityLevel)} method. */ 125 | @Override 126 | public void decrypt(@NonNull DecryptionResultHandler handler, 127 | @NonNull String service, 128 | @NonNull byte[] username, 129 | @NonNull byte[] password, 130 | @NonNull final SecurityLevel level) { 131 | 132 | try { 133 | final DecryptionResult results = decrypt(service, username, password, level); 134 | 135 | handler.onDecrypt(results, null); 136 | } catch (Throwable fail) { 137 | handler.onDecrypt(null, fail); 138 | } 139 | } 140 | 141 | @Override 142 | public void removeKey(@NonNull final String alias) { 143 | // Facebook Conceal stores only one key across all services, so we cannot 144 | // delete the key (otherwise decryption will fail for encrypted data of other services). 145 | Log.w(LOG_TAG, "CipherStorageFacebookConceal removeKey called. alias: " + alias); 146 | } 147 | 148 | @NonNull 149 | @Override 150 | protected KeyGenParameterSpec.Builder getKeyGenSpecBuilder(@NonNull final String alias) 151 | throws GeneralSecurityException { 152 | throw new CryptoFailedException("Not designed for a call"); 153 | } 154 | 155 | @NonNull 156 | @Override 157 | protected KeyGenParameterSpec.Builder getKeyGenSpecBuilder(@NonNull final String alias, @NonNull final boolean isForTesting) 158 | throws GeneralSecurityException { 159 | throw new CryptoFailedException("Not designed for a call"); 160 | } 161 | 162 | @NonNull 163 | @Override 164 | protected KeyInfo getKeyInfo(@NonNull final Key key) throws GeneralSecurityException { 165 | throw new CryptoFailedException("Not designed for a call"); 166 | } 167 | 168 | @NonNull 169 | @Override 170 | protected Key generateKey(@NonNull final KeyGenParameterSpec spec) throws GeneralSecurityException { 171 | throw new CryptoFailedException("Not designed for a call"); 172 | } 173 | 174 | @NonNull 175 | @Override 176 | protected String getEncryptionAlgorithm() { 177 | throw new AssertionException("Not designed for a call"); 178 | } 179 | 180 | @NonNull 181 | @Override 182 | protected String getEncryptionTransformation() { 183 | throw new AssertionException("Not designed for a call"); 184 | } 185 | 186 | /** Verify availability of the Crypto API. */ 187 | private void throwIfNoCryptoAvailable() throws CryptoFailedException { 188 | if (!crypto.isAvailable()) { 189 | throw new CryptoFailedException("Crypto is missing"); 190 | } 191 | } 192 | //endregion 193 | 194 | //region Helper methods 195 | @NonNull 196 | private static Entity createUsernameEntity(@NonNull final String alias) { 197 | final String prefix = getEntityPrefix(alias); 198 | 199 | return Entity.create(prefix + "user"); 200 | } 201 | 202 | @NonNull 203 | private static Entity createPasswordEntity(@NonNull final String alias) { 204 | final String prefix = getEntityPrefix(alias); 205 | 206 | return Entity.create(prefix + "pass"); 207 | } 208 | 209 | @NonNull 210 | private static String getEntityPrefix(@NonNull final String alias) { 211 | return KEYCHAIN_DATA + ":" + alias; 212 | } 213 | //endregion 214 | } 215 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/cipherStorage/CipherStorageKeystoreAesCbc.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain.cipherStorage; 2 | 3 | import android.annotation.TargetApi; 4 | import android.os.Build; 5 | import android.security.keystore.KeyGenParameterSpec; 6 | import android.security.keystore.KeyInfo; 7 | import android.security.keystore.KeyProperties; 8 | import android.util.Log; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.annotation.Nullable; 12 | 13 | import com.dorianmazur.keychain.KeychainModule.KnownCiphers; 14 | import com.dorianmazur.keychain.SecurityLevel; 15 | import com.dorianmazur.keychain.decryptionHandler.DecryptionResultHandler; 16 | import com.dorianmazur.keychain.exceptions.CryptoFailedException; 17 | import com.dorianmazur.keychain.exceptions.KeyStoreAccessException; 18 | 19 | import java.io.IOException; 20 | import java.security.GeneralSecurityException; 21 | import java.security.Key; 22 | import java.security.spec.KeySpec; 23 | import java.util.concurrent.atomic.AtomicInteger; 24 | 25 | import javax.crypto.Cipher; 26 | import javax.crypto.KeyGenerator; 27 | import javax.crypto.SecretKey; 28 | import javax.crypto.SecretKeyFactory; 29 | import javax.crypto.spec.IvParameterSpec; 30 | 31 | /** 32 | * @see Secure Data in Android 33 | * @see AES cipher 34 | */ 35 | @TargetApi(Build.VERSION_CODES.M) 36 | @SuppressWarnings({"unused", "WeakerAccess"}) 37 | public class CipherStorageKeystoreAesCbc extends CipherStorageBase { 38 | //region Constants 39 | /** AES */ 40 | public static final String ALGORITHM_AES = KeyProperties.KEY_ALGORITHM_AES; 41 | /** CBC */ 42 | public static final String BLOCK_MODE_CBC = KeyProperties.BLOCK_MODE_CBC; 43 | /** PKCS7 */ 44 | public static final String PADDING_PKCS7 = KeyProperties.ENCRYPTION_PADDING_PKCS7; 45 | /** Transformation path. */ 46 | public static final String ENCRYPTION_TRANSFORMATION = 47 | ALGORITHM_AES + "/" + BLOCK_MODE_CBC + "/" + PADDING_PKCS7; 48 | /** Key size. */ 49 | public static final int ENCRYPTION_KEY_SIZE = 256; 50 | 51 | public static final String DEFAULT_SERVICE = "RN_KEYCHAIN_DEFAULT_ALIAS"; 52 | //endregion 53 | 54 | //region Configuration 55 | @Override 56 | public String getCipherStorageName() { 57 | return KnownCiphers.AES; 58 | } 59 | 60 | /** API23 is a requirement. */ 61 | @Override 62 | public int getMinSupportedApiLevel() { 63 | return Build.VERSION_CODES.M; 64 | } 65 | 66 | /** it can guarantee security levels up to SECURE_HARDWARE/SE/StrongBox */ 67 | @Override 68 | public SecurityLevel securityLevel() { 69 | return SecurityLevel.SECURE_HARDWARE; 70 | } 71 | 72 | /** Biometry is Not Supported. */ 73 | @Override 74 | public boolean isBiometrySupported() { 75 | return false; 76 | } 77 | 78 | /** AES. */ 79 | @Override 80 | @NonNull 81 | protected String getEncryptionAlgorithm() { 82 | return ALGORITHM_AES; 83 | } 84 | 85 | /** AES/CBC/PKCS7Padding */ 86 | @NonNull 87 | @Override 88 | protected String getEncryptionTransformation() { 89 | return ENCRYPTION_TRANSFORMATION; 90 | } 91 | 92 | /** {@inheritDoc}. Override for saving the compatibility with previous version of lib. */ 93 | @Override 94 | public String getDefaultAliasServiceName() { 95 | return DEFAULT_SERVICE; 96 | } 97 | 98 | //endregion 99 | 100 | //region Overrides 101 | @Override 102 | @NonNull 103 | public EncryptionResult encrypt(@NonNull final String alias, 104 | @NonNull final String username, 105 | @NonNull final String password, 106 | @NonNull final SecurityLevel level) 107 | throws CryptoFailedException { 108 | 109 | throwIfInsufficientLevel(level); 110 | 111 | final String safeAlias = getDefaultAliasIfEmpty(alias, getDefaultAliasServiceName()); 112 | final AtomicInteger retries = new AtomicInteger(1); 113 | 114 | try { 115 | final Key key = extractGeneratedKey(safeAlias, level, retries); 116 | 117 | return new EncryptionResult( 118 | encryptString(key, username), 119 | encryptString(key, password), 120 | this); 121 | } catch (GeneralSecurityException e) { 122 | throw new CryptoFailedException("Could not encrypt data with alias: " + alias, e); 123 | } catch (Throwable fail) { 124 | throw new CryptoFailedException("Unknown error with alias: " + alias + 125 | ", error: " + fail.getMessage(), fail); 126 | } 127 | } 128 | 129 | @Override 130 | @NonNull 131 | public DecryptionResult decrypt(@NonNull final String alias, 132 | @NonNull final byte[] username, 133 | @NonNull final byte[] password, 134 | @NonNull final SecurityLevel level) 135 | throws CryptoFailedException { 136 | 137 | throwIfInsufficientLevel(level); 138 | 139 | final String safeAlias = getDefaultAliasIfEmpty(alias, getDefaultAliasServiceName()); 140 | final AtomicInteger retries = new AtomicInteger(1); 141 | 142 | try { 143 | final Key key = extractGeneratedKey(safeAlias, level, retries); 144 | 145 | return new DecryptionResult( 146 | decryptBytes(key, username), 147 | decryptBytes(key, password), 148 | getSecurityLevel(key)); 149 | } catch (GeneralSecurityException e) { 150 | throw new CryptoFailedException("Could not decrypt data with alias: " + alias, e); 151 | } catch (Throwable fail) { 152 | throw new CryptoFailedException("Unknown error with alias: " + alias + 153 | ", error: " + fail.getMessage(), fail); 154 | } 155 | } 156 | 157 | /** Redirect call to {@link #decrypt(String, byte[], byte[], SecurityLevel)} method. */ 158 | @Override 159 | public void decrypt(@NonNull final DecryptionResultHandler handler, 160 | @NonNull final String service, 161 | @NonNull final byte[] username, 162 | @NonNull final byte[] password, 163 | @NonNull final SecurityLevel level) { 164 | try { 165 | final DecryptionResult results = decrypt(service, username, password, level); 166 | 167 | handler.onDecrypt(results, null); 168 | } catch (Throwable fail) { 169 | handler.onDecrypt(null, fail); 170 | } 171 | } 172 | //endregion 173 | 174 | //region Implementation 175 | 176 | /** Get builder for encryption and decryption operations with required user Authentication. */ 177 | @NonNull 178 | @Override 179 | protected KeyGenParameterSpec.Builder getKeyGenSpecBuilder(@NonNull final String alias) throws GeneralSecurityException { 180 | return getKeyGenSpecBuilder(alias, false); 181 | } 182 | 183 | /** Get encryption algorithm specification builder instance. */ 184 | @NonNull 185 | @Override 186 | protected KeyGenParameterSpec.Builder getKeyGenSpecBuilder(@NonNull final String alias, @NonNull final boolean isForTesting) 187 | throws GeneralSecurityException { 188 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 189 | throw new KeyStoreAccessException("Unsupported API" + Build.VERSION.SDK_INT + " version detected."); 190 | } 191 | 192 | final int purposes = KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT; 193 | 194 | return new KeyGenParameterSpec.Builder(alias, purposes) 195 | .setBlockModes(BLOCK_MODE_CBC) 196 | .setEncryptionPaddings(PADDING_PKCS7) 197 | .setRandomizedEncryptionRequired(true) 198 | .setKeySize(ENCRYPTION_KEY_SIZE); 199 | } 200 | 201 | /** Get information about provided key. */ 202 | @NonNull 203 | @Override 204 | protected KeyInfo getKeyInfo(@NonNull final Key key) throws GeneralSecurityException { 205 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 206 | throw new KeyStoreAccessException("Unsupported API" + Build.VERSION.SDK_INT + " version detected."); 207 | } 208 | 209 | final SecretKeyFactory factory = SecretKeyFactory.getInstance(key.getAlgorithm(), KEYSTORE_TYPE); 210 | final KeySpec keySpec = factory.getKeySpec((SecretKey) key, KeyInfo.class); 211 | 212 | return (KeyInfo) keySpec; 213 | } 214 | 215 | /** Try to generate key from provided specification. */ 216 | @NonNull 217 | @Override 218 | protected Key generateKey(@NonNull final KeyGenParameterSpec spec) throws GeneralSecurityException { 219 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 220 | throw new KeyStoreAccessException("Unsupported API" + Build.VERSION.SDK_INT + " version detected."); 221 | } 222 | 223 | final KeyGenerator generator = KeyGenerator.getInstance(getEncryptionAlgorithm(), KEYSTORE_TYPE); 224 | 225 | // initialize key generator 226 | generator.init(spec); 227 | 228 | return generator.generateKey(); 229 | } 230 | 231 | /** Decrypt provided bytes to a string. */ 232 | @NonNull 233 | @Override 234 | protected String decryptBytes(@NonNull final Key key, @NonNull final byte[] bytes, 235 | @Nullable final DecryptBytesHandler handler) 236 | throws GeneralSecurityException, IOException { 237 | final Cipher cipher = getCachedInstance(); 238 | 239 | try { 240 | // read the initialization vector from bytes array 241 | final IvParameterSpec iv = IV.readIv(bytes); 242 | cipher.init(Cipher.DECRYPT_MODE, key, iv); 243 | 244 | // decrypt the bytes using cipher.doFinal(). Using a CipherInputStream for decryption has historically led to issues 245 | // on the Pixel family of devices. 246 | byte[] decryptedBytes = cipher.doFinal(bytes, IV.IV_LENGTH, bytes.length - IV.IV_LENGTH); 247 | return new String(decryptedBytes, UTF8); 248 | } catch (Throwable fail) { 249 | Log.w(LOG_TAG, fail.getMessage(), fail); 250 | 251 | throw fail; 252 | } 253 | } 254 | //endregion 255 | 256 | //region Initialization Vector encrypt/decrypt support 257 | @NonNull 258 | @Override 259 | public byte[] encryptString(@NonNull final Key key, @NonNull final String value) 260 | throws GeneralSecurityException, IOException { 261 | 262 | return encryptString(key, value, IV.encrypt); 263 | } 264 | 265 | @NonNull 266 | @Override 267 | public String decryptBytes(@NonNull final Key key, @NonNull final byte[] bytes) 268 | throws GeneralSecurityException, IOException { 269 | return decryptBytes(key, bytes, IV.decrypt); 270 | } 271 | //endregion 272 | } 273 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/cipherStorage/CipherStorageKeystoreRsaEcb.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain.cipherStorage; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.os.Build; 5 | import android.security.keystore.KeyGenParameterSpec; 6 | import android.security.keystore.KeyInfo; 7 | import android.security.keystore.KeyProperties; 8 | import android.security.keystore.UserNotAuthenticatedException; 9 | import android.util.Log; 10 | 11 | import androidx.annotation.NonNull; 12 | import androidx.annotation.RequiresApi; 13 | 14 | import com.dorianmazur.keychain.KeychainModule; 15 | import com.dorianmazur.keychain.SecurityLevel; 16 | import com.dorianmazur.keychain.decryptionHandler.DecryptionResultHandler; 17 | import com.dorianmazur.keychain.decryptionHandler.DecryptionResultHandlerNonInteractive; 18 | import com.dorianmazur.keychain.exceptions.CryptoFailedException; 19 | import com.dorianmazur.keychain.exceptions.KeyStoreAccessException; 20 | 21 | import java.io.IOException; 22 | import java.security.GeneralSecurityException; 23 | import java.security.InvalidKeyException; 24 | import java.security.Key; 25 | import java.security.KeyFactory; 26 | import java.security.KeyPairGenerator; 27 | import java.security.KeyStore; 28 | import java.security.KeyStoreException; 29 | import java.security.NoSuchAlgorithmException; 30 | import java.security.PublicKey; 31 | import java.security.cert.Certificate; 32 | import java.security.spec.InvalidKeySpecException; 33 | import java.security.spec.X509EncodedKeySpec; 34 | import java.util.concurrent.atomic.AtomicInteger; 35 | 36 | import javax.crypto.NoSuchPaddingException; 37 | 38 | /** Fingerprint biometry protected storage. */ 39 | @RequiresApi(api = Build.VERSION_CODES.M) 40 | @SuppressWarnings({"unused", "WeakerAccess"}) 41 | public class CipherStorageKeystoreRsaEcb extends CipherStorageBase { 42 | //region Constants 43 | /** Selected algorithm. */ 44 | public static final String ALGORITHM_RSA = KeyProperties.KEY_ALGORITHM_RSA; 45 | /** Selected block mode. */ 46 | public static final String BLOCK_MODE_ECB = KeyProperties.BLOCK_MODE_ECB; 47 | /** Selected padding transformation. */ 48 | public static final String PADDING_PKCS1 = KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1; 49 | /** Composed transformation algorithms. */ 50 | public static final String TRANSFORMATION_RSA_ECB_PKCS1 = 51 | ALGORITHM_RSA + "/" + BLOCK_MODE_ECB + "/" + PADDING_PKCS1; 52 | /** Selected encryption key size. */ 53 | public static final int ENCRYPTION_KEY_SIZE = 2048; 54 | public static final int ENCRYPTION_KEY_SIZE_WHEN_TESTING = 512; 55 | 56 | //endregion 57 | 58 | //region Overrides 59 | @Override 60 | @NonNull 61 | public EncryptionResult encrypt(@NonNull final String alias, 62 | @NonNull final String username, 63 | @NonNull final String password, 64 | @NonNull final SecurityLevel level) 65 | throws CryptoFailedException { 66 | 67 | throwIfInsufficientLevel(level); 68 | 69 | final String safeAlias = getDefaultAliasIfEmpty(alias, getDefaultAliasServiceName()); 70 | 71 | try { 72 | return innerEncryptedCredentials(safeAlias, password, username, level); 73 | 74 | // KeyStoreException | KeyStoreAccessException | NoSuchAlgorithmException | InvalidKeySpecException | 75 | // IOException | NoSuchPaddingException | InvalidKeyException e 76 | } catch (NoSuchAlgorithmException | InvalidKeySpecException | NoSuchPaddingException | InvalidKeyException e) { 77 | throw new CryptoFailedException("Could not encrypt data for service " + alias, e); 78 | } catch (KeyStoreException | KeyStoreAccessException e) { 79 | throw new CryptoFailedException("Could not access Keystore for service " + alias, e); 80 | } catch (IOException io) { 81 | throw new CryptoFailedException("I/O error: " + io.getMessage(), io); 82 | } catch (final Throwable ex) { 83 | throw new CryptoFailedException("Unknown error: " + ex.getMessage(), ex); 84 | } 85 | } 86 | 87 | @NonNull 88 | @Override 89 | public DecryptionResult decrypt(@NonNull String alias, 90 | @NonNull byte[] username, 91 | @NonNull byte[] password, 92 | @NonNull final SecurityLevel level) 93 | throws CryptoFailedException { 94 | 95 | final DecryptionResultHandlerNonInteractive handler = new DecryptionResultHandlerNonInteractive(); 96 | decrypt(handler, alias, username, password, level); 97 | 98 | CryptoFailedException.reThrowOnError(handler.getError()); 99 | 100 | if (null == handler.getResult()) { 101 | throw new CryptoFailedException("No decryption results and no error. Something deeply wrong!"); 102 | } 103 | 104 | return handler.getResult(); 105 | } 106 | 107 | @Override 108 | @SuppressLint("NewApi") 109 | public void decrypt(@NonNull DecryptionResultHandler handler, 110 | @NonNull String alias, 111 | @NonNull byte[] username, 112 | @NonNull byte[] password, 113 | @NonNull final SecurityLevel level) 114 | throws CryptoFailedException { 115 | 116 | throwIfInsufficientLevel(level); 117 | 118 | final String safeAlias = getDefaultAliasIfEmpty(alias, getDefaultAliasServiceName()); 119 | final AtomicInteger retries = new AtomicInteger(1); 120 | boolean shouldAskPermissions = false; 121 | 122 | Key key = null; 123 | 124 | try { 125 | // key is always NOT NULL otherwise GeneralSecurityException raised 126 | key = extractGeneratedKey(safeAlias, level, retries); 127 | 128 | final DecryptionResult results = new DecryptionResult( 129 | decryptBytes(key, username), 130 | decryptBytes(key, password) 131 | ); 132 | 133 | handler.onDecrypt(results, null); 134 | } catch (final UserNotAuthenticatedException ex) { 135 | Log.d(LOG_TAG, "Unlock of keystore is needed. Error: " + ex.getMessage(), ex); 136 | 137 | // expected that KEY instance is extracted and we caught exception on decryptBytes operation 138 | @SuppressWarnings("ConstantConditions") final DecryptionContext context = 139 | new DecryptionContext(safeAlias, key, password, username); 140 | 141 | handler.askAccessPermissions(context); 142 | } catch (final Throwable fail) { 143 | // any other exception treated as a failure 144 | handler.onDecrypt(null, fail); 145 | } 146 | } 147 | 148 | //endregion 149 | 150 | //region Configuration 151 | 152 | /** RSAECB. */ 153 | @Override 154 | public String getCipherStorageName() { 155 | return KeychainModule.KnownCiphers.RSA; 156 | } 157 | 158 | /** API23 is a requirement. */ 159 | @Override 160 | public int getMinSupportedApiLevel() { 161 | return Build.VERSION_CODES.M; 162 | } 163 | 164 | /** Biometry is supported. */ 165 | @Override 166 | public boolean isBiometrySupported() { 167 | return true; 168 | } 169 | 170 | /** RSA. */ 171 | @NonNull 172 | @Override 173 | protected String getEncryptionAlgorithm() { 174 | return ALGORITHM_RSA; 175 | } 176 | 177 | /** RSA/ECB/PKCS1Padding */ 178 | @NonNull 179 | @Override 180 | protected String getEncryptionTransformation() { 181 | return TRANSFORMATION_RSA_ECB_PKCS1; 182 | } 183 | //endregion 184 | 185 | //region Implementation 186 | 187 | /** Clean code without try/catch's that encrypt username and password with a key specified by alias. */ 188 | @NonNull 189 | private EncryptionResult innerEncryptedCredentials(@NonNull final String alias, 190 | @NonNull final String password, 191 | @NonNull final String username, 192 | @NonNull final SecurityLevel level) 193 | throws GeneralSecurityException, IOException { 194 | 195 | final KeyStore store = getKeyStoreAndLoad(); 196 | 197 | // on first access create a key for storage 198 | if (!store.containsAlias(alias)) { 199 | generateKeyAndStoreUnderAlias(alias, level); 200 | } 201 | 202 | final KeyFactory kf = KeyFactory.getInstance(ALGORITHM_RSA); 203 | final Certificate certificate = store.getCertificate(alias); 204 | final PublicKey publicKey = certificate.getPublicKey(); 205 | final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKey.getEncoded()); 206 | final PublicKey key = kf.generatePublic(keySpec); 207 | 208 | return new EncryptionResult( 209 | encryptString(key, username), 210 | encryptString(key, password), 211 | this); 212 | } 213 | 214 | /** Get builder for encryption and decryption operations with required user Authentication. */ 215 | @NonNull 216 | @Override 217 | @SuppressLint("NewApi") 218 | protected KeyGenParameterSpec.Builder getKeyGenSpecBuilder(@NonNull final String alias) throws GeneralSecurityException{ 219 | return getKeyGenSpecBuilder(alias, false); 220 | } 221 | 222 | /** Get builder for encryption and decryption operations with required user Authentication. */ 223 | @NonNull 224 | @Override 225 | @SuppressLint("NewApi") 226 | protected KeyGenParameterSpec.Builder getKeyGenSpecBuilder(@NonNull final String alias, @NonNull final boolean isForTesting) 227 | throws GeneralSecurityException { 228 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 229 | throw new KeyStoreAccessException("Unsupported API" + Build.VERSION.SDK_INT + " version detected."); 230 | } 231 | 232 | final int purposes = KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT; 233 | 234 | final int keySize = isForTesting ? ENCRYPTION_KEY_SIZE_WHEN_TESTING : ENCRYPTION_KEY_SIZE; 235 | 236 | final int validityDuration = 5; 237 | final KeyGenParameterSpec.Builder keyGenParameterSpecBuilder = new KeyGenParameterSpec.Builder(alias, purposes) 238 | .setBlockModes(BLOCK_MODE_ECB) 239 | .setEncryptionPaddings(PADDING_PKCS1) 240 | .setRandomizedEncryptionRequired(true) 241 | .setUserAuthenticationRequired(true) 242 | .setKeySize(keySize); 243 | 244 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 245 | keyGenParameterSpecBuilder.setUserAuthenticationParameters(validityDuration, KeyProperties.AUTH_BIOMETRIC_STRONG); 246 | } else { 247 | keyGenParameterSpecBuilder.setUserAuthenticationValidityDurationSeconds(validityDuration); 248 | } 249 | 250 | return keyGenParameterSpecBuilder; 251 | } 252 | 253 | /** Get information about provided key. */ 254 | @NonNull 255 | @Override 256 | protected KeyInfo getKeyInfo(@NonNull final Key key) throws GeneralSecurityException { 257 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 258 | throw new KeyStoreAccessException("Unsupported API" + Build.VERSION.SDK_INT + " version detected."); 259 | } 260 | 261 | final KeyFactory factory = KeyFactory.getInstance(key.getAlgorithm(), KEYSTORE_TYPE); 262 | 263 | return factory.getKeySpec(key, KeyInfo.class); 264 | } 265 | 266 | /** Try to generate key from provided specification. */ 267 | @NonNull 268 | @Override 269 | protected Key generateKey(@NonNull final KeyGenParameterSpec spec) throws GeneralSecurityException { 270 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { 271 | throw new KeyStoreAccessException("Unsupported API" + Build.VERSION.SDK_INT + " version detected."); 272 | } 273 | 274 | final KeyPairGenerator generator = KeyPairGenerator.getInstance(getEncryptionAlgorithm(), KEYSTORE_TYPE); 275 | generator.initialize(spec); 276 | 277 | return generator.generateKeyPair().getPrivate(); 278 | } 279 | 280 | //endregion 281 | } 282 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/decryptionHandler/DecryptionResultHandler.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain.decryptionHandler; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import com.dorianmazur.keychain.cipherStorage.CipherStorage.DecryptionContext; 7 | import com.dorianmazur.keychain.cipherStorage.CipherStorage.DecryptionResult; 8 | 9 | /** Handler that allows to inject some actions during decrypt operations. */ 10 | public interface DecryptionResultHandler { 11 | /** Ask user for interaction, often its unlock of keystore by biometric data providing. */ 12 | void askAccessPermissions(@NonNull final DecryptionContext context); 13 | 14 | /** 15 | * 16 | */ 17 | void onDecrypt(@Nullable final DecryptionResult decryptionResult, @Nullable final Throwable error); 18 | 19 | /** Get reference on results. */ 20 | @Nullable 21 | DecryptionResult getResult(); 22 | 23 | /** Get reference on capture error. */ 24 | @Nullable 25 | Throwable getError(); 26 | 27 | /** Block thread and wait for any result of execution. */ 28 | void waitResult(); 29 | } 30 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/decryptionHandler/DecryptionResultHandlerInteractiveBiometric.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain.decryptionHandler; 2 | 3 | import android.os.Looper; 4 | import android.util.Log; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.annotation.Nullable; 8 | import androidx.biometric.BiometricPrompt; 9 | import androidx.fragment.app.FragmentActivity; 10 | 11 | import com.facebook.react.bridge.AssertionException; 12 | import com.facebook.react.bridge.ReactApplicationContext; 13 | import com.dorianmazur.keychain.DeviceAvailability; 14 | import com.dorianmazur.keychain.cipherStorage.CipherStorage; 15 | import com.dorianmazur.keychain.cipherStorage.CipherStorage.DecryptionResult; 16 | import com.dorianmazur.keychain.cipherStorage.CipherStorage.DecryptionContext; 17 | import com.dorianmazur.keychain.cipherStorage.CipherStorageBase; 18 | import com.dorianmazur.keychain.exceptions.CryptoFailedException; 19 | 20 | import java.util.concurrent.Executor; 21 | import java.util.concurrent.Executors; 22 | 23 | public class DecryptionResultHandlerInteractiveBiometric extends BiometricPrompt.AuthenticationCallback implements DecryptionResultHandler { 24 | protected CipherStorage.DecryptionResult result; 25 | protected Throwable error; 26 | protected final ReactApplicationContext reactContext; 27 | protected final CipherStorageBase storage; 28 | protected final Executor executor = Executors.newSingleThreadExecutor(); 29 | protected CipherStorage.DecryptionContext context; 30 | protected BiometricPrompt.PromptInfo promptInfo; 31 | 32 | /** Logging tag. */ 33 | protected static final String LOG_TAG = DecryptionResultHandlerInteractiveBiometric.class.getSimpleName(); 34 | 35 | public DecryptionResultHandlerInteractiveBiometric( 36 | @NonNull ReactApplicationContext reactContext, 37 | @NonNull final CipherStorage storage, 38 | @NonNull final BiometricPrompt.PromptInfo promptInfo) { 39 | this.reactContext = reactContext; 40 | this.storage = (CipherStorageBase) storage; 41 | this.promptInfo = promptInfo; 42 | } 43 | 44 | @Override 45 | public void askAccessPermissions(@NonNull final DecryptionContext context) { 46 | this.context = context; 47 | 48 | if (!DeviceAvailability.isPermissionsGranted(reactContext)) { 49 | final CryptoFailedException failure = new CryptoFailedException( 50 | "Could not start fingerprint Authentication. No permissions granted."); 51 | 52 | onDecrypt(null, failure); 53 | } else { 54 | startAuthentication(); 55 | } 56 | } 57 | 58 | @Override 59 | public void onDecrypt(@Nullable final DecryptionResult decryptionResult, @Nullable final Throwable error) { 60 | this.result = decryptionResult; 61 | this.error = error; 62 | 63 | synchronized (this) { 64 | notifyAll(); 65 | } 66 | } 67 | 68 | @Nullable 69 | @Override 70 | public CipherStorage.DecryptionResult getResult() { 71 | return result; 72 | } 73 | 74 | @Nullable 75 | @Override 76 | public Throwable getError() { 77 | return error; 78 | } 79 | 80 | /** Called when an unrecoverable error has been encountered and the operation is complete. */ 81 | @Override 82 | public void onAuthenticationError(final int errorCode, @NonNull final CharSequence errString) { 83 | final CryptoFailedException error = new CryptoFailedException("code: " + errorCode + ", msg: " + errString); 84 | 85 | onDecrypt(null, error); 86 | } 87 | 88 | /** Called when a biometric is recognized. */ 89 | @Override 90 | public void onAuthenticationSucceeded(@NonNull final BiometricPrompt.AuthenticationResult result) { 91 | try { 92 | if (null == context) throw new NullPointerException("Decrypt context is not assigned yet."); 93 | 94 | final CipherStorage.DecryptionResult decrypted = new CipherStorage.DecryptionResult( 95 | storage.decryptBytes(context.key, context.username), 96 | storage.decryptBytes(context.key, context.password) 97 | ); 98 | 99 | onDecrypt(decrypted, null); 100 | } catch (Throwable fail) { 101 | onDecrypt(null, fail); 102 | } 103 | } 104 | 105 | /** trigger interactive authentication. */ 106 | public void startAuthentication() { 107 | FragmentActivity activity = getCurrentActivity(); 108 | 109 | // code can be executed only from MAIN thread 110 | if (Thread.currentThread() != Looper.getMainLooper().getThread()) { 111 | activity.runOnUiThread(this::startAuthentication); 112 | waitResult(); 113 | return; 114 | } 115 | 116 | authenticateWithPrompt(activity); 117 | } 118 | 119 | protected FragmentActivity getCurrentActivity() { 120 | final FragmentActivity activity = (FragmentActivity) reactContext.getCurrentActivity(); 121 | if (null == activity) throw new NullPointerException("Not assigned current activity"); 122 | 123 | return activity; 124 | } 125 | 126 | protected BiometricPrompt authenticateWithPrompt(@NonNull final FragmentActivity activity) { 127 | final BiometricPrompt prompt = new BiometricPrompt(activity, executor, this); 128 | prompt.authenticate(this.promptInfo); 129 | 130 | return prompt; 131 | } 132 | 133 | /** Block current NON-main thread and wait for user authentication results. */ 134 | @Override 135 | public void waitResult() { 136 | if (Thread.currentThread() == Looper.getMainLooper().getThread()) 137 | throw new AssertionException("method should not be executed from MAIN thread"); 138 | 139 | Log.i(LOG_TAG, "blocking thread. waiting for done UI operation."); 140 | 141 | try { 142 | synchronized (this) { 143 | wait(); 144 | } 145 | } catch (InterruptedException ignored) { 146 | /* shutdown sequence */ 147 | } 148 | 149 | Log.i(LOG_TAG, "unblocking thread."); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/decryptionHandler/DecryptionResultHandlerInteractiveBiometricManualRetry.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain.decryptionHandler; 2 | 3 | import android.os.Looper; 4 | import android.util.Log; 5 | 6 | import androidx.annotation.NonNull; 7 | import androidx.biometric.BiometricPrompt; 8 | import androidx.fragment.app.FragmentActivity; 9 | 10 | import com.facebook.react.bridge.ReactApplicationContext; 11 | import com.dorianmazur.keychain.cipherStorage.CipherStorage; 12 | 13 | public class DecryptionResultHandlerInteractiveBiometricManualRetry extends DecryptionResultHandlerInteractiveBiometric implements DecryptionResultHandler { 14 | private BiometricPrompt presentedPrompt; 15 | private Boolean didFailBiometric = false; 16 | 17 | public DecryptionResultHandlerInteractiveBiometricManualRetry(@NonNull ReactApplicationContext reactContext, 18 | @NonNull CipherStorage storage, 19 | @NonNull BiometricPrompt.PromptInfo promptInfo) { 20 | super(reactContext, storage, promptInfo); 21 | } 22 | 23 | /** Manually cancel current (invisible) authentication to clear the fragment. */ 24 | private void cancelPresentedAuthentication() { 25 | Log.d(LOG_TAG, "Cancelling authentication"); 26 | if (presentedPrompt == null) { 27 | return; 28 | } 29 | 30 | try { 31 | presentedPrompt.cancelAuthentication(); 32 | } catch (Exception e) { 33 | e.printStackTrace(); 34 | } finally { 35 | this.presentedPrompt = null; 36 | } 37 | } 38 | 39 | /** Called when an unrecoverable error has been encountered and the operation is complete. */ 40 | @Override 41 | public void onAuthenticationError(final int errorCode, @NonNull final CharSequence errString) { 42 | if (didFailBiometric) { 43 | this.presentedPrompt = null; 44 | this.didFailBiometric = false; 45 | retryAuthentication(); 46 | return; 47 | } 48 | 49 | super.onAuthenticationError(errorCode, errString); 50 | } 51 | 52 | /** Called when a biometric (e.g. fingerprint, face, etc.) is presented but not recognized as belonging to the user. */ 53 | @Override 54 | public void onAuthenticationFailed() { 55 | Log.d(LOG_TAG, "Authentication failed: biometric not recognized."); 56 | if (presentedPrompt != null) { 57 | this.didFailBiometric = true; 58 | cancelPresentedAuthentication(); 59 | } 60 | } 61 | 62 | /** Called when a biometric is recognized. */ 63 | @Override 64 | public void onAuthenticationSucceeded(@NonNull final BiometricPrompt.AuthenticationResult result) { 65 | this.presentedPrompt = null; 66 | this.didFailBiometric = false; 67 | 68 | super.onAuthenticationSucceeded(result); 69 | } 70 | 71 | /** trigger interactive authentication. */ 72 | @Override 73 | public void startAuthentication() { 74 | FragmentActivity activity = getCurrentActivity(); 75 | 76 | // code can be executed only from MAIN thread 77 | if (Thread.currentThread() != Looper.getMainLooper().getThread()) { 78 | activity.runOnUiThread(this::startAuthentication); 79 | waitResult(); 80 | return; 81 | } 82 | 83 | this.presentedPrompt = authenticateWithPrompt(activity); 84 | } 85 | 86 | /** trigger interactive authentication without invoking another waitResult() */ 87 | protected void retryAuthentication() { 88 | Log.d(LOG_TAG, "Retrying biometric authentication."); 89 | 90 | FragmentActivity activity = getCurrentActivity(); 91 | 92 | if (Thread.currentThread() != Looper.getMainLooper().getThread()) { 93 | try { 94 | /* 95 | * NOTE: Applications should not cancel and authenticate in a short succession 96 | * Waiting 100ms in a non-UI thread to make sure previous BiometricPrompt is cleared by OS 97 | */ 98 | Thread.sleep(100); 99 | } catch (InterruptedException ignored) { 100 | /* shutdown sequence */ 101 | } 102 | 103 | activity.runOnUiThread(this::retryAuthentication); 104 | return; 105 | } 106 | 107 | this.presentedPrompt = authenticateWithPrompt(activity); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/decryptionHandler/DecryptionResultHandlerNonInteractive.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain.decryptionHandler; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import com.dorianmazur.keychain.cipherStorage.CipherStorage.DecryptionContext; 7 | import com.dorianmazur.keychain.cipherStorage.CipherStorage.DecryptionResult; 8 | import com.dorianmazur.keychain.exceptions.CryptoFailedException; 9 | 10 | public class DecryptionResultHandlerNonInteractive implements DecryptionResultHandler { 11 | private DecryptionResult result; 12 | private Throwable error; 13 | 14 | @Override 15 | public void askAccessPermissions(@NonNull final DecryptionContext context) { 16 | final CryptoFailedException failure = new CryptoFailedException( 17 | "Non interactive decryption mode."); 18 | 19 | onDecrypt(null, failure); 20 | } 21 | 22 | @Override 23 | public void onDecrypt(@Nullable final DecryptionResult decryptionResult, 24 | @Nullable final Throwable error) { 25 | this.result = decryptionResult; 26 | this.error = error; 27 | } 28 | 29 | @Nullable 30 | @Override 31 | public DecryptionResult getResult() { 32 | return result; 33 | } 34 | 35 | @Nullable 36 | @Override 37 | public Throwable getError() { 38 | return error; 39 | } 40 | 41 | @Override 42 | public void waitResult() { 43 | /* do nothing, expected synchronized call in one thread */ 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/decryptionHandler/DecryptionResultHandlerProvider.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain.decryptionHandler; 2 | 3 | import android.os.Build; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.biometric.BiometricPrompt; 7 | 8 | import com.facebook.react.bridge.ReactApplicationContext; 9 | import com.dorianmazur.keychain.cipherStorage.CipherStorage; 10 | 11 | import java.util.Arrays; 12 | 13 | // NOTE: the logic for handling OnePlus bug is taken from the following forum post: 14 | // https://forums.oneplus.com/threads/oneplus-7-pro-fingerprint-biometricprompt-does-not-show.1035821/#post-21710422 15 | public class DecryptionResultHandlerProvider { 16 | private static final String ONE_PLUS_BRAND = "oneplus"; 17 | private static final String[] ONE_PLUS_MODELS_WITHOUT_BIOMETRIC_BUG = { 18 | "A0001", // OnePlus One 19 | "ONE A2001", "ONE A2003", "ONE A2005", // OnePlus 2 20 | "ONE E1001", "ONE E1003", "ONE E1005", // OnePlus X 21 | "ONEPLUS A3000", "ONEPLUS SM-A3000", "ONEPLUS A3003", // OnePlus 3 22 | "ONEPLUS A3010", // OnePlus 3T 23 | "ONEPLUS A5000", // OnePlus 5 24 | "ONEPLUS A5010", // OnePlus 5T 25 | "ONEPLUS A6000", "ONEPLUS A6003" // OnePlus 6 26 | }; 27 | 28 | private static boolean hasOnePlusBiometricBug() { 29 | return Build.BRAND.toLowerCase().equals(ONE_PLUS_BRAND) && 30 | !Arrays.asList(ONE_PLUS_MODELS_WITHOUT_BIOMETRIC_BUG).contains(Build.MODEL); 31 | } 32 | 33 | public static DecryptionResultHandler getHandler(@NonNull ReactApplicationContext reactContext, 34 | @NonNull final CipherStorage storage, 35 | @NonNull final BiometricPrompt.PromptInfo promptInfo) { 36 | if (storage.isBiometrySupported()) { 37 | if (hasOnePlusBiometricBug()) { 38 | return new DecryptionResultHandlerInteractiveBiometricManualRetry(reactContext, storage, promptInfo); 39 | } 40 | 41 | return new DecryptionResultHandlerInteractiveBiometric(reactContext, storage, promptInfo); 42 | } 43 | 44 | return new DecryptionResultHandlerNonInteractive(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/exceptions/CryptoFailedException.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain.exceptions; 2 | 3 | import androidx.annotation.Nullable; 4 | 5 | import java.security.GeneralSecurityException; 6 | 7 | public class CryptoFailedException extends GeneralSecurityException { 8 | public CryptoFailedException(String message) { 9 | super(message); 10 | } 11 | 12 | public CryptoFailedException(String message, Throwable t) { 13 | super(message, t); 14 | } 15 | 16 | public static void reThrowOnError(@Nullable final Throwable error) throws CryptoFailedException { 17 | if(null == error) return; 18 | 19 | if (error instanceof CryptoFailedException) 20 | throw (CryptoFailedException) error; 21 | 22 | throw new CryptoFailedException("Wrapped error: " + error.getMessage(), error); 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/exceptions/EmptyParameterException.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain.exceptions; 2 | 3 | public class EmptyParameterException extends Exception { 4 | public EmptyParameterException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /android/src/main/java/com/dorianmazur/keychain/exceptions/KeyStoreAccessException.java: -------------------------------------------------------------------------------- 1 | package com.dorianmazur.keychain.exceptions; 2 | 3 | import java.security.GeneralSecurityException; 4 | 5 | public class KeyStoreAccessException extends GeneralSecurityException { 6 | public KeyStoreAccessException(final String message) { 7 | super(message); 8 | } 9 | 10 | public KeyStoreAccessException(final String message, final Throwable t) { 11 | super(message, t); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['module:react-native-builder-bob/babel-preset', { modules: 'commonjs' }], 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /ios/RNKeychainManager.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 4F9F3CC426B8FD3700F34E8C /* RNKeychainManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */; }; 11 | 5D82368F1B0CE3CB005A9EF3 /* RNKeychainManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */; }; 12 | 5D8236911B0CE3D6005A9EF3 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5D8236901B0CE3D6005A9EF3 /* Security.framework */; }; 13 | 5DE632D52043423E004F9598 /* LocalAuthentication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DE632D42043423E004F9598 /* LocalAuthentication.framework */; }; 14 | 5DE632DB204342AE004F9598 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DE632DA204342AE004F9598 /* Security.framework */; }; 15 | 6478986B1F38BFA100DA1C12 /* RNKeychainManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 5D82366D1B0CE05B005A9EF3 /* Copy Headers */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = "include/$(PRODUCT_NAME)"; 23 | dstSubfolderSpec = 16; 24 | files = ( 25 | ); 26 | name = "Copy Headers"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | 6478985D1F38BF9100DA1C12 /* Copy Headers */ = { 30 | isa = PBXCopyFilesBuildPhase; 31 | buildActionMask = 2147483647; 32 | dstPath = "include/$(PRODUCT_NAME)"; 33 | dstSubfolderSpec = 16; 34 | files = ( 35 | ); 36 | name = "Copy Headers"; 37 | runOnlyForDeploymentPostprocessing = 0; 38 | }; 39 | /* End PBXCopyFilesBuildPhase section */ 40 | 41 | /* Begin PBXFileReference section */ 42 | 4F9F3CBB26B8FD1000F34E8C /* libRNKeychainManager-macOS.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libRNKeychainManager-macOS.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 43 | 5D82366F1B0CE05B005A9EF3 /* libRNKeychainManager.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNKeychainManager.a; sourceTree = BUILT_PRODUCTS_DIR; }; 44 | 5D82368B1B0CE2A6005A9EF3 /* RNKeychainManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = RNKeychainManager.h; sourceTree = ""; }; 45 | 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 2; lastKnownFileType = sourcecode.c.objc; path = RNKeychainManager.m; sourceTree = ""; tabWidth = 2; }; 46 | 5D8236901B0CE3D6005A9EF3 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; }; 47 | 5DE632D42043423E004F9598 /* LocalAuthentication.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = LocalAuthentication.framework; path = System/Library/Frameworks/LocalAuthentication.framework; sourceTree = SDKROOT; }; 48 | 5DE632DA204342AE004F9598 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS11.2.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; }; 49 | 6478985F1F38BF9100DA1C12 /* libRNKeychain.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNKeychain.a; sourceTree = BUILT_PRODUCTS_DIR; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | 4F9F3CB926B8FD1000F34E8C /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | ); 58 | runOnlyForDeploymentPostprocessing = 0; 59 | }; 60 | 5D82366C1B0CE05B005A9EF3 /* Frameworks */ = { 61 | isa = PBXFrameworksBuildPhase; 62 | buildActionMask = 2147483647; 63 | files = ( 64 | 5DE632D52043423E004F9598 /* LocalAuthentication.framework in Frameworks */, 65 | 5D8236911B0CE3D6005A9EF3 /* Security.framework in Frameworks */, 66 | ); 67 | runOnlyForDeploymentPostprocessing = 0; 68 | }; 69 | 6478985C1F38BF9100DA1C12 /* Frameworks */ = { 70 | isa = PBXFrameworksBuildPhase; 71 | buildActionMask = 2147483647; 72 | files = ( 73 | 5DE632DB204342AE004F9598 /* Security.framework in Frameworks */, 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXFrameworksBuildPhase section */ 78 | 79 | /* Begin PBXGroup section */ 80 | 5D8236661B0CE05B005A9EF3 = { 81 | isa = PBXGroup; 82 | children = ( 83 | 5D82368A1B0CE2A6005A9EF3 /* RNKeychainManager */, 84 | 5D8236701B0CE05B005A9EF3 /* Products */, 85 | 647898681F38BF9C00DA1C12 /* Frameworks */, 86 | ); 87 | sourceTree = ""; 88 | wrapsLines = 0; 89 | }; 90 | 5D8236701B0CE05B005A9EF3 /* Products */ = { 91 | isa = PBXGroup; 92 | children = ( 93 | 5D82366F1B0CE05B005A9EF3 /* libRNKeychainManager.a */, 94 | 6478985F1F38BF9100DA1C12 /* libRNKeychain.a */, 95 | 4F9F3CBB26B8FD1000F34E8C /* libRNKeychainManager-macOS.a */, 96 | ); 97 | name = Products; 98 | sourceTree = ""; 99 | }; 100 | 5D82368A1B0CE2A6005A9EF3 /* RNKeychainManager */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | 5D82368B1B0CE2A6005A9EF3 /* RNKeychainManager.h */, 104 | 5D82368C1B0CE2A6005A9EF3 /* RNKeychainManager.m */, 105 | ); 106 | path = RNKeychainManager; 107 | sourceTree = ""; 108 | }; 109 | 647898681F38BF9C00DA1C12 /* Frameworks */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 5DE632DA204342AE004F9598 /* Security.framework */, 113 | 5DE632D42043423E004F9598 /* LocalAuthentication.framework */, 114 | 5D8236901B0CE3D6005A9EF3 /* Security.framework */, 115 | ); 116 | name = Frameworks; 117 | sourceTree = ""; 118 | }; 119 | /* End PBXGroup section */ 120 | 121 | /* Begin PBXHeadersBuildPhase section */ 122 | 4F9F3CB726B8FD1000F34E8C /* Headers */ = { 123 | isa = PBXHeadersBuildPhase; 124 | buildActionMask = 2147483647; 125 | files = ( 126 | ); 127 | runOnlyForDeploymentPostprocessing = 0; 128 | }; 129 | 5DE632D820434281004F9598 /* Headers */ = { 130 | isa = PBXHeadersBuildPhase; 131 | buildActionMask = 2147483647; 132 | files = ( 133 | ); 134 | runOnlyForDeploymentPostprocessing = 0; 135 | }; 136 | 5DE632DD204342BC004F9598 /* Headers */ = { 137 | isa = PBXHeadersBuildPhase; 138 | buildActionMask = 2147483647; 139 | files = ( 140 | ); 141 | runOnlyForDeploymentPostprocessing = 0; 142 | }; 143 | /* End PBXHeadersBuildPhase section */ 144 | 145 | /* Begin PBXNativeTarget section */ 146 | 4F9F3CBA26B8FD1000F34E8C /* RNKeychainManager-macOS */ = { 147 | isa = PBXNativeTarget; 148 | buildConfigurationList = 4F9F3CC326B8FD1000F34E8C /* Build configuration list for PBXNativeTarget "RNKeychainManager-macOS" */; 149 | buildPhases = ( 150 | 4F9F3CB726B8FD1000F34E8C /* Headers */, 151 | 4F9F3CB826B8FD1000F34E8C /* Sources */, 152 | 4F9F3CB926B8FD1000F34E8C /* Frameworks */, 153 | ); 154 | buildRules = ( 155 | ); 156 | dependencies = ( 157 | ); 158 | name = "RNKeychainManager-macOS"; 159 | productName = "RNKeychainManger-macOS"; 160 | productReference = 4F9F3CBB26B8FD1000F34E8C /* libRNKeychainManager-macOS.a */; 161 | productType = "com.apple.product-type.library.static"; 162 | }; 163 | 5D82366E1B0CE05B005A9EF3 /* RNKeychainManager */ = { 164 | isa = PBXNativeTarget; 165 | buildConfigurationList = 5D8236831B0CE05B005A9EF3 /* Build configuration list for PBXNativeTarget "RNKeychainManager" */; 166 | buildPhases = ( 167 | 5D82366B1B0CE05B005A9EF3 /* Sources */, 168 | 5D82366C1B0CE05B005A9EF3 /* Frameworks */, 169 | 5DE632D820434281004F9598 /* Headers */, 170 | 5D82366D1B0CE05B005A9EF3 /* Copy Headers */, 171 | ); 172 | buildRules = ( 173 | ); 174 | dependencies = ( 175 | ); 176 | name = RNKeychainManager; 177 | productName = RNKeychainManager; 178 | productReference = 5D82366F1B0CE05B005A9EF3 /* libRNKeychainManager.a */; 179 | productType = "com.apple.product-type.library.static"; 180 | }; 181 | 6478985E1F38BF9100DA1C12 /* RNKeychainManager-tvOS */ = { 182 | isa = PBXNativeTarget; 183 | buildConfigurationList = 647898671F38BF9100DA1C12 /* Build configuration list for PBXNativeTarget "RNKeychainManager-tvOS" */; 184 | buildPhases = ( 185 | 6478985B1F38BF9100DA1C12 /* Sources */, 186 | 6478985C1F38BF9100DA1C12 /* Frameworks */, 187 | 5DE632DD204342BC004F9598 /* Headers */, 188 | 6478985D1F38BF9100DA1C12 /* Copy Headers */, 189 | ); 190 | buildRules = ( 191 | ); 192 | dependencies = ( 193 | ); 194 | name = "RNKeychainManager-tvOS"; 195 | productName = "RNKeychainManager-tvOS"; 196 | productReference = 6478985F1F38BF9100DA1C12 /* libRNKeychain.a */; 197 | productType = "com.apple.product-type.library.static"; 198 | }; 199 | /* End PBXNativeTarget section */ 200 | 201 | /* Begin PBXProject section */ 202 | 5D8236671B0CE05B005A9EF3 /* Project object */ = { 203 | isa = PBXProject; 204 | attributes = { 205 | LastUpgradeCheck = 0630; 206 | ORGANIZATIONNAME = "Joel Arvidsson"; 207 | TargetAttributes = { 208 | 4F9F3CBA26B8FD1000F34E8C = { 209 | CreatedOnToolsVersion = 12.5.1; 210 | ProvisioningStyle = Automatic; 211 | }; 212 | 5D82366E1B0CE05B005A9EF3 = { 213 | CreatedOnToolsVersion = 6.3.1; 214 | }; 215 | 6478985E1F38BF9100DA1C12 = { 216 | CreatedOnToolsVersion = 8.3.3; 217 | ProvisioningStyle = Automatic; 218 | }; 219 | }; 220 | }; 221 | buildConfigurationList = 5D82366A1B0CE05B005A9EF3 /* Build configuration list for PBXProject "RNKeychainManager" */; 222 | compatibilityVersion = "Xcode 3.2"; 223 | developmentRegion = English; 224 | hasScannedForEncodings = 0; 225 | knownRegions = ( 226 | English, 227 | en, 228 | ); 229 | mainGroup = 5D8236661B0CE05B005A9EF3; 230 | productRefGroup = 5D8236701B0CE05B005A9EF3 /* Products */; 231 | projectDirPath = ""; 232 | projectRoot = ""; 233 | targets = ( 234 | 5D82366E1B0CE05B005A9EF3 /* RNKeychainManager */, 235 | 6478985E1F38BF9100DA1C12 /* RNKeychainManager-tvOS */, 236 | 4F9F3CBA26B8FD1000F34E8C /* RNKeychainManager-macOS */, 237 | ); 238 | }; 239 | /* End PBXProject section */ 240 | 241 | /* Begin PBXSourcesBuildPhase section */ 242 | 4F9F3CB826B8FD1000F34E8C /* Sources */ = { 243 | isa = PBXSourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | 4F9F3CC426B8FD3700F34E8C /* RNKeychainManager.m in Sources */, 247 | ); 248 | runOnlyForDeploymentPostprocessing = 0; 249 | }; 250 | 5D82366B1B0CE05B005A9EF3 /* Sources */ = { 251 | isa = PBXSourcesBuildPhase; 252 | buildActionMask = 2147483647; 253 | files = ( 254 | 5D82368F1B0CE3CB005A9EF3 /* RNKeychainManager.m in Sources */, 255 | ); 256 | runOnlyForDeploymentPostprocessing = 0; 257 | }; 258 | 6478985B1F38BF9100DA1C12 /* Sources */ = { 259 | isa = PBXSourcesBuildPhase; 260 | buildActionMask = 2147483647; 261 | files = ( 262 | 6478986B1F38BFA100DA1C12 /* RNKeychainManager.m in Sources */, 263 | ); 264 | runOnlyForDeploymentPostprocessing = 0; 265 | }; 266 | /* End PBXSourcesBuildPhase section */ 267 | 268 | /* Begin XCBuildConfiguration section */ 269 | 4F9F3CC126B8FD1000F34E8C /* Debug */ = { 270 | isa = XCBuildConfiguration; 271 | buildSettings = { 272 | CLANG_ANALYZER_NONNULL = YES; 273 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 274 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 275 | CLANG_ENABLE_OBJC_WEAK = YES; 276 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 277 | CLANG_WARN_COMMA = YES; 278 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 279 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 280 | CLANG_WARN_INFINITE_RECURSION = YES; 281 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 282 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 283 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 284 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 285 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 286 | CLANG_WARN_STRICT_PROTOTYPES = YES; 287 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 288 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 289 | CODE_SIGN_STYLE = Automatic; 290 | DEBUG_INFORMATION_FORMAT = dwarf; 291 | ENABLE_TESTABILITY = YES; 292 | EXECUTABLE_PREFIX = lib; 293 | GCC_C_LANGUAGE_STANDARD = gnu11; 294 | MACOSX_DEPLOYMENT_TARGET = 11.3; 295 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 296 | MTL_FAST_MATH = YES; 297 | PRODUCT_NAME = "$(TARGET_NAME)"; 298 | SDKROOT = macosx; 299 | SKIP_INSTALL = YES; 300 | }; 301 | name = Debug; 302 | }; 303 | 4F9F3CC226B8FD1000F34E8C /* Release */ = { 304 | isa = XCBuildConfiguration; 305 | buildSettings = { 306 | CLANG_ANALYZER_NONNULL = YES; 307 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 308 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 309 | CLANG_ENABLE_OBJC_WEAK = YES; 310 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 311 | CLANG_WARN_COMMA = YES; 312 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 313 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 314 | CLANG_WARN_INFINITE_RECURSION = YES; 315 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 316 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 317 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 318 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 319 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 320 | CLANG_WARN_STRICT_PROTOTYPES = YES; 321 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 322 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 323 | CODE_SIGN_STYLE = Automatic; 324 | EXECUTABLE_PREFIX = lib; 325 | GCC_C_LANGUAGE_STANDARD = gnu11; 326 | MACOSX_DEPLOYMENT_TARGET = 11.3; 327 | MTL_FAST_MATH = YES; 328 | PRODUCT_NAME = "$(TARGET_NAME)"; 329 | SDKROOT = macosx; 330 | SKIP_INSTALL = YES; 331 | }; 332 | name = Release; 333 | }; 334 | 5D8236811B0CE05B005A9EF3 /* Debug */ = { 335 | isa = XCBuildConfiguration; 336 | buildSettings = { 337 | ALWAYS_SEARCH_USER_PATHS = NO; 338 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 339 | CLANG_CXX_LIBRARY = "libc++"; 340 | CLANG_ENABLE_MODULES = YES; 341 | CLANG_ENABLE_OBJC_ARC = YES; 342 | CLANG_WARN_BOOL_CONVERSION = YES; 343 | CLANG_WARN_CONSTANT_CONVERSION = YES; 344 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 345 | CLANG_WARN_EMPTY_BODY = YES; 346 | CLANG_WARN_ENUM_CONVERSION = YES; 347 | CLANG_WARN_INT_CONVERSION = YES; 348 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 349 | CLANG_WARN_UNREACHABLE_CODE = YES; 350 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 351 | COPY_PHASE_STRIP = NO; 352 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 353 | ENABLE_STRICT_OBJC_MSGSEND = YES; 354 | GCC_C_LANGUAGE_STANDARD = gnu99; 355 | GCC_DYNAMIC_NO_PIC = NO; 356 | GCC_NO_COMMON_BLOCKS = YES; 357 | GCC_OPTIMIZATION_LEVEL = 0; 358 | GCC_PREPROCESSOR_DEFINITIONS = ( 359 | "DEBUG=1", 360 | "$(inherited)", 361 | ); 362 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 363 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 364 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 365 | GCC_WARN_UNDECLARED_SELECTOR = YES; 366 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 367 | GCC_WARN_UNUSED_FUNCTION = YES; 368 | GCC_WARN_UNUSED_VARIABLE = YES; 369 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 370 | MTL_ENABLE_DEBUG_INFO = YES; 371 | ONLY_ACTIVE_ARCH = YES; 372 | SDKROOT = iphoneos; 373 | }; 374 | name = Debug; 375 | }; 376 | 5D8236821B0CE05B005A9EF3 /* Release */ = { 377 | isa = XCBuildConfiguration; 378 | buildSettings = { 379 | ALWAYS_SEARCH_USER_PATHS = NO; 380 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 381 | CLANG_CXX_LIBRARY = "libc++"; 382 | CLANG_ENABLE_MODULES = YES; 383 | CLANG_ENABLE_OBJC_ARC = YES; 384 | CLANG_WARN_BOOL_CONVERSION = YES; 385 | CLANG_WARN_CONSTANT_CONVERSION = YES; 386 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 387 | CLANG_WARN_EMPTY_BODY = YES; 388 | CLANG_WARN_ENUM_CONVERSION = YES; 389 | CLANG_WARN_INT_CONVERSION = YES; 390 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 391 | CLANG_WARN_UNREACHABLE_CODE = YES; 392 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 393 | COPY_PHASE_STRIP = NO; 394 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 395 | ENABLE_NS_ASSERTIONS = NO; 396 | ENABLE_STRICT_OBJC_MSGSEND = YES; 397 | GCC_C_LANGUAGE_STANDARD = gnu99; 398 | GCC_NO_COMMON_BLOCKS = YES; 399 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 400 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 401 | GCC_WARN_UNDECLARED_SELECTOR = YES; 402 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 403 | GCC_WARN_UNUSED_FUNCTION = YES; 404 | GCC_WARN_UNUSED_VARIABLE = YES; 405 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 406 | MTL_ENABLE_DEBUG_INFO = NO; 407 | SDKROOT = iphoneos; 408 | VALIDATE_PRODUCT = YES; 409 | }; 410 | name = Release; 411 | }; 412 | 5D8236841B0CE05B005A9EF3 /* Debug */ = { 413 | isa = XCBuildConfiguration; 414 | buildSettings = { 415 | HEADER_SEARCH_PATHS = ( 416 | "$(inherited)", 417 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 418 | "$(SRCROOT)/../../React/**", 419 | "$(SRCROOT)/../react-native/React/**", 420 | "$(SRCROOT)/node_modules/react-native/React/**", 421 | ); 422 | OTHER_LDFLAGS = "-ObjC"; 423 | PRODUCT_NAME = "$(TARGET_NAME)"; 424 | SKIP_INSTALL = YES; 425 | }; 426 | name = Debug; 427 | }; 428 | 5D8236851B0CE05B005A9EF3 /* Release */ = { 429 | isa = XCBuildConfiguration; 430 | buildSettings = { 431 | HEADER_SEARCH_PATHS = ( 432 | "$(inherited)", 433 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 434 | "$(SRCROOT)/../../React/**", 435 | "$(SRCROOT)/../react-native/React/**", 436 | "$(SRCROOT)/node_modules/react-native/React/**", 437 | ); 438 | OTHER_LDFLAGS = "-ObjC"; 439 | PRODUCT_NAME = "$(TARGET_NAME)"; 440 | SKIP_INSTALL = YES; 441 | }; 442 | name = Release; 443 | }; 444 | 647898651F38BF9100DA1C12 /* Debug */ = { 445 | isa = XCBuildConfiguration; 446 | buildSettings = { 447 | CLANG_ANALYZER_NONNULL = YES; 448 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 449 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 450 | CLANG_WARN_INFINITE_RECURSION = YES; 451 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 452 | DEBUG_INFORMATION_FORMAT = dwarf; 453 | ENABLE_TESTABILITY = YES; 454 | OTHER_LDFLAGS = "-ObjC"; 455 | PRODUCT_NAME = RNKeychainManager; 456 | SDKROOT = appletvos; 457 | SKIP_INSTALL = YES; 458 | TVOS_DEPLOYMENT_TARGET = 10.2; 459 | }; 460 | name = Debug; 461 | }; 462 | 647898661F38BF9100DA1C12 /* Release */ = { 463 | isa = XCBuildConfiguration; 464 | buildSettings = { 465 | CLANG_ANALYZER_NONNULL = YES; 466 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 467 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 468 | CLANG_WARN_INFINITE_RECURSION = YES; 469 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 470 | OTHER_LDFLAGS = "-ObjC"; 471 | PRODUCT_NAME = RNKeychainManager; 472 | SDKROOT = appletvos; 473 | SKIP_INSTALL = YES; 474 | TVOS_DEPLOYMENT_TARGET = 10.2; 475 | }; 476 | name = Release; 477 | }; 478 | /* End XCBuildConfiguration section */ 479 | 480 | /* Begin XCConfigurationList section */ 481 | 4F9F3CC326B8FD1000F34E8C /* Build configuration list for PBXNativeTarget "RNKeychainManager-macOS" */ = { 482 | isa = XCConfigurationList; 483 | buildConfigurations = ( 484 | 4F9F3CC126B8FD1000F34E8C /* Debug */, 485 | 4F9F3CC226B8FD1000F34E8C /* Release */, 486 | ); 487 | defaultConfigurationIsVisible = 0; 488 | defaultConfigurationName = Release; 489 | }; 490 | 5D82366A1B0CE05B005A9EF3 /* Build configuration list for PBXProject "RNKeychainManager" */ = { 491 | isa = XCConfigurationList; 492 | buildConfigurations = ( 493 | 5D8236811B0CE05B005A9EF3 /* Debug */, 494 | 5D8236821B0CE05B005A9EF3 /* Release */, 495 | ); 496 | defaultConfigurationIsVisible = 0; 497 | defaultConfigurationName = Release; 498 | }; 499 | 5D8236831B0CE05B005A9EF3 /* Build configuration list for PBXNativeTarget "RNKeychainManager" */ = { 500 | isa = XCConfigurationList; 501 | buildConfigurations = ( 502 | 5D8236841B0CE05B005A9EF3 /* Debug */, 503 | 5D8236851B0CE05B005A9EF3 /* Release */, 504 | ); 505 | defaultConfigurationIsVisible = 0; 506 | defaultConfigurationName = Release; 507 | }; 508 | 647898671F38BF9100DA1C12 /* Build configuration list for PBXNativeTarget "RNKeychainManager-tvOS" */ = { 509 | isa = XCConfigurationList; 510 | buildConfigurations = ( 511 | 647898651F38BF9100DA1C12 /* Debug */, 512 | 647898661F38BF9100DA1C12 /* Release */, 513 | ); 514 | defaultConfigurationIsVisible = 0; 515 | defaultConfigurationName = Release; 516 | }; 517 | /* End XCConfigurationList section */ 518 | }; 519 | rootObject = 5D8236671B0CE05B005A9EF3 /* Project object */; 520 | } 521 | -------------------------------------------------------------------------------- /ios/RNKeychainManager/RNKeychainManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // RNKeychainManager.h 3 | // RNKeychainManager 4 | // 5 | // Created by Joel Arvidsson on 2015-05-20. 6 | // Copyright (c) 2015 Joel Arvidsson. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface RNKeychainManager : NSObject 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-keychain-manager", 3 | "version": "1.2.1", 4 | "description": "Keychain Access for React Native", 5 | "main": "./lib/commonjs/index.js", 6 | "module": "./lib/module/index", 7 | "types": "./lib/typescript/index.d.ts", 8 | "react-native": "./src/index", 9 | "source": "./src/index", 10 | "files": [ 11 | "src", 12 | "android", 13 | "ios/RNKeychainManager.xcodeproj", 14 | "ios/RNKeychainManager", 15 | "lib", 16 | "RNKeychainManager.podspec" 17 | ], 18 | "scripts": { 19 | "format": "prettier '{,typings/,KeychainExample/}*.{md,js,json,ts,tsx}' --write", 20 | "lint": "eslint \"**/*.{js,ts,tsx}\"", 21 | "typecheck": "tsc -p tsconfig.json", 22 | "prepare": "bob build" 23 | }, 24 | "workspaces": [ 25 | "KeychainExample" 26 | ], 27 | "keywords": [ 28 | "react-native", 29 | "react-component", 30 | "react-native-component", 31 | "react", 32 | "mobile", 33 | "ios", 34 | "android", 35 | "keychain" 36 | ], 37 | "author": { 38 | "name": "Dorian Mazur", 39 | "email": "mazur.dorian15@gmail.com" 40 | }, 41 | "contributors": [ 42 | "Joel Arvidsson (https://github.com/oblador)" 43 | ], 44 | "homepage": "https://github.com/DorianMazur/react-native-keychain-manager", 45 | "bugs": { 46 | "url": "https://github.com/DorianMazur/react-native-keychain-manager/issues" 47 | }, 48 | "packageManager": "yarn@3.6.1", 49 | "repository": { 50 | "type": "git", 51 | "url": "git://github.com/DorianMazur/react-native-keychain-manager.git" 52 | }, 53 | "license": "MIT", 54 | "devDependencies": { 55 | "@react-native/eslint-config": "^0.74.84", 56 | "@react-native/typescript-config": "^0.74.84", 57 | "eslint": "^8.46.0", 58 | "eslint-plugin-prettier": "^5.1.3", 59 | "prettier": "^3.0.3", 60 | "react": "18.2.0", 61 | "react-native": "0.74.5", 62 | "react-native-builder-bob": "^0.30.0", 63 | "typescript": "^5.2.2" 64 | }, 65 | "volta": { 66 | "node": "16.14.2" 67 | }, 68 | "engines": { 69 | "node": ">=18" 70 | }, 71 | "codegenConfig": { 72 | "name": "RNKeychainManagerSpec", 73 | "type": "modules", 74 | "jsSrcsDir": "src", 75 | "android": { 76 | "javaPackageName": "com.dorianmazur.keychain" 77 | } 78 | }, 79 | "react-native-builder-bob": { 80 | "source": "src", 81 | "output": "lib", 82 | "targets": [ 83 | [ 84 | "commonjs", 85 | { 86 | "esm": true 87 | } 88 | ], 89 | [ 90 | "module", 91 | { 92 | "esm": true 93 | } 94 | ], 95 | [ 96 | "typescript", 97 | { 98 | "project": "tsconfig.build.json" 99 | } 100 | ] 101 | ] 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /react-native.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dependency: { 3 | platforms: { 4 | android: { 5 | packageImportPath: 'import com.dorianmazur.keychain.KeychainPackage;', 6 | packageInstance: 'new KeychainPackage()', 7 | }, 8 | }, 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { NativeModules, Platform } from 'react-native'; 2 | 3 | const { RNKeychainManager } = NativeModules; 4 | 5 | export enum ACCESSIBLE { 6 | WHEN_UNLOCKED = 'AccessibleWhenUnlocked', 7 | AFTER_FIRST_UNLOCK = 'AccessibleAfterFirstUnlock', 8 | ALWAYS = 'AccessibleAlways', 9 | WHEN_PASSCODE_SET_THIS_DEVICE_ONLY = 'AccessibleWhenPasscodeSetThisDeviceOnly', 10 | WHEN_UNLOCKED_THIS_DEVICE_ONLY = 'AccessibleWhenUnlockedThisDeviceOnly', 11 | AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY = 'AccessibleAfterFirstUnlockThisDeviceOnly', 12 | } 13 | 14 | export enum ACCESS_CONTROL { 15 | USER_PRESENCE = 'UserPresence', 16 | BIOMETRY_ANY = 'BiometryAny', 17 | BIOMETRY_CURRENT_SET = 'BiometryCurrentSet', 18 | DEVICE_PASSCODE = 'DevicePasscode', 19 | APPLICATION_PASSWORD = 'ApplicationPassword', 20 | BIOMETRY_ANY_OR_DEVICE_PASSCODE = 'BiometryAnyOrDevicePasscode', 21 | BIOMETRY_CURRENT_SET_OR_DEVICE_PASSCODE = 'BiometryCurrentSetOrDevicePasscode', 22 | } 23 | 24 | export enum AUTHENTICATION_TYPE { 25 | DEVICE_PASSCODE_OR_BIOMETRICS = 'AuthenticationWithBiometricsDevicePasscode', 26 | BIOMETRICS = 'AuthenticationWithBiometrics', 27 | } 28 | 29 | export enum SECURITY_LEVEL { 30 | SECURE_SOFTWARE = RNKeychainManager && 31 | RNKeychainManager.SECURITY_LEVEL_SECURE_SOFTWARE, 32 | SECURE_HARDWARE = RNKeychainManager && 33 | RNKeychainManager.SECURITY_LEVEL_SECURE_HARDWARE, 34 | ANY = RNKeychainManager && RNKeychainManager.SECURITY_LEVEL_ANY, 35 | } 36 | 37 | export enum BIOMETRY_TYPE { 38 | TOUCH_ID = 'TouchID', 39 | FACE_ID = 'FaceID', 40 | OPTIC_ID = 'OpticID', 41 | FINGERPRINT = 'Fingerprint', 42 | FACE = 'Face', 43 | IRIS = 'Iris', 44 | } 45 | 46 | export enum STORAGE_TYPE { 47 | FB = 'FacebookConceal', 48 | AES = 'KeystoreAESCBC', 49 | RSA = 'KeystoreRSAECB', 50 | KC = 'keychain', 51 | } 52 | 53 | export enum SECURITY_RULES { 54 | NONE = 'none', 55 | AUTOMATIC_UPGRADE = 'automaticUpgradeToMoreSecuredStorage', 56 | } 57 | 58 | export type AuthenticationPrompt = { 59 | title?: string; 60 | subtitle?: string; 61 | description?: string; 62 | cancel?: string; 63 | }; 64 | 65 | type BaseOptions = { 66 | accessControl?: ACCESS_CONTROL; 67 | accessGroup?: string; 68 | accessible?: ACCESSIBLE; 69 | authenticationType?: AUTHENTICATION_TYPE; 70 | service?: string; 71 | securityLevel?: SECURITY_LEVEL; 72 | storage?: STORAGE_TYPE; 73 | rules?: SECURITY_RULES; 74 | }; 75 | 76 | type NormalizedOptions = { 77 | authenticationPrompt?: AuthenticationPrompt; 78 | } & BaseOptions; 79 | 80 | export type Options = Partial< 81 | { 82 | authenticationPrompt?: string | AuthenticationPrompt; 83 | } & BaseOptions 84 | >; 85 | 86 | export type Result = { 87 | service: string; 88 | storage: string; 89 | }; 90 | 91 | export type UserCredentials = { 92 | username: string; 93 | password: string; 94 | } & Result; 95 | 96 | export type SharedWebCredentials = { 97 | server: string; 98 | } & UserCredentials; 99 | 100 | const AUTH_PROMPT_DEFAULTS = { 101 | title: 'Authenticate to retrieve secret', 102 | cancel: 'Cancel', 103 | }; 104 | 105 | function normalizeServiceOption(serviceOrOptions?: string | Options): Options { 106 | if (typeof serviceOrOptions === 'string') { 107 | console.warn( 108 | `You passed a service string as an argument to one of the react-native-keychain-manager functions. 109 | This way of passing service is deprecated and will be removed in a future major. 110 | Please update your code to use { service: ${JSON.stringify( 111 | serviceOrOptions 112 | )} }` 113 | ); 114 | return { service: serviceOrOptions }; 115 | } 116 | return serviceOrOptions || {}; 117 | } 118 | 119 | function normalizeOptions( 120 | serviceOrOptions?: string | Options 121 | ): NormalizedOptions { 122 | const options = { 123 | ...normalizeServiceOption(serviceOrOptions), 124 | } as NormalizedOptions; 125 | const { authenticationPrompt } = options; 126 | 127 | if (typeof authenticationPrompt === 'string') { 128 | console.warn( 129 | `You passed a authenticationPrompt string as an argument to one of the react-native-keychain-manager functions. 130 | This way of passing authenticationPrompt is deprecated and will be removed in a future major. 131 | Please update your code to use { authenticationPrompt: { title: ${JSON.stringify( 132 | authenticationPrompt 133 | )} }` 134 | ); 135 | options.authenticationPrompt = { 136 | ...AUTH_PROMPT_DEFAULTS, 137 | title: authenticationPrompt, 138 | }; 139 | } else { 140 | options.authenticationPrompt = { 141 | ...AUTH_PROMPT_DEFAULTS, 142 | ...authenticationPrompt, 143 | }; 144 | } 145 | 146 | // $FlowFixMe >=0.107.x – remove in next major, when authenticationPrompt as string is removed 147 | return options; 148 | } 149 | 150 | //* EXPORTS */ 151 | 152 | /** 153 | * Saves the `username` and `password` combination for `service`. 154 | * @param {string} username Associated username or e-mail to be saved. 155 | * @param {string} password Associated password to be saved. 156 | * @param {object} options A keychain options object. 157 | * @return {Promise} Resolves to `{ service, storage }` when successful 158 | */ 159 | export function setGenericPassword( 160 | username: string, 161 | password: string, 162 | serviceOrOptions?: string | Options 163 | ): Promise { 164 | const options = normalizeOptions(serviceOrOptions); 165 | return RNKeychainManager.setGenericPasswordForOptions( 166 | options, 167 | username, 168 | password 169 | ); 170 | } 171 | 172 | /** 173 | * Fetches login combination for `service`. 174 | * @param {object} options A keychain options object. 175 | * @return {Promise} Resolves to `{ service, username, password, storage }` when successful 176 | */ 177 | export function getGenericPassword( 178 | serviceOrOptions?: string | Options 179 | ): Promise { 180 | const options = normalizeOptions(serviceOrOptions); 181 | return RNKeychainManager.getGenericPasswordForOptions(options); 182 | } 183 | 184 | /** 185 | * Checks if we have generic password for `service`. 186 | * @param {string} service Service to fetch generic password for. 187 | * @return {Promise} Resolved to `true` when successful 188 | */ 189 | export function hasGenericPassword( 190 | serviceOrOptions?: string | Options 191 | ): Promise { 192 | const options = normalizeOptions(serviceOrOptions); 193 | return RNKeychainManager.hasGenericPasswordForOptions(options); 194 | } 195 | 196 | /** 197 | * Deletes all generic password keychain entries for `service`. 198 | * @param {object} options An Keychain options object. 199 | * @return {Promise} Resolves to `true` when successful 200 | */ 201 | export function resetGenericPassword( 202 | serviceOrOptions?: string | Options 203 | ): Promise { 204 | const options = normalizeOptions(serviceOrOptions); 205 | return RNKeychainManager.resetGenericPasswordForOptions(options); 206 | } 207 | 208 | /** 209 | * Gets all `service` keys used in keychain entries. 210 | * @return {Promise} Resolves to an array of strings 211 | */ 212 | export function getAllGenericPasswordServices(): Promise { 213 | return RNKeychainManager.getAllGenericPasswordServices(); 214 | } 215 | 216 | /** 217 | * Checks if we have a login combination for `server`. 218 | * @param {string} server URL to server. 219 | * @return {Promise} Resolves to `{service, storage}` when successful 220 | */ 221 | export function hasInternetCredentials( 222 | server: string 223 | ): Promise { 224 | return RNKeychainManager.hasInternetCredentialsForServer(server); 225 | } 226 | 227 | /** 228 | * Saves the `username` and `password` combination for `server`. 229 | * @param {string} server URL to server. 230 | * @param {string} username Associated username or e-mail to be saved. 231 | * @param {string} password Associated password to be saved. 232 | * @param {object} options A keychain options object. 233 | * @return {Promise} Resolves to `{service, storage}` when successful 234 | */ 235 | export function setInternetCredentials( 236 | server: string, 237 | username: string, 238 | password: string, 239 | options?: Options 240 | ): Promise { 241 | return RNKeychainManager.setInternetCredentialsForServer( 242 | server, 243 | username, 244 | password, 245 | options 246 | ); 247 | } 248 | 249 | /** 250 | * Fetches login combination for `server`. 251 | * @param {string} server URL to server. 252 | * @param {object} options A keychain options object. 253 | * @return {Promise} Resolves to `{ server, username, password }` when successful 254 | */ 255 | export function getInternetCredentials( 256 | server: string, 257 | options?: Options 258 | ): Promise { 259 | return RNKeychainManager.getInternetCredentialsForServer( 260 | server, 261 | normalizeOptions(options) 262 | ); 263 | } 264 | 265 | /** 266 | * Deletes all internet password keychain entries for `server`. 267 | * @param {string} server URL to server. 268 | * @param {object} options Keychain options, iOS only 269 | * @return {Promise} Resolves to `true` when successful 270 | */ 271 | export function resetInternetCredentials(server: string): Promise { 272 | return RNKeychainManager.resetInternetCredentialsForServer(server); 273 | } 274 | 275 | /** 276 | * Get what type of Class 3 (strong) biometry support the device has. 277 | * @param {object} options An Keychain options object. 278 | * @return {Promise} Resolves to a `BIOMETRY_TYPE` when supported, otherwise `null` 279 | */ 280 | export function getSupportedBiometryType(): Promise { 281 | if (!RNKeychainManager.getSupportedBiometryType) { 282 | return Promise.resolve(null); 283 | } 284 | 285 | return RNKeychainManager.getSupportedBiometryType(); 286 | } 287 | 288 | //* IOS ONLY */ 289 | 290 | /** 291 | * Asks the user for a shared web credential. 292 | * @return {Promise} Resolves to `{ server, username, password }` if approved and 293 | * `false` if denied and throws an error if not supported on platform or there's no shared credentials 294 | */ 295 | export function requestSharedWebCredentials(): Promise< 296 | false | SharedWebCredentials 297 | > { 298 | if (Platform.OS !== 'ios') { 299 | return Promise.reject( 300 | new Error( 301 | `requestSharedWebCredentials() is not supported on ${Platform.OS} yet` 302 | ) 303 | ); 304 | } 305 | return RNKeychainManager.requestSharedWebCredentials(); 306 | } 307 | 308 | /** 309 | * Sets a shared web credential. 310 | * @param {string} server URL to server. 311 | * @param {string} username Associated username or e-mail to be saved. 312 | * @param {string} password Associated password to be saved. 313 | * @return {Promise} Resolves to `true` when successful 314 | */ 315 | export function setSharedWebCredentials( 316 | server: string, 317 | username: string, 318 | password?: string 319 | ): Promise { 320 | if (Platform.OS !== 'ios') { 321 | return Promise.reject( 322 | new Error( 323 | `setSharedWebCredentials() is not supported on ${Platform.OS} yet` 324 | ) 325 | ); 326 | } 327 | return RNKeychainManager.setSharedWebCredentialsForServer( 328 | server, 329 | username, 330 | password 331 | ); 332 | } 333 | 334 | /** 335 | * Inquire if the type of local authentication policy (LAPolicy) is supported 336 | * on this device with the device settings the user chose. 337 | * @param {object} options LAPolicy option, iOS only 338 | * @return {Promise} Resolves to `true` when supported, otherwise `false` 339 | */ 340 | export function canImplyAuthentication(options?: Options): Promise { 341 | if (!RNKeychainManager.canCheckAuthentication) { 342 | return Promise.resolve(false); 343 | } 344 | return RNKeychainManager.canCheckAuthentication(options); 345 | } 346 | 347 | //* ANDROID ONLY */ 348 | 349 | /** 350 | * (Android only) Returns guaranteed security level supported by this library 351 | * on the current device. 352 | * @param {object} options A keychain options object. 353 | * @return {Promise} Resolves to `SECURITY_LEVEL` when supported, otherwise `null`. 354 | */ 355 | export function getSecurityLevel( 356 | options?: Options 357 | ): Promise { 358 | if (!RNKeychainManager.getSecurityLevel) { 359 | return Promise.resolve(null); 360 | } 361 | return RNKeychainManager.getSecurityLevel(options); 362 | } 363 | 364 | /** Refs: https://www.saltycrane.com/cheat-sheets/flow-type/latest/ */ 365 | 366 | export default { 367 | SECURITY_LEVEL, 368 | ACCESSIBLE, 369 | ACCESS_CONTROL, 370 | AUTHENTICATION_TYPE, 371 | BIOMETRY_TYPE, 372 | STORAGE_TYPE, 373 | SECURITY_RULES, 374 | getSecurityLevel, 375 | canImplyAuthentication, 376 | getSupportedBiometryType, 377 | setInternetCredentials, 378 | getInternetCredentials, 379 | resetInternetCredentials, 380 | setGenericPassword, 381 | getGenericPassword, 382 | getAllGenericPasswordServices, 383 | resetGenericPassword, 384 | requestSharedWebCredentials, 385 | setSharedWebCredentials, 386 | }; 387 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": ["KeychainExample", "lib"] 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowUnreachableCode": false, 4 | "allowUnusedLabels": false, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "jsx": "react-jsx", 8 | "lib": ["ESNext"], 9 | "module": "ESNext", 10 | "moduleResolution": "Bundler", 11 | "noEmit": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "noImplicitReturns": true, 14 | "noImplicitUseStrict": false, 15 | "noStrictGenericChecks": false, 16 | "noUncheckedIndexedAccess": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "resolveJsonModule": true, 20 | "skipLibCheck": true, 21 | "strict": true, 22 | "target": "ESNext", 23 | "verbatimModuleSyntax": true 24 | }, 25 | "include": ["src", ".eslintrc.js", "react-native.config.js"] 26 | } 27 | --------------------------------------------------------------------------------