├── .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 |
--------------------------------------------------------------------------------