From 7fe15c305c68dcb26401306d734f371764824610 Mon Sep 17 00:00:00 2001 From: Lim Min Kuan Date: Thu, 27 May 2021 15:54:18 +0800 Subject: [PATCH 1/5] Add encryption for kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM --- applecryptointeroperability/build.gradle | 1 + .../AppleCryptoInteroperability.kt | 75 ++++++++++++++++++- .../ize/applecryptointeroperability/RSAKey.kt | 58 ++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/RSAKey.kt 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/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt b/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt index c0b2442..f4eacf0 100644 --- a/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt +++ b/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt @@ -1,11 +1,17 @@ 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.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 +135,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") } } @@ -207,5 +269,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..be61148 --- /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.PrivateKey +import java.security.PublicKey +import java.security.interfaces.RSAPrivateCrtKey +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 PublicKey.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 PrivateKey.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 PrivateKey.derivePublicKey(): PublicKey { + val keyFactory = KeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_RSA) + val publicKeySpec = derivePublicKeySpec() + return keyFactory.generatePublic(publicKeySpec) +} From 7a24348e62116ba97efbd9302278b0f78dc012b0 Mon Sep 17 00:00:00 2001 From: Lim Min Kuan Date: Thu, 27 May 2021 16:02:22 +0800 Subject: [PATCH 2/5] Add decryption method for kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM --- .../AppleCryptoInteroperability.kt | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt b/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt index f4eacf0..22cf299 100644 --- a/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt +++ b/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt @@ -4,6 +4,7 @@ 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 @@ -181,7 +182,7 @@ fun SecKeyCreateEncryptedData(key: Key, algorithm: SecKeyAlgorithm, plaintext: B "SHA-256", "MGF1", MGF1ParameterSpec.SHA256, - PSource.PSpecified.DEFAULT, + PSource.PSpecified.DEFAULT ) ) } @@ -250,6 +251,53 @@ 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" + ) + + // Extract the encrypted AES-GCM Secret Key, the encrypted data and the GCM tag + val encryptedAesGcmKey = ciphertext.copyOfRange(0, 256) + val encryptedDataAndGcmTag = ciphertext.copyOfRange(256, 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 publicKey = privateKey.derivePublicKey() + 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") } } From e9308d98ee257bf335b8e68ce7863541c2912da0 Mon Sep 17 00:00:00 2001 From: Lim Min Kuan Date: Thu, 27 May 2021 16:21:10 +0800 Subject: [PATCH 3/5] Add Instrumented test for kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM --- .../RSAInstrumentedTest.kt | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 applecryptointeroperability/src/androidTest/java/company/ize/applecryptointeroperability/RSAInstrumentedTest.kt 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..7141daa --- /dev/null +++ b/applecryptointeroperability/src/androidTest/java/company/ize/applecryptointeroperability/RSAInstrumentedTest.kt @@ -0,0 +1,39 @@ +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 val publicKey: RSAPublicKey + private val privateKey: RSAPrivateKey + + init { + val keyPairGenerator = KeyPairGenerator.getInstance("RSA").apply { + initialize(2048) + } + val keyPair = keyPairGenerator.generateKeyPair() + publicKey = keyPair.public as RSAPublicKey + privateKey = keyPair.private as RSAPrivateKey + } + + @Test + fun kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM() { + // 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)) + } +} \ No newline at end of file From 70ab7d281bce3d5153029d263311d40ffa4289de Mon Sep 17 00:00:00 2001 From: Lim Min Kuan Date: Thu, 10 Jun 2021 15:17:43 +0800 Subject: [PATCH 4/5] Ensure decryption that works for rsa public key >= 4096 bits --- .../RSAInstrumentedTest.kt | 42 ++++++++++++------- .../AppleCryptoInteroperability.kt | 18 ++++++-- .../ize/applecryptointeroperability/RSAKey.kt | 5 ++- 3 files changed, 46 insertions(+), 19 deletions(-) diff --git a/applecryptointeroperability/src/androidTest/java/company/ize/applecryptointeroperability/RSAInstrumentedTest.kt b/applecryptointeroperability/src/androidTest/java/company/ize/applecryptointeroperability/RSAInstrumentedTest.kt index 7141daa..eaf2136 100644 --- a/applecryptointeroperability/src/androidTest/java/company/ize/applecryptointeroperability/RSAInstrumentedTest.kt +++ b/applecryptointeroperability/src/androidTest/java/company/ize/applecryptointeroperability/RSAInstrumentedTest.kt @@ -9,20 +9,10 @@ import java.security.interfaces.RSAPublicKey class RSAInstrumentedTest { private val plaintext = "Hello, World!".toByteArray() - private val publicKey: RSAPublicKey - private val privateKey: RSAPrivateKey - - init { - val keyPairGenerator = KeyPairGenerator.getInstance("RSA").apply { - initialize(2048) - } - val keyPair = keyPairGenerator.generateKeyPair() - publicKey = keyPair.public as RSAPublicKey - privateKey = keyPair.private as RSAPrivateKey - } - - @Test - fun kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM() { + private fun kSecKeyAlgorithmRSAEncryptionOAEPSHA256AESGCM( + publicKey: RSAPublicKey, + privateKey: RSAPrivateKey + ) { // Encryption and decryption val encryptedData = SecKeyCreateEncryptedData(publicKey, SecKeyAlgorithm.RSA_ENCRYPTION_OAEP_SHA_256_AES_GCM, plaintext) @@ -36,4 +26,28 @@ class RSAInstrumentedTest { 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 22cf299..269cb12 100644 --- a/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt +++ b/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/AppleCryptoInteroperability.kt @@ -255,10 +255,23 @@ fun SecKeyCreateDecryptedData(key: Key, algorithm: SecKeyAlgorithm, ciphertext: 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, 256) - val encryptedDataAndGcmTag = ciphertext.copyOfRange(256, ciphertext.size) + 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 { @@ -282,7 +295,6 @@ fun SecKeyCreateDecryptedData(key: Key, algorithm: SecKeyAlgorithm, ciphertext: // Use all-zero 16 bytes long initialisation vector (IV) val iv = ByteArray(16) - val publicKey = privateKey.derivePublicKey() val publicKeyPKCS1 = publicKey.getAsn1Primitive() // Use AES/GCM/NoPadding to encrypt the plaintext and generate a GCM tag diff --git a/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/RSAKey.kt b/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/RSAKey.kt index be61148..79c5021 100644 --- a/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/RSAKey.kt +++ b/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/RSAKey.kt @@ -7,6 +7,7 @@ import java.security.KeyFactory import java.security.PrivateKey import java.security.PublicKey import java.security.interfaces.RSAPrivateCrtKey +import java.security.interfaces.RSAPublicKey import java.security.spec.RSAPrivateKeySpec import java.security.spec.RSAPublicKeySpec @@ -51,8 +52,8 @@ fun PrivateKey.derivePublicKeySpec(): RSAPublicKeySpec = * * @see [derivePublicKeySpec] */ -fun PrivateKey.derivePublicKey(): PublicKey { +fun PrivateKey.derivePublicKey(): RSAPublicKey { val keyFactory = KeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_RSA) val publicKeySpec = derivePublicKeySpec() - return keyFactory.generatePublic(publicKeySpec) + return keyFactory.generatePublic(publicKeySpec) as RSAPublicKey } From 8bcac76cb3710ab3890690a23f0fc828f26f56f7 Mon Sep 17 00:00:00 2001 From: Lim Min Kuan Date: Fri, 11 Jun 2021 09:12:30 +0800 Subject: [PATCH 5/5] Use a more accurate data type for RSA Key conversion --- .../company/ize/applecryptointeroperability/RSAKey.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/RSAKey.kt b/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/RSAKey.kt index 79c5021..2f6d471 100644 --- a/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/RSAKey.kt +++ b/applecryptointeroperability/src/main/java/company/ize/applecryptointeroperability/RSAKey.kt @@ -4,9 +4,8 @@ import android.security.keystore.KeyProperties import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import java.math.BigInteger import java.security.KeyFactory -import java.security.PrivateKey -import java.security.PublicKey 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 @@ -19,7 +18,7 @@ import java.security.spec.RSAPublicKeySpec /** * Return the default BER or DER encoding for this key. */ -fun PublicKey.getAsn1Primitive(): ByteArray = SubjectPublicKeyInfo +fun RSAPublicKey.getAsn1Primitive(): ByteArray = SubjectPublicKeyInfo .getInstance(encoded) .parsePublicKey() .encoded @@ -33,7 +32,7 @@ fun PublicKey.getAsn1Primitive(): ByteArray = SubjectPublicKeyInfo * * Note: This is not a fool proof solution and can only yield a probabilistic result */ -fun PrivateKey.derivePublicKeySpec(): RSAPublicKeySpec = +fun RSAPrivateKey.derivePublicKeySpec(): RSAPublicKeySpec = if (this is RSAPrivateCrtKey) { RSAPublicKeySpec(this.modulus, this.publicExponent) } else { @@ -52,7 +51,7 @@ fun PrivateKey.derivePublicKeySpec(): RSAPublicKeySpec = * * @see [derivePublicKeySpec] */ -fun PrivateKey.derivePublicKey(): RSAPublicKey { +fun RSAPrivateKey.derivePublicKey(): RSAPublicKey { val keyFactory = KeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_RSA) val publicKeySpec = derivePublicKeySpec() return keyFactory.generatePublic(publicKeySpec) as RSAPublicKey