Search code examples
kotlinencryptionecdsa

Kotlin ECC Encryption


Is there any information about Elliptic Curve Encryption within Kotlin?

For generating key pairs and encrypting, decrypting messages.

There is very little to non information about this topic.

I want to implement the ECC P-521 elliptic curve for example.

Is it maybe possible to use the Java version within Kotlin?

And how do we implement this?


Solution

  • ECC offers ECIES, a hybrid encryption scheme that combines ECC based asymmetric encryption with symmetric encryption. Here a shared secret is generated from which a key for the symmetric encryption of the data is derived. A MAC is used for authentication. ECIES is specified in various crypto standards. More details can be found here.

    ECIES uses the components you listed in your question (shared secret via ECC, symmetric encryption, MAC for authentication). However, the specific algorithms depend on the standard or implementation used, so you have no direct control over them. If this is enough for you, ECIES would be a good option.

    ECIES is supported e.g. by BouncyCastle, which implements the IEEE P 1363a standard. To use ECIES, BouncyCastle must therefore first be added (e.g. for the Android Studio in the dependencies section of app/gradle), see also here:

    implementation 'org.bouncycastle:bcprov-jdk15to18:1.67'
    

    The following Kotlin code then performs an encryption/decryption with ECIES and NIST P-521:

    // Add BouncyCastle
    Security.removeProvider("BC")
    Security.addProvider(BouncyCastleProvider())
    
    // Key Pair Generation
    val keyPairGenerator = KeyPairGenerator.getInstance("ECDH")
    keyPairGenerator.initialize(ECGenParameterSpec("secp521r1"))
    val keyPair = keyPairGenerator.generateKeyPair()
    
    // Encryption
    val plaintext = "The quick brown fox jumps over the lazy dog".toByteArray(StandardCharsets.UTF_8)
    val cipherEnc = Cipher.getInstance("ECIES")
    cipherEnc.init(Cipher.ENCRYPT_MODE, keyPair.public) // In practice, the public key of the recipient side is used
    val ciphertext = cipherEnc.doFinal(plaintext)
    
    // Decryption
    val cipherDec = Cipher.getInstance("ECIES")
    cipherDec.init(Cipher.DECRYPT_MODE, keyPair.private)
    val decrypted = cipherDec.doFinal(ciphertext)
    println(String(decrypted, StandardCharsets.UTF_8))
    

    tested with API level 28 / Android 9 Pie.


    If you want to have more control over the algorithms used, the individual components can be implemented manually, e.g.

    • ECDH with NIST P-521 to determine the shared secret
    • SHA-512 to determine the AES-256 key as the first 32 bytes of the hash (see also here for the use of a KDF as in the context of ECIES)
    • AES-256/GCM for symmetric encryption (GCM is already authenticated encryption, so an explicit MAC is not necessary)

    The following Kotlin code then performs an encryption/decryption with these components:

    // Generate Keys
    val keyPairA = generateKeyPair()
    val keyPairB = generateKeyPair()
    
    // Generate shared secrets
    val sharedSecretA = getSharedSecret(keyPairA.private, keyPairB.public)
    val sharedSecretB = getSharedSecret(keyPairB.private, keyPairA.public)
    
    // Generate AES-keys
    val aesKeyA = getAESKey(sharedSecretA)
    val aesKeyB = getAESKey(sharedSecretB)
    
    // Encryption (WLOG by A)
    val plaintextA = "The quick brown fox jumps over the lazy dog".toByteArray(StandardCharsets.UTF_8)
    val ciphertextA = encrypt(aesKeyA, plaintextA)
    
    // Decryption (WLOG by B)
    val plaintextB = decrypt(aesKeyB, ciphertextA)
    println(String(plaintextB, StandardCharsets.UTF_8))
    

    with:

    private fun generateKeyPair(): KeyPair {
        val keyPairGenerator = KeyPairGenerator.getInstance("EC")
        keyPairGenerator.initialize(ECGenParameterSpec("secp521r1"))
        return keyPairGenerator.generateKeyPair()
    }
    
    private fun getSharedSecret(privateKey: PrivateKey, publicKey: PublicKey): ByteArray {
        val keyAgreement = KeyAgreement.getInstance("ECDH")
        keyAgreement.init(privateKey)
        keyAgreement.doPhase(publicKey, true)
        return keyAgreement.generateSecret()
    }
    
    private fun getAESKey(sharedSecret: ByteArray): ByteArray {
        val digest = MessageDigest.getInstance("SHA-512")
        return digest.digest(sharedSecret).copyOfRange(0, 32)
    }
    
    private fun encrypt(aesKey: ByteArray, plaintext: ByteArray): ByteArray {
        val secretKeySpec = SecretKeySpec(aesKey, "AES")
        val iv = ByteArray(12) // Create random IV, 12 bytes for GCM
        SecureRandom().nextBytes(iv)
        val gCMParameterSpec = GCMParameterSpec(128, iv)
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gCMParameterSpec)
        val ciphertext = cipher.doFinal(plaintext)
        val ivCiphertext = ByteArray(iv.size + ciphertext.size) // Concatenate IV and ciphertext (the MAC is implicitly appended to the ciphertext)
        System.arraycopy(iv, 0, ivCiphertext, 0, iv.size)
        System.arraycopy(ciphertext, 0, ivCiphertext, iv.size, ciphertext.size)
        return ivCiphertext
    }
    
    private fun decrypt(aesKey: ByteArray, ivCiphertext: ByteArray): ByteArray {
        val secretKeySpec = SecretKeySpec(aesKey, "AES")
        val iv = ivCiphertext.copyOfRange(0, 12) // Separate IV
        val ciphertext = ivCiphertext.copyOfRange(12, ivCiphertext.size) // Separate ciphertext (the MAC is implicitly separated from the ciphertext)
        val gCMParameterSpec = GCMParameterSpec(128, iv)
        val cipher = Cipher.getInstance("AES/GCM/NoPadding")
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, gCMParameterSpec)
        return cipher.doFinal(ciphertext)
    }
    

    again tested with API Level 28 / Android 9 Pie.