From 5da8054e34b37850941d9b7af2adbf3cb886c64f Mon Sep 17 00:00:00 2001 From: MazurDorian Date: Tue, 5 Nov 2024 23:32:55 +0200 Subject: [PATCH] fix: AES_GCM implementation --- .../e2e/testCases/acessControlTest.spec.js | 20 ++++--- .../e2e/testCases/storageTypesTest.spec.js | 7 +-- .../cipherStorage/CipherStorageBase.kt | 43 --------------- .../CipherStorageKeystoreAesCbc.kt | 47 ++++++++++++++++ .../CipherStorageKeystoreAesGcm.kt | 54 +++++++++++++++++++ 5 files changed, 118 insertions(+), 53 deletions(-) diff --git a/KeychainExample/e2e/testCases/acessControlTest.spec.js b/KeychainExample/e2e/testCases/acessControlTest.spec.js index 06b2cb0f..abebce6c 100644 --- a/KeychainExample/e2e/testCases/acessControlTest.spec.js +++ b/KeychainExample/e2e/testCases/acessControlTest.spec.js @@ -114,7 +114,7 @@ describe('Access Control', () => { ); it( - 'should save and retrieve username and password without biometrics' + + 'should save and retrieve username and password without biometrics - ' + type, async () => { await expect(element(by.text('Keychain Example'))).toExist(); @@ -136,12 +136,18 @@ describe('Access Control', () => { } ); - 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 retrieve username and password after app launch without biometrics - ' + + type, + 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 () => { diff --git a/KeychainExample/e2e/testCases/storageTypesTest.spec.js b/KeychainExample/e2e/testCases/storageTypesTest.spec.js index 02b577a7..79457564 100644 --- a/KeychainExample/e2e/testCases/storageTypesTest.spec.js +++ b/KeychainExample/e2e/testCases/storageTypesTest.spec.js @@ -8,7 +8,8 @@ describe(':android:Storage Types', () => { }); ['genericPassword', 'internetCredentials'].forEach((type) => { it( - ':android:should save with FB storage and migrate it to AES - ' + type, + ':android:should save with FB storage and migrate it to AES_GCM - ' + + type, async () => { await expect(element(by.text('Keychain Example'))).toExist(); await element(by.id('usernameInput')).typeText('testUsernameFB'); @@ -36,7 +37,7 @@ describe(':android:Storage Types', () => { await matchLoadInfo( 'testUsernameFB', 'testPasswordFB', - 'KeystoreAESCBC', + 'KeystoreAESGCM', type === 'internetCredentials' ? 'https://example.com' : undefined ); } @@ -83,7 +84,7 @@ describe(':android:Storage Types', () => { await matchLoadInfo( 'testUsernameAESGCM', 'testPasswordAESGCM', - 'KeystoreAESCBC', + 'KeystoreAESGCM', type === 'internetCredentials' ? 'https://example.com' : undefined ); }); diff --git a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageBase.kt b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageBase.kt index d3959ffa..4680a8a6 100644 --- a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageBase.kt +++ b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageBase.kt @@ -510,49 +510,6 @@ abstract class CipherStorageBase(protected val applicationContext: Context) : Ci } } - /** Initialization vector support. */ - object IV { - /** Encryption/Decryption initialization vector length. */ - const val IV_LENGTH = 16 - - /** Save Initialization vector to output stream. */ - val encrypt = EncryptStringHandler { cipher, key, output -> - cipher.init(Cipher.ENCRYPT_MODE, key) - val iv = cipher.iv - output.write(iv, 0, iv.size) - } - - /** Read initialization vector from input stream and configure cipher by it. */ - val decrypt = DecryptBytesHandler { cipher, key, input -> - val iv = readIv(input) - cipher.init(Cipher.DECRYPT_MODE, key, iv) - } - - /** Extract initialization vector from provided bytes array. */ - @Throws(IOException::class) - fun readIv(bytes: ByteArray): IvParameterSpec { - val iv = ByteArray(IV_LENGTH) - - if (IV_LENGTH >= bytes.size) - throw IOException("Insufficient length of input data for IV extracting.") - - System.arraycopy(bytes, 0, iv, 0, IV_LENGTH) - - return IvParameterSpec(iv) - } - - /** Extract initialization vector from provided input stream. */ - @Throws(IOException::class) - fun readIv(inputStream: InputStream): IvParameterSpec { - val iv = ByteArray(IV_LENGTH) - val result = inputStream.read(iv, 0, IV_LENGTH) - - if (result != IV_LENGTH) throw IOException("Input stream has insufficient data.") - - return IvParameterSpec(iv) - } - } - /** Handler for storing cipher configuration in output stream. */ fun interface EncryptStringHandler { @Throws(GeneralSecurityException::class, IOException::class) 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 f73475eb..6cfdeaa8 100644 --- a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAesCbc.kt +++ b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAesCbc.kt @@ -14,6 +14,7 @@ import com.oblador.keychain.decryptionHandler.DecryptionResultHandler import com.oblador.keychain.exceptions.CryptoFailedException import com.oblador.keychain.exceptions.KeyStoreAccessException import java.io.IOException +import java.io.InputStream import java.security.GeneralSecurityException import java.security.Key import java.security.spec.KeySpec @@ -22,6 +23,7 @@ import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.SecretKey import javax.crypto.SecretKeyFactory +import javax.crypto.spec.IvParameterSpec @TargetApi(Build.VERSION_CODES.M) class CipherStorageKeystoreAesCbc(@NonNull reactContext: ReactApplicationContext) : @@ -225,6 +227,50 @@ class CipherStorageKeystoreAesCbc(@NonNull reactContext: ReactApplicationContext // endregion // region Initialization Vector encrypt/decrypt support + + /** Initialization vector support. */ + object IV { + /** Encryption/Decryption initialization vector length. */ + const val IV_LENGTH = 16 + + /** Save Initialization vector to output stream. */ + val encrypt = EncryptStringHandler { cipher, key, output -> + cipher.init(Cipher.ENCRYPT_MODE, key) + val iv = cipher.iv + output.write(iv, 0, iv.size) + } + + /** Read initialization vector from input stream and configure cipher by it. */ + val decrypt = DecryptBytesHandler { cipher, key, input -> + val iv = readIv(input) + cipher.init(Cipher.DECRYPT_MODE, key, iv) + } + + /** Extract initialization vector from provided bytes array. */ + @Throws(IOException::class) + fun readIv(bytes: ByteArray): IvParameterSpec { + val iv = ByteArray(IV_LENGTH) + + if (IV_LENGTH >= bytes.size) + throw IOException("Insufficient length of input data for IV extracting.") + + System.arraycopy(bytes, 0, iv, 0, IV_LENGTH) + + return IvParameterSpec(iv) + } + + /** Extract initialization vector from provided input stream. */ + @Throws(IOException::class) + fun readIv(inputStream: InputStream): IvParameterSpec { + val iv = ByteArray(IV_LENGTH) + val result = inputStream.read(iv, 0, IV_LENGTH) + + if (result != IV_LENGTH) throw IOException("Input stream has insufficient data.") + + return IvParameterSpec(iv) + } + } + @NonNull @Throws(GeneralSecurityException::class, IOException::class) override fun encryptString(@NonNull key: Key, @NonNull value: String): ByteArray = @@ -234,5 +280,6 @@ class CipherStorageKeystoreAesCbc(@NonNull reactContext: ReactApplicationContext @Throws(GeneralSecurityException::class, IOException::class) override fun decryptBytes(@NonNull key: Key, @NonNull bytes: ByteArray): String = decryptBytes(key, bytes, IV.decrypt) + // endregion } diff --git a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAesGcm.kt b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAesGcm.kt index 8257c35a..c2c294a1 100644 --- a/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAesGcm.kt +++ b/android/src/main/java/com/oblador/keychain/cipherStorage/CipherStorageKeystoreAesGcm.kt @@ -14,14 +14,18 @@ import com.oblador.keychain.decryptionHandler.DecryptionResultHandler import com.oblador.keychain.exceptions.CryptoFailedException import com.oblador.keychain.exceptions.KeyStoreAccessException import java.io.IOException +import java.io.InputStream import java.security.GeneralSecurityException import java.security.Key +import java.security.SecureRandom import java.security.spec.KeySpec import java.util.concurrent.atomic.AtomicInteger import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.SecretKey import javax.crypto.SecretKeyFactory +import javax.crypto.spec.IvParameterSpec +import javax.crypto.spec.GCMParameterSpec @TargetApi(Build.VERSION_CODES.M) @@ -226,6 +230,55 @@ class CipherStorageKeystoreAesGcm(@NonNull reactContext: ReactApplicationContext // endregion // region Initialization Vector encrypt/decrypt support + + /** Initialization vector support. */ + object IV { + /** Encryption/Decryption initialization vector length. */ + const val IV_LENGTH = 12 + const val TAG_LENGTH = 128 + + /** Save Initialization vector to output stream. */ + val encrypt = EncryptStringHandler { cipher, key, output -> + val iv = ByteArray(IV_LENGTH) + + SecureRandom().nextBytes(iv) + val gcmParamSpec = GCMParameterSpec(TAG_LENGTH, iv) + + cipher.init(Cipher.ENCRYPT_MODE, key, gcmParamSpec) + output.write(iv, 0, iv.size) + } + + /** Read initialization vector from input stream and configure cipher by it. */ + val decrypt = DecryptBytesHandler { cipher, key, input -> + val iv = readIv(input) + cipher.init(Cipher.DECRYPT_MODE, key, iv) + } + + /** Extract initialization vector from provided bytes array. */ + @Throws(IOException::class) + fun readIv(bytes: ByteArray): GCMParameterSpec { + val iv = ByteArray(IV_LENGTH) + + if (IV_LENGTH >= bytes.size) + throw IOException("Insufficient length of input data for IV extracting.") + + System.arraycopy(bytes, 0, iv, 0, IV_LENGTH) + + return GCMParameterSpec(TAG_LENGTH, iv) + } + + /** Extract initialization vector from provided input stream. */ + @Throws(IOException::class) + fun readIv(inputStream: InputStream): GCMParameterSpec { + val iv = ByteArray(IV_LENGTH) + val result = inputStream.read(iv, 0, IV_LENGTH) + + if (result != IV_LENGTH) throw IOException("Input stream has insufficient data.") + + return GCMParameterSpec(TAG_LENGTH, iv) + } + } + @NonNull @Throws(GeneralSecurityException::class, IOException::class) override fun encryptString(@NonNull key: Key, @NonNull value: String): ByteArray = @@ -235,5 +288,6 @@ class CipherStorageKeystoreAesGcm(@NonNull reactContext: ReactApplicationContext @Throws(GeneralSecurityException::class, IOException::class) override fun decryptBytes(@NonNull key: Key, @NonNull bytes: ByteArray): String = decryptBytes(key, bytes, IV.decrypt) + // endregion }