Skip to content

Instantly share code, notes, and snippets.

@hurui200320
Last active May 6, 2022 11:57
Show Gist options
  • Save hurui200320/f86833eaaf0d33574562024f290ae861 to your computer and use it in GitHub Desktop.
Save hurui200320/f86833eaaf0d33574562024f290ae861 to your computer and use it in GitHub Desktop.
Kotlin ECDH with BouncyCastle
plugins {
id 'java'
id 'org.jetbrains.kotlin.jvm' version '1.6.21'
}
group 'info.skyblond.demo'
version '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.21'
implementation 'org.bouncycastle:bcprov-jdk18on:1.71'
}
compileKotlin {
kotlinOptions.jvmTarget = "17"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "17"
}
test {
useJUnitPlatform()
}
package info.skyblond.crypto
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
import org.bouncycastle.jce.ECNamedCurveTable
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.jce.spec.ECPrivateKeySpec
import org.bouncycastle.jce.spec.ECPublicKeySpec
import java.math.BigInteger
import java.security.*
import java.security.spec.ECGenParameterSpec
import javax.crypto.KeyAgreement
object ECDH {
// use `curve25519`, or you can use `secp256r1` or `secp256k1` (this one is more recommended)
private const val curveName = "curve25519"
private val secureRandom = SecureRandom()
private var ecSpec = ECNamedCurveTable.getParameterSpec(curveName)
private val bcProvider = BouncyCastleProvider()
/**
* Dump key pair into (Private key D, Public key Q)
*
* The private key is just a number, and public key is a bunch of bytes.
* */
private fun dumpKeyPair(keyPair: KeyPair): Pair<BigInteger, ByteArray> {
val privateKey = keyPair.private
println("Private key type: " + privateKey.javaClass.canonicalName)
require(privateKey is BCECPrivateKey)
val publicKey = keyPair.public
println("Public key type: " + publicKey.javaClass.canonicalName)
require(publicKey is BCECPublicKey)
// compressed make the pub key shorter, no effect on this demo
return privateKey.d to publicKey.q.getEncoded(true)
}
private fun parsePrivateKey(d: BigInteger): PrivateKey {
println("Private key (Hex number): " + d.toString(16))
val privateKeySpec = ECPrivateKeySpec(d, ecSpec)
val keyFactory = KeyFactory.getInstance("ECDH", bcProvider)
return keyFactory.generatePrivate(privateKeySpec)
}
private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
private fun parsePublicKey(publicKeyBytes: ByteArray): PublicKey {
println("Public key (Hex string): " + publicKeyBytes.toHex())
val pubKey = ECPublicKeySpec(ecSpec.curve.decodePoint(publicKeyBytes), ecSpec)
val keyFactory = KeyFactory.getInstance("ECDH", bcProvider)
return keyFactory.generatePublic(pubKey)
}
private fun dumpAndLoadKeyPair(keyPair: KeyPair): Pair<PrivateKey, PublicKey> {
val (privateKeyD, publicKeyBytes) = dumpKeyPair(keyPair)
return parsePrivateKey(privateKeyD) to parsePublicKey(publicKeyBytes)
}
private fun doECDH(selfPrivateKey: PrivateKey, remotePublicKey: PublicKey): ByteArray {
val keyAgreement = KeyAgreement.getInstance("ECDH", bcProvider)
keyAgreement.init(selfPrivateKey)
keyAgreement.doPhase(remotePublicKey, true)
return keyAgreement.generateSecret()
}
@JvmStatic
fun main(args: Array<String>) {
val keyPairGenerator = KeyPairGenerator.getInstance("ECDH", bcProvider)
keyPairGenerator.initialize(ECGenParameterSpec(curveName), secureRandom)
// generate pki
val (serverPrivateKey, serverPublicKey) = dumpAndLoadKeyPair(keyPairGenerator.genKeyPair())
val (clientPrivateKey, clientPublicKey) = dumpAndLoadKeyPair(keyPairGenerator.genKeyPair())
val serverSideSecret = doECDH(serverPrivateKey, clientPublicKey).toHex()
val clientSideSecret = doECDH(clientPrivateKey, serverPublicKey).toHex()
println("Server side secret (Hex string): ")
println(serverSideSecret)
println("Client side secret (Hex string): ")
println(clientSideSecret)
}
}
package info.skyblond.crypto
import org.bouncycastle.crypto.agreement.X25519Agreement
import org.bouncycastle.crypto.digests.SHA3Digest
import org.bouncycastle.crypto.engines.AESEngine
import org.bouncycastle.crypto.modes.GCMBlockCipher
import org.bouncycastle.crypto.params.AEADParameters
import org.bouncycastle.crypto.params.KeyParameter
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters
import org.bouncycastle.crypto.params.X25519PublicKeyParameters
import java.security.SecureRandom
/**
* This class demonstrate how to perform a X25519 key exchange with pure BouncyCastle.
* */
object X25519WithPureBC {
private val secureRandom = SecureRandom()
/**
* Generate private key depends on curve.
* */
private fun generatePrivateKey(): ByteArray =
X25519PrivateKeyParameters(secureRandom).encoded
/**
* Generate the public key for ECDH key exchange.
* */
private fun generatePublicKey(privateKeyBytes: ByteArray): ByteArray =
X25519PrivateKeyParameters(privateKeyBytes).generatePublicKey().encoded
private fun doECDH(selfPrivateKeyBytes: ByteArray, remotePublicKeyBytes: ByteArray): ByteArray {
val agreement = X25519Agreement()
val result = ByteArray(agreement.agreementSize)
agreement.init(X25519PrivateKeyParameters(selfPrivateKeyBytes))
agreement.calculateAgreement(X25519PublicKeyParameters(remotePublicKeyBytes), result, 0)
return result
}
/**
* Return: (Encrypted message, nonce)
* */
private fun encryptMessage(key: ByteArray, message: ByteArray): Pair<ByteArray, ByteArray> {
val digest = SHA3Digest(512)
val nonce = ByteArray(digest.digestSize)
digest.update(message, 0, message.size)
digest.doFinal(nonce, 0)
val cipher = GCMBlockCipher(AESEngine())
// mac size is ranged from 32~128 bits, for message integrity check
// nonce can be any size, but must not be reused
// here we use the SHA3-512 of the message as the nonce
cipher.init(true, AEADParameters(KeyParameter(key), 128, nonce))
val result = ByteArray(cipher.getOutputSize(message.size))
val resultSize = cipher.processBytes(message, 0, message.size, result, 0)
cipher.doFinal(result, resultSize)
return result to nonce
}
private fun decryptMessage(key: ByteArray, encryptedMessage: ByteArray, nonce: ByteArray): ByteArray {
val cipher = GCMBlockCipher(AESEngine())
cipher.init(false, AEADParameters(KeyParameter(key), 128, nonce))
val result = ByteArray(cipher.getOutputSize(encryptedMessage.size))
val resultSize = cipher.processBytes(encryptedMessage, 0, encryptedMessage.size, result, 0)
cipher.doFinal(result, resultSize)
return result
}
private fun ByteArray.toHex(): String = joinToString(separator = "") { eachByte -> "%02x".format(eachByte) }
@JvmStatic
fun main(args: Array<String>) {
val serverPrivateKeyBytes = generatePrivateKey()
println("Server x25519 private key: " + serverPrivateKeyBytes.toHex())
val clientPrivateKeyBytes = generatePrivateKey()
println("Client x25519 private key: " + clientPrivateKeyBytes.toHex())
// test key exchange
val serverECDHPublicKey = generatePublicKey(serverPrivateKeyBytes)
println("Server x25519 public key: " + serverECDHPublicKey.toHex())
val clientECDHPublicKey = generatePublicKey(clientPrivateKeyBytes)
println("Client x25519 public key: " + clientECDHPublicKey.toHex())
val serverECDHResult = doECDH(serverPrivateKeyBytes, clientECDHPublicKey)
println("Server x25519 result: " + serverECDHResult.toHex())
val clientECDHResult = doECDH(clientPrivateKeyBytes, serverECDHPublicKey)
println("Client x25519 result: " + clientECDHResult.toHex())
require(serverECDHResult.contentEquals(clientECDHResult))
// test encryption
val message = "This is a message. ".repeat(200).encodeToByteArray()
println("Message: " + message.toHex())
val (serverSendEncryptedMessage, serverSendNonce) = encryptMessage(serverECDHResult, message)
println("Server sent message: " + serverSendEncryptedMessage.toHex())
println("Server sent nonce: " + serverSendNonce.toHex())
val clientDecrypt = decryptMessage(clientECDHResult, serverSendEncryptedMessage, serverSendNonce)
println("Client decrypt: " + clientDecrypt.toHex())
require(message.contentEquals(clientDecrypt))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment