diff --git a/native/expo-modules/aes-crypto/android/src/main/java/app/comm/android/aescrypto/AESCryptoModule.kt b/native/expo-modules/aes-crypto/android/src/main/java/app/comm/android/aescrypto/AESCryptoModule.kt index ada56f110..162195fd0 100644 --- a/native/expo-modules/aes-crypto/android/src/main/java/app/comm/android/aescrypto/AESCryptoModule.kt +++ b/native/expo-modules/aes-crypto/android/src/main/java/app/comm/android/aescrypto/AESCryptoModule.kt @@ -1,129 +1,174 @@ package app.comm.android.aescrypto import expo.modules.core.errors.CodedException import expo.modules.kotlin.modules.Module import expo.modules.kotlin.modules.ModuleDefinition import expo.modules.kotlin.typedarray.Uint8Array import java.security.SecureRandom import javax.crypto.Cipher import javax.crypto.KeyGenerator import javax.crypto.SecretKey +import javax.crypto.spec.GCMParameterSpec import javax.crypto.spec.SecretKeySpec private const val ALGORITHM_AES = "AES" private const val CIPHER_TRANSFORMATION_NAME = "AES/GCM/NoPadding" private const val KEY_SIZE = 32 // bytes private const val IV_LENGTH = 12 // bytes - unique Initialization Vector (nonce) private const val TAG_LENGTH = 16 // bytes - GCM auth tag class AESCryptoModule : Module() { private val secureRandom by lazy { SecureRandom() } override fun definition() = ModuleDefinition { Name("AESCrypto") Function("generateKey", this@AESCryptoModule::generateKey) Function("encrypt", this@AESCryptoModule::encrypt) + Function("decrypt", this@AESCryptoModule::decrypt) } // region Function implementations /** * Generates AES-256 key and stores it in [destination] array * * @param destination JS typed array, must be [KEY_SIZE] bytes long */ private fun generateKey(destination: Uint8Array) { if (destination.byteLength != KEY_SIZE) { throw InvalidKeyLengthException() } val keygen = KeyGenerator.getInstance(ALGORITHM_AES).apply { init(KEY_SIZE * 8, secureRandom) } val keyBytes = keygen.generateKey().encoded destination.write(keyBytes, position = 0, size = keyBytes.size) } /** * Encrypts given [plaintext] with provided key and saves encrypted results * (sealed data) into [destination]. After the encryption, the destination * array will contain the following, concatenated in order: * - IV * - Ciphertext with GCM tag * * @param rawKey AES-256 key bytes. Must be of length [KEY_SIZE] * @param plaintext * @param destination must be of length: [plaintext]+[IV_LENGTH]+[TAG_LENGTH] */ private fun encrypt( rawKey: Uint8Array, plaintext: Uint8Array, destination: Uint8Array ) { if (destination.length != plaintext.length + IV_LENGTH + TAG_LENGTH) { throw InvalidDataLengthException() } val key = rawKey.toAESSecretKey() val plaintextBuffer = plaintext.toDirectBuffer() val plaintextBytes = ByteArray(plaintext.byteLength) .also(plaintextBuffer::get) val (iv, ciphertextWithTag) = encryptAES(plaintextBytes, key) destination.write(iv, position = 0, size = IV_LENGTH) destination.write(ciphertextWithTag, IV_LENGTH, ciphertextWithTag.size) } + /** + * Decrypts given [sealedData] using provided key and stores decrypted + * plaintext in the [destination] array. + * + * @param rawKey AES-256 key bytes. Must be of length [KEY_SIZE] + * @param sealedData Typed array consisting of 12-byte IV, followed by + * actual ciphertext content and ending with 16-byte GCM tag. + * @param destination should be of ciphertext content length + */ + private fun decrypt( + rawKey: Uint8Array, + sealedData: Uint8Array, + destination: Uint8Array + ) { + if (destination.byteLength + != sealedData.byteLength - IV_LENGTH - TAG_LENGTH) { + throw InvalidDataLengthException() + } + val key = rawKey.toAESSecretKey() + val input = sealedData.toDirectBuffer() + val iv = ByteArray(IV_LENGTH).also(input::get) + val ciphertextWithTagBytes = ByteArray(input.remaining()).also(input::get) + val plaintext = decryptAES(ciphertextWithTagBytes, key, iv) + + destination.write(plaintext, position = 0, size = plaintext.size) + } + // endregion } // region RN-agnostic implementations /** * Encrypts given [plaintext] with given [key] using AES-256 GCM algorithm * * @return A pair of: * - IV (initialization vector) - 12 bytes long * - [ByteArray] containing ciphertext with 16-byte GCM auth tag appended */ private fun encryptAES( plaintext: ByteArray, key: SecretKey ): Pair { val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION_NAME).apply { init(Cipher.ENCRYPT_MODE, key) } val iv = cipher.iv.copyOf() val ciphertextWithTag = cipher.doFinal(plaintext) return Pair(iv, ciphertextWithTag) } +/** + * Does the reverse of the [encryptAES] function. + * Decrypts the [ciphertext] with given [key] and [iv] + */ +private fun decryptAES( + ciphertextWithTag: ByteArray, + key: SecretKey, + iv: ByteArray +): ByteArray { + val spec = GCMParameterSpec(TAG_LENGTH * 8, iv) + val cipher = Cipher.getInstance(CIPHER_TRANSFORMATION_NAME).apply { + init(Cipher.DECRYPT_MODE, key, spec) + } + return cipher.doFinal(ciphertextWithTag) +} + // endregion // region Utility extension functions fun ByteArray.toSecretKey(algorithm: String = ALGORITHM_AES) = SecretKeySpec(this, 0, this.size, algorithm) fun Uint8Array.toAESSecretKey(): SecretKey { if (this.byteLength != KEY_SIZE) { throw InvalidKeyLengthException() } return ByteArray(KEY_SIZE) .also { bytes -> this.read(bytes, 0, KEY_SIZE) } .toSecretKey() } // endregion // region Exception definitions private class InvalidKeyLengthException : CodedException("The AES key has invalid length") private class InvalidDataLengthException : CodedException("Source or destination array has invalid length") // endregion diff --git a/native/utils/aes-crypto-module.js b/native/utils/aes-crypto-module.js index 7ac0c7eba..ae76f8aa2 100644 --- a/native/utils/aes-crypto-module.js +++ b/native/utils/aes-crypto-module.js @@ -1,42 +1,41 @@ // @flow import { requireNativeModule } from 'expo-modules-core'; import invariant from 'invariant'; const KEY_SIZE = 32; // bytes const IV_LENGTH = 12; // bytes, IV - unique Initialization Vector (nonce) const TAG_LENGTH = 16; // bytes - GCM auth tag const AESCryptoModule: { +generateKey: (destination: Uint8Array) => void, +encrypt: ( key: Uint8Array, data: Uint8Array, destination: Uint8Array, ) => void, +decrypt: ( key: Uint8Array, data: Uint8Array, destination: Uint8Array, ) => void, } = requireNativeModule('AESCrypto'); export function generateKey(): Uint8Array { const keyBuffer = new Uint8Array(KEY_SIZE); AESCryptoModule.generateKey(keyBuffer); return keyBuffer; } export function encrypt(key: Uint8Array, data: Uint8Array): Uint8Array { const sealedDataBuffer = new Uint8Array(data.length + IV_LENGTH + TAG_LENGTH); AESCryptoModule.encrypt(key, data, sealedDataBuffer); return sealedDataBuffer; } export function decrypt(key: Uint8Array, data: Uint8Array): Uint8Array { - invariant(AESCryptoModule.decrypt, 'AESCrypto.decrypt is not implemented'); invariant(data.length >= IV_LENGTH + TAG_LENGTH, 'Invalid data length'); const plaintextBuffer = new Uint8Array(data.length - IV_LENGTH - TAG_LENGTH); AESCryptoModule.decrypt(key, data, plaintextBuffer); return plaintextBuffer; }