Fixes key creation for groups.

This commit is contained in:
Vitor Pamplona
2026-04-15 12:04:38 -04:00
parent e9996f474b
commit db18954d91
9 changed files with 387 additions and 223 deletions
+3 -2
View File
@@ -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 {
/**
@@ -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
}
}
@@ -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)
}
}
@@ -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 =
@@ -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")
}
@@ -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)