Last active
May 6, 2022 11:57
-
-
Save hurui200320/f86833eaaf0d33574562024f290ae861 to your computer and use it in GitHub Desktop.
Kotlin ECDH with BouncyCastle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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