Add seeded private key generation
This commit is contained in:
parent
8015a4310b
commit
d0f1fffe6d
@ -20,7 +20,7 @@ plugins {
|
||||
}
|
||||
|
||||
group = "net.sergeych"
|
||||
version = "0.9.1-SNAPSHOT"
|
||||
version = "0.9.2-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
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
|
||||
|
||||
import com.ionspin.kotlin.crypto.box.Box
|
||||
import com.ionspin.kotlin.crypto.box.crypto_box_SEEDBYTES
|
||||
import com.ionspin.kotlin.crypto.scalarmult.ScalarMultiplication
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@ -87,6 +88,11 @@ class DecryptingSecretKey(
|
||||
companion object {
|
||||
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.
|
||||
*/
|
||||
@ -96,7 +102,30 @@ class DecryptingSecretKey(
|
||||
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
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
import com.ionspin.kotlin.crypto.signature.Signature
|
||||
import com.ionspin.kotlin.crypto.signature.crypto_sign_SEEDBYTES
|
||||
import kotlin.time.Instant
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@ -76,13 +77,41 @@ class SigningSecretKey(
|
||||
|
||||
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 {
|
||||
val p = Signature.keypair()
|
||||
val publicKey = VerifyingPublicKey(p.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
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
/**
|
||||
* 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
|
||||
* decrypt data.
|
||||
*/
|
||||
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
|
||||
fun secretEncryptTest() = runTest {
|
||||
initCrypto()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user