1

I'm trying to encrypt communication between Java (BouncyCastle) and iOS using the Apple algorithm eciesEncryptionCofactorVariableIVX963SHA256AESGCM.

The algorithm by Apple is not well documented, but I found this article which helps quite a bit.

I also found the following algorithm in the BouncyCastle documentation that seems to be close to what I'm looking for:

  • ECCDHwithSHA256KDF which represents EC cofactor DH using the X9.63 KDF with SHA256 as the PRF
package com.example.ios.encryption;

import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.ECPointUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.util.encoders.Base64;

import java.math.BigInteger;
import java.security.*;
import java.security.spec.ECPoint;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.ECPublicKeySpec;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.KeyAgreement;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class IOSEncryptionECwithAES {

    public void testDecrypt() {

        // Receiver EC Public Key
        String pubKeyBase64 = "BBPT50Rn0PeeV0LxUbhDV7U1FUgVw9YLVctQx5HA+TiA3lp3k/cud8Xsjh6lytgaI5S7IUW1YouUiPNR/7LPArk=";
        PublicKey pubKey = getPublicKey(Base64.decode(pubKeyBase64));
        
        // Receiver EC Private Key
        String privateKeyBase64 = "BBPT50Rn0PeeV0LxUbhDV7U1FUgVw9YLVctQx5HA+TiA3lp3k/cud8Xsjh6lytgaI5S7IUW1YouUiPNR/7LPArkWcIYOQWtdkbTqmy++lz0cQ8ukWvUyhD9yzqZHPLQgQg==";
        PrivateKey privateKey = getPrivateKey(Base64.decode(privateKeyBase64));
    
        // Encrypted data
        String iosOutputBase64 = "BNNzHjSJQxP8jNuj5W9XSW0XNgpOlEHY/S4KzZQJFxwjzoujuwz5kJeOLj6cASBaYKePGLhkbE0qN20y8aHpU+PmeuDJWY7LZ25LjvutafOJGugdRZdURRwFSke7hzhXlSneaTFegT3xOoq9ffjCynwD7iRD";
        byte[] iosOutput = Base64.decode(iosOutputBase64);

        // Plaintext is a random UUID
        String plainText = "514227F0-51E9-41AC-9A39-42752E2ABADF";

        byte[] decryptedData = decryptEciesEncryptionCofactorVariableIVX963SHA256AESGCM(privateKey, iosOutput);
        System.out.println(new String(decryptedData));
    }

    public byte[] decryptEciesEncryptionCofactorVariableIVX963SHA256AESGCM(PrivateKey privateKey, byte[] iosOutput) throws Exception {

        // 1. Take ephemeral public key
        byte[] ephemeralKeyBytes = Arrays.copyOfRange(iosOutput, 0, 65);
        PublicKey ephemeralPublicKey = getPublicKey(ephemeralKeyBytes);
        byte[] encryptedData = Arrays.copyOfRange(iosOutput, 65, iosOutput.length);

        // 2. Key agreement using ECDH with Cofactor and integrated X9.63 
        byte[] kdfOut = getSharedSecret(ephemeralPublicKey, privateKey);

        byte[] secretKeyBytes = Arrays.copyOfRange(kdfOut, 0, 16);
        SecretKey secretKey = new SecretKeySpec(secretKeyBytes, "AES");

        // 4. Decrypt with AES key
        int tagLength = 128;
        byte[] iv = Arrays.copyOfRange(kdfOut, 16, kdfOut.length);
        GCMParameterSpec aesGcmParams = new GCMParameterSpec(tagLength, iv);
        Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
        c.init(Cipher.DECRYPT_MODE, secretKey, aesGcmParams);
        byte[] decryptedData = c.doFinal(encryptedData);

        return decryptedData;
    }

    /**
     * Convert uncompressed public key into PublicKey using BouncyCastle
     * For an elliptic curve public key, the format follows the ANSI X9.63 standard
     * using a byte string of 04 || X || Y
     *
     * @param encodedBytes raw bytes received
     * @return the Elliptic-Curve Public Key based on curve SECP256R1
     */
    private PublicKey getPublicKey(byte[] encodedBytes) throws Exception {
        KeyFactory keyFactory = KeyFactory.getInstance("EC");
        ECParameterSpec ecParameterSpec = ECNamedCurveTable.getParameterSpec("secp256r1");
        ECNamedCurveSpec params = new ECNamedCurveSpec("secp256r1", ecParameterSpec.getCurve(), ecParameterSpec.getG(), ecParameterSpec.getN());
        ECPoint publicPoint =  ECPointUtil.decodePoint(params.getCurve(), encodedBytes);
        ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(publicPoint, params);
        return keyFactory.generatePublic(pubKeySpec);
    }

  /**
   * Convert private key for external output from iOS
   * For an elliptic curve private key, the output is formatted as the public key
   * concatenated with the big endian encoding of the secret scalar, or 04 || X || Y || K.
   *
   * @param encodedBytes raw bytes received
   * @return the Elliptic-Curve Private Key based on curve SECP256R1
   */
  private PrivateKey getPrivateKey(byte[] encodedBytes) throws Exception {
        BigInteger s = new BigInteger(Arrays.copyOfRange(encodedBytes, 65, encodedBytes.length));
        ECParameterSpec ecParameterSpec = ECNamedCurveTable.getParameterSpec("secp256r1");
        ECNamedCurveSpec params = new ECNamedCurveSpec("secp256r1", ecParameterSpec.getCurve(), ecParameterSpec.getG(), ecParameterSpec.getN());
        ECPrivateKeySpec privateKeySpec = new ECPrivateKeySpec(s, params);
        KeyFactory keyFactory = KeyFactory.getInstance("EC");
        return keyFactory.generatePrivate(privateKeySpec);
  }

  /**
   * Key agreement using ECDH with Cofactor and integrated X9.63 KDF SHA-256
   * 
   * @param ephemeralPublicKey created by the sender
   * @param privateKey from the receiver
   * @return shared secret of 32-bytes containing the 128-bit AES key and 16-byte IV
   */
   private byte[] getSharedSecret(PublicKey ephemeralPublicKey, PrivateKey privateKey) throws Exception {
      String keyAgreementAlgorithm = "ECCDHwithSHA256KDF";
      KeyAgreement keyAgreement = KeyAgreement.getInstance(keyAgreementAlgorithm, new BouncyCastleProvider());
      keyAgreement.init(privateKey);
      keyAgreement.doPhase(ephemeralPublicKey, true);
      return keyAgreement.generateSecret();
  }
}

Unfortunately this doesn't work and results in an exception.

javax.crypto.AEADBadTagException: Tag mismatch!

    at java.base/com.sun.crypto.provider.NativeGaloisCounterMode.decryptFinal(NativeGaloisCounterMode.java:454)

What am I missing? Can this code be fixed with minor alterations?

mahler
  • 414
  • 4
  • 21

1 Answers1

1

I did manage to get this to work by using the the code from O2 Czech Republic and that does work, but that code is from 2017 and I'm hoping this can these days be done in fewer lines with the current version of BouncyCastle.

See the working code below. It separates the KeyAgreement and KeyDerivationFunction into two separate functions.

   /**
     * Key agreement using ECDH with Cofactor and integrated X9.63 KDF SHA-256
     *
     * @param ephemeralPublicKey created by the sender
     * @param privateKey from the receiver
     * @return shared secret of 32-bytes containing the 128-bit AES key and 16-byte IV
     */
    private byte[] getInitialSecret(PublicKey ephemeralPublicKey, PrivateKey privateKey) throws Exception {
        String keyAgreementAlgorithm = "ECCDH"; // Seems to be equivalent to "ECDHC"
        KeyAgreement keyAgreement = KeyAgreement.getInstance(keyAgreementAlgorithm, new BouncyCastleProvider());
        keyAgreement.init(privateKey);
        keyAgreement.doPhase(ephemeralPublicKey, true);
        return keyAgreement.generateSecret();
    }

    /**
     * Derive actual SecretKey with X9.63 KDF SHA-256
     *
     * @param initialSecret output from the ECDH agreement with Cofactor
     * @param ephemeralKeyBytes emphemeral public key from sender
     * @return shared secret of 32-bytes containing the 128-bit AES key and 16-byte IV
     */
    private byte[] getDerivation(byte[] initialSecret, byte[] ephemeralKeyBytes) {
        KDF2BytesGenerator kdfGenerator = new KDF2BytesGenerator(new SHA256Digest());
        kdfGenerator.init(new KDFParameters(initialSecret, ephemeralKeyBytes));
        byte[] kdfOut = new byte[32]; 
        kdfGenerator.generateBytes(kdfOut, 0, 32);
        return kdfOut;
    }
  }

and changing

byte[] kdfOut = getSharedSecret(ephemeralPublicKey, privateKey);

into

byte[] initialSecret = getInitialSecret(ephemeralPublicKey, privateKey);
byte[] kdfOut = getDerivation(initialSecret, ephemeralKeyBytes);
mahler
  • 414
  • 4
  • 21