alexward1230
alexward1230

Reputation: 589

Derive Shared Secret From ECDH with existing foreign public key

Im converting a method from nodeSJ to Java, but im having trouble getting it to work. Im stuck trying to calculate a derived shared secret.

Hoping someone can catch what im doing wrong porting over the nodeJS to Java.

NodeJS code:

 //the public_key param here is from a different device. 
 sign: function(public_key)
    {
        //dummy values 
        var PRE_SALT_VALUE = 'f0f0f0f0f0';
        var POST_SALT_VALUE = '0101010101';

        const crypto = require('crypto');
        var sha512 = crypto.createHash("sha512");

        var EC = require('elliptic').ec;
        var ec = new EC('p256');

        // Generate keys
        var key1 = ec.genKeyPair(); //key1 is gen before pub key
        var key2 = ec.keyFromPublic(public_key, 'hex') //pub key gen from saved cert

        var derived_secret = key1.derive(key2.getPublic()); 
        var derived_secret = Buffer.from(derived_secret.toString(16), 'hex')

        var public_key_client = key1.getPublic('hex') 

        var pre_salt = Buffer.from(PRE_SALT_VALUE, 'hex')
        var post_salt = Buffer.from(POST_SALT_VALUE, 'hex')

        derived_secret = Buffer.from(pre_salt.toString('hex')+derived_secret.toString('hex')+post_salt.toString('hex'), 'hex') // finalyze shared secret 
        // Hash shared secret
        var sha = sha512.update(derived_secret);
        derived_secret = sha.digest();

        return {
            public_key: public_key_client.toString('hex').slice(2), //anyone know why we drop the first byte here?
            secret: derived_secret.toString('hex')  
        }
    }

In progress Java Code:

        //load cert from pem string (passed in from file), foreign cert
        ByteArrayInputStream input = new ByteArrayInputStream(pem);
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        Certificate cert = cf.generateCertificate(input);
        X509Certificate x509Cert = (X509Certificate) cert;

        // get pub key from cert
        PublicKey publicKeyForSignature = x509Cert.getPublicKey();

        // Generate ephemeral ECDH keypair KEY1
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
        kpg.initialize(256);
        KeyPair kp1 = kpg.generateKeyPair();
        byte[] ourPk = kp1.getPublic().getEncoded(); //use this later

        //load KEY2 from others public key
        KeyFactory kf = KeyFactory.getInstance("EC");
        X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(publicKeyForSignature.getEncoded());
        PublicKey otherPublicKey = kf.generatePublic(pkSpec);

        // Perform key agreement
        KeyAgreement ka = KeyAgreement.getInstance("ECDH");
        ka.init(kp1.getPrivate());
        ka.doPhase(otherPublicKey, true);

        // Read shared secret
        byte[] sharedSecret = ka.generateSecret();

        // Derive a key from the shared secret and both salt keys
        MessageDigest hash = MessageDigest.getInstance("SHA-512");
        hash.update(Util.PRE_SALT_VALUE);
        hash.update(sharedSecret);
        hash.update(Util.POST_SALT_VALUE);

        byte[] derivedKey = hash.digest();

        ... etc, derivedKey = secret returned in JS method, ourPk = public_key returned in JS method. 

One thing i notice is that the public/private keys generated from nodejs vs java are different sizes? 65 bytes in node an 91 bytes in java. No idea why that would happen.

What stands out as wrong here?

Thanx

Edit:

So basically, I just need to know how to do this in Java

 var EC = require('elliptic').ec;
 var ec = new EC('p256');
 // Generate keys
 var key1 = ec.genKeyPair();
 var key2 = ec.keyFromPublic(public_key, 'hex') //pub key from foreign device     
 var derived_secret = key1.derive(key2.getPublic());  

Upvotes: 0

Views: 2441

Answers (1)

Stephan Schlecht
Stephan Schlecht

Reputation: 27126

Like already mentioned in the comments, to be able to use the shared secret between Java and Node you need to convert the keys accordingly.

For the key conversion code from this two fine stackoverflow answers can be used:

https://stackoverflow.com/a/57209308/2331445

https://stackoverflow.com/a/36033552

Test

To get a complete test case, one can write a Java program that generates a DER public key, converts it into an uncompressed EC key (65 bytes) and outputs it to the console. It then reads the other PK from the console, converts it to a public key, and prints out the shared secret.

The Node code uses the PK from the Java program, determines the shared secret and a public key. This public key can then be passed to the Java program via copy/paste, which is still waiting for the input.

The Java program finally determines the shared secret and prints it out.

If both shared secrets have the same value, we know it works.

Java

import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.asn1.ASN1Sequence;
import org.bouncycastle.asn1.DERBitString;
import org.bouncycastle.asn1.DERSequence;
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.ECPointUtil;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec;
import org.bouncycastle.jce.spec.ECNamedCurveSpec;

import javax.crypto.KeyAgreement;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.security.*;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;

public class Main {

    public static byte[] ecKeyBytesFromDERKey(byte[] ourPk) {
        ASN1Sequence sequence = DERSequence.getInstance(ourPk);
        DERBitString subjectPublicKey = (DERBitString) sequence.getObjectAt(1);
        return subjectPublicKey.getBytes();
    }

    private static PublicKey publicKeyFromEC(byte[] ecKeyByteArray) throws NoSuchProviderException, NoSuchAlgorithmException, InvalidKeySpecException {
        KeyFactory kf = KeyFactory.getInstance("EC", "BC");
        ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("secp256r1");
        ECNamedCurveSpec params = new ECNamedCurveSpec("secp256r1", spec.getCurve(), spec.getG(), spec.getN());
        ECPoint publicPoint = ECPointUtil.decodePoint(params.getCurve(), ecKeyByteArray);
        ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(publicPoint, params);
        return kf.generatePublic(pubKeySpec);
    }

    public static void main(String[] args) throws Exception {
        Security.addProvider(new BouncyCastleProvider());
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC");
        kpg.initialize(256);
        KeyPair kp = kpg.generateKeyPair();
        byte[] ourPk = kp.getPublic().getEncoded();
        byte[] ecPublicKey = ecKeyBytesFromDERKey(ourPk);
        System.out.println("our ec public key (65 bytes): " + Hex.encodeHexString(ecPublicKey));

        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        System.out.println("other public key (65 bytes): ");
        String input = br.readLine();
        br.close();

        byte[] otherPk = Hex.decodeHex(input);
        PublicKey otherPublicKey = publicKeyFromEC(otherPk);

        KeyAgreement ka = KeyAgreement.getInstance("ECDH");
        ka.init(kp.getPrivate());
        ka.doPhase(otherPublicKey, true);

        byte[] sharedSecret = ka.generateSecret();
        System.out.println("Shared secret: " + Hex.encodeHexString(sharedSecret));
    }
}

Node

One change is required in your node program:

In the line

public_key: public_key_client.toString('hex').slice(2), //anyone know why we drop the first byte here?

the .slice(2) needs to be removed:

public_key: public_key_client.toString('hex'),

because it removes the first byte (which is hex 04) needed to indicate that it is an uncompressed key.

So just using the public key from the Java program (which will be different for each run) the Node part could look like this:

var publickey = Buffer.from("<public key from java>", 'hex');
var derived = sign(publickey);
console.log(derived);

Test

shared secret test

In the upper area you see the Java program and in the lower area the output of the Node program. The shared secret is the same.

Upvotes: 2

Related Questions