diff --git a/.github/workflows/e2e_tests.yaml b/.github/workflows/e2e_tests.yaml index f79651c9..ad4f864b 100644 --- a/.github/workflows/e2e_tests.yaml +++ b/.github/workflows/e2e_tests.yaml @@ -120,6 +120,7 @@ jobs: avd-name: TestingAVD script: | cd KeychainExample + ./e2e/utils/enrollFingerprintAndroid.sh yarn test:android:run env: API_LEVEL: ${{ matrix.api-level }} diff --git a/KeychainExample/e2e/jest.config.js b/KeychainExample/e2e/jest.config.js index ed553668..992dc8fa 100644 --- a/KeychainExample/e2e/jest.config.js +++ b/KeychainExample/e2e/jest.config.js @@ -6,10 +6,11 @@ module.exports = { maxWorkers: 1, globalSetup: 'detox/runners/jest/globalSetup', globalTeardown: 'detox/runners/jest/globalTeardown', + testEnvironment: 'detox/runners/jest/testEnvironment', + setupFilesAfterEnv: ['/e2e/setup.ts'], reporters: [ 'detox/runners/jest/reporter', ['jest-junit', { outputDirectory: 'e2e/output', outputName: 'report.xml' }], ], - testEnvironment: 'detox/runners/jest/testEnvironment', verbose: true, }; diff --git a/KeychainExample/e2e/setup.ts b/KeychainExample/e2e/setup.ts new file mode 100644 index 00000000..bf96b946 --- /dev/null +++ b/KeychainExample/e2e/setup.ts @@ -0,0 +1,12 @@ +import { device } from 'detox'; + +beforeAll(async () => { + if (device.getPlatform() === 'ios') { + await device.setBiometricEnrollment(true); + } +}); + +afterAll(async () => { + await device.uninstallApp(); + await device.installApp(); +}); diff --git a/KeychainExample/e2e/testCases/acessControlTest.spec.js b/KeychainExample/e2e/testCases/acessControlTest.spec.js new file mode 100644 index 00000000..06b2cb0f --- /dev/null +++ b/KeychainExample/e2e/testCases/acessControlTest.spec.js @@ -0,0 +1,154 @@ +import { by, device, element, expect } from 'detox'; +import { matchLoadInfo } from '../utils/matchLoadInfo'; +import cp from 'child_process'; + +describe('Access Control', () => { + beforeEach(async () => { + await device.launchApp({ newInstance: true }); + }); + ['genericPassword', 'internetCredentials'].forEach((type) => { + it( + ' should save and retrieve username and password with biometrics - ' + + type, + async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + await element(by.id('usernameInput')).typeText( + 'testUsernameBiometrics' + ); + await element(by.id('passwordInput')).typeText( + 'testPasswordBiometrics' + ); + // Hide keyboard + await element(by.text('Keychain Example')).tap(); + + if (device.getPlatform() === 'android') { + await element(by.text('Fingerprint')).tap(); + await element(by.text('Software')).tap(); + } else { + await element(by.text('FaceID')).tap(); + } + + await expect(element(by.text('Save'))).toBeVisible(); + await element(by.text('Save')).tap(); + await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); + // Biometric prompt is not available in the IOS simulator + // https://github.com/oblador/react-native-keychain/issues/340 + if (device.getPlatform() === 'android') { + setTimeout(() => { + cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); + }, 1000); + } + await element(by.text('Load')).tap(); + await matchLoadInfo('testUsernameBiometrics', 'testPasswordBiometrics'); + } + ); + + it( + 'should retrieve username and password after app launch with biometrics - ' + + type, + async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + await expect( + element(by.text('hasGenericPassword: true')) + ).toBeVisible(); + // Biometric prompt is not available in the IOS simulator + // https://github.com/oblador/react-native-keychain/issues/340 + if (device.getPlatform() === 'android') { + setTimeout(() => { + cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); + }, 1000); + } + await element(by.text('Load')).tap(); + await matchLoadInfo('testUsernameBiometrics', 'testPasswordBiometrics'); + } + ); + + it( + ':android:should save and retrieve username and password with biometrics for hardware security level - ' + + type, + async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + await element(by.id('usernameInput')).typeText('testUsernameHardware'); + await element(by.id('passwordInput')).typeText('testPasswordHardware'); + // Hide keyboard + await element(by.text('Keychain Example')).tap(); + await element(by.text('Fingerprint')).tap(); + await element(by.text('Hardware')).tap(); + + await expect(element(by.text('Save'))).toBeVisible(); + await element(by.text('Save')).tap(); + await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); + + setTimeout(() => { + cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); + }, 1000); + + await element(by.text('Load')).tap(); + await matchLoadInfo('testUsernameHardware', 'testPasswordHardware'); + } + ); + + it( + ':android:should save and retrieve username and password with biometrics for software security level - ' + + type, + async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + await element(by.id('usernameInput')).typeText('testUsernameSoftware'); + await element(by.id('passwordInput')).typeText('testPasswordSoftware'); + // Hide keyboard + await element(by.text('Keychain Example')).tap(); + await element(by.text('Fingerprint')).tap(); + await element(by.text('Software')).tap(); + + await expect(element(by.text('Save'))).toBeVisible(); + await element(by.text('Save')).tap(); + await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); + + setTimeout(() => { + cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); + }, 1000); + + await element(by.text('Load')).tap(); + await matchLoadInfo('testUsernameSoftware', 'testPasswordSoftware'); + } + ); + + it( + 'should save and retrieve username and password without biometrics' + + type, + async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + await element(by.id('usernameInput')).typeText('testUsernameAny'); + await element(by.id('passwordInput')).typeText('testPasswordAny'); + // Hide keyboard + await element(by.text('Keychain Example')).tap(); + await element(by.text('None')).tap(); + + if (device.getPlatform() === 'android') { + await element(by.text('Software')).tap(); + } + + await expect(element(by.text('Save'))).toBeVisible(); + await element(by.text('Save')).tap(); + await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); + await element(by.text('Load')).tap(); + await matchLoadInfo('testUsernameAny', 'testPasswordAny'); + } + ); + + it('should retrieve username and password after app launch without biometrics', async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + await expect(element(by.text('hasGenericPassword: true'))).toBeVisible(); + await element(by.text('Load')).tap(); + await matchLoadInfo('testUsernameAny', 'testPasswordAny'); + }); + }); + + it('should reset all credentials', async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + // Hide keyboard + + await element(by.text('Reset')).tap(); + await expect(element(by.text(/^Credentials Reset!$/))).toBeVisible(); + }); +}); diff --git a/KeychainExample/e2e/testCases/biometricsAccessControlTest.spec.js b/KeychainExample/e2e/testCases/biometricsAccessControlTest.spec.js deleted file mode 100644 index 00bbb394..00000000 --- a/KeychainExample/e2e/testCases/biometricsAccessControlTest.spec.js +++ /dev/null @@ -1,77 +0,0 @@ -import { by, device, element, expect } from 'detox'; -import { enrollBiometric } from '../utils/enrollBiometrics'; -import { matchLoadInfo } from '../utils/matchLoadInfo'; -import cp from 'child_process'; - -describe('Biometrics Access Control', () => { - beforeAll(async () => { - await enrollBiometric(); - }); - - beforeEach(async () => { - await device.launchApp({ newInstance: true }); - }); - - it('should save and retrieve username and password', async () => { - await expect(element(by.text('Keychain Example'))).toExist(); - await element(by.id('usernameInput')).typeText('testUsername'); - await element(by.id('passwordInput')).typeText('testPassword'); - // Hide keyboard - await element(by.text('Keychain Example')).tap(); - - if (device.getPlatform() === 'android') { - await element(by.text('Fingerprint')).tap(); - await element(by.text('Software')).tap(); - } else { - await element(by.text('FaceID')).tap(); - } - - await expect(element(by.text('Save'))).toBeVisible(); - await element(by.text('Save')).tap(); - await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); - // Biometric prompt is not available in the IOS simulator - // https://github.com/oblador/react-native-keychain/issues/340 - if (device.getPlatform() === 'android') { - setTimeout(() => { - cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); - }, 1000); - } - await element(by.text('Load')).tap(); - await matchLoadInfo('testUsername', 'testPassword'); - }); - - it('should retrieve username and password after app launch', async () => { - await expect(element(by.text('Keychain Example'))).toExist(); - await expect(element(by.text('hasGenericPassword: true'))).toBeVisible(); - // Biometric prompt is not available in the IOS simulator - // https://github.com/oblador/react-native-keychain/issues/340 - if (device.getPlatform() === 'android') { - setTimeout(() => { - cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); - }, 1000); - } - await element(by.text('Load')).tap(); - await matchLoadInfo('testUsername', 'testPassword'); - }); - - it(':android:should save and retrieve username and password for hardware security level', async () => { - await expect(element(by.text('Keychain Example'))).toExist(); - await element(by.id('usernameInput')).typeText('testUsernameHardware'); - await element(by.id('passwordInput')).typeText('testPasswordHardware'); - // Hide keyboard - await element(by.text('Keychain Example')).tap(); - await element(by.text('Fingerprint')).tap(); - await element(by.text('Hardware')).tap(); - - await expect(element(by.text('Save'))).toBeVisible(); - await element(by.text('Save')).tap(); - await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); - - setTimeout(() => { - cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); - }, 1000); - - await element(by.text('Load')).tap(); - await matchLoadInfo('testUsernameHardware', 'testPasswordHardware'); - }); -}); diff --git a/KeychainExample/e2e/testCases/noneAccessControTest.spec.js b/KeychainExample/e2e/testCases/noneAccessControTest.spec.js deleted file mode 100644 index bb6292a3..00000000 --- a/KeychainExample/e2e/testCases/noneAccessControTest.spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import { by, device, element, expect } from 'detox'; -import { matchLoadInfo } from '../utils/matchLoadInfo'; - -describe('None Access Control', () => { - beforeEach(async () => { - await device.launchApp({ newInstance: true }); - }); - - it('should save and retrieve username and password', async () => { - await expect(element(by.text('Keychain Example'))).toExist(); - await element(by.id('usernameInput')).typeText('testUsername'); - await element(by.id('passwordInput')).typeText('testPassword'); - // Hide keyboard - await element(by.text('Keychain Example')).tap(); - await element(by.text('None')).tap(); - - if (device.getPlatform() === 'android') { - await element(by.text('Software')).tap(); - } - - await expect(element(by.text('Save'))).toBeVisible(); - await element(by.text('Save')).tap(); - await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); - await element(by.text('Load')).tap(); - await matchLoadInfo('testUsername', 'testPassword'); - }); - - it('should retrieve username and password after app launch', async () => { - await expect(element(by.text('Keychain Example'))).toExist(); - await expect(element(by.text('hasGenericPassword: true'))).toBeVisible(); - await element(by.text('Load')).tap(); - await matchLoadInfo('testUsername', 'testPassword'); - }); -}); diff --git a/KeychainExample/e2e/testCases/securityLevelTest.spec.js b/KeychainExample/e2e/testCases/securityLevelTest.spec.js new file mode 100644 index 00000000..29143f0a --- /dev/null +++ b/KeychainExample/e2e/testCases/securityLevelTest.spec.js @@ -0,0 +1,92 @@ +import { by, device, element, expect } from 'detox'; +import { matchLoadInfo } from '../utils/matchLoadInfo'; + +describe(':android:Security Level', () => { + beforeEach(async () => { + await device.launchApp({ newInstance: true }); + }); + ['genericPassword', 'internetCredentials'].forEach((type) => { + it(':android:should save with Any security level - ' + type, async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + await element(by.id('usernameInput')).typeText('testUsernameAny'); + await element(by.id('passwordInput')).typeText('testPasswordAny'); + // Hide keyboard + await element(by.text('Keychain Example')).tap(); + + await element(by.text(type)).tap(); + await element(by.text('None')).tap(); + await element(by.text('Any')).tap(); + + await expect(element(by.text('Save'))).toBeVisible(); + await element(by.text('Save')).tap(); + await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); + await element(by.text('Load')).tap(); + await matchLoadInfo( + 'testUsernameAny', + 'testPasswordAny', + undefined, + type === 'internetCredentials' ? 'https://example.com' : undefined + ); + }); + + it( + ':android:should save with Software security level - ' + type, + async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + await element(by.id('usernameInput')).typeText('testUsernameSoftware'); + await element(by.id('passwordInput')).typeText('testPasswordSoftware'); + // Hide keyboard + await element(by.text('Keychain Example')).tap(); + + await element(by.text(type)).tap(); + await element(by.text('None')).tap(); + await element(by.text('Software')).tap(); + + await expect(element(by.text('Save'))).toBeVisible(); + await element(by.text('Save')).tap(); + await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); + await element(by.text('Load')).tap(); + await matchLoadInfo( + 'testUsernameSoftware', + 'testPasswordSoftware', + undefined, + type === 'internetCredentials' ? 'https://example.com' : undefined + ); + } + ); + + it( + ':android:should save with Hardware security level - ' + type, + async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + await element(by.id('usernameInput')).typeText('testUsernameHardware'); + await element(by.id('passwordInput')).typeText('testPasswordHardware'); + // Hide keyboard + await element(by.text('Keychain Example')).tap(); + + await element(by.text(type)).tap(); + await element(by.text('None')).tap(); + await element(by.text('Hardware')).tap(); + + await expect(element(by.text('Save'))).toBeVisible(); + await element(by.text('Save')).tap(); + await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); + await element(by.text('Load')).tap(); + await matchLoadInfo( + 'testUsernameHardware', + 'testPasswordHardware', + undefined, + type === 'internetCredentials' ? 'https://example.com' : undefined + ); + } + ); + }); + + it(':android:should reset all credentials', async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + // Hide keyboard + + await element(by.text('Reset')).tap(); + await expect(element(by.text(/^Credentials Reset!$/))).toBeVisible(); + }); +}); diff --git a/KeychainExample/e2e/testCases/storageTypesTest.spec.js b/KeychainExample/e2e/testCases/storageTypesTest.spec.js index 20159fba..a5f419d7 100644 --- a/KeychainExample/e2e/testCases/storageTypesTest.spec.js +++ b/KeychainExample/e2e/testCases/storageTypesTest.spec.js @@ -1,70 +1,103 @@ import { by, device, element, expect } from 'detox'; -import { enrollBiometric } from '../utils/enrollBiometrics'; import { matchLoadInfo } from '../utils/matchLoadInfo'; import cp from 'child_process'; describe(':android:Storage Types', () => { - beforeAll(async () => { - await enrollBiometric(); - }); - beforeEach(async () => { await device.launchApp({ newInstance: true }); }); + ['genericPassword', 'internetCredentials'].forEach((type) => { + it( + ':android:should save with FB storage and migrate it to AES - ' + type, + async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + await element(by.id('usernameInput')).typeText('testUsernameFB'); + await element(by.id('passwordInput')).typeText('testPasswordFB'); + // Hide keyboard + await element(by.text('Keychain Example')).tap(); - it(':android:should save with FB storage and migrate it to AES', async () => { - await expect(element(by.text('Keychain Example'))).toExist(); - await element(by.id('usernameInput')).typeText('testUsernameFB'); - await element(by.id('passwordInput')).typeText('testPasswordFB'); - // Hide keyboard - await element(by.text('Keychain Example')).tap(); - await element(by.text('None')).tap(); - await element(by.text('No upgrade')).tap(); - await element(by.text('FB')).tap(); + await element(by.text(type)).tap(); + await element(by.text('None')).tap(); + await element(by.text('No upgrade')).tap(); + await element(by.text('FB')).tap(); - await expect(element(by.text('Save'))).toBeVisible(); - await element(by.text('Save')).tap(); - await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); - await element(by.text('Load')).tap(); - await matchLoadInfo('testUsernameFB', 'testPasswordFB', 'FacebookConceal'); - await element(by.text('Automatic upgrade')).tap(); - await element(by.text('Load')).tap(); - await matchLoadInfo('testUsernameFB', 'testPasswordFB', 'KeystoreAESCBC'); - }); + await expect(element(by.text('Save'))).toBeVisible(); + await element(by.text('Save')).tap(); + await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); + await element(by.text('Load')).tap(); + await matchLoadInfo( + 'testUsernameFB', + 'testPasswordFB', + 'FacebookConceal', + type === 'internetCredentials' ? 'https://example.com' : undefined + ); + await element(by.text('Automatic upgrade')).tap(); + await element(by.text('Load')).tap(); + await matchLoadInfo( + 'testUsernameFB', + 'testPasswordFB', + 'KeystoreAESCBC', + type === 'internetCredentials' ? 'https://example.com' : undefined + ); + } + ); - it(':android:should save with AES storage', async () => { - await expect(element(by.text('Keychain Example'))).toExist(); - await element(by.id('usernameInput')).typeText('testUsernameAES'); - await element(by.id('passwordInput')).typeText('testPasswordAES'); - // Hide keyboard - await element(by.text('Keychain Example')).tap(); - await element(by.text('None')).tap(); - await element(by.text('AES')).tap(); + it(':android:should save with AES storage - ' + type, async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + await element(by.id('usernameInput')).typeText('testUsernameAES'); + await element(by.id('passwordInput')).typeText('testPasswordAES'); + // Hide keyboard + await element(by.text('Keychain Example')).tap(); + + await element(by.text(type)).tap(); + await element(by.text('None')).tap(); + await element(by.text('AES')).tap(); + + await expect(element(by.text('Save'))).toBeVisible(); + await element(by.text('Save')).tap(); + await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); + await element(by.text('Load')).tap(); + await matchLoadInfo( + 'testUsernameAES', + 'testPasswordAES', + 'KeystoreAESCBC', + type === 'internetCredentials' ? 'https://example.com' : undefined + ); + }); + + it(':android:should save with RSA storage - ' + type, async () => { + await expect(element(by.text('Keychain Example'))).toExist(); + await element(by.id('usernameInput')).typeText('testUsernameRSA'); + await element(by.id('passwordInput')).typeText('testPasswordRSA'); + // Hide keyboard + await element(by.text('Keychain Example')).tap(); + + await element(by.text(type)).tap(); + await element(by.text('None')).tap(); + await element(by.text('RSA')).tap(); - await expect(element(by.text('Save'))).toBeVisible(); - await element(by.text('Save')).tap(); - await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); - await element(by.text('Load')).tap(); - await matchLoadInfo('testUsernameAES', 'testPasswordAES', 'KeystoreAESCBC'); + await expect(element(by.text('Save'))).toBeVisible(); + await element(by.text('Save')).tap(); + await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); + setTimeout(() => { + cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); + }, 1000); + await element(by.text('Load')).tap(); + await expect(element(by.text(/^Credentials loaded! .*$/))).toBeVisible(); + await matchLoadInfo( + 'testUsernameRSA', + 'testPasswordRSA', + 'KeystoreRSAECB', + type === 'internetCredentials' ? 'https://example.com' : undefined + ); + }); }); - it(':android:should save with RSA storage', async () => { + it(':android:should reset all credentials', async () => { await expect(element(by.text('Keychain Example'))).toExist(); - await element(by.id('usernameInput')).typeText('testUsernameRSA'); - await element(by.id('passwordInput')).typeText('testPasswordRSA'); // Hide keyboard - await element(by.text('Keychain Example')).tap(); - await element(by.text('None')).tap(); - await element(by.text('RSA')).tap(); - await expect(element(by.text('Save'))).toBeVisible(); - await element(by.text('Save')).tap(); - await expect(element(by.text(/^Credentials saved! .*$/))).toBeVisible(); - setTimeout(() => { - cp.spawnSync('adb', ['-e', 'emu', 'finger', 'touch', '1']); - }, 1000); - await element(by.text('Load')).tap(); - await expect(element(by.text(/^Credentials loaded! .*$/))).toBeVisible(); - await matchLoadInfo('testUsernameRSA', 'testPasswordRSA', 'KeystoreRSAECB'); + await element(by.text('Reset')).tap(); + await expect(element(by.text(/^Credentials Reset!$/))).toBeVisible(); }); }); diff --git a/KeychainExample/e2e/utils/enrollBiometrics.ts b/KeychainExample/e2e/utils/enrollBiometrics.ts deleted file mode 100644 index bb3e8d68..00000000 --- a/KeychainExample/e2e/utils/enrollBiometrics.ts +++ /dev/null @@ -1,19 +0,0 @@ -import path from 'path'; -import { device } from 'detox'; -import cp from 'child_process'; - -export const enrollBiometric = async () => { - if (device.getPlatform() === 'android') { - const script = path.resolve(__dirname, './enrollFingerprintAndroid.sh'); - const result = cp.spawnSync('sh', [script], { - stdio: 'inherit', - }); - - // Check for errors - if (result.error) { - console.error('Error executing script:', result.error); - } - } else { - await device.setBiometricEnrollment(true); - } -}; diff --git a/KeychainExample/e2e/utils/matchLoadInfo.ts b/KeychainExample/e2e/utils/matchLoadInfo.ts index 17b92eec..dabc38ba 100644 --- a/KeychainExample/e2e/utils/matchLoadInfo.ts +++ b/KeychainExample/e2e/utils/matchLoadInfo.ts @@ -1,7 +1,10 @@ +import { by, element, expect } from 'detox'; + export const matchLoadInfo = async ( username: string, password: string, - storage?: string + storage?: string, + service?: string ) => { let regexPattern; @@ -11,6 +14,10 @@ export const matchLoadInfo = async ( regexPattern = `^Credentials loaded! .*"storage":"${storage}","password":"${password}","username":"${username}"`; } + if (service) { + regexPattern += `,"service":"${service}"`; + } + regexPattern += '.*$'; const regex = new RegExp(regexPattern); await expect(element(by.text(regex))).toBeVisible(); diff --git a/KeychainExample/package.json b/KeychainExample/package.json index 84e98c33..f7d10e3d 100644 --- a/KeychainExample/package.json +++ b/KeychainExample/package.json @@ -29,10 +29,11 @@ "@react-native/metro-config": "^0.74.5", "@react-native/typescript-config": "^0.74.5", "@rnx-kit/polyfills": "^0.1.1", + "@types/jest": "^29.2.1", "@types/react": "^18.2.0", "babel-plugin-module-resolver": "^5.0.2", - "detox": "^20.25.6", - "jest": "^29.7.0", + "detox": "^20.27.5", + "jest": "^29.6.3", "jest-junit": "^16.0.0", "react-native-test-app": "^3.9.7" }, diff --git a/KeychainExample/src/App.tsx b/KeychainExample/src/App.tsx index 4dc18c77..b37c9a02 100644 --- a/KeychainExample/src/App.tsx +++ b/KeychainExample/src/App.tsx @@ -1,6 +1,5 @@ import React, { useState, useEffect } from 'react'; import { - Alert, Keyboard, KeyboardAvoidingView, Platform, @@ -42,10 +41,13 @@ const SECURITY_STORAGE_MAP = [ const SECURITY_RULES_OPTIONS = ['No upgrade', 'Automatic upgrade']; const SECURITY_RULES_MAP = [null, Keychain.SECURITY_RULES.AUTOMATIC_UPGRADE]; +const TYPE_OPTIONS = ['genericPassword', 'internetCredentials']; + export default function App() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [status, setStatus] = useState(''); + const [type, setType] = useState(TYPE_OPTIONS[0]); const [biometryType, setBiometryType] = useState(null); const [accessControl, setAccessControl] = useState< @@ -66,6 +68,7 @@ export default function App() { useState(0); const [selectedRulesIndex, setSelectedRulesIndex] = useState(0); const [hasGenericPassword, setHasGenericPassword] = useState(false); + const [hasInternetCredentials, setHasInternetCredentials] = useState(false); useEffect(() => { Keychain.getSupportedBiometryType().then((result) => { @@ -74,17 +77,36 @@ export default function App() { Keychain.hasGenericPassword().then((result) => { setHasGenericPassword(result); }); + Keychain.hasInternetCredentials({ server: 'https://example.com' }).then( + (result) => { + setHasInternetCredentials(result); + } + ); }, []); const save = async () => { try { const start = new Date(); - await Keychain.setGenericPassword(username, password, { - accessControl, - securityLevel, - storage, - rules, - }); + if (type === 'internetCredentials') { + await Keychain.setInternetCredentials( + 'https://example.com', + username, + password, + { + accessControl, + securityLevel, + storage, + rules, + } + ); + } else { + await Keychain.setGenericPassword(username, password, { + accessControl, + securityLevel, + storage, + rules, + }); + } const end = new Date(); setUsername(''); @@ -107,10 +129,21 @@ export default function App() { cancel: 'Cancel', }, }; - const credentials = await Keychain.getGenericPassword({ - ...options, - rules: rules, - }); + let credentials; + if (type === 'internetCredentials') { + credentials = await Keychain.getInternetCredentials( + 'https://example.com', + { + ...options, + rules: rules, + } + ); + } else { + credentials = await Keychain.getGenericPassword({ + ...options, + rules: rules, + }); + } if (credentials) { setStatus('Credentials loaded! ' + JSON.stringify(credentials)); } else { @@ -124,6 +157,7 @@ export default function App() { const reset = async () => { try { await Keychain.resetGenericPassword(); + await Keychain.resetInternetCredentials('https://example.com'); setStatus('Credentials Reset!'); setUsername(''); setPassword(''); @@ -132,15 +166,6 @@ export default function App() { } }; - const getAll = async () => { - try { - const result = await Keychain.getAllGenericPasswordServices(); - setStatus(`All keys successfully fetched! Found: ${result.length} keys.`); - } catch (err) { - setStatus('Could not get all keys. ' + err); - } - }; - const AC_VALUES = Platform.OS === 'ios' ? ACCESS_CONTROL_OPTIONS @@ -182,6 +207,16 @@ export default function App() { underlineColorAndroid="transparent" /> + + Type + { + setType(TYPE_OPTIONS[index]); + }} + /> + Access Control - - - - Get Used Keys - - - {Platform.OS === 'android' && ( - { - const level = await Keychain.getSecurityLevel(); - if (level !== null) { - Alert.alert('Security Level', JSON.stringify(level)); - } - }} - style={styles.button} - > - - Get security level - - - )} - hasGenericPassword: {String(hasGenericPassword)} + + hasInternetCredentials: {String(hasInternetCredentials)} + ); diff --git a/src/types.ts b/src/types.ts index 7d8cc689..c9245584 100644 --- a/src/types.ts +++ b/src/types.ts @@ -51,7 +51,9 @@ export type Options = { service?: string; /** The server name to associate with the keychain item. */ server?: string; - /** The desired security level of the keychain item. */ + /** The desired security level of the keychain item. + * @platform Android + */ securityLevel?: SECURITY_LEVEL; /** The storage type. * @platform Android diff --git a/yarn.lock b/yarn.lock index 8fd173a1..6a9eea66 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5356,6 +5356,16 @@ __metadata: languageName: node linkType: hard +"@types/jest@npm:^29.2.1": + version: 29.5.14 + resolution: "@types/jest@npm:29.5.14" + dependencies: + expect: ^29.0.0 + pretty-format: ^29.0.0 + checksum: 18dba4623f26661641d757c63da2db45e9524c9be96a29ef713c703a9a53792df9ecee9f7365a0858ddbd6440d98fe6b65ca67895ca5884b73cbc7ffc11f3838 + languageName: node + linkType: hard + "@types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.15 resolution: "@types/json-schema@npm:7.0.15" @@ -5993,10 +6003,11 @@ __metadata: "@react-native/metro-config": ^0.74.5 "@react-native/typescript-config": ^0.74.5 "@rnx-kit/polyfills": ^0.1.1 + "@types/jest": ^29.2.1 "@types/react": ^18.2.0 babel-plugin-module-resolver: ^5.0.2 - detox: ^20.25.6 - jest: ^29.7.0 + detox: ^20.27.5 + jest: ^29.6.3 jest-junit: ^16.0.0 react: 18.2.0 react-native: ^0.74.5 @@ -8499,9 +8510,16 @@ __metadata: languageName: node linkType: hard -"detox@npm:^20.25.6": - version: 20.25.6 - resolution: "detox@npm:20.25.6" +"detox-copilot@npm:^0.0.23": + version: 0.0.23 + resolution: "detox-copilot@npm:0.0.23" + checksum: ae209f519b1f7b97b657de49e752f1fc18292ed049d26056682010231203b3c07bdb61a06d610ffead2a75cdf5b11849cdab03ae2670debe5e2f8b0191006635 + languageName: node + linkType: hard + +"detox@npm:^20.27.5": + version: 20.27.5 + resolution: "detox@npm:20.27.5" dependencies: ajv: ^8.6.3 bunyan: ^1.8.12 @@ -8509,6 +8527,7 @@ __metadata: caf: ^15.0.1 chalk: ^4.0.0 child-process-promise: ^2.2.0 + detox-copilot: ^0.0.23 execa: ^5.1.1 find-up: ^5.0.0 fs-extra: ^11.0.0 @@ -8545,7 +8564,7 @@ __metadata: optional: true bin: detox: local-cli/cli.js - checksum: 8ed6ee124cc86e7c16684eb8172f7cf3b3e440c8a292a758fd93d08e2b75887b5b7dbf5281d4c7e63672c690826883be362161632e00f70f4d20d5c63398b0af + checksum: 97ebf78f0eff67c3dca21719d91581640dad61416136c4c776b58fe68207bb400f39f9e408b25efa06164bcbf3e2dc98f9ee75193b1620ac1408fa540b7bcc8b languageName: node linkType: hard @@ -9594,7 +9613,7 @@ __metadata: languageName: node linkType: hard -"expect@npm:^29.7.0": +"expect@npm:^29.0.0, expect@npm:^29.7.0": version: 29.7.0 resolution: "expect@npm:29.7.0" dependencies: @@ -12596,7 +12615,7 @@ __metadata: languageName: node linkType: hard -"jest@npm:^29.7.0": +"jest@npm:^29.6.3": version: 29.7.0 resolution: "jest@npm:29.7.0" dependencies: @@ -16258,7 +16277,7 @@ __metadata: languageName: node linkType: hard -"pretty-format@npm:^29.7.0": +"pretty-format@npm:^29.0.0, pretty-format@npm:^29.7.0": version: 29.7.0 resolution: "pretty-format@npm:29.7.0" dependencies: