diff --git a/applecryptointeroperability/build.gradle b/applecryptointeroperability/build.gradle index b7f315d..94f2189 100644 --- a/applecryptointeroperability/build.gradle +++ b/applecryptointeroperability/build.gradle @@ -24,6 +24,7 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.bouncycastle:bcpkix-jdk15on:1.56' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' } diff --git a/applecryptointeroperability/src/androidTest/java/company/ize/applecryptointeroperability/RSAInstrumentedTest.kt b/applecryptointeroperability/src/androidTest/java/company/ize/applecryptointeroperability/RSAInstrumentedTest.kt new file mode 100644 index 0000000..eaf2136 --- /dev/null +++ b/applecryptointeroperability/src/androidTest/java/company/ize/applecryptointeroperability/RSAInstrumentedTest.kt @@ -0,0 +1,53 @@ +package company.ize.applecryptointeroperability + +import org.junit.Assert +import org.junit.Test +import java.security.KeyPairGenerator +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey + +class RSAInstrumentedTest { + private val plaintext = "Hello, World!".toByteArray() + + private fun kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM( + publicKey: RSAPublicKey, + privateKey: RSAPrivateKey + ) { + // Encryption and decryption + val encryptedData = SecKeyCreateEncryptedData(publicKey, + SecKeyAlgorithm.RSA_ENCRYPTION_OAEP_SHA_256_AES_GCM, plaintext) + val decryptedData = SecKeyCreateDecryptedData(privateKey, + SecKeyAlgorithm.RSA_ENCRYPTION_OAEP_SHA_256_AES_GCM, encryptedData) + Assert.assertTrue(decryptedData.contentEquals(plaintext)) + + // With this algorithm we expect that outputs are different when the same data is + // encrypted with the same key + val otherEncryptedData = SecKeyCreateEncryptedData(publicKey, + SecKeyAlgorithm.RSA_ENCRYPTION_OAEP_SHA_256_AES_GCM, plaintext) + Assert.assertFalse(otherEncryptedData.contentEquals(encryptedData)) + } + + @Test + fun kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM_RSA2048() { + val keyPairGenerator = KeyPairGenerator.getInstance("RSA").apply { + initialize(2048) + } + val keyPair = keyPairGenerator.generateKeyPair() + val publicKey = keyPair.public as RSAPublicKey + val privateKey = keyPair.private as RSAPrivateKey + + kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM(publicKey, privateKey) + } + + @Test + fun kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM_RSA4096() { + val keyPairGenerator = KeyPairGenerator.getInstance("RSA").apply { + initialize(4096) + } + val keyPair = keyPairGenerator.generateKeyPair() + val publicKey = keyPair.public as RSAPublicKey + val privateKey = keyPair.private as RSAPrivateKey + + kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM(publicKey, privateKey) + } +} \ No newline at end of file diff --git a/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt b/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt index c0b2442..269cb12 100644 --- a/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt +++ b/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt @@ -1,11 +1,18 @@ package company.ize.applecryptointeroperability +import android.security.keystore.KeyProperties import java.security.* import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.MGF1ParameterSpec import javax.crypto.Cipher import javax.crypto.KeyAgreement +import javax.crypto.KeyGenerator import javax.crypto.spec.GCMParameterSpec +import javax.crypto.spec.OAEPParameterSpec +import javax.crypto.spec.PSource import javax.crypto.spec.SecretKeySpec // Created by Zsombor SZABO on 08/03/2019. @@ -129,6 +136,62 @@ fun SecKeyCreateEncryptedData(key: Key, algorithm: SecKeyAlgorithm, plaintext: B // and the GCM tag return sharedInfo + encryptedDataAndGcmTag } + SecKeyAlgorithm.RSA_ENCRYPTION_OAEP_SHA_256_AES_GCM -> { + val publicKey = key as? RSAPublicKey ?: throw java.lang.IllegalArgumentException( + "Expected RSA public key") + + val publicKeyBitLength = publicKey.modulus.bitLength() + + // 256bit AES key is used if RSA key is 4096bit or bigger, + // otherwise 128bit AES key is used. + val aesKeyBitLength = if (publicKeyBitLength >= 4096) { + 256 + } else { + 128 + } + + // Generate Random AES GCM session key + val secureRandom = SecureRandom.getInstanceStrong() + val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES).apply { + init(aesKeyBitLength, secureRandom) + } + val aesGcmKey = keyGenerator.generateKey() + + // Use all-zero 16 bytes long initialisation vector (IV) + val iv = ByteArray(16) + + // Use AES/GCM/NoPadding to encrypt the plaintext and generate a GCM tag + val aesGcmCipher = Cipher.getInstance("AES/GCM/NoPadding").apply { + init( + Cipher.ENCRYPT_MODE, + aesGcmKey, + GCMParameterSpec(16 * 8, iv) + ) + // Use public key as authentication data for AES-GCM encryption + updateAAD(publicKey.getAsn1Primitive()) + } + + val encryptedDataAndGcmTag = aesGcmCipher.doFinal(plaintext) + + // Wrap/Encrypt AES GCM Key with public key + val rsaCipher = Cipher.getInstance("RSA/ECB/OAEPPadding").apply { + init( + Cipher.WRAP_MODE, + publicKey, + OAEPParameterSpec( + "SHA-256", + "MGF1", + MGF1ParameterSpec.SHA256, + PSource.PSpecified.DEFAULT + ) + ) + } + val encryptedAesGcmKey = rsaCipher.wrap(aesGcmKey) + + // Construct the envelope by combining the encrypted AES key, the encrypted data + // and the GCM tag + return encryptedAesGcmKey + encryptedDataAndGcmTag + } else -> throw IllegalArgumentException("Not supported algorithm") } } @@ -188,6 +251,65 @@ fun SecKeyCreateDecryptedData(key: Key, algorithm: SecKeyAlgorithm, ciphertext: return cipher.doFinal(encryptedDataAndGcmTag) } + SecKeyAlgorithm.RSA_ENCRYPTION_OAEP_SHA_256_AES_GCM -> { + val privateKey = key as? RSAPrivateKey ?: throw java.lang.IllegalArgumentException( + "Expected RSA private key" + ) + val publicKey = privateKey.derivePublicKey() + val publicKeyBitLength = publicKey.modulus.bitLength() + + // 256bit AES key is used if RSA key is 4096bit or bigger, + // otherwise 128bit AES key is used. + val encryptedAesGcmKeyLength = if (publicKeyBitLength >= 4096) { + 512 + } else { + 256 + } + + // Extract the encrypted AES-GCM Secret Key, the encrypted data and the GCM tag + val encryptedAesGcmKey = ciphertext.copyOfRange(0, encryptedAesGcmKeyLength) + val encryptedDataAndGcmTag = ciphertext.copyOfRange( + encryptedAesGcmKeyLength, + ciphertext.size + ) + + // Unwrap/Decrypt AES GCM Key with private key + val rsaCipher = Cipher.getInstance("RSA/ECB/OAEPPadding").apply { + init( + Cipher.UNWRAP_MODE, + privateKey, + OAEPParameterSpec( + "SHA-256", + "MGF1", + MGF1ParameterSpec.SHA256, + PSource.PSpecified.DEFAULT + ) + ) + } + val aesGcmKey = rsaCipher.unwrap( + encryptedAesGcmKey, + KeyProperties.KEY_ALGORITHM_RSA, + Cipher.SECRET_KEY + ) + + // Use all-zero 16 bytes long initialisation vector (IV) + val iv = ByteArray(16) + + val publicKeyPKCS1 = publicKey.getAsn1Primitive() + + // Use AES/GCM/NoPadding to encrypt the plaintext and generate a GCM tag + val aesGcmCipher = Cipher.getInstance("AES/GCM/NoPadding").apply { + init( + Cipher.DECRYPT_MODE, + aesGcmKey, + GCMParameterSpec(16 * 8, iv) + ) + // Use public key as authentication data for AES-GCM encryption + updateAAD(publicKeyPKCS1) + } + + return aesGcmCipher.doFinal(encryptedDataAndGcmTag) + } else -> throw IllegalArgumentException("Not supported algorithm") } } @@ -207,5 +329,16 @@ enum class SecKeyAlgorithm { and static public key data is used as authenticationData for AES-GCM processing. AES-GCM uses 16 bytes long TAG, AES key is first half of KDF output and 16 byte long IV (initialization vector) is second half of KDF output. */ - ECIES_ENCRYPTION_STANDARD_VARIABLE_IV_X963_SHA256_AES_GCM + ECIES_ENCRYPTION_STANDARD_VARIABLE_IV_X963_SHA256_AES_GCM, + + /** + * kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM + * + * Randomly generated AES session key is encrypted by RSA with OAEP padding. + * User data are encrypted using session key in GCM mode with all-zero 16 bytes long IV (initialization vector). + * Finally 16 byte AES-GCM tag is appended to ciphertext. + * 256bit AES key is used if RSA key is 4096bit or bigger, otherwise 128bit AES key is used. + * Raw public key data is used as authentication data for AES-GCM encryption. + */ + RSA_ENCRYPTION_OAEP_SHA_256_AES_GCM } diff --git a/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/RSAKey.kt b/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/RSAKey.kt new file mode 100644 index 0000000..2f6d471 --- /dev/null +++ b/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/RSAKey.kt @@ -0,0 +1,58 @@ +package company.ize.applecryptointeroperability + +import android.security.keystore.KeyProperties +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import java.math.BigInteger +import java.security.KeyFactory +import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.RSAPrivateKeySpec +import java.security.spec.RSAPublicKeySpec + +// Created by Min Kuan LIM on 27/05/2021. +// Copyright © IZE. All rights reserved. +// See LICENSE.txt for licensing information. +// + +/** + * Return the default BER or DER encoding for this key. + */ +fun RSAPublicKey.getAsn1Primitive(): ByteArray = SubjectPublicKeyInfo + .getInstance(encoded) + .parsePublicKey() + .encoded + +/** + * Derive the public key spec by first attempting to convert + * this key into a [RSAPrivateCrtKey]. + * + * If it fails, attempt to derive the public exponent by using + * the most common value (65537). + * + * Note: This is not a fool proof solution and can only yield a probabilistic result + */ +fun RSAPrivateKey.derivePublicKeySpec(): RSAPublicKeySpec = + if (this is RSAPrivateCrtKey) { + RSAPublicKeySpec(this.modulus, this.publicExponent) + } else { + val keyFactory = KeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_RSA) + val rsaPrivateKeySpec = keyFactory.getKeySpec(this, RSAPrivateKeySpec::class.java) + // Making a wild guess on what might be the public exponent + // by using the most common value (65537) + RSAPublicKeySpec( + rsaPrivateKeySpec.modulus, + BigInteger.valueOf(65537) + ) + } + +/** + * Get the derived public key + * + * @see [derivePublicKeySpec] + */ +fun RSAPrivateKey.derivePublicKey(): RSAPublicKey { + val keyFactory = KeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_RSA) + val publicKeySpec = derivePublicKeySpec() + return keyFactory.generatePublic(publicKeySpec) as RSAPublicKey +}