indutny / elliptic

Fast Elliptic Curve Cryptography in plain javascript
1.7k stars 376 forks source link

Public key hex differs between Java and Javascript #122

Closed andrewparks closed 7 years ago

andrewparks commented 7 years ago

To reproduce:

In Java, generate Curve25519 ECDH keypair:

  public static String toHex(byte[] data) {
    StringBuilder sb = new StringBuilder();
    for (byte b: data) sb.append(String.format("%02x", b&0xff));
    return sb.toString();
  }

  public static void main(String[] args) throws Exception {
    Security.addProvider(new BouncyCastleProvider());
    ECParameterSpec ecsp = ECNamedCurveTable.getParameterSpec("curve25519");
    KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDH", "BC");
    kpg.initialize(ecsp, new SecureRandom());
    KeyPair processorKeyPair = kpg.genKeyPair();
    System.out.println("private key: " + ((ECPrivateKey) processorKeyPair.getPrivate()).getS().toString(16));
    System.out.println("public key:  " + toHex(((ECPublicKey) processorKeyPair.getPublic()).getQ().getEncoded(true)));
  }

The output produced is:

private key: 843f2237a9597939c62512ab405e1d92838044b757e1395dbc8e3deeb4e9f9c
public key:  02244cf6ac4da4e85c2188d6d2870230156a5d95d00df5692ae9283eae44a620fc

Then, run the following code in Javascript:

  var ec = new EC('curve25519');
  var existingPrivKeyHex = '843f2237a9597939c62512ab405e1d92838044b757e1395dbc8e3deeb4e9f9c';
  var existingPrivKey = ec.keyFromPrivate(existingPrivKeyHex, 'hex');
  console.log('private:  ' + existingPrivKey.getPrivate('hex'));
  console.log('public:   ' + existingPrivKey.getPublic('hex'));

The output produced is:

private:  0843f2237a9597939c62512ab405e1d92838044b757e1395dbc8e3deeb4e9f9c
public:   79a24c01a2fa3db176de2c27dc57856abfb2eb25634abe803e7d940399f8fc98

Note that the public key generated in Java is not the same as the public key generated from Javascript

fanatid commented 7 years ago

@andrewparks sorry, but can you give full example of Java source which can be pasted to compilejava.net (I not familiar with Java unfortunately)

andrewparks commented 7 years ago

Thanks, unfortunately there is a dependency on a jar file, so it won't work at compilejava.net. If you have the JDK installed, you can run it by doing the following:

First, place this full source code in a file called ECDHKeygen.java

import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;

import java.security.*;
import java.security.interfaces.ECPrivateKey;

public class ECDHKeygen {

  public static String toHex(byte[] data) {
    StringBuilder sb = new StringBuilder();
    for (byte b: data) sb.append(String.format("%02x", b&0xff));
    return sb.toString();
  }

  public static void main(String[] args) throws Exception {
    Security.addProvider(new BouncyCastleProvider());
    ECParameterSpec ecsp = ECNamedCurveTable.getParameterSpec("curve25519");
    KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDH", "BC");
    kpg.initialize(ecsp, new SecureRandom());
    KeyPair processorKeyPair = kpg.genKeyPair();
    System.out.println("private key: " + ((ECPrivateKey) processorKeyPair.getPrivate()).getS().toString(16));
    System.out.println("public key:  " + toHex(((ECPublicKey) processorKeyPair.getPublic()).getQ().getEncoded(true)));
  }

}

Then, to execute it run:

wget https://www.bouncycastle.org/download/bcprov-jdk15on-156.jar
javac ECDHKeygen.java -extdirs .
java -cp "bcprov-jdk15on-156.jar:." ECDHKeygen
andrewparks commented 7 years ago

By the way, as a sanity check, here is Java code that derives the public key from the private key, to check that the public key really is G multiplied by the private key:

import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.math.ec.ECPoint;

import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.Security;
import java.security.interfaces.ECPrivateKey;

public class ECDHPrivateToPublicKey {

  public static String toHex(byte[] data) {
    StringBuilder sb = new StringBuilder();
    for (byte b: data) sb.append(String.format("%02x", b&0xff));
    return sb.toString();
  }

  public static void main(String[] args) throws Exception {
    Security.addProvider(new BouncyCastleProvider());
    ECParameterSpec ecsp = ECNamedCurveTable.getParameterSpec("curve25519");
    KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDH", "BC");
    kpg.initialize(ecsp, new SecureRandom());
    KeyPair processorKeyPair = kpg.genKeyPair();
    System.out.println("private key: " + ((ECPrivateKey) processorKeyPair.getPrivate()).getS().toString(16));
    System.out.println("public key:  " + toHex(((ECPublicKey) processorKeyPair.getPublic()).getQ().getEncoded(true)));

    ECPoint publicKeyPoint = ecsp.getG().multiply(new BigInteger(((ECPrivateKey) processorKeyPair.getPrivate()).getS().toByteArray()));
    System.out.println("recreated public key: " + toHex(publicKeyPoint.getEncoded(true)));
  }

}

To run this, place the source in a file called ECDHPrivateToPublicKey.java and then do:

wget https://www.bouncycastle.org/download/bcprov-jdk15on-156.jar
javac ECDHPrivateToPublicKey.java -extdirs .
java -cp "bcprov-jdk15on-156.jar:." ECDHPrivateToPublicKey

Example output is:

private key: a8a677e2ebc857cf36e8634ca1731977737bc993b123e2ee68a6fb82e276bda
public key:  034b8acc13b97df5aec3d7a84959f1c4d6c6d19c3029cf6c62d28122c24eb5d537
recreated public key: 034b8acc13b97df5aec3d7a84959f1c4d6c6d19c3029cf6c62d28122c24eb5d537
andrewparks commented 7 years ago

I also tried printing out G in Java, and I get this:

(2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaad245a,20ae19a1b8a086b4e01edd2c7748d14c923d4d7e6d7c61b229e9c5a27eced3d9,1,2aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa984914a144)

In contrast, in Javascript, when I run the code:

  console.log(ec.g.x.toString(16));
  console.log(ec.g.z.toString(16));

I get the result:

9
1
andrewparks commented 7 years ago

This is the source code for the Java Curve25519 implementation:

 /*
             * NOTE: Curve25519 was specified in Montgomery form. Rewriting in Weierstrass form
             * involves substitution of variables, so the base-point x coordinate is 9 + (486662 / 3).
             * 
             * The Curve25519 paper doesn't say which of the two possible y values the base
             * point has. The choice here is guided by language in the Ed25519 paper.
             * 
             * (The other possible y value is 5F51E65E475F794B1FE122D388B72EB36DC2B28192839E4DD6163A5D81312C14) 
             */
            X9ECPoint G = new X9ECPoint(curve, Hex.decode("04"
                + "2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD245A"
                + "20AE19A1B8A086B4E01EDD2C7748D14C923D4D7E6D7C61B229E9C5A27ECED3D9"));

Source: https://github.com/bcgit/bc-java/blob/master/core/src/main/java/org/bouncycastle/crypto/ec/CustomNamedCurves.java

andrewparks commented 7 years ago

This may also be of interest:

this.a = fromBigInteger(new BigInteger(1,
            Hex.decode("2AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA984914A144")));
this.b = fromBigInteger(new BigInteger(1,
            Hex.decode("7B425ED097B425ED097B425ED097B425ED097B425ED097B4260B5E9C7710C864")));
this.order = new BigInteger(1, Hex.decode("1000000000000000000000000000000014DEF9DEA2F79CD65812631A5CF5D3ED"));
this.cofactor = BigInteger.valueOf(8);

Source: https://github.com/bcgit/bc-java/blob/ae63147936376e85e068c7b63373d4e930c3fe58/core/src/main/java/org/bouncycastle/math/ec/custom/djb/Curve25519.java

andrewparks commented 7 years ago

If it's helpful, this code can be used to test changing the curve parameters:

(Put in a file called ECDHKeygenAlternativeCurve.java)

import org.bouncycastle.jce.interfaces.ECPublicKey;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.math.ec.ECCurve;
import org.bouncycastle.util.encoders.Hex;

import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.Security;
import java.security.interfaces.ECPrivateKey;

public class ECDHKeygenAlternativeCurve {

  public static String toHex(byte[] data) {
    StringBuilder sb = new StringBuilder();
    for (byte b: data) sb.append(String.format("%02x", b&0xff));
    return sb.toString();
  }

  public static void main(String[] args) throws Exception {
    Security.addProvider(new BouncyCastleProvider());

    ECCurve curve = new ECCurve.Fp(
      new BigInteger("883423532389192164791648750360308885314476597252960362792450860609699839"), // q
      new BigInteger("7fffffffffffffffffffffff7fffffffffff8000000000007ffffffffffc", 16), // a
      new BigInteger("6b016c3bdcf18941d0d654921475ca71a9db2fb27d1d37796185c2942c0a", 16)); // b
    ECParameterSpec ecsp = new ECParameterSpec(
      curve,
      curve.decodePoint(Hex.decode("020ffa963cdca8816ccc33b8642bedf905c3d358573d3f27fbbd3b3cb9aaaf")), // G
      new BigInteger("883423532389192164791648750360308884807550341691627752275345424702807307")); // n
    KeyPairGenerator g = KeyPairGenerator.getInstance("ECDSA", "BC");
    g.initialize(ecsp, new SecureRandom());

    KeyPairGenerator kpg = KeyPairGenerator.getInstance("ECDH", "BC");
    kpg.initialize(ecsp, new SecureRandom());
    KeyPair processorKeyPair = kpg.genKeyPair();
    System.out.println("private key: " + ((ECPrivateKey) processorKeyPair.getPrivate()).getS().toString(16));
    System.out.println("public key:  " + toHex(((ECPublicKey) processorKeyPair.getPublic()).getQ().getEncoded(true)));
  }

}
andrewparks commented 7 years ago

Problem solved. I just saw this pull request which fixes everything: https://github.com/indutny/elliptic/pull/113/commits (i've tested it and it works)

andrewparks commented 7 years ago

Doh, problem not actually solved. The shared secrets between Java and Javascript implementations differ. Can you think of why this might be?

indutny commented 7 years ago

@andrewparks sad to hear this, may I ask you to post private/public keys and expected/actual shared secret?

andrewparks commented 7 years ago

I've rewritten the code and performed some more tests, and I've now confirmed the problem was actually fully solved by the pull request mentioned above. Everything is working perfectly with the shared secret, thanks for this fantastic project.

indutny commented 7 years ago

Glad it worked out in the end!