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 Read more, 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.
Answer
To generate a shared 128-bit AES-GCM key using ECDH (Elliptic Curve Diffie-Hellman) in Kotlin/Java, you can use the javax.crypto
package or BouncyCastle library to perform the key agreement (ECDH). The process involves creating an ECDH key pair, performing the key exchange, and then deriving the shared secret using a cryptographic hash function (SHA1). Here's a breakdown of the process and a Kotlin implementation:
Steps:
Generate ECDH Key Pair: You need to create an ECDH key pair (private and public keys) using the "P-256" curve (this corresponds to the curve name "secp256r1").
Perform ECDH Key Agreement: Use your private key and the vehicle’s public key to compute the shared secret.
Derive the AES-GCM Key: The shared secret is hashed using SHA1, and the first 16 bytes (128 bits) of the result are used as the AES-GCM key.
Example Code in Kotlin (Using BouncyCastle for ECDH):
kotlin
CopyEdit
importorg.bouncycastle.jce.provider.BouncyCastleProvider import java.security.KeyFactory import java.security.PublicKey import java.security.PrivateKey import java.security.Signature import java.security.interfaces.ECPublicKey import java.security.interfaces.ECPrivateKey import java.security.spec.X509EncodedKeySpec import javax.crypto.KeyAgreement import javax.crypto.Mac import javax.crypto.spec.SecretKeySpec import java.security.MessageDigest fun createECDH(): KeyAgreement { val keyAgreement = KeyAgreement.getInstance("ECDH", BouncyCastleProvider.PROVIDER_NAME) return keyAgreement } fun computeSharedSecret(privateKey: ECPrivateKey, vehiclePublicKey: ECPublicKey): ByteArray { val keyAgreement = createECDH() keyAgreement.init(privateKey) keyAgreement.doPhase(vehiclePublicKey, true) return keyAgreement.generateSecret() } fun deriveAESKey(sharedSecret: ByteArray): ByteArray { // Apply SHA1 to the shared secret val sha1Digest = MessageDigest.getInstance("SHA-1") val hash = sha1Digest.digest(sharedSecret) // Return the first 16 bytes (128-bit) return hash.copyOfRange(0, 16) } fun sharedSecretFetcher(vehiclePublicKeyBytes: ByteArray, privateKeyBytes: ByteArray): String { // Load vehicle's public key val vehiclePublicKey = loadPublicKey(vehiclePublicKeyBytes) // Load your private key (client's private key) val privateKey = loadPrivateKey(privateKeyBytes) // Compute the shared secret val sharedSecret = computeSharedSecret(privateKey, vehiclePublicKey) // Derive the AES-GCM key val aesGCMKey = deriveAESKey(sharedSecret) // Return the AES key as a hex string return aesGCMKey.joinToString("") { "%02x".format(it) } } fun loadPublicKey(publicKeyBytes: ByteArray): ECPublicKey { val keyFactory = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME) val keySpec = X509EncodedKeySpec(publicKeyBytes) return keyFactory.generatePublic(keySpec) as ECPublicKey } fun loadPrivateKey(privateKeyBytes: ByteArray): ECPrivateKey { val keyFactory = KeyFactory.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME) val keySpec = PKCS8EncodedKeySpec(privateKeyBytes) return keyFactory.generatePrivate(keySpec) as ECPrivateKey } // Example usage val vehiclePublicKeyBytes = ... // Get vehicle's public key as byte array val privateKeyBytes = ... // Get your private key as byte array val aesGCMKeyHex = sharedSecretFetcher(vehiclePublicKeyBytes, privateKeyBytes) println("AES-GCM Key: $aesGCMKeyHex")
Explanation:
createECDH()
: Initializes an ECDH key agreement instance using the BouncyCastle provider.computeSharedSecret()
: Computes the shared secret by performing the ECDH key exchange using your private key and the vehicle's public key.deriveAESKey()
: Hashes the shared secret using SHA-1 and then extracts the first 16 bytes for the AES-GCM key.sharedSecretFetcher()
: A wrapper function that performs the entire process of computing the shared secret and deriving the AES-GCM key.
Enjoyed this article?
Check out more content on our blog or follow us on social media.
Browse more articles