Skip to content

Commit

Permalink
fix: AES_GCM implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
DorianMazur committed Nov 5, 2024
1 parent fb9a499 commit 5da8054
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 53 deletions.
20 changes: 13 additions & 7 deletions KeychainExample/e2e/testCases/acessControlTest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 () => {
Expand Down
7 changes: 4 additions & 3 deletions KeychainExample/e2e/testCases/storageTypesTest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -36,7 +37,7 @@ describe(':android:Storage Types', () => {
await matchLoadInfo(
'testUsernameFB',
'testPasswordFB',
'KeystoreAESCBC',
'KeystoreAESGCM',
type === 'internetCredentials' ? 'https://example.com' : undefined
);
}
Expand Down Expand Up @@ -83,7 +84,7 @@ describe(':android:Storage Types', () => {
await matchLoadInfo(
'testUsernameAESGCM',
'testPasswordAESGCM',
'KeystoreAESCBC',
'KeystoreAESGCM',
type === 'internetCredentials' ? 'https://example.com' : undefined
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) :
Expand Down Expand Up @@ -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 =
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 =
Expand All @@ -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
}

0 comments on commit 5da8054

Please sign in to comment.