1import java.io.File
2import java.security.KeyFactory
3import java.security.spec.PKCS8EncodedKeySpec
4import java.security.spec.X509EncodedKeySpec
5import java.util.Base64
6import java.util.HexFormat
7import javax.crypto.KDF
8import javax.crypto.KeyAgreement
9import javax.crypto.spec.HKDFParameterSpec
10
11fun pemBody(file: String): ByteArray =
12 Base64.getMimeDecoder().decode(File(file).readText().replace(Regex("-----[^-]+-----"), ""))
13
14/** keyType "XDH" + agreement "XDH" for X25519; "EC" + "ECDH" for P-256. */
15fun agree(keyType: String, agreement: String, privateKeyFile: String, peerPublicKeyFile: String): ByteArray {
16 val factory = KeyFactory.getInstance(keyType)
17 return KeyAgreement.getInstance(agreement).run {
18 init(factory.generatePrivate(PKCS8EncodedKeySpec(pemBody(privateKeyFile))))
19 doPhase(factory.generatePublic(X509EncodedKeySpec(pemBody(peerPublicKeyFile))), true)
20 generateSecret() // the raw shared secret
21 }
22}
23
24/** HKDF-SHA256 through javax.crypto.KDF (JDK 25+); no salt means an all-zero salt. */
25fun deriveKey(sharedSecret: ByteArray): ByteArray =
26 KDF.getInstance("HKDF-SHA256").deriveData(
27 HKDFParameterSpec.ofExtract().addIKM(sharedSecret).thenExpand("AES-256-GCM key v1".toByteArray(), 32),
28 )
29
30fun main() {
31 val hex = HexFormat.of()
32 for ((curve, types, files) in listOf(
33 Triple("X25519", "XDH" to "XDH", "alice-x25519.pem" to "bob-x25519.pub.pem"),
34 Triple("P-256", "EC" to "ECDH", "alice-p256.pem" to "bob-p256.pub.pem"),
35 )) {
36 val secret = agree(types.first, types.second, files.first, files.second)
37 println("${curve} shared secret: ${hex.formatHex(secret)}")
38 println("${curve} derived key: ${hex.formatHex(deriveKey(secret))}")
39 }
40}