package net.i2p.crypto.eddsa.math; import net.i2p.crypto.eddsa.Utils; import net.i2p.crypto.eddsa.math.ed25519.*; import net.i2p.crypto.eddsa.spec.*; import org.hamcrest.core.IsEqual; import org.junit.*; import java.math.BigInteger; import java.security.SecureRandom; /** * Utility class to help with calculations. */ public class MathUtils { private static final int[] exponents = {0, 26, 26 + 25, 2*26 + 25, 2*26 + 2*25, 3*26 + 2*25, 3*26 + 3*25, 4*26 + 3*25, 4*26 + 4*25, 5*26 + 4*25}; private static final SecureRandom random = new SecureRandom(); private static final EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("ed25519-sha-512"); private static final Curve curve = ed25519.getCurve(); private static final BigInteger d = new BigInteger("-121665").multiply(new BigInteger("121666").modInverse(getQ())); private static final BigInteger groupOrder = BigInteger.ONE.shiftLeft(252).add(new BigInteger("27742317777372353535851937790883648493")); /** * Gets q = 2^255 - 19 as BigInteger. */ public static BigInteger getQ() { return new BigInteger("7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffed", 16); } /** * Gets group order = 2^252 + 27742317777372353535851937790883648493 as BigInteger. */ public static BigInteger getGroupOrder() { return groupOrder; } /** * Gets the underlying finite field with q=2^255 - 19 elements. * * @return The finite field. */ public static Field getField() { return new Field( 256, // b Utils.hexToBytes("edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"), // q new Ed25519LittleEndianEncoding()); } // region field element /** * Converts a 2^25.5 bit representation to a BigInteger. * <p> * Value: 2^exponents[0] * t[0] + 2^exponents[1] * t[1] + ... + 2^exponents[9] * t[9] * * @param t The 2^25.5 bit representation. * @return The BigInteger. */ public static BigInteger toBigInteger(final int[] t) { BigInteger b = BigInteger.ZERO; for (int i=0; i<10; i++) { b = b.add(BigInteger.ONE.multiply(BigInteger.valueOf(t[i])).shiftLeft(exponents[i])); } return b; } /** * Converts a 2^8 bit representation to a BigInteger. * <p> * Value: bytes[0] + 2^8 * bytes[1] + ... * * @param bytes The 2^8 bit representation. * @return The BigInteger. */ public static BigInteger toBigInteger(final byte[] bytes) { BigInteger b = BigInteger.ZERO; for (int i=0; i<bytes.length; i++) { b = b.add(BigInteger.ONE.multiply(BigInteger.valueOf(bytes[i] & 0xff)).shiftLeft(i * 8)); } return b; } /** * Converts a field element to a BigInteger. * * @param f The field element. * @return The BigInteger. */ public static BigInteger toBigInteger(final FieldElement f) { return toBigInteger(f.toByteArray()); } /** * Converts a BigInteger to a field element. * * @param b The BigInteger. * @return The field element. */ public static FieldElement toFieldElement(final BigInteger b) { return getField().getEncoding().decode(toByteArray(b)); } /** * Converts a BigInteger to a little endian 32 byte representation. * * @param b The BigInteger. * @return The 32 byte representation. */ public static byte[] toByteArray(final BigInteger b) { if (b.compareTo(BigInteger.ONE.shiftLeft(256)) >= 0) { throw new RuntimeException("only numbers < 2^256 are allowed"); } final byte[] bytes = new byte[32]; final byte[] original = b.toByteArray(); // Although b < 2^256, original can have length > 32 with some bytes set to 0. final int offset = original.length > 32? original.length - 32 : 0; for (int i=0; i<original.length - offset; i++) { bytes[original.length - i - offset - 1] = original[i + offset]; } return bytes; } /** * Reduces an integer in 2^8 bit representation modulo the group order and returns the result. * * @param bytes The integer in 2^8 bit representation. * @return The mod group order reduced integer. */ public static byte[] reduceModGroupOrder(final byte[] bytes) { final BigInteger b = toBigInteger(bytes).mod(groupOrder); return toByteArray(b); } /** * Calculates (a * b + c) mod group order and returns the result. * <p> * a, b and c are given in 2^8 bit representation. * * @param a The first integer. * @param b The second integer. * @param c The third integer. * @return The mod group order reduced result. */ public static byte[] multiplyAndAddModGroupOrder(final byte[] a, final byte[] b, final byte[] c) { final BigInteger result = toBigInteger(a).multiply(toBigInteger(b)).add(toBigInteger(c)).mod(groupOrder); return toByteArray(result); } public static byte[] getRandomByteArray(final int length) { final byte[] bytes = new byte[length]; random.nextBytes(bytes); return bytes; } /** * Gets a random field element where |t[i]| <= 2^24 for 0 <= i <= 9. * * @return The field element. */ public static FieldElement getRandomFieldElement() { final int[] t = new int[10]; for (int j=0; j<10; j++) { t[j] = random.nextInt(1 << 25) - (1 << 24); } return new Ed25519FieldElement(getField(), t); } // endregion // region group element /** * Gets a random group element in P3 representation. * * @return The group element. */ public static GroupElement getRandomGroupElement() { final byte[] bytes = new byte[32]; while (true) { try { random.nextBytes(bytes); return new GroupElement(curve, bytes); } catch (IllegalArgumentException e) { // Will fail in about 87.5%, so try again. } } } /** * Creates a group element from a byte array. * <p> * Bit 0 to 254 are the affine y-coordinate, bit 255 is the sign of the affine x-coordinate. * * @param bytes the byte array. * @return The group element. */ public static GroupElement toGroupElement(final byte[] bytes) { final boolean shouldBeNegative = (bytes[31] >> 7) != 0; bytes[31] &= 0x7f; final BigInteger y = MathUtils.toBigInteger(bytes); // x = sign(x) * sqrt((y^2 - 1) / (d * y^2 + 1)) final BigInteger u = y.multiply(y).subtract(BigInteger.ONE).mod(getQ()); final BigInteger v = d.multiply(y).multiply(y).add(BigInteger.ONE).mod(getQ()); final BigInteger tmp = u.multiply(v.pow(7)).modPow(BigInteger.ONE.shiftLeft(252).subtract(new BigInteger("3")), getQ()).mod(getQ()); BigInteger x = tmp.multiply(u).multiply(v.pow(3)).mod(getQ()); if (!v.multiply(x).multiply(x).subtract(u).mod(getQ()).equals(BigInteger.ZERO)) { if (!v.multiply(x).multiply(x).add(u).mod(getQ()).equals(BigInteger.ZERO)) { throw new IllegalArgumentException("not a valid GroupElement"); } x = x.multiply(toBigInteger(curve.getI())).mod(getQ()); } final boolean isNegative = x.mod(new BigInteger("2")).equals(BigInteger.ONE); if ((shouldBeNegative && !isNegative) || (!shouldBeNegative && isNegative)) { x = x.negate().mod(getQ()); } return GroupElement.p3(curve, toFieldElement(x), toFieldElement(y), getField().ONE, toFieldElement(x.multiply(y).mod(getQ()))); } /** * Converts a group element from one representation to another. * This method is a helper used to test various methods in GroupElement. * * @param g The group element. * @param repr The desired representation. * @return The same group element in the new representation. */ public static GroupElement toRepresentation(final GroupElement g, final GroupElement.Representation repr) { BigInteger x; BigInteger y; final BigInteger gX = toBigInteger(g.getX().toByteArray()); final BigInteger gY = toBigInteger(g.getY().toByteArray()); final BigInteger gZ = toBigInteger(g.getZ().toByteArray()); final BigInteger gT = null == g.getT()? null : toBigInteger(g.getT().toByteArray()); // Switch to affine coordinates. switch (g.getRepresentation()) { case P2: case P3: x = gX.multiply(gZ.modInverse(getQ())).mod(getQ()); y = gY.multiply(gZ.modInverse(getQ())).mod(getQ()); break; case P1P1: x = gX.multiply(gZ.modInverse(getQ())).mod(getQ()); y = gY.multiply(gT.modInverse(getQ())).mod(getQ()); break; case CACHED: x = gX.subtract(gY).multiply(gZ.multiply(new BigInteger("2")).modInverse(getQ())).mod(getQ()); y = gX.add(gY).multiply(gZ.multiply(new BigInteger("2")).modInverse(getQ())).mod(getQ()); break; case PRECOMP: x = gX.subtract(gY).multiply(new BigInteger("2").modInverse(getQ())).mod(getQ()); y = gX.add(gY).multiply(new BigInteger("2").modInverse(getQ())).mod(getQ()); break; default: throw new UnsupportedOperationException(); } // Now back to the desired representation. switch (repr) { case P2: return GroupElement.p2( curve, toFieldElement(x), toFieldElement(y), getField().ONE); case P3: return GroupElement.p3( curve, toFieldElement(x), toFieldElement(y), getField().ONE, toFieldElement(x.multiply(y).mod(getQ()))); case P1P1: return GroupElement.p1p1( curve, toFieldElement(x), toFieldElement(y), getField().ONE, getField().ONE); case CACHED: return GroupElement.cached( curve, toFieldElement(y.add(x).mod(getQ())), toFieldElement(y.subtract(x).mod(getQ())), getField().ONE, toFieldElement(d.multiply(new BigInteger("2")).multiply(x).multiply(y).mod(getQ()))); case PRECOMP: return GroupElement.precomp( curve, toFieldElement(y.add(x).mod(getQ())), toFieldElement(y.subtract(x).mod(getQ())), toFieldElement(d.multiply(new BigInteger("2")).multiply(x).multiply(y).mod(getQ()))); default: throw new UnsupportedOperationException(); } } /** * Adds two group elements and returns the result in P3 representation. * It uses BigInteger arithmetic and the affine representation. * This method is a helper used to test the projective group addition formulas in GroupElement. * * @param g1 The first group element. * @param g2 The second group element. * @return The result of the addition. */ public static GroupElement addGroupElements(final GroupElement g1, final GroupElement g2) { // Relying on a special representation of the group elements. if ((g1.getRepresentation() != GroupElement.Representation.P2 && g1.getRepresentation() != GroupElement.Representation.P3) || (g2.getRepresentation() != GroupElement.Representation.P2 && g2.getRepresentation() != GroupElement.Representation.P3)) { throw new IllegalArgumentException("g1 and g2 must have representation P2 or P3"); } // Projective coordinates final BigInteger g1X = toBigInteger(g1.getX().toByteArray()); final BigInteger g1Y = toBigInteger(g1.getY().toByteArray()); final BigInteger g1Z = toBigInteger(g1.getZ().toByteArray()); final BigInteger g2X = toBigInteger(g2.getX().toByteArray()); final BigInteger g2Y = toBigInteger(g2.getY().toByteArray()); final BigInteger g2Z = toBigInteger(g2.getZ().toByteArray()); // Affine coordinates final BigInteger g1x = g1X.multiply(g1Z.modInverse(getQ())).mod(getQ()); final BigInteger g1y = g1Y.multiply(g1Z.modInverse(getQ())).mod(getQ()); final BigInteger g2x = g2X.multiply(g2Z.modInverse(getQ())).mod(getQ()); final BigInteger g2y = g2Y.multiply(g2Z.modInverse(getQ())).mod(getQ()); // Addition formula for affine coordinates. The formula is complete in our case. // // (x3, y3) = (x1, y1) + (x2, y2) where // // x3 = (x1 * y2 + x2 * y1) / (1 + d * x1 * x2 * y1 * y2) and // y3 = (x1 * x2 + y1 * y2) / (1 - d * x1 * x2 * y1 * y2) and // d = -121665/121666 BigInteger dx1x2y1y2 = d.multiply(g1x).multiply(g2x).multiply(g1y).multiply(g2y).mod(getQ()); BigInteger x3 = g1x.multiply(g2y).add(g2x.multiply(g1y)) .multiply(BigInteger.ONE.add(dx1x2y1y2).modInverse(getQ())).mod(getQ()); BigInteger y3 = g1x.multiply(g2x).add(g1y.multiply(g2y)) .multiply(BigInteger.ONE.subtract(dx1x2y1y2).modInverse(getQ())).mod(getQ()); BigInteger t3 = x3.multiply(y3).mod(getQ()); return GroupElement.p3(g1.getCurve(), toFieldElement(x3), toFieldElement(y3), getField().ONE, toFieldElement(t3)); } /** * Doubles a group element and returns the result in P3 representation. * It uses BigInteger arithmetic and the affine representation. * This method is a helper used to test the projective group doubling formula in GroupElement. * * @param g The group element. * @return g+g. */ public static GroupElement doubleGroupElement(final GroupElement g) { return addGroupElements(g, g); } /** * Scalar multiply the group element by the field element. * * @param g The group element. * @param f The field element. * @return The resulting group element. */ public static GroupElement scalarMultiplyGroupElement(final GroupElement g, final FieldElement f) { final byte[] bytes = f.toByteArray(); GroupElement h = curve.getZero(GroupElement.Representation.P3); for (int i=254; i>=0; i--) { h = doubleGroupElement(h); if (Utils.bit(bytes, i) == 1) { h = addGroupElements(h, g); } } return h; } /** * Calculates f1 * g1 + f2 * g2. * * @param g1 The first group element. * @param f1 The first multiplier. * @param g2 The second group element. * @param f2 The second multiplier. * @return The resulting group element. */ public static GroupElement doubleScalarMultiplyGroupElements( final GroupElement g1, final FieldElement f1, final GroupElement g2, final FieldElement f2) { final GroupElement h1 = scalarMultiplyGroupElement(g1, f1); final GroupElement h2 = scalarMultiplyGroupElement(g2, f2); return addGroupElements(h1, h2); } /** * Negates a group element. * * @param g The group element. * @return The negated group element. */ public static GroupElement negateGroupElement(final GroupElement g) { if (g.getRepresentation() != GroupElement.Representation.P3) { throw new IllegalArgumentException("g must have representation P3"); } return GroupElement.p3(g.getCurve(), g.getX().negate(), g.getY(), g.getZ(), g.getT().negate()); } // Start TODO BR: Remove when finished! @Test public void mathUtilsWorkAsExpected() { final GroupElement neutral = GroupElement.p3(curve, curve.getField().ZERO, curve.getField().ONE, curve.getField().ONE, curve.getField().ZERO); for (int i=0; i<1000; i++) { final GroupElement g = getRandomGroupElement(); // Act: final GroupElement h1 = addGroupElements(g, neutral); final GroupElement h2 = addGroupElements(neutral, g); // Assert: Assert.assertThat(g, IsEqual.equalTo(h1)); Assert.assertThat(g, IsEqual.equalTo(h2)); } for (int i=0; i<1000; i++) { GroupElement g = getRandomGroupElement(); // P3 -> P2. GroupElement h = toRepresentation(g, GroupElement.Representation.P2); Assert.assertThat(h, IsEqual.equalTo(g)); // P3 -> P1P1. h = toRepresentation(g, GroupElement.Representation.P1P1); Assert.assertThat(g, IsEqual.equalTo(h)); // P3 -> CACHED. h = toRepresentation(g, GroupElement.Representation.CACHED); Assert.assertThat(h, IsEqual.equalTo(g)); // P3 -> P2 -> P3. g = toRepresentation(g, GroupElement.Representation.P2); h = toRepresentation(g, GroupElement.Representation.P3); Assert.assertThat(g, IsEqual.equalTo(h)); // P3 -> P2 -> P1P1. g = toRepresentation(g, GroupElement.Representation.P2); h = toRepresentation(g, GroupElement.Representation.P1P1); Assert.assertThat(g, IsEqual.equalTo(h)); } for (int i=0; i<10; i++) { // Arrange: final GroupElement g = MathUtils.getRandomGroupElement(); // Act: final GroupElement h = MathUtils.scalarMultiplyGroupElement(g, curve.getField().ZERO); // Assert: Assert.assertThat(curve.getZero(GroupElement.Representation.P3), IsEqual.equalTo(h)); } } // End TODO BR: Remove when finished! }