Fixes key creation for groups.
This commit is contained in:
@@ -145,6 +145,9 @@ kotlin {
|
||||
|
||||
// Negentropy set reconciliation (NIP-77)
|
||||
api(libs.negentropy.kmp)
|
||||
|
||||
implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4")
|
||||
implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -323,8 +326,6 @@ kotlin {
|
||||
dependencies {
|
||||
implementation(libs.net.thauvin.erik.urlencoder.lib)
|
||||
implementation(libs.dev.whyoleg.cryptography.provider.apple.optimal)
|
||||
implementation("io.github.andreypfau:kotlinx-crypto-hmac:0.0.4")
|
||||
implementation("io.github.andreypfau:kotlinx-crypto-sha2:0.0.4")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@ package com.vitorpamplona.quartz.marmot.mls.crypto
|
||||
* Ed25519 digital signature operations for MLS ciphersuite 0x0001.
|
||||
*
|
||||
* Platform-specific implementations:
|
||||
* - JVM/Android: java.security EdDSA (Java 15+, Android API 33+)
|
||||
* - Native: expect/actual with kotlinx-crypto or platform crypto
|
||||
* - JVM/Android: Pure Kotlin implementation
|
||||
* - Native: Pure Kotlin implementation
|
||||
*/
|
||||
expect object Ed25519 {
|
||||
/**
|
||||
|
||||
+308
-105
@@ -20,68 +20,26 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.marmot.mls.crypto
|
||||
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.Provider
|
||||
import java.security.Security
|
||||
import java.security.Signature
|
||||
import java.security.spec.EdECPrivateKeySpec
|
||||
import java.security.spec.EdECPublicKeySpec
|
||||
import java.security.spec.NamedParameterSpec
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
import io.github.andreypfau.kotlinx.crypto.Sha512
|
||||
|
||||
/**
|
||||
* JVM/Android Ed25519 implementation using java.security EdDSA.
|
||||
* JVM/Android Ed25519 implementation using pure Kotlin field arithmetic.
|
||||
*
|
||||
* Requires Java 15+ or Android API 33+.
|
||||
* This implementation is used to avoid issues with Android's KeyStore provider,
|
||||
* which requires platform-specific initialization that breaks standard JCA usage.
|
||||
*
|
||||
* Private key format: 32-byte seed + 32-byte public key (64 bytes total).
|
||||
* Public key format: 32-byte compressed Edwards point.
|
||||
*/
|
||||
actual object Ed25519 {
|
||||
private const val ALGORITHM = "Ed25519"
|
||||
private const val SEED_LENGTH = 32
|
||||
private const val PUBLIC_KEY_LENGTH = 32
|
||||
|
||||
/**
|
||||
* Picks a non-AndroidKeyStore provider for the given JCA service.
|
||||
*
|
||||
* On Android, `KeyPairGenerator.getInstance("Ed25519")` can resolve to
|
||||
* `AndroidKeyStoreKeyPairGeneratorSpi`, which rejects `NamedParameterSpec`
|
||||
* and requires `KeyGenParameterSpec` instead. We need a software provider
|
||||
* (Conscrypt / SunEC) that supports the standard JCA Ed25519 interface.
|
||||
*/
|
||||
private fun findProvider(service: String): Provider? =
|
||||
Security
|
||||
.getProviders("$service.$ALGORITHM")
|
||||
?.firstOrNull { !it.name.contains("AndroidKeyStore", ignoreCase = true) }
|
||||
|
||||
private val keyPairGeneratorProvider: Provider? = findProvider("KeyPairGenerator")
|
||||
private val keyFactoryProvider: Provider? = findProvider("KeyFactory")
|
||||
private val signatureProvider: Provider? = findProvider("Signature")
|
||||
|
||||
private fun keyPairGenerator(): KeyPairGenerator =
|
||||
keyPairGeneratorProvider?.let { KeyPairGenerator.getInstance(ALGORITHM, it) }
|
||||
?: KeyPairGenerator.getInstance(ALGORITHM)
|
||||
|
||||
private fun keyFactory(): KeyFactory =
|
||||
keyFactoryProvider?.let { KeyFactory.getInstance(ALGORITHM, it) }
|
||||
?: KeyFactory.getInstance(ALGORITHM)
|
||||
|
||||
private fun signatureInstance(): Signature =
|
||||
signatureProvider?.let { Signature.getInstance(ALGORITHM, it) }
|
||||
?: Signature.getInstance(ALGORITHM)
|
||||
|
||||
actual fun generateKeyPair(): Ed25519KeyPair {
|
||||
// Ed25519 is fully specified by its algorithm name, so no initialize()
|
||||
// call is required. Calling initialize(NamedParameterSpec) would fail
|
||||
// on Android's keystore provider (which requires KeyGenParameterSpec).
|
||||
val kpg = keyPairGenerator()
|
||||
val kp = kpg.generateKeyPair()
|
||||
|
||||
val publicKey = extractPublicKeyBytes(kp.public as java.security.interfaces.EdECPublicKey)
|
||||
val seed = extractPrivateKeyBytes(kp.private as java.security.interfaces.EdECPrivateKey)
|
||||
val seed = RandomInstance.bytes(SEED_LENGTH)
|
||||
val publicKey = derivePublicKey(seed)
|
||||
val privateKey = seed + publicKey
|
||||
|
||||
return Ed25519KeyPair(privateKey, publicKey)
|
||||
}
|
||||
|
||||
@@ -92,16 +50,36 @@ actual object Ed25519 {
|
||||
require(privateKey.size == SEED_LENGTH * 2) { "Private key must be 64 bytes (seed + public)" }
|
||||
|
||||
val seed = privateKey.copyOfRange(0, SEED_LENGTH)
|
||||
val pubBytes = privateKey.copyOfRange(SEED_LENGTH, SEED_LENGTH * 2)
|
||||
val publicKey = privateKey.copyOfRange(SEED_LENGTH, SEED_LENGTH * 2)
|
||||
|
||||
val kf = keyFactory()
|
||||
val privKeySpec = EdECPrivateKeySpec(NamedParameterSpec(ALGORITHM), seed)
|
||||
val jcaPrivateKey = kf.generatePrivate(privKeySpec)
|
||||
val d = sha512(seed)
|
||||
d[0] = (d[0].toInt() and 248).toByte()
|
||||
d[31] = ((d[31].toInt() and 63) or 64).toByte()
|
||||
|
||||
val sig = signatureInstance()
|
||||
sig.initSign(jcaPrivateKey)
|
||||
sig.update(message)
|
||||
return sig.sign()
|
||||
val rHash = sha512(d.copyOfRange(32, 64) + message)
|
||||
val r = reduce(rHash)
|
||||
|
||||
val rPoint = scalarMultBase(r)
|
||||
val rBytes = packPoint(rPoint)
|
||||
|
||||
val hramHash = sha512(rBytes + publicKey + message)
|
||||
val hram = reduce(hramHash)
|
||||
|
||||
val signature = ByteArray(64)
|
||||
rBytes.copyInto(signature, 0)
|
||||
|
||||
val x = LongArray(64)
|
||||
for (i in 0 until 32) x[i] = r[i].toLong() and 0xFF
|
||||
for (i in 0 until 32) {
|
||||
for (j in 0 until 32) {
|
||||
x[i + j] += (hram[i].toLong() and 0xFF) * (d[j].toLong() and 0xFF)
|
||||
}
|
||||
}
|
||||
|
||||
val sBytes = modL(x)
|
||||
sBytes.copyInto(signature, 32)
|
||||
|
||||
return signature
|
||||
}
|
||||
|
||||
actual fun verify(
|
||||
@@ -110,16 +88,25 @@ actual object Ed25519 {
|
||||
publicKey: ByteArray,
|
||||
): Boolean {
|
||||
require(publicKey.size == PUBLIC_KEY_LENGTH) { "Public key must be 32 bytes" }
|
||||
if (signature.size != 64) return false
|
||||
|
||||
val kf = keyFactory()
|
||||
val point = bytesToEdECPoint(publicKey)
|
||||
val pubKeySpec = EdECPublicKeySpec(NamedParameterSpec(ALGORITHM), point)
|
||||
val jcaPublicKey = kf.generatePublic(pubKeySpec)
|
||||
val aPoint = unpackPoint(publicKey) ?: return false
|
||||
|
||||
val sig = signatureInstance()
|
||||
sig.initVerify(jcaPublicKey)
|
||||
sig.update(message)
|
||||
return sig.verify(signature)
|
||||
val rBytes = signature.copyOfRange(0, 32)
|
||||
val sBytes = signature.copyOfRange(32, 64)
|
||||
|
||||
if (!isCanonicalScalar(sBytes)) return false
|
||||
|
||||
val hramHash = sha512(rBytes + publicKey + message)
|
||||
val hram = reduce(hramHash)
|
||||
|
||||
val sPoint = scalarMultBase(sBytes)
|
||||
val hramA = scalarMult(aPoint, hram)
|
||||
val negHramA = negatePoint(hramA)
|
||||
val rCheck = addPoints(sPoint, negHramA)
|
||||
val rCheckBytes = packPoint(rCheck)
|
||||
|
||||
return rCheckBytes.contentEquals(rBytes)
|
||||
}
|
||||
|
||||
actual fun publicFromPrivate(privateKey: ByteArray): ByteArray {
|
||||
@@ -127,57 +114,273 @@ actual object Ed25519 {
|
||||
return privateKey.copyOfRange(SEED_LENGTH, SEED_LENGTH * 2)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract 32-byte compressed Edwards point from JCA EdECPublicKey.
|
||||
* The point encoding follows RFC 8032 Section 5.1.2.
|
||||
*/
|
||||
private fun extractPublicKeyBytes(pubKey: java.security.interfaces.EdECPublicKey): ByteArray {
|
||||
val point = pubKey.point
|
||||
val yBytes = point.y.toByteArray()
|
||||
val result = ByteArray(PUBLIC_KEY_LENGTH)
|
||||
private fun derivePublicKey(seed: ByteArray): ByteArray {
|
||||
val d = sha512(seed)
|
||||
d[0] = (d[0].toInt() and 248).toByte()
|
||||
d[31] = ((d[31].toInt() and 63) or 64).toByte()
|
||||
|
||||
// BigInteger is big-endian, Edwards encoding is little-endian
|
||||
for (i in yBytes.indices) {
|
||||
val targetIdx = yBytes.size - 1 - i
|
||||
if (targetIdx < PUBLIC_KEY_LENGTH) {
|
||||
result[targetIdx] = yBytes[i]
|
||||
}
|
||||
}
|
||||
val p = scalarMultBase(d.copyOfRange(0, 32))
|
||||
return packPoint(p)
|
||||
}
|
||||
|
||||
// Set high bit of last byte if x is odd
|
||||
if (point.isXOdd) {
|
||||
result[PUBLIC_KEY_LENGTH - 1] = (result[PUBLIC_KEY_LENGTH - 1].toInt() or 0x80).toByte()
|
||||
}
|
||||
private fun sha512(data: ByteArray): ByteArray {
|
||||
val digest = Sha512()
|
||||
digest.update(data)
|
||||
return digest.digest()
|
||||
}
|
||||
|
||||
private fun newPoint(): Array<LongArray> =
|
||||
arrayOf(
|
||||
LongArray(16),
|
||||
LongArray(16),
|
||||
LongArray(16),
|
||||
LongArray(16),
|
||||
)
|
||||
|
||||
private fun identityPoint(): Array<LongArray> {
|
||||
val p = newPoint()
|
||||
p[1][0] = 1
|
||||
p[2][0] = 1
|
||||
return p
|
||||
}
|
||||
|
||||
private fun addPoints(
|
||||
p: Array<LongArray>,
|
||||
q: Array<LongArray>,
|
||||
): Array<LongArray> {
|
||||
val result =
|
||||
arrayOf(
|
||||
p[0].copyOf(),
|
||||
p[1].copyOf(),
|
||||
p[2].copyOf(),
|
||||
p[3].copyOf(),
|
||||
)
|
||||
addPointInPlace(result, q)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract seed bytes from JCA EdECPrivateKey.
|
||||
*/
|
||||
private fun extractPrivateKeyBytes(privKey: java.security.interfaces.EdECPrivateKey): ByteArray {
|
||||
val bytes = privKey.bytes.orElseThrow { IllegalStateException("No seed in private key") }
|
||||
return bytes.copyOf()
|
||||
private fun addPointInPlace(
|
||||
p: Array<LongArray>,
|
||||
q: Array<LongArray>,
|
||||
) {
|
||||
val a = Curve25519Field.sub(p[1], p[0])
|
||||
val t = Curve25519Field.sub(q[1], q[0])
|
||||
val aMul = Curve25519Field.mul(a, t)
|
||||
val b = Curve25519Field.add(p[0], p[1])
|
||||
val t2 = Curve25519Field.add(q[0], q[1])
|
||||
val bMul = Curve25519Field.mul(b, t2)
|
||||
val c = Curve25519Field.mul(p[3], q[3])
|
||||
val cMul = Curve25519Field.mul(c, Curve25519Field.D2)
|
||||
val d = Curve25519Field.mul(p[2], q[2])
|
||||
val dAdd = Curve25519Field.add(d, d)
|
||||
val e = Curve25519Field.sub(bMul, aMul)
|
||||
val f = Curve25519Field.sub(dAdd, cMul)
|
||||
val g = Curve25519Field.add(dAdd, cMul)
|
||||
val h = Curve25519Field.add(bMul, aMul)
|
||||
|
||||
Curve25519Field.mul(e, f).copyInto(p[0])
|
||||
Curve25519Field.mul(h, g).copyInto(p[1])
|
||||
Curve25519Field.mul(g, f).copyInto(p[2])
|
||||
Curve25519Field.mul(e, h).copyInto(p[3])
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert 32-byte compressed Edwards point to JCA EdECPoint.
|
||||
*/
|
||||
private fun bytesToEdECPoint(publicKey: ByteArray): java.security.spec.EdECPoint {
|
||||
// RFC 8032: last bit of last byte encodes x parity
|
||||
val xOdd = (publicKey[PUBLIC_KEY_LENGTH - 1].toInt() and 0x80) != 0
|
||||
private fun negatePoint(p: Array<LongArray>): Array<LongArray> {
|
||||
val result = newPoint()
|
||||
Curve25519Field.sub(Curve25519Field.GF0, p[0]).copyInto(result[0])
|
||||
p[1].copyInto(result[1])
|
||||
p[2].copyInto(result[2])
|
||||
Curve25519Field.sub(Curve25519Field.GF0, p[3]).copyInto(result[3])
|
||||
return result
|
||||
}
|
||||
|
||||
// Clear the high bit and reverse to big-endian for BigInteger
|
||||
val yLE = publicKey.copyOf()
|
||||
yLE[PUBLIC_KEY_LENGTH - 1] = (yLE[PUBLIC_KEY_LENGTH - 1].toInt() and 0x7F).toByte()
|
||||
private fun scalarMult(
|
||||
p: Array<LongArray>,
|
||||
s: ByteArray,
|
||||
): Array<LongArray> {
|
||||
val result = identityPoint()
|
||||
val q =
|
||||
arrayOf(
|
||||
p[0].copyOf(),
|
||||
p[1].copyOf(),
|
||||
p[2].copyOf(),
|
||||
p[3].copyOf(),
|
||||
)
|
||||
for (i in 255 downTo 0) {
|
||||
val b = ((s[i shr 3].toInt() shr (i and 7)) and 1).toLong()
|
||||
cswap(result, q, b)
|
||||
addPointInPlace(q, result)
|
||||
addPointInPlace(result, result)
|
||||
cswap(result, q, b)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Reverse to big-endian
|
||||
val yBE = ByteArray(PUBLIC_KEY_LENGTH)
|
||||
for (i in 0 until PUBLIC_KEY_LENGTH) {
|
||||
yBE[i] = yLE[PUBLIC_KEY_LENGTH - 1 - i]
|
||||
private fun scalarMultBase(s: ByteArray): Array<LongArray> {
|
||||
val basePoint = newPoint()
|
||||
Curve25519Field.BX.copyInto(basePoint[0])
|
||||
Curve25519Field.BY.copyInto(basePoint[1])
|
||||
Curve25519Field.GF1.copyInto(basePoint[2])
|
||||
Curve25519Field.mul(Curve25519Field.BX, Curve25519Field.BY).copyInto(basePoint[3])
|
||||
return scalarMult(basePoint, s)
|
||||
}
|
||||
|
||||
private fun cswap(
|
||||
p: Array<LongArray>,
|
||||
q: Array<LongArray>,
|
||||
b: Long,
|
||||
) {
|
||||
for (i in 0 until 4) {
|
||||
Curve25519Field.sel25519(p[i], q[i], b)
|
||||
}
|
||||
}
|
||||
|
||||
private fun packPoint(p: Array<LongArray>): ByteArray {
|
||||
val zi = Curve25519Field.inv25519(p[2])
|
||||
val tx = Curve25519Field.mul(p[0], zi)
|
||||
val ty = Curve25519Field.mul(p[1], zi)
|
||||
val r = Curve25519Field.pack25519(ty)
|
||||
r[31] = (r[31].toInt() xor (Curve25519Field.par25519(tx) shl 7)).toByte()
|
||||
return r
|
||||
}
|
||||
|
||||
private fun unpackPoint(s: ByteArray): Array<LongArray>? {
|
||||
val p = newPoint()
|
||||
val r = Curve25519Field.unpack25519(s)
|
||||
r.copyInto(p[1])
|
||||
Curve25519Field.GF1.copyInto(p[2])
|
||||
|
||||
val y2 = Curve25519Field.sqr(r)
|
||||
val d =
|
||||
Curve25519Field.gf(
|
||||
0x78A3,
|
||||
0x1359,
|
||||
0x4DCA,
|
||||
0x75EB,
|
||||
0xD8AB,
|
||||
0x4141,
|
||||
0x0A4D,
|
||||
0x0070,
|
||||
0xE898,
|
||||
0x7779,
|
||||
0x4079,
|
||||
0x8CC7,
|
||||
0xFE73,
|
||||
0x2B6F,
|
||||
0x6CEE,
|
||||
0x5203,
|
||||
)
|
||||
val num = Curve25519Field.sub(y2, Curve25519Field.GF1)
|
||||
val den = Curve25519Field.add(Curve25519Field.mul(d, y2), Curve25519Field.GF1)
|
||||
val denInv = Curve25519Field.inv25519(den)
|
||||
val x2 = Curve25519Field.mul(num, denInv)
|
||||
|
||||
var x = Curve25519Field.pow2523(x2)
|
||||
x = Curve25519Field.mul(x, x2)
|
||||
|
||||
val check = Curve25519Field.sub(Curve25519Field.sqr(x), x2)
|
||||
val checkPacked = Curve25519Field.pack25519(check)
|
||||
if (!checkPacked.all { it == 0.toByte() }) {
|
||||
x = Curve25519Field.mul(x, Curve25519Field.I)
|
||||
val check2 = Curve25519Field.sub(Curve25519Field.sqr(x), x2)
|
||||
val check2Packed = Curve25519Field.pack25519(check2)
|
||||
if (!check2Packed.all { it == 0.toByte() }) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
val y = java.math.BigInteger(1, yBE)
|
||||
return java.security.spec.EdECPoint(xOdd, y)
|
||||
if (Curve25519Field.par25519(x) != ((s[31].toInt() shr 7) and 1)) {
|
||||
x = Curve25519Field.sub(Curve25519Field.GF0, x)
|
||||
}
|
||||
|
||||
x.copyInto(p[0])
|
||||
Curve25519Field.mul(p[0], p[1]).copyInto(p[3])
|
||||
return p
|
||||
}
|
||||
|
||||
private val L =
|
||||
longArrayOf(
|
||||
0xED,
|
||||
0xD3,
|
||||
0xF5,
|
||||
0x5C,
|
||||
0x1A,
|
||||
0x63,
|
||||
0x12,
|
||||
0x58,
|
||||
0xD6,
|
||||
0x9C,
|
||||
0xF7,
|
||||
0xA2,
|
||||
0xDE,
|
||||
0xF9,
|
||||
0xDE,
|
||||
0x14,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0x10,
|
||||
)
|
||||
|
||||
private fun reduce(input: ByteArray): ByteArray {
|
||||
val x = LongArray(64)
|
||||
for (i in 0 until 64) x[i] = input[i].toLong() and 0xFF
|
||||
return modL(x)
|
||||
}
|
||||
|
||||
private fun modL(x: LongArray): ByteArray {
|
||||
for (i in 63 downTo 32) {
|
||||
var carry: Long = 0
|
||||
var j = i - 32
|
||||
val k = i - 12
|
||||
while (j < k) {
|
||||
x[j] += carry - 16 * x[i] * L[j - (i - 32)]
|
||||
carry = (x[j] + 128) shr 8
|
||||
x[j] -= carry shl 8
|
||||
j++
|
||||
}
|
||||
x[j] += carry
|
||||
x[i] = 0
|
||||
}
|
||||
|
||||
var carry: Long = 0
|
||||
for (j in 0 until 32) {
|
||||
x[j] += carry - (x[31] shr 4) * L[j]
|
||||
carry = x[j] shr 8
|
||||
x[j] = x[j] and 0xFF
|
||||
}
|
||||
for (j in 0 until 32) {
|
||||
x[j] -= carry * L[j]
|
||||
}
|
||||
|
||||
val r = ByteArray(32)
|
||||
for (i in 0 until 32) {
|
||||
x[i + 1] += x[i] shr 8
|
||||
r[i] = (x[i] and 0xFF).toByte()
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
private fun isCanonicalScalar(s: ByteArray): Boolean {
|
||||
var borrow: Long = 0
|
||||
for (i in 31 downTo 0) {
|
||||
val si = s[i].toLong() and 0xFF
|
||||
val li = L[i]
|
||||
if (si < li + borrow) return true
|
||||
if (si > li + borrow) return false
|
||||
borrow = 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
+66
-110
@@ -20,32 +20,22 @@
|
||||
*/
|
||||
package com.vitorpamplona.quartz.marmot.mls.crypto
|
||||
|
||||
import java.security.KeyFactory
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.spec.NamedParameterSpec
|
||||
import java.security.spec.XECPrivateKeySpec
|
||||
import java.security.spec.XECPublicKeySpec
|
||||
import javax.crypto.KeyAgreement
|
||||
import com.vitorpamplona.quartz.utils.RandomInstance
|
||||
|
||||
/**
|
||||
* JVM/Android X25519 implementation using java.security XDH.
|
||||
* JVM/Android X25519 implementation using pure Kotlin field arithmetic.
|
||||
*
|
||||
* Requires Java 11+ or Android API 31+.
|
||||
* This implementation is used to avoid issues with Android's KeyStore provider,
|
||||
* which requires platform-specific initialization that breaks standard JCA usage.
|
||||
*
|
||||
* Key format: raw 32-byte Curve25519 keys (little-endian per RFC 7748).
|
||||
*/
|
||||
actual object X25519 {
|
||||
private const val ALGORITHM = "X25519"
|
||||
private const val KEY_LENGTH = 32
|
||||
|
||||
actual fun generateKeyPair(): X25519KeyPair {
|
||||
val kpg = KeyPairGenerator.getInstance("XDH")
|
||||
kpg.initialize(NamedParameterSpec(ALGORITHM))
|
||||
val kp = kpg.generateKeyPair()
|
||||
|
||||
val publicKey = extractPublicKeyBytes(kp.public as java.security.interfaces.XECPublicKey)
|
||||
val privateKey = extractPrivateKeyBytes(kp.private as java.security.interfaces.XECPrivateKey)
|
||||
|
||||
val privateKey = RandomInstance.bytes(KEY_LENGTH)
|
||||
val publicKey = publicFromPrivate(privateKey)
|
||||
return X25519KeyPair(privateKey, publicKey)
|
||||
}
|
||||
|
||||
@@ -56,116 +46,82 @@ actual object X25519 {
|
||||
require(privateKey.size == KEY_LENGTH) { "Private key must be 32 bytes" }
|
||||
require(publicKey.size == KEY_LENGTH) { "Public key must be 32 bytes" }
|
||||
|
||||
val kf = KeyFactory.getInstance("XDH")
|
||||
val result = scalarmult(privateKey, publicKey)
|
||||
|
||||
// Build JCA private key from raw bytes
|
||||
val privKeySpec = XECPrivateKeySpec(NamedParameterSpec(ALGORITHM), privateKey)
|
||||
val jcaPrivateKey = kf.generatePrivate(privKeySpec)
|
||||
|
||||
// Build JCA public key from raw bytes (u-coordinate as BigInteger)
|
||||
val u = bytesToBigInteger(publicKey)
|
||||
val pubKeySpec = XECPublicKeySpec(NamedParameterSpec(ALGORITHM), u)
|
||||
val jcaPublicKey = kf.generatePublic(pubKeySpec)
|
||||
|
||||
val ka = KeyAgreement.getInstance("XDH")
|
||||
ka.init(jcaPrivateKey)
|
||||
ka.doPhase(jcaPublicKey, true)
|
||||
|
||||
val secret = ka.generateSecret()
|
||||
|
||||
// Check for small-subgroup attack (RFC 9180 Section 4.1)
|
||||
require(!secret.all { it == 0.toByte() }) {
|
||||
require(!result.all { it == 0.toByte() }) {
|
||||
"DH produced all-zero shared secret (possible small-subgroup attack)"
|
||||
}
|
||||
|
||||
// XDH returns big-endian, X25519 shared secret is 32 bytes
|
||||
// Pad or trim to KEY_LENGTH
|
||||
return if (secret.size == KEY_LENGTH) {
|
||||
secret
|
||||
} else if (secret.size < KEY_LENGTH) {
|
||||
ByteArray(KEY_LENGTH - secret.size) + secret
|
||||
} else {
|
||||
secret.copyOfRange(secret.size - KEY_LENGTH, secret.size)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
actual fun publicFromPrivate(privateKey: ByteArray): ByteArray {
|
||||
require(privateKey.size == KEY_LENGTH) { "Private key must be 32 bytes" }
|
||||
|
||||
val kf = KeyFactory.getInstance("XDH")
|
||||
val privKeySpec = XECPrivateKeySpec(NamedParameterSpec(ALGORITHM), privateKey)
|
||||
val jcaPrivateKey = kf.generatePrivate(privKeySpec)
|
||||
|
||||
// Generate key pair from the same seed to get the public key
|
||||
val kpg = KeyPairGenerator.getInstance("XDH")
|
||||
kpg.initialize(NamedParameterSpec(ALGORITHM))
|
||||
|
||||
// Re-derive: use the private key to create a keypair, then extract public
|
||||
// Unfortunately JCA doesn't have a direct way to do this, so we use key factory
|
||||
// The JCA will compute the public key when creating the key pair
|
||||
// Workaround: generate a dummy keypair and use DH with basepoint
|
||||
// Actually, XECPrivateKeySpec can be used with KeyFactory to get the paired public key
|
||||
val kp = kf.generatePrivate(privKeySpec)
|
||||
// Get the public key by computing DH with the basepoint (9)
|
||||
val basepoint = ByteArray(KEY_LENGTH)
|
||||
basepoint[0] = 9
|
||||
|
||||
return dh(privateKey, basepoint)
|
||||
return scalarmult(privateKey, basepoint)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract 32-byte raw X25519 public key from JCA XECPublicKey.
|
||||
* The u-coordinate is stored as a BigInteger, we convert to little-endian bytes.
|
||||
* X25519 scalar multiplication via Montgomery ladder (RFC 7748).
|
||||
*
|
||||
* Computes [n]P on Curve25519 in Montgomery form.
|
||||
* Based on the TweetNaCl algorithm by Bernstein et al.
|
||||
*/
|
||||
private fun extractPublicKeyBytes(pubKey: java.security.interfaces.XECPublicKey): ByteArray {
|
||||
val u = pubKey.u
|
||||
return bigIntegerToBytes(u)
|
||||
}
|
||||
private fun scalarmult(
|
||||
n: ByteArray,
|
||||
p: ByteArray,
|
||||
): ByteArray {
|
||||
val z = n.copyOf()
|
||||
// Clamp scalar per RFC 7748 Section 5
|
||||
z[0] = (z[0].toInt() and 248).toByte()
|
||||
z[31] = ((z[31].toInt() and 127) or 64).toByte()
|
||||
|
||||
/**
|
||||
* Extract raw scalar bytes from JCA XECPrivateKey.
|
||||
* Scalar is in little-endian byte order per JCA spec.
|
||||
*/
|
||||
private fun extractPrivateKeyBytes(privKey: java.security.interfaces.XECPrivateKey): ByteArray {
|
||||
val scalar = privKey.scalar.orElseThrow { IllegalStateException("No scalar in private key") }
|
||||
return if (scalar.size == KEY_LENGTH) {
|
||||
scalar
|
||||
} else if (scalar.size < KEY_LENGTH) {
|
||||
// Pad with trailing zeros (little-endian: high-order bytes at end)
|
||||
val result = ByteArray(KEY_LENGTH)
|
||||
scalar.copyInto(result, 0)
|
||||
result
|
||||
} else {
|
||||
// Take only the first KEY_LENGTH bytes (little-endian low-order bytes)
|
||||
scalar.copyOfRange(0, KEY_LENGTH)
|
||||
val x = Curve25519Field.unpack25519(p)
|
||||
val a = Curve25519Field.GF1.copyOf()
|
||||
val b = x.copyOf()
|
||||
val c = Curve25519Field.GF0.copyOf()
|
||||
val d = Curve25519Field.GF1.copyOf()
|
||||
|
||||
for (i in 254 downTo 0) {
|
||||
val r = ((z[i shr 3].toLong() shr (i and 7)) and 1)
|
||||
Curve25519Field.sel25519(a, b, r)
|
||||
Curve25519Field.sel25519(c, d, r)
|
||||
|
||||
val e = Curve25519Field.add(a, c)
|
||||
val aMc = Curve25519Field.sub(a, c)
|
||||
val f = Curve25519Field.add(b, d)
|
||||
val bMd = Curve25519Field.sub(b, d)
|
||||
|
||||
val dd = Curve25519Field.sqr(e)
|
||||
val ff = Curve25519Field.sqr(aMc)
|
||||
val da = Curve25519Field.mul(bMd, e)
|
||||
val cb = Curve25519Field.mul(f, aMc)
|
||||
|
||||
val ePrime = Curve25519Field.add(da, cb)
|
||||
val aPrime = Curve25519Field.sub(da, cb)
|
||||
|
||||
val bNew = Curve25519Field.sqr(ePrime)
|
||||
val aSqr = Curve25519Field.sqr(aPrime)
|
||||
val dNew = Curve25519Field.mul(aSqr, x)
|
||||
|
||||
val aNew = Curve25519Field.mul(dd, ff)
|
||||
val cc = Curve25519Field.sub(dd, ff)
|
||||
val tmp = Curve25519Field.mul(cc, Curve25519Field.A24)
|
||||
val ddPlusTmp = Curve25519Field.add(dd, tmp)
|
||||
val cNew = Curve25519Field.mul(cc, ddPlusTmp)
|
||||
|
||||
aNew.copyInto(a)
|
||||
bNew.copyInto(b)
|
||||
cNew.copyInto(c)
|
||||
dNew.copyInto(d)
|
||||
|
||||
Curve25519Field.sel25519(a, b, r)
|
||||
Curve25519Field.sel25519(c, d, r)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert little-endian 32-byte X25519 key to BigInteger for JCA.
|
||||
* RFC 7748 uses little-endian, JCA uses BigInteger (unsigned).
|
||||
*/
|
||||
private fun bytesToBigInteger(bytes: ByteArray): java.math.BigInteger {
|
||||
// Reverse to big-endian and create unsigned BigInteger
|
||||
val be = ByteArray(bytes.size + 1) // prepend 0 for positive
|
||||
for (i in bytes.indices) {
|
||||
be[bytes.size - i] = bytes[i]
|
||||
}
|
||||
return java.math.BigInteger(be)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert BigInteger (u-coordinate) to 32-byte little-endian.
|
||||
*/
|
||||
private fun bigIntegerToBytes(bi: java.math.BigInteger): ByteArray {
|
||||
val beBytes = bi.toByteArray()
|
||||
val result = ByteArray(KEY_LENGTH)
|
||||
// BigInteger is big-endian, possibly with a leading zero sign byte.
|
||||
// Reverse into little-endian, taking at most KEY_LENGTH bytes from the low end.
|
||||
val bytesToCopy = minOf(beBytes.size, KEY_LENGTH)
|
||||
for (i in 0 until bytesToCopy) {
|
||||
result[i] = beBytes[beBytes.size - 1 - i]
|
||||
}
|
||||
return result
|
||||
val invC = Curve25519Field.inv25519(c)
|
||||
val result = Curve25519Field.mul(a, invC)
|
||||
return Curve25519Field.pack25519(result)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -153,7 +153,7 @@ class MlsConformanceTest {
|
||||
// Must contain external_pub extension (type 0x0003)
|
||||
val externalPubExt = decoded.extensions.find { it.extensionType == 0x0003 }
|
||||
assertTrue(externalPubExt != null, "GroupInfo must contain external_pub extension")
|
||||
assertEquals(32, externalPubExt!!.extensionData.size, "external_pub must be 32 bytes (X25519)")
|
||||
assertEquals(32, externalPubExt.extensionData.size, "external_pub must be 32 bytes (X25519)")
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -410,7 +410,7 @@ class MlsConformanceTest {
|
||||
|
||||
assertTrue(result.commitBytes.isNotEmpty(), "Commit bytes must not be empty")
|
||||
assertTrue(result.welcomeBytes != null, "Add commit must produce Welcome")
|
||||
assertTrue(result.welcomeBytes!!.isNotEmpty(), "Welcome bytes must not be empty")
|
||||
assertTrue(result.welcomeBytes.isNotEmpty(), "Welcome bytes must not be empty")
|
||||
|
||||
// Commit should be deserializable
|
||||
val commit =
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ class MlsGroupLifecycleTest {
|
||||
assertEquals(2, alice.memberCount)
|
||||
|
||||
// Bob processes the Welcome to join the group
|
||||
val bob = MlsGroup.processWelcome(result.welcomeBytes!!, bobBundle)
|
||||
val bob = MlsGroup.processWelcome(result.welcomeBytes, bobBundle)
|
||||
assertEquals(1L, bob.epoch, "Bob should be at same epoch as Alice after Welcome")
|
||||
assertEquals(2, bob.memberCount, "Bob should see 2 members")
|
||||
}
|
||||
|
||||
+1
-1
@@ -173,7 +173,7 @@ class MlsGroupTest {
|
||||
|
||||
assertTrue(result.commitBytes.isNotEmpty(), "Commit bytes should not be empty")
|
||||
assertNotNull(result.welcomeBytes, "Welcome bytes should be present for Add")
|
||||
assertTrue(result.welcomeBytes!!.isNotEmpty(), "Welcome bytes should not be empty")
|
||||
assertTrue(result.welcomeBytes.isNotEmpty(), "Welcome bytes should not be empty")
|
||||
|
||||
// After commit, epoch should advance
|
||||
assertEquals(1L, aliceGroup.epoch)
|
||||
|
||||
Reference in New Issue
Block a user