Add seeded private key generation
This commit is contained in:
parent
8015a4310b
commit
d0f1fffe6d
@ -20,7 +20,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
group = "net.sergeych"
|
group = "net.sergeych"
|
||||||
version = "0.9.1-SNAPSHOT"
|
version = "0.9.2-SNAPSHOT"
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
|||||||
65
src/commonMain/kotlin/net/sergeych/crypto2/KeySeed.kt
Normal file
65
src/commonMain/kotlin/net/sergeych/crypto2/KeySeed.kt
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025. Sergey S. Chernov - All Rights Reserved
|
||||||
|
*
|
||||||
|
* You may use, distribute and modify this code under the
|
||||||
|
* terms of the private license, which you must obtain from the author
|
||||||
|
*
|
||||||
|
* To obtain the license, contact the author: https://t.me/real_sergeych or email to
|
||||||
|
* real dot sergeych at gmail.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
|
import com.ionspin.kotlin.crypto.generichash.GenericHash
|
||||||
|
import com.ionspin.kotlin.crypto.util.encodeToUByteArray
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deterministic seed derivation helper.
|
||||||
|
*
|
||||||
|
* It compresses arbitrary byte material to an exact seed size using domain-separated BLAKE2b.
|
||||||
|
* This is intended for already strong random byte material. Do not use it as a password KDF;
|
||||||
|
* use [KDF] and [PBKD] for passwords and other low-entropy secrets.
|
||||||
|
*/
|
||||||
|
object KeySeed {
|
||||||
|
private const val maxBlake2bOutput = 64
|
||||||
|
private const val minBlake2bOutput = 16
|
||||||
|
private val apiKey = "net.sergeych.crypto2.KeySeed.v1".encodeToUByteArray()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive exactly [size] bytes suitable for APIs expecting fixed-size cryptographic seeds.
|
||||||
|
*
|
||||||
|
* [purpose] domain-separates seeds for different key types. Use a stable, key-specific value
|
||||||
|
* when deriving more than one seed from the same [source].
|
||||||
|
*/
|
||||||
|
fun fromBytes(source: UByteArray, size: Int, purpose: String = "default"): UByteArray {
|
||||||
|
require(size > 0) { "seed size should be positive" }
|
||||||
|
require(source.isNotEmpty()) { "source bytes should not be empty" }
|
||||||
|
val key = GenericHash.genericHash(apiKey + purpose.encodeToUByteArray(), 32)
|
||||||
|
if (size <= maxBlake2bOutput) {
|
||||||
|
val hashSize = size.coerceAtLeast(minBlake2bOutput)
|
||||||
|
return GenericHash.genericHash(source, hashSize, key).copyOf(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = mutableListOf<UByte>()
|
||||||
|
var round = 0
|
||||||
|
while (result.size < size) {
|
||||||
|
val block = GenericHash.genericHash(
|
||||||
|
round.toLittleEndianBytes() + size.toLittleEndianBytes() + source,
|
||||||
|
maxBlake2bOutput,
|
||||||
|
key
|
||||||
|
)
|
||||||
|
val count = minOf(block.size, size - result.size)
|
||||||
|
result += block.take(count)
|
||||||
|
round += 1
|
||||||
|
}
|
||||||
|
return result.toUByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Int.toLittleEndianBytes(): UByteArray =
|
||||||
|
ubyteArrayOf(
|
||||||
|
(this and 0xff).toUByte(),
|
||||||
|
((this ushr 8) and 0xff).toUByte(),
|
||||||
|
((this ushr 16) and 0xff).toUByte(),
|
||||||
|
((this ushr 24) and 0xff).toUByte(),
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@
|
|||||||
package net.sergeych.crypto2
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
import com.ionspin.kotlin.crypto.box.Box
|
import com.ionspin.kotlin.crypto.box.Box
|
||||||
|
import com.ionspin.kotlin.crypto.box.crypto_box_SEEDBYTES
|
||||||
import com.ionspin.kotlin.crypto.scalarmult.ScalarMultiplication
|
import com.ionspin.kotlin.crypto.scalarmult.ScalarMultiplication
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@ -87,6 +88,11 @@ class DecryptingSecretKey(
|
|||||||
companion object {
|
companion object {
|
||||||
data class KeyPair(val secretKey: DecryptingSecretKey, val publicKey: EncryptingPublicKey)
|
data class KeyPair(val secretKey: DecryptingSecretKey, val publicKey: EncryptingPublicKey)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required seed size for deterministic decrypting key generation.
|
||||||
|
*/
|
||||||
|
val seedLength = crypto_box_SEEDBYTES
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a new random pair of public and secret keys.
|
* Generate a new random pair of public and secret keys.
|
||||||
*/
|
*/
|
||||||
@ -96,7 +102,30 @@ class DecryptingSecretKey(
|
|||||||
return KeyPair(DecryptingSecretKey(p.secretKey, pk), pk)
|
return KeyPair(DecryptingSecretKey(p.secretKey, pk), pk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the key pair deterministically from an exact [seedLength]-byte seed.
|
||||||
|
*/
|
||||||
|
fun generateKeys(seed: UByteArray): KeyPair {
|
||||||
|
require(seed.size == seedLength) { "seed should be $seedLength bytes" }
|
||||||
|
val p = Box.seedKeypair(seed)
|
||||||
|
val pk = EncryptingPublicKey(p.publicKey)
|
||||||
|
return KeyPair(DecryptingSecretKey(p.secretKey, pk), pk)
|
||||||
|
}
|
||||||
|
|
||||||
fun new(): DecryptingSecretKey = generateKeys().secretKey
|
fun new(): DecryptingSecretKey = generateKeys().secretKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a deterministic secret key from an exact [seedLength]-byte seed.
|
||||||
|
*/
|
||||||
|
fun fromSeed(seed: UByteArray): DecryptingSecretKey = generateKeys(seed).secretKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive an exact [seedLength]-byte seed from arbitrary byte material.
|
||||||
|
*
|
||||||
|
* Use this only with high-entropy source bytes. It is not a password KDF.
|
||||||
|
*/
|
||||||
|
fun seedFromBytes(source: UByteArray): UByteArray =
|
||||||
|
KeySeed.fromBytes(source, seedLength, "DecryptingSecretKey")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
package net.sergeych.crypto2
|
package net.sergeych.crypto2
|
||||||
|
|
||||||
import com.ionspin.kotlin.crypto.signature.Signature
|
import com.ionspin.kotlin.crypto.signature.Signature
|
||||||
|
import com.ionspin.kotlin.crypto.signature.crypto_sign_SEEDBYTES
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
@ -76,13 +77,41 @@ class SigningSecretKey(
|
|||||||
|
|
||||||
data class SigningKeyPair(val secretKey: SigningSecretKey, val publicKey: VerifyingPublicKey)
|
data class SigningKeyPair(val secretKey: SigningSecretKey, val publicKey: VerifyingPublicKey)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Required seed size for deterministic signing key generation.
|
||||||
|
*/
|
||||||
|
const val seedLength = crypto_sign_SEEDBYTES
|
||||||
|
|
||||||
fun generatePair(): SigningKeyPair {
|
fun generatePair(): SigningKeyPair {
|
||||||
val p = Signature.keypair()
|
val p = Signature.keypair()
|
||||||
val publicKey = VerifyingPublicKey(p.publicKey)
|
val publicKey = VerifyingPublicKey(p.publicKey)
|
||||||
return SigningKeyPair(SigningSecretKey(p.secretKey, publicKey), publicKey)
|
return SigningKeyPair(SigningSecretKey(p.secretKey, publicKey), publicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate the key pair deterministically from an exact [seedLength]-byte seed.
|
||||||
|
*/
|
||||||
|
fun generatePair(seed: UByteArray): SigningKeyPair {
|
||||||
|
require(seed.size == seedLength) { "seed should be $seedLength bytes" }
|
||||||
|
val p = Signature.seedKeypair(seed)
|
||||||
|
val publicKey = VerifyingPublicKey(p.publicKey)
|
||||||
|
return SigningKeyPair(SigningSecretKey(p.secretKey, publicKey), publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
fun new(): SigningSecretKey = generatePair().secretKey
|
fun new(): SigningSecretKey = generatePair().secretKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a deterministic signing key from an exact [seedLength]-byte seed.
|
||||||
|
*/
|
||||||
|
fun fromSeed(seed: UByteArray): SigningSecretKey = generatePair(seed).secretKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive an exact [seedLength]-byte seed from arbitrary byte material.
|
||||||
|
*
|
||||||
|
* Use this only with high-entropy source bytes. It is not a password KDF.
|
||||||
|
*/
|
||||||
|
fun seedFromBytes(source: UByteArray): UByteArray =
|
||||||
|
KeySeed.fromBytes(source, seedLength, "SigningSecretKey")
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,10 +48,38 @@ class UniversalPrivateKey(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Required seed size for deterministic universal private key generation.
|
||||||
|
*
|
||||||
|
* It contains independent signing and decrypting seeds.
|
||||||
|
*/
|
||||||
|
val seedLength = SigningSecretKey.seedLength + DecryptingSecretKey.seedLength
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate 2 new random keys (4 key pairs under the hood) to securely signd and
|
* Generate 2 new random keys (4 key pairs under the hood) to securely signd and
|
||||||
* decrypt data.
|
* decrypt data.
|
||||||
*/
|
*/
|
||||||
fun new() = UniversalPrivateKey(SigningSecretKey.new(), DecryptingSecretKey.new())
|
fun new() = UniversalPrivateKey(SigningSecretKey.new(), DecryptingSecretKey.new())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a deterministic private key from an exact [seedLength]-byte seed.
|
||||||
|
*/
|
||||||
|
fun fromSeed(seed: UByteArray): UniversalPrivateKey {
|
||||||
|
require(seed.size == seedLength) { "seed should be $seedLength bytes" }
|
||||||
|
val signingSeed = seed.sliceArray(0..<SigningSecretKey.seedLength)
|
||||||
|
val decryptingSeed = seed.sliceArray(SigningSecretKey.seedLength..<seedLength)
|
||||||
|
return UniversalPrivateKey(
|
||||||
|
SigningSecretKey.fromSeed(signingSeed),
|
||||||
|
DecryptingSecretKey.fromSeed(decryptingSeed)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Derive an exact [seedLength]-byte seed from arbitrary byte material.
|
||||||
|
*
|
||||||
|
* Use this only with high-entropy source bytes. It is not a password KDF.
|
||||||
|
*/
|
||||||
|
fun seedFromBytes(source: UByteArray): UByteArray =
|
||||||
|
KeySeed.fromBytes(source, seedLength, "UniversalPrivateKey")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -82,6 +82,48 @@ class KeysTest {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun seededPrivateKeysTest() = runTest {
|
||||||
|
initCrypto()
|
||||||
|
val source = "stable high entropy key material placeholder".encodeToUByteArray() + randomUBytes(128)
|
||||||
|
val data = "seeded generation works".encodeToUByteArray()
|
||||||
|
|
||||||
|
val signingSeed = SigningSecretKey.seedFromBytes(source)
|
||||||
|
val decryptingSeed = DecryptingSecretKey.seedFromBytes(source)
|
||||||
|
assertEquals(SigningSecretKey.seedLength, signingSeed.size)
|
||||||
|
assertEquals(DecryptingSecretKey.seedLength, decryptingSeed.size)
|
||||||
|
assertFalse(signingSeed contentEquals decryptingSeed)
|
||||||
|
|
||||||
|
val signingKey1 = SigningSecretKey.fromSeed(signingSeed)
|
||||||
|
val signingKey2 = SigningSecretKey.fromSeed(signingSeed)
|
||||||
|
assertEquals(signingKey1, signingKey2)
|
||||||
|
val signature = signingKey1.sign(data)
|
||||||
|
assertTrue(signingKey2.verifyingKey.verify(signature, data))
|
||||||
|
|
||||||
|
val decryptingKey1 = DecryptingSecretKey.fromSeed(decryptingSeed)
|
||||||
|
val decryptingKey2 = DecryptingSecretKey.fromSeed(decryptingSeed)
|
||||||
|
assertEquals(decryptingKey1.publicKey, decryptingKey2.publicKey)
|
||||||
|
assertContentEquals(data, decryptingKey2.decrypt(decryptingKey1.publicKey.encrypt(data)))
|
||||||
|
|
||||||
|
val universalSeed = UniversalPrivateKey.seedFromBytes(source)
|
||||||
|
assertEquals(UniversalPrivateKey.seedLength, universalSeed.size)
|
||||||
|
val universalKey1 = UniversalPrivateKey.fromSeed(universalSeed)
|
||||||
|
val universalKey2 = UniversalPrivateKey.fromSeed(universalSeed)
|
||||||
|
assertEquals(universalKey1.publicKey, universalKey2.publicKey)
|
||||||
|
assertTrue(universalKey2.publicKey.verify(universalKey1.sign(data), data))
|
||||||
|
assertContentEquals(data, universalKey2.decrypt(universalKey1.publicKey.encrypt(data)))
|
||||||
|
|
||||||
|
assertFailsWith<IllegalArgumentException> {
|
||||||
|
SigningSecretKey.fromSeed(signingSeed.dropLast(1).toUByteArray())
|
||||||
|
}
|
||||||
|
assertFailsWith<IllegalArgumentException> {
|
||||||
|
DecryptingSecretKey.fromSeed(decryptingSeed.dropLast(1).toUByteArray())
|
||||||
|
}
|
||||||
|
assertFailsWith<IllegalArgumentException> {
|
||||||
|
UniversalPrivateKey.fromSeed(universalSeed.dropLast(1).toUByteArray())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun secretEncryptTest() = runTest {
|
fun secretEncryptTest() = runTest {
|
||||||
initCrypto()
|
initCrypto()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user