Reputation: 589
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
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
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