javacryptographyecdh

How do I create a shared 128-bit AES-GCM key using ECDH?


Given a supplied public key, plus my own private key, how do I generate "a shared 128-bit AES-GCM key... using ECDH", using something suitable for the JVM and/or Android?


The Tesla Fleet API uses signing for commands that affect the vehicle. Part of that involves a "key agreement", described as:

The client and the vehicle derive a shared 128-bit AES-GCM key K using ECDH:

S = (Sx, Sy) = ECDH(c, V) = ECDH(v, C)
K = SHA1(BIG_ENDIAN(Sx, 32))[:16]

I am using this Kotlin library for accessing the Fleet API. That library specifically does not implement the above algorithm. Instead, consumers of their library need to supply it themselves, in the form of a SharedSecretFetcher implementation:

SharedSecretFetcher... takes in the Vehicle's public key, which the SDK will provide, and requres that you use that to return the hex-encoded SHA1 digest of the ECDH shared secret.

...and shows the following pseudocode:

val sharedSecretFetcher = { vehiclePublicKey ->
  /** A pseudocode example:
  val ecdh = createECDH(curveName = "p256")
  ecdh.setPrivateKey(YOUR_CLIENT_PRIVATE_KEY)
  val sharedSecret = ecdh.computeSecretAsHex(vehiclePublicKey)
  sharedSecret.sha1Hash().toHex()
   */
}

Notably, the implementation of createECDH() and computeSecretAsHex() are left as an exercise for the reader. There is no sample code, and I have not identified a consumer of this library.

Tesla shows using OpenSSL at the command line for this:

export K=$(openssl pkeyutl -derive -inkey client.key -peerkey vehicle.pem \
    | openssl dgst -sha1 -binary \
    | head -c 16 \
    | xxd -p)
echo $K
1b2fce19967b79db696f909cff89ea9a

I also see this JavaScript and this Python, which seem to implement this algorithm.

I am just at a loss as to how to use javax.crypto, BouncyCastle/SpongyCastle (if the latter is still a thing...), or anything else Java/Kotlin-friendly to accomplish the same.


Solution

  • What I wound up with (minus one redaction), is:

        private val sharedSecretFetcher = { vehiclePublicKey: ByteArray ->
            computeSecretAsHex(
                pubKey = loadPublicKey(vehiclePublicKey),
                privKey = loadPrivateKey(Pem(/* REDACTION */).byteArray())
            )
        }
    
        @Throws(Exception::class)
        private fun computeSecretAsHex(privKey: PrivateKey, pubKey: PublicKey): ByteArray {
            val keyAgreement = KeyAgreement.getInstance("ECDH")
            keyAgreement.init(privKey)
            keyAgreement.doPhase(pubKey, true)
            val sharedSecret = keyAgreement.generateSecret()
            val hash = MessageDigest.getInstance("SHA-1").digest(sharedSecret)
            return hash.copyOfRange(0, 16)
        }
    
        private fun loadPublicKey(publicKeyBytes: ByteArray): ECPublicKey {
            val keyFactory = KeyFactory.getInstance("EC")
            val keySpec = X509EncodedKeySpec(publicKeyBytes)
            return keyFactory.generatePublic(keySpec) as ECPublicKey
        }
    
        private fun loadPrivateKey(privateKeyBytes: ByteArray): ECPrivateKey {
            val keyFactory = KeyFactory.getInstance("EC")
            val keySpec = PKCS8EncodedKeySpec(privateKeyBytes)
            return keyFactory.generatePrivate(keySpec) as ECPrivateKey
        }
    

    The /* REDACTION */ would be replaced with the text of your private key, obtained from somewhere.

    As it turns out, the specific Tesla that I am interacting with is older and might not actually be using this logic. So, YMMV.