diff --git a/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/Bn256.kt b/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/Bn256.kt new file mode 100644 index 0000000..f2679a9 --- /dev/null +++ b/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/Bn256.kt @@ -0,0 +1,145 @@ +@file:OptIn(ExperimentalUnsignedTypes::class) + +package net.agorise.library.crypto.bn256 + +import java.math.BigInteger +import java.security.SecureRandom + +/* + * Package bn256 implements a particular bilinear group at the 128-bit security + * level. + * + * Bilinear groups are the basis of many of the new cryptographic protocols that + * have been proposed over the past decade. They consist of a triplet of groups + * (G₁, G₂ and GT) such that there exists a function e(g₁ˣ,g₂ʸ)=gTˣʸ (where gₓ + * is a generator of the respective group). That function is called a pairing + * function. + * + * This package specifically implements the Optimal Ate pairing over a 256-bit + * Barreto-Naehrig curve as described in + * http://cryptojedi.org/papers/dclxvi-20100714.pdf. Its output is not + * compatible with the implementation described in that paper, as different + * parameters are chosen. + * + * (This package previously claimed to operate at a 128-bit security level. + * However, recent improvements in attacks mean that is no longer true. See + * https://moderncrypto.org/mail-archive/curves/2016/000740.html.) + * + * Ported over from https://github.com/deroproject/derohe/blob/main/cryptography/bn256/bn256.go + */ + +fun randomK(random: SecureRandom): BigInteger { + var k: BigInteger + do { + k = BigInteger(Constants.Order.bitLength(), random).mod(Constants.Order) + } while (k.signum() != 1) + return k +} + +/* + * G1 is an abstract cyclic group. The zero value is suitable for use as the + * output of an operation, but cannot be used as an input. + */ +class G1(var p: CurvePoint = CurvePoint()) { + override fun toString(): String { + return "bn256.G1: $p" + } + + // ScalarBaseMult sets e to g*k where g is the generator of the group and then returns e. + fun scalarBaseMult(k: BigInteger): G1 { + this.p.mul(CurvePoint.curveGen, k) + return this + } + + // ScalarMult sets e to a*k and then returns e. + fun scalarMult(a: G1, k: BigInteger): G1 { + this.p.mul(a.p, k) + return this + } + + // Add sets e to a+b and then returns e. + fun add(a: G1, b: G1): G1 { + this.p.add(a.p, b.p) + return this + } + + // Neg sets e to -a and then returns e. + fun neg(a: G1): G1 { + this.p.neg(a.p) + return this + } + + // Set sets e to a and then returns e. + fun set(a: G1): G1 { + p.set(a.p) + return this + } + + // Marshal converts e to a byte slice. + fun marshal(): UByteArray { + // Each value is a 256-bit number. + val numBytes = 256 / 8 + + this.p.makeAffine() + val ret = UByteArray(numBytes * 2) + if (this.p.isInfinity()) { + return ret + } + val temp = GfP() + + montDecode(temp, this.p.x) + temp.marshal(ret) + montDecode(temp, this.p.y) + temp.marshal(ret, numBytes) + + return ret + } + + // Unmarshal sets e to the result of converting the output of Marshal back into + // a group element and then returns e. + fun unmarshal(m: UByteArray): Result { + // Each value is a 256-bit number. + val numBytes = 256 / 8 + if (m.size < 2 * numBytes) { + return Result.failure(IllegalArgumentException("bn256: not enough data")) + } + // Unmarshal the points and check their caps + this.p.x = GfP() + this.p.y = GfP() + + try { + this.p.x.unmarshal(m) + this.p.y.unmarshal(m, numBytes) + } catch (e: Throwable) { + return Result.failure(e) + } + + // Encode into Montgomery form and ensure it's on the curve + montEncode(this.p.x, this.p.x) + montEncode(this.p.y, this.p.y) + + val zero = GfP() + if (this.p.x == zero && this.p.y == zero) { + // This is the point at infinity. + this.p.y = GfP.newGfP(1) + this.p.z = GfP() + this.p.t = GfP() + } else { + this.p.z = GfP.newGfP(1) + this.p.t = GfP.newGfP(1) + + if (this.p.isOnCurve().not()) { + return Result.failure(IllegalArgumentException("bn256: malformed point")) + } + } + return Result.success(m.copyOfRange(2 * numBytes, m.size)) + } + + companion object { + // RandomG1 returns x and g₁ˣ where x is a random, non-zero number read from r. + fun randomG1(random: SecureRandom): Pair { + val k = randomK(random) + return Pair(k, G1().scalarBaseMult(k)) + } + } +} diff --git a/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/CurvePoint.kt b/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/CurvePoint.kt index 3376823..daab5d3 100644 --- a/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/CurvePoint.kt +++ b/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/CurvePoint.kt @@ -7,20 +7,23 @@ import java.math.BigInteger * form and t=z² when valid. G₁ is the set of points of this curve on GF(p). * Ported over from https://github.com/deroproject/derohe/blob/main/cryptography/bn256/curve.go */ -internal class CurvePoint( - private var x: GfP, - private var y: GfP, - private var z: GfP, - private var t: GfP, +class CurvePoint( + var x: GfP, + var y: GfP, + var z: GfP, + var t: GfP, ) { - private constructor() : this(GfP(0UL), GfP(0UL), GfP(0UL), GfP(0UL)) + constructor() : this(GfP(), GfP(), GfP(), GfP()) override fun toString(): String { - makeAffine() + // Create a copy of the object to avoid modifying the original one + val copy = CurvePoint(this.x.copy(), this.y.copy(), this.z.copy(), this.t.copy()) + + copy.makeAffine() val x = GfP() val y = GfP() - montDecode(x, this.x) - montDecode(y, this.y) + montDecode(x, copy.x) + montDecode(y, copy.y) return "($x, $y)" } @@ -223,10 +226,10 @@ internal class CurvePoint( // curveGen is the generator of G₁. internal val curveGen = CurvePoint( - GfP.newGfP(1), - GfP.newGfP(2), - GfP.newGfP(1), - GfP.newGfP(1), + x = GfP.newGfP(1), + y = GfP.newGfP(2), + z = GfP.newGfP(1), + t = GfP.newGfP(1), ) } } diff --git a/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/GfP.kt b/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/GfP.kt index 2277b65..936033a 100644 --- a/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/GfP.kt +++ b/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/GfP.kt @@ -18,7 +18,9 @@ class GfP(val data: ULongArray) { } override fun toString(): String { - return String.format("%016x %016x %016x %016x", data[3].toLong(), data[2].toLong(), data[1].toLong(), data[0].toLong()) + // Easier for testing + return data.joinToString(separator = " ", transform = { it.toString(10) }) +// return data.reversed().joinToString(separator = " ", transform = { it.toString(16) }) } override fun equals(other: Any?): Boolean { @@ -54,19 +56,19 @@ class GfP(val data: ULongArray) { set(sum) } - fun marshal(out: ByteArray) { + fun marshal(out: UByteArray, offset: Int = 0) { for (w in 0 until 4) { for (b in 0 until 8) { - out[8 * w + b] = (data[3 - w] shr (56 - 8 * b)).toByte() + out[offset + 8 * w + b] = (data[3 - w] shr (56 - 8 * b)).toUByte() } } } - fun unmarshal(`in`: ByteArray) { + fun unmarshal(`in`: UByteArray, offset: Int = 0) { for (w in 0 until 4) { data[3 - w] = 0UL for (b in 0 until 8) { - data[3 - w] += `in`[8 * w + b].toULong() shl (56 - 8 * b) + data[3 - w] += `in`[offset + 8 * w + b].toULong() shl (56 - 8 * b) } } diff --git a/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/GfPGeneric.kt b/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/GfPGeneric.kt index 6218b43..bf800fd 100644 --- a/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/GfPGeneric.kt +++ b/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/GfPGeneric.kt @@ -78,7 +78,7 @@ fun gfpMul(c: GfP, a: GfP, b: GfP) { val ti = t[i] val zi = Ti + ti + carry T[i] = zi - carry = (Ti and ti or (Ti or ti) and zi.inv()) shr 63 + carry = ((Ti and ti) or ((Ti or ti) and zi.inv())) shr 63 } c.data[0] = T[4] diff --git a/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/Lattice.kt b/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/Lattice.kt index 1167d3d..d2bc943 100644 --- a/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/Lattice.kt +++ b/library/crypto/src/main/kotlin/net/agorise/library/crypto/bn256/Lattice.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalUnsignedTypes::class) + package net.agorise.library.crypto.bn256 import java.math.BigInteger @@ -52,14 +54,15 @@ internal class Lattice( } } - fun multi(scalar: BigInteger): ByteArray { + fun multi(scalar: BigInteger): UByteArray { val decomp = decompose(scalar) val maxLen = decomp.maxOf { it.bitLength() } - val out = ByteArray(maxLen) + val out = UByteArray(maxLen) for ((j, x) in decomp.withIndex()) { for (i in 0 until maxLen) { - out[i] = (out[i] + (if (x.testBit(i)) 1 else 0) shl j).toByte() + val bitValue = if (x.testBit(i)) 1u else 0u + out[i] = (out[i] + (bitValue shl j)).toUByte() } } return out diff --git a/library/crypto/src/test/kotlin/net/agorise/library/crypto/bn256/Bn256Test.kt b/library/crypto/src/test/kotlin/net/agorise/library/crypto/bn256/Bn256Test.kt new file mode 100644 index 0000000..c68a608 --- /dev/null +++ b/library/crypto/src/test/kotlin/net/agorise/library/crypto/bn256/Bn256Test.kt @@ -0,0 +1,24 @@ +@file:OptIn(ExperimentalUnsignedTypes::class) + +package net.agorise.library.crypto.bn256 + +import java.security.SecureRandom +import kotlin.test.Test +import kotlin.test.assertEquals + +class Bn256Test { + @Test + fun `given a randomly generated G1 - when marshal and unmarshal are called - then result is correct`() { + val random = SecureRandom() + val (_, ga) = G1.randomG1(random) + val ma = ga.marshal() + + val gb = G1() + gb.unmarshal(ma) + + val mb = gb.marshal() + + println(ma.toList()) + assertEquals(ma.toList(), mb.toList(), "bytes are different") + } +} diff --git a/library/crypto/src/test/kotlin/net/agorise/library/crypto/bn256/GfPTest.kt b/library/crypto/src/test/kotlin/net/agorise/library/crypto/bn256/GfPTest.kt index b4e3132..aba71bf 100644 --- a/library/crypto/src/test/kotlin/net/agorise/library/crypto/bn256/GfPTest.kt +++ b/library/crypto/src/test/kotlin/net/agorise/library/crypto/bn256/GfPTest.kt @@ -51,4 +51,16 @@ class GfPTest { assertEquals(expectedGfP.data.toList(), actualGfP.data.toList()) } + + @Test + fun `given two dec GfP values - when gfpMul is called - then result is correct`() { + val aGfP = GfP(ulongArrayOf(15230403791020821917UL, 754611498739239741UL, 7381016538464732716UL, 1011752739694698287UL)) + val bGfP = GfP(ulongArrayOf(4792239181621121553UL, 12809537390334529128UL, 17379557665190828237UL, 110455090769457822UL)) + val expectedGfP = GfP(ulongArrayOf(4792239181621121553UL, 12809537390334529128UL, 17379557665190828237UL, 110455090769457822UL)) + val actualGfP = GfP() + + gfpMul(actualGfP, aGfP, bGfP) + + assertEquals(expectedGfP.data.toList(), actualGfP.data.toList()) + } }