diff --git a/KeychainExample/e2e/testCases/storageTypesTest.spec.js b/KeychainExample/e2e/testCases/storageTypesTest.spec.js index 736d1d7b..3c38e8cc 100644 --- a/KeychainExample/e2e/testCases/storageTypesTest.spec.js +++ b/KeychainExample/e2e/testCases/storageTypesTest.spec.js @@ -29,7 +29,6 @@ describe(':android:Storage Types', () => { await matchLoadInfo( 'testUsernameFB', 'testPasswordFB', - 'FacebookConceal', type === 'internetCredentials' ? 'https://example.com' : undefined ); await element(by.text('Automatic upgrade')).tap(); diff --git a/android/build.gradle b/android/build.gradle index 4f2ab553..3d9985d2 100755 --- a/android/build.gradle +++ b/android/build.gradle @@ -79,10 +79,6 @@ dependencies { // Needed for BiometricPrompt in androidx.biometric implementation "androidx.fragment:fragment:1.3.2@aar" - /* version higher 1.1.3 has problems with included soloader packages, - https://github.com/facebook/conceal/releases */ - implementation "com.facebook.conceal:conceal:1.1.3@aar" - // Used to store encrypted data implementation("androidx.datastore:datastore-preferences:1.1.1") } diff --git a/android/src/main/java/com/oblador/keychain/DataStorePrefsStorage.kt b/android/src/main/java/com/oblador/keychain/DataStorePrefsStorage.kt index 7227ba18..06164b76 100644 --- a/android/src/main/java/com/oblador/keychain/DataStorePrefsStorage.kt +++ b/android/src/main/java/com/oblador/keychain/DataStorePrefsStorage.kt @@ -45,12 +45,7 @@ class DataStorePrefsStorage( var cipherStorageName = getCipherStorageName(service) // in case of wrong password or username - if (bytesForUsername == null || bytesForPassword == null) return null - if (cipherStorageName == null) { - // If the CipherStorage name is not found, we assume it is because the entry was written by an - // older version of this library which used Facebook Conceal, so we default to that. - cipherStorageName = KnownCiphers.FB - } + if (bytesForUsername == null || bytesForPassword == null || cipherStorageName == null) return null return ResultSet(cipherStorageName, bytesForUsername, bytesForPassword) } diff --git a/android/src/main/java/com/oblador/keychain/KeychainModule.kt b/android/src/main/java/com/oblador/keychain/KeychainModule.kt index 7a2c6793..1866b18f 100644 --- a/android/src/main/java/com/oblador/keychain/KeychainModule.kt +++ b/android/src/main/java/com/oblador/keychain/KeychainModule.kt @@ -16,7 +16,6 @@ import com.facebook.react.module.annotations.ReactModule import com.oblador.keychain.cipherStorage.CipherStorage import com.oblador.keychain.cipherStorage.CipherStorage.DecryptionResult import com.oblador.keychain.cipherStorage.CipherStorageBase -import com.oblador.keychain.cipherStorage.CipherStorageFacebookConceal import com.oblador.keychain.cipherStorage.CipherStorageKeystoreAesCbc import com.oblador.keychain.cipherStorage.CipherStorageKeystoreAesGcm import com.oblador.keychain.cipherStorage.CipherStorageKeystoreRsaEcb @@ -103,12 +102,9 @@ class KeychainModule(reactContext: ReactApplicationContext) : } /** Supported ciphers. */ - @StringDef(KnownCiphers.FB, KnownCiphers.AES_CBC, KnownCiphers.AES_GCM, KnownCiphers.RSA) + @StringDef(KnownCiphers.AES_CBC, KnownCiphers.AES_GCM, KnownCiphers.RSA) annotation class KnownCiphers { companion object { - /** Facebook conceal compatibility lib in use. */ - const val FB = "FacebookConceal" - /** AES CBC encryption. */ const val AES_CBC = "KeystoreAESCBC" @@ -151,10 +147,9 @@ class KeychainModule(reactContext: ReactApplicationContext) : /** Default constructor. */ init { prefsStorage = DataStorePrefsStorage(reactContext, coroutineScope) - addCipherStorageToMap(CipherStorageFacebookConceal(reactContext)) addCipherStorageToMap(CipherStorageKeystoreAesCbc(reactContext)) - addCipherStorageToMap(CipherStorageKeystoreAesGcm(reactContext, false)) - addCipherStorageToMap(CipherStorageKeystoreAesGcm(reactContext, true)) + // addCipherStorageToMap(CipherStorageKeystoreAesGcm(reactContext, false)) + // addCipherStorageToMap(CipherStorageKeystoreAesGcm(reactContext, true)) // we have a references to newer api that will fail load of app classes in old androids OS if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { @@ -289,17 +284,7 @@ class KeychainModule(reactContext: ReactApplicationContext) : val promptInfo = getPromptInfo(options) var cipher: CipherStorage? = null - // Only check for upgradable ciphers for FacebookConseal as that - // is the only cipher that can be upgraded - cipher = - if (rules == Rules.AUTOMATIC_UPGRADE && storageName == KnownCiphers.FB) { - // get the best storage - val accessControl = getAccessControlOrDefault(options) - val useBiometry = getUseBiometry(accessControl) - getCipherStorageForCurrentAPILevel(useBiometry) - } else { - getCipherStorageByName(storageName) - } + cipher = getCipherStorageByName(storageName) val decryptionResult = decryptCredentials(alias, cipher!!, resultSet, rules, promptInfo) val credentials = Arguments.createMap() credentials.putString(Maps.SERVICE, alias) diff --git a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.kt b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.kt deleted file mode 100644 index ce419736..00000000 --- a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageFacebookConceal.kt +++ /dev/null @@ -1,178 +0,0 @@ -package com.oblador.keychain.cipherStorage - -import android.os.Build -import android.security.keystore.KeyGenParameterSpec -import android.security.keystore.KeyInfo -import android.util.Log -import com.facebook.android.crypto.keychain.AndroidConceal -import com.facebook.android.crypto.keychain.SharedPrefsBackedKeyChain -import com.facebook.crypto.Crypto -import com.facebook.crypto.CryptoConfig -import com.facebook.crypto.Entity -import com.facebook.react.bridge.AssertionException -import com.facebook.react.bridge.ReactApplicationContext -import com.oblador.keychain.KeychainModule.KnownCiphers -import com.oblador.keychain.SecurityLevel -import com.oblador.keychain.resultHandler.ResultHandler -import com.oblador.keychain.exceptions.CryptoFailedException -import java.security.GeneralSecurityException -import java.security.Key - -/** - * @see [Conceal Project](https://github.com/facebook/conceal) - * @see - * [Fast Cryptographics](https://medium.com/@ssaurel/make-fast-cryptographic-operations-on-android-with-conceal-77a751e89b8e) - */ -@Suppress("unused", "MemberVisibilityCanBePrivate") -class CipherStorageFacebookConceal(reactContext: ReactApplicationContext) : - CipherStorageBase(reactContext) { - - companion object { - const val KEYCHAIN_DATA = "RN_KEYCHAIN" - } - - private val crypto: Crypto - - init { - val keyChain = SharedPrefsBackedKeyChain(reactContext, CryptoConfig.KEY_256) - crypto = AndroidConceal.get().createDefaultCrypto(keyChain) - } - - // region Configuration - override fun getCipherStorageName(): String = KnownCiphers.FB - - override fun getMinSupportedApiLevel(): Int = Build.VERSION_CODES.JELLY_BEAN - - override fun securityLevel(): SecurityLevel = SecurityLevel.ANY - - override fun supportsSecureHardware(): Boolean = false - - override fun isBiometrySupported(): Boolean = false - - // endregion - - // region Overrides - - @Throws(CryptoFailedException::class) - override fun encrypt( - handler: ResultHandler, - alias: String, - username: String, - password: String, - level: SecurityLevel - ) { - - throwIfInsufficientLevel(level) - throwIfNoCryptoAvailable() - - val usernameEntity = createUsernameEntity(alias) - val passwordEntity = createPasswordEntity(alias) - - try { - val encryptedUsername = crypto.encrypt(username.toByteArray(UTF8), usernameEntity) - val encryptedPassword = crypto.encrypt(password.toByteArray(UTF8), passwordEntity) - - val result = CipherStorage.EncryptionResult(encryptedUsername, encryptedPassword, this) - handler.onEncrypt(result, null) - } catch (fail: Throwable) { - throw CryptoFailedException("Encryption failed for alias: $alias", fail) - } - } - - /** Redirect call to default [decrypt] method. */ - override fun decrypt( - handler: ResultHandler, - alias: String, - username: ByteArray, - password: ByteArray, - level: SecurityLevel - ) { - throwIfInsufficientLevel(level) - throwIfNoCryptoAvailable() - - val usernameEntity = createUsernameEntity(alias) - val passwordEntity = createPasswordEntity(alias) - - try { - val decryptedUsername = crypto.decrypt(username, usernameEntity) - val decryptedPassword = crypto.decrypt(password, passwordEntity) - - val results = CipherStorage.DecryptionResult( - String(decryptedUsername, UTF8), String(decryptedPassword, UTF8), SecurityLevel.ANY - ) - handler.onDecrypt(results, null) - } catch (fail: Throwable) { - handler.onDecrypt(null, fail) - } - } - - override fun removeKey(alias: String) { - // Facebook Conceal stores only one key across all services, so we cannot - // delete the key (otherwise decryption will fail for encrypted data of other services). - Log.w(LOG_TAG, "CipherStorageFacebookConceal removeKey called. alias: $alias") - } - - - @Throws(GeneralSecurityException::class) - override fun getKeyGenSpecBuilder(alias: String): KeyGenParameterSpec.Builder { - throw CryptoFailedException("Not designed for a call") - } - - - @Throws(GeneralSecurityException::class) - override fun getKeyGenSpecBuilder( - alias: String, - isForTesting: Boolean - ): KeyGenParameterSpec.Builder { - throw CryptoFailedException("Not designed for a call") - } - - - @Throws(GeneralSecurityException::class) - override fun getKeyInfo(key: Key): KeyInfo { - throw CryptoFailedException("Not designed for a call") - } - - - @Throws(GeneralSecurityException::class) - override fun generateKey(spec: KeyGenParameterSpec): Key { - throw CryptoFailedException("Not designed for a call") - } - - - override fun getEncryptionAlgorithm(): String { - throw AssertionException("Not designed for a call") - } - - - override fun getEncryptionTransformation(): String { - throw AssertionException("Not designed for a call") - } - - /** Verify availability of the Crypto API. */ - @Throws(CryptoFailedException::class) - private fun throwIfNoCryptoAvailable() { - if (!crypto.isAvailable) { - throw CryptoFailedException("Crypto is missing") - } - } - - // endregion - - // region Helper methods - - private fun createUsernameEntity(alias: String): Entity { - val prefix = getEntityPrefix(alias) - return Entity.create(prefix + "user") - } - - - private fun createPasswordEntity(alias: String): Entity { - val prefix = getEntityPrefix(alias) - return Entity.create(prefix + "pass") - } - - - private fun getEntityPrefix(alias: String): String = "$KEYCHAIN_DATA:$alias" - // endregion -} diff --git a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAesCbc.kt b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAesCbc.kt index 99ae741e..6f16a87c 100644 --- a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAesCbc.kt +++ b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAesCbc.kt @@ -221,7 +221,12 @@ class CipherStorageKeystoreAesCbc(reactContext: ReactApplicationContext) : cipher.init(Cipher.DECRYPT_MODE, key, spec) // Decrypt the bytes using cipher.doFinal() - val decryptedBytes = cipher.doFinal(bytes, IV.IV_LENGTH, bytes.size - IV.IV_LENGTH) + // Using a CipherInputStream for decryption has historically led to issues on the Pixel family of devices + // see https://github.com/oblador/react-native-keychain/issues/383 + val _decryptedBytes = cipher.doFinal(bytes, IV.IV_LENGTH, bytes.size - IV.IV_LENGTH) + + val decryptedBytes = maybeRemovePKCS7Padding(_decryptedBytes, IV.IV_LENGTH) + String(decryptedBytes, UTF8) } catch (fail: Throwable) { Log.w(LOG_TAG, fail.message, fail) @@ -270,4 +275,22 @@ class CipherStorageKeystoreAesCbc(reactContext: ReactApplicationContext) : decryptBytes(key, bytes, IV.decrypt) // endregion + + private fun maybeRemovePKCS7Padding(paddedBytes: ByteArray, maxPaddingLength: Int): ByteArray { + val paddingLength = paddedBytes.last().toInt() + + // Validate the padding + if (paddingLength < 1 || paddingLength > paddedBytes.size || paddingLength > maxPaddingLength) { + return paddedBytes + } + + for (i in paddedBytes.size - paddingLength until paddedBytes.size) { + if (paddedBytes[i] != paddingLength.toByte()) { + return paddedBytes + } + } + + // Remove the padding + return paddedBytes.copyOfRange(0, paddedBytes.size - paddingLength) + } } diff --git a/package.json b/package.json index dc89107f..2838e130 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "react-native-keychain", - "version": "9.2.3", + "name": "@exodus/react-native-keychain", + "version": "9.2.4", "description": "Keychain Access for React Native", "main": "./lib/commonjs/index.js", "module": "./lib/module/index", diff --git a/src/enums.ts b/src/enums.ts index 39bd4157..71c9d549 100644 --- a/src/enums.ts +++ b/src/enums.ts @@ -63,7 +63,7 @@ export enum SECURITY_LEVEL { * */ SECURE_HARDWARE = RNKeychainManager && RNKeychainManager.SECURITY_LEVEL_SECURE_HARDWARE, - /** No security guarantees needed (default value). Credentials can be stored in FB Secure Storage. */ + /** No security guarantees needed (default value). */ ANY = RNKeychainManager && RNKeychainManager.SECURITY_LEVEL_ANY, } @@ -109,17 +109,9 @@ export enum BIOMETRY_TYPE { * 2. Medium Security (No Authentication): * - AES_GCM_NO_AUTH: For app-level secrets and cached data * - * 3. Legacy/Deprecated: - * - AES_CBC: Outdated, use AES_GCM_NO_AUTH instead - * - FB: Archived Facebook Conceal implementation - * * @platform Android */ export enum STORAGE_TYPE { - /** Facebook compatibility cipher. - * @deprecated Facebook Conceal was deprecated and archived in Mar 3, 2020. https://github.com/facebookarchive/conceal - */ - FB = 'FacebookConceal', /** Encryptions without human interaction. * @deprecated Use AES_GCM_NO_AUTH instead. */ @@ -154,6 +146,6 @@ export enum STORAGE_TYPE { export enum SECURITY_RULES { /** No special security rules applied. */ NONE = 'none', - /** Upgrade secret to the best available storage as soon as it is available and user request secret extraction. Upgrade not applied till we request the secret. This rule only applies to secrets stored with FacebookConseal. */ + /** Upgrade secret to the best available storage as soon as it is available and user request secret extraction. Upgrade not applied till we request the secret. */ AUTOMATIC_UPGRADE = 'automaticUpgradeToMoreSecuredStorage', } diff --git a/website/docs/choosing-storage-type.md b/website/docs/choosing-storage-type.md index c5fa0983..e0c96278 100644 --- a/website/docs/choosing-storage-type.md +++ b/website/docs/choosing-storage-type.md @@ -20,11 +20,6 @@ We offer three security levels for data storage: - **AES_GCM_NO_AUTH**: Symmetric encryption without biometric requirements - Best for: Cached data, non-sensitive encrypted data -### 3. Legacy/Deprecated - -- **AES_CBC**, **FB** (Facebook Conceal) -- ⚠️ Not recommended for new implementations - ## Storage Type Selection Guide ### Use AES_GCM (High Security) for: diff --git a/website/docs/faq.md b/website/docs/faq.md index 695a710b..5413632b 100644 --- a/website/docs/faq.md +++ b/website/docs/faq.md @@ -5,7 +5,7 @@ title: Frequently Asked Questions **Q: How does the library handle encryption when storing secrets, and can it upgrade the encryption?** -**A:** The library automatically applies the highest possible encryption when storing secrets. However, once a secret is stored, it does not attempt to upgrade the encryption unless **Facebook Conceal** was used and the `SECURITY_RULES` option is set to `AUTOMATIC_UPGRADE`. +**A:** The library automatically applies the highest possible encryption when storing secrets. However, once a secret is stored, it does not attempt to upgrade the encryption. --- @@ -23,18 +23,6 @@ title: Frequently Asked Questions --- -**Q: How do I enable automatic upgrades for Facebook Conceal?** - -**A:** Use the following call: - -```tsx -getGenericPassword({ ...otherProps, rules: "AUTOMATIC_UPGRADE" }); -``` - -Ensure the `rules` property is set to the string value `AUTOMATIC_UPGRADE`. - ---- - **Q: How do I force a specific level of encryption when saving a secret?** **A:** To force a specific encryption level, call: diff --git a/website/docs/index.md b/website/docs/index.md index 06b49608..21046c10 100644 --- a/website/docs/index.md +++ b/website/docs/index.md @@ -15,12 +15,12 @@ This library supports various security features such as biometric authentication ## Support This library supports both iOS and Android platforms. Additionally, it has support for macOS Catalyst and visionOS. -For iOS, the library uses the Keychain Services API, while on Android, it uses Facebook Conceal or the Android Keystore depending on the API level. +For iOS, the library uses the Keychain Services API, while on Android, it uses the Android Keystore. Supported platforms and versions: - **iOS**: Requires iOS 9.0+ -- **Android**: API 16+ (uses Facebook Conceal for API levels 16-22, Android Keystore for API 23+) +- **Android**: API 23+ - **macOS Catalyst**: Supported - **visionOS**: Supported diff --git a/website/docs/jest.md b/website/docs/jest.md index e2d814c3..05a51e32 100644 --- a/website/docs/jest.md +++ b/website/docs/jest.md @@ -40,7 +40,6 @@ const keychainMock = { BIOMETRICS: 'MOCK_AuthenticationWithBiometrics', }, STORAGE_TYPE: { - FB: 'MOCK_FacebookConceal', AES: 'MOCK_KeystoreAESCBC', RSA: 'MOCK_KeystoreRSAECB', KC: 'MOCK_keychain', diff --git a/website/docs/usage.md b/website/docs/usage.md index cedb8904..2fcc3b62 100644 --- a/website/docs/usage.md +++ b/website/docs/usage.md @@ -46,7 +46,6 @@ See the `KeychainExample` for a fully working project example. The module automatically selects the appropriate `CipherStorage` implementation based on the device's API level: -- **API levels 16-22**: Uses Facebook Conceal for encryption/decryption. - **API level 23+**: Uses Android Keystore for encryption/decryption. Encrypted data is stored in `Jetpack DataStore`. diff --git a/yarn.lock b/yarn.lock index 6a9eea66..1bb9e3b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3573,6 +3573,22 @@ __metadata: languageName: node linkType: hard +"@exodus/react-native-keychain@workspace:.": + version: 0.0.0-use.local + resolution: "@exodus/react-native-keychain@workspace:." + dependencies: + "@react-native/eslint-config": ^0.74.84 + "@react-native/typescript-config": ^0.74.84 + eslint: ^8.46.0 + eslint-plugin-prettier: ^5.1.3 + prettier: ^3.0.3 + react: 18.2.0 + react-native: 0.74.5 + react-native-builder-bob: ^0.30.0 + typescript: ^5.2.2 + languageName: unknown + linkType: soft + "@flatten-js/interval-tree@npm:^1.1.2": version: 1.1.3 resolution: "@flatten-js/interval-tree@npm:1.1.3" @@ -16734,22 +16750,6 @@ __metadata: languageName: node linkType: hard -"react-native-keychain@workspace:.": - version: 0.0.0-use.local - resolution: "react-native-keychain@workspace:." - dependencies: - "@react-native/eslint-config": ^0.74.84 - "@react-native/typescript-config": ^0.74.84 - eslint: ^8.46.0 - eslint-plugin-prettier: ^5.1.3 - prettier: ^3.0.3 - react: 18.2.0 - react-native: 0.74.5 - react-native-builder-bob: ^0.30.0 - typescript: ^5.2.2 - languageName: unknown - linkType: soft - "react-native-segmented-control-tab@npm:^4.0.0": version: 4.0.0 resolution: "react-native-segmented-control-tab@npm:4.0.0"