Docs on how to use PBKDF2
Opened this issue · 3 comments
Hello! Thanks for making this amazing library!
I was trying to understand how to implement encryption/decryption using a password. I'm trying to use PBKDF2 like this:
val salt = CryptographyRandom.nextBytes(16)
val password = "123".toByteArray()
val provider = CryptographyProvider.Default
val secretDerivation =
provider.get(PBKDF2).secretDerivation(
digest = SHA256,
iterations = 480_000,
outputSize = 32.bytes,
salt = ByteString(salt),
)
val secret = secretDerivation.deriveSecretBlocking(password)and then I'm stuck. I get a ByteString like this: 6e8bcd0034451381a970d435125a2f653156f5710c82e8a992e1cdec725124e3. But how do I convert it to a key? I want to use the key to encrypt and decrypt some data.
Also I'm using digest = SHA256, but should I use PBKDF2HMAC? I was looking up the python cryptography library as an example: https://cryptography.io/en/stable/fernet/#using-passwords-with-fernet , and they use PBKDF2HMAC there.
Could you help me on how to proceed? And maybe adding this to the docs would help.
UPDATE
OK, so I was able to figure out the next steps:
val decoder = provider.get(AES.CBC).keyDecoder()
val key = decoder.decodeFromByteStringBlocking(AES.Key.Format.RAW, secret)
val token = key.cipher().encryptBlocking("Hello!".toByteArray())but it fails for me with an error:
Exception in thread "main" java.lang.NoClassDefFoundError: dev/whyoleg/cryptography/providers/base/algorithms/BaseAesIvCipher
at java.base/java.lang.ClassLoader.defineClass1(Native Method)
at java.base/java.lang.ClassLoader.defineClass(Unknown Source)
at java.base/java.security.SecureClassLoader.defineClass(Unknown Source)
at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(Unknown Source)
at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(Unknown Source)
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(Unknown Source)
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)
at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
at dev.whyoleg.cryptography.providers.jdk.algorithms.JdkAesCbcKey.cipher(JdkAesCbc.kt:29)
at dev.whyoleg.cryptography.algorithms.AES$CBC$Key.cipher$default(AES.kt:59)
at org.jetbrains.kotlin.idea.scratch.generated.ScratchFileRunnerGenerated$ScratchFileRunnerGenerated.<init>(tmp.kt:28)
at org.jetbrains.kotlin.idea.scratch.generated.ScratchFileRunnerGenerated.main(tmp.kt:34)
Caused by: java.lang.ClassNotFoundException: dev.whyoleg.cryptography.providers.base.algorithms.BaseAesIvCipher
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)
at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
... 13 more
I have this in my dependencies, the latest version 4.0.0:
implementation(libs.cryptography.core)
implementation(libs.cryptography.provider.jdk)
I figured it out! The error I was getting was because I was using a scratch file in IntelliJ, and they give weird errors sometimes. If I try to use this code in my app, it all works!
To decode a token I can just use:
key.cipher().decryptBlocking(token).decodeToString()
// will print "Hello!"
Would you still want to add this to the docs? Otherwise we can close this issue.
Hey! Yeah, that looks like the correct way to use PBKDF2 with AES.
Would you still want to add this to the docs? Otherwise we can close this issue.
Yeah, I think that could be a nice example/use-case for the docs. Let's keep the issue opened, as I would like to provide more comprehensive documentation and it will be easier to track.
Greetings, dear friends
First of all, great job on the library, I've found it way easier to use than any other and I am enjoying it immensely!
- I have a question about key exchange and deriving a shared secret. Is this process correct?
Step 1: Client sends Client Public Key, Client nonce
Step 2: Server sends Server Public Key, Server nonce, Salt
Shared Secret = Own Private Key + Received Public Key + Salt
The salt must be shared and be the same for both, client and server, right? and it's also not important to send it unencrypted, right?
- I'm working on a local prototype to start understanding better the library and the encryption process in general. Feel free to put it in the documentation (once it's fixed 😄 ). My code is crashing when trying to derive the shared secret, and I'm really lost. Would someone please be so kind to help me out? The code prototype below should be ready to be copy-pasted into the editor and compile
import dev.whyoleg.cryptography.BinarySize.Companion.bytes
import dev.whyoleg.cryptography.CryptographyProvider
import dev.whyoleg.cryptography.algorithms.*
import dev.whyoleg.cryptography.providers.jdk.JDK
import dev.whyoleg.cryptography.random.CryptographyRandom
import kotlinx.serialization.Serializable
import java.security.SecureRandom
import java.security.Security
object EncryptionTest
{
private val javaProvider = Security.getProvider("BC")
private val provider = CryptographyProvider.JDK(javaProvider)
suspend fun testEncryptionProcess()
{
println("Starting encryption test...")
val client = TestClient(provider)
val server = TestServer(provider)
// Client sends public key and nonce to server
val clientKeyExchange = client.generateKeyExchange()
println("Client sends: $clientKeyExchange")
// Server processes key exchange and responds
val serverResponse = server.processKeyExchange(clientKeyExchange)
println("Server responds: $serverResponse")
// Client processes server's response and completes key derivation
client.processServerResponse(serverResponse)
// Simulate encrypted message exchange
simulateMessageExchange(client, server)
}
private suspend fun simulateMessageExchange(client: TestClient, server: TestServer)
{
val message = "Hello, secure world!"
println("Original message: $message")
// Client encrypts the message
val encryptedMessage = client.encryptMessage(message)
println("Encrypted message: ${encryptedMessage.decodeToString()}")
// Server decrypts the message
val decryptedMessage = server.decryptMessage(encryptedMessage)
println("Decrypted message: $decryptedMessage")
assert(message == decryptedMessage) { "Decrypted message does not match the original!" }
}
}
class TestClient(private val provider: CryptographyProvider)
{
private lateinit var keyPair: ECDH.KeyPair
private lateinit var sharedSecret: ByteArray
private lateinit var symmetricKey: AES.GCM.Key
private val nonce = ByteArray(12).apply { SecureRandom().nextBytes(this) }
suspend fun generateKeyExchange(): KeyExchange
{
val ecdh = provider.get(ECDH)
keyPair = ecdh.keyPairGenerator(EC.Curve.P256).generateKey()
println("Client generated ECDH key pair.")
return KeyExchange(
publicKey = keyPair.publicKey.encodeToByteArray(EC.PublicKey.Format.RAW), nonce = nonce
)
}
suspend fun processServerResponse(response: KeyExchangeResponse)
{
val ecdh = provider.get(ECDH)
val serverPublicKey = ecdh.publicKeyDecoder(EC.Curve.P256).decodeFromByteArray(EC.PublicKey.Format.RAW, response.publicKey)
// Generate shared secret
sharedSecret = keyPair.privateKey.sharedSecretGenerator().generateSharedSecretToByteArray(serverPublicKey)
println("Client generated shared secret: ${sharedSecret.size} bytes")
// Derive symmetric key
val pbkdf2 = provider.get(PBKDF2)
val derivedKey = pbkdf2.secretDerivation(
digest = SHA256, iterations = 480_000, outputSize = 32.bytes, salt = response.salt
).deriveSecretBlocking(sharedSecret).toByteArray()
symmetricKey = provider.get(AES.GCM).keyDecoder().decodeFromByteArray(AES.Key.Format.RAW, derivedKey)
println("Client derived symmetric key successfully.")
}
suspend fun encryptMessage(message: String): ByteArray
{
return symmetricKey.cipher().encrypt(message.encodeToByteArray())
}
suspend fun decryptMessage(message: String): ByteArray
{
return symmetricKey.cipher().decrypt(message.encodeToByteArray())
}
}
class TestServer(private val provider: CryptographyProvider)
{
private lateinit var keyPair: ECDH.KeyPair
private lateinit var sharedSecret: ByteArray
private lateinit var symmetricKey: AES.GCM.Key
private val nonce = ByteArray(12).apply { SecureRandom().nextBytes(this) }
private val salt = CryptographyRandom.nextBytes(16)
suspend fun processKeyExchange(exchange: KeyExchange): KeyExchangeResponse
{
val ecdh = provider.get(ECDH)
keyPair = ecdh.keyPairGenerator(EC.Curve.P256).generateKey()
println("Server generated ECDH key pair.")
val clientPublicKey = ecdh.publicKeyDecoder(EC.Curve.P256).decodeFromByteArray(EC.PublicKey.Format.RAW, exchange.publicKey)
// Generate shared secret
sharedSecret = keyPair.privateKey.sharedSecretGenerator().generateSharedSecretToByteArray(clientPublicKey)
println("Server generated shared secret: ${sharedSecret.size} bytes")
// Derive symmetric key
val pbkdf2 = provider.get(PBKDF2)
val derivedKey = pbkdf2.secretDerivation(
digest = SHA256, iterations = 480_000, outputSize = 32.bytes, salt = salt
).deriveSecretBlocking(sharedSecret).toByteArray()
symmetricKey = provider.get(AES.GCM).keyDecoder().decodeFromByteArray(AES.Key.Format.RAW, derivedKey)
println("Server derived symmetric key successfully.")
return KeyExchangeResponse(
publicKey = keyPair.publicKey.encodeToByteArray(EC.PublicKey.Format.RAW), salt = salt, nonce = nonce
)
}
suspend fun encryptMessage(message: String): ByteArray
{
return symmetricKey.cipher().encrypt(message.encodeToByteArray())
}
suspend fun decryptMessage(encryptedMessage: ByteArray): String
{
return symmetricKey.cipher().decrypt(encryptedMessage).decodeToString()
}
}
@Serializable data class KeyExchange(val publicKey: ByteArray, val nonce: ByteArray)
@Serializable data class KeyExchangeResponse(val publicKey: ByteArray, val nonce: ByteArray, val salt: ByteArray)Starting encryption test...
Client generated ECDH key pair.
Client sends: KeyExchange(publicKey=[4, -70, 95, -49, -118, 13, 68, -86, 16, -6, -95, 120, -40, 83, -102, 22, 40, 68, 10, -27, -44, 19, -86, -119, -112, -18, -53, -114, 116, 51, 63, 106, -70, -40, 43, -24, 14, -81, 70, 26, 8, -111, -98, -86, 49, -30, 73, 57, 8, -33, -29, 94, 121, -91, -13, 8, -105, 58, -29, -74, 95, 115, -67, -40, -70], nonce=[-43, -21, 49, 96, -125, 46, 67, -98, -42, -32, -36, 47])
Server generated ECDH key pair.
Server generated shared secret: 32 bytes
Exception in thread "main" java.nio.charset.MalformedInputException: Input length = 1
The class where it crashes is class TestServer(private val provider: CryptographyProvider)
and the line is the last one of this code block
val pbkdf2 = provider.get(PBKDF2)
val derivedKey = pbkdf2.secretDerivation(
digest = SHA256, iterations = 480_000, outputSize = 32.bytes, salt = salt
).deriveSecretBlocking(sharedSecret).toByteArray() // CRASHES HERE
Relevant Gradle settings
plugins {
kotlin("jvm") version "2.0.21"
kotlin("plugin.serialization") version "2.0.21"
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.7.3")
implementation("dev.whyoleg.cryptography:cryptography-core:0.4.0")
implementation("dev.whyoleg.cryptography:cryptography-provider-jdk:0.4.0")
implementation("org.bouncycastle:bcprov-jdk18on:1.79")
implementation("org.bouncycastle:bcpkix-jdk18on:1.79")
}Also if you have any other tips let me know! I'd love to hear them
Thank you so much ❤️