Fix generating X25519 shared secret.

X25519 shared secrets now match those generated by libsodium.

New tests show that shared secrets are the same using either set
of private+public key combinations.

Changed proxy private key generation from using simple SHA256
of shared secret to using SHA256(shared secret + both public keys).

Added a temporary "BouncyCastle25519" shim class to provide missing
key conversion from Ed25519 to X25519.
This commit is contained in:
catbref
2019-05-21 17:06:01 +01:00
parent 4279ad0673
commit 0259702df2
6 changed files with 243 additions and 40 deletions

View File

@@ -5,16 +5,18 @@ import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters;
import org.bouncycastle.crypto.params.X25519PrivateKeyParameters;
import org.bouncycastle.crypto.params.X25519PublicKeyParameters;
import org.bouncycastle.math.ec.rfc8032.Ed25519;
import org.qora.crypto.BouncyCastle25519;
import org.qora.crypto.Crypto;
import org.qora.repository.Repository;
// TODO change "seed" to "privateKey" to keep things consistent
import com.google.common.primitives.Bytes;
public class PrivateKeyAccount extends PublicKeyAccount {
private static final int SIGNATURE_LENGTH = 64;
private static final int SHARED_SECRET_LENGTH = 32;
private final byte[] seed;
private final byte[] privateKey;
private final Ed25519PrivateKeyParameters edPrivateKeyParams;
/**
@@ -36,12 +38,12 @@ public class PrivateKeyAccount extends PublicKeyAccount {
private PrivateKeyAccount(Repository repository, Ed25519PrivateKeyParameters edPrivateKeyParams, Ed25519PublicKeyParameters edPublicKeyParams) {
super(repository, edPublicKeyParams);
this.seed = edPrivateKeyParams.getEncoded();
this.privateKey = edPrivateKeyParams.getEncoded();
this.edPrivateKeyParams = edPrivateKeyParams;
}
public byte[] getSeed() {
return this.seed;
public byte[] getPrivateKey() {
return this.privateKey;
}
public byte[] sign(byte[] message) {
@@ -53,8 +55,11 @@ public class PrivateKeyAccount extends PublicKeyAccount {
}
public byte[] getSharedSecret(byte[] publicKey) {
X25519PrivateKeyParameters xPrivateKeyParams = new X25519PrivateKeyParameters(this.seed, 0);
X25519PublicKeyParameters xPublicKeyParams = new X25519PublicKeyParameters(publicKey, 0);
byte[] x25519PrivateKey = BouncyCastle25519.toX25519PrivateKey(this.privateKey);
X25519PrivateKeyParameters xPrivateKeyParams = new X25519PrivateKeyParameters(x25519PrivateKey, 0);
byte[] x25519PublicKey = BouncyCastle25519.toX25519PublicKey(publicKey);
X25519PublicKeyParameters xPublicKeyParams = new X25519PublicKeyParameters(x25519PublicKey, 0);
byte[] sharedSecret = new byte[SHARED_SECRET_LENGTH];
xPrivateKeyParams.generateSecret(xPublicKeyParams, sharedSecret, 0);
@@ -62,4 +67,11 @@ public class PrivateKeyAccount extends PublicKeyAccount {
return sharedSecret;
}
public byte[] getProxyPrivateKey(byte[] publicKey) {
byte[] sharedSecret = this.getSharedSecret(publicKey);
byte[] proxyHashData = Bytes.concat(sharedSecret, this.getPublicKey(), publicKey);
return Crypto.digest(proxyHashData);
}
}

View File

@@ -338,11 +338,10 @@ public class AddressesResource {
if (generatorKey.length != Transformer.PRIVATE_KEY_LENGTH || recipientKey.length != Transformer.PRIVATE_KEY_LENGTH)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
PrivateKeyAccount generator = new PrivateKeyAccount(null, generatorKey);
byte[] sharedSecret = generator.getSharedSecret(recipientKey);
byte[] proxySeed = Crypto.digest(sharedSecret);
byte[] proxyPrivateKey = generator.getProxyPrivateKey(recipientKey);
return Base58.encode(proxySeed);
return Base58.encode(proxyPrivateKey);
} catch (NumberFormatException e) {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY, e);
}

View File

@@ -0,0 +1,97 @@
package org.qora.crypto;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import org.bouncycastle.crypto.Digest;
import org.bouncycastle.math.ec.rfc7748.X25519;
import org.bouncycastle.math.ec.rfc7748.X25519Field;
import org.bouncycastle.math.ec.rfc8032.Ed25519;
/** Additions to BouncyCastle providing Ed25519 to X25519 key conversion. */
public class BouncyCastle25519 {
private static final Class<?> pointExtClass;
private static final Constructor<?> pointExtCtor;
private static final Method decodePointVarMethod;
private static final Field yField;
static {
try {
Class<?> ed25519Class = Ed25519.class;
pointExtClass = Arrays.stream(ed25519Class.getDeclaredClasses()).filter(clazz -> clazz.getSimpleName().equals("PointExt")).findFirst().get();
if (pointExtClass == null)
throw new ClassNotFoundException("Can't locate PointExt inner class inside Ed25519");
decodePointVarMethod = ed25519Class.getDeclaredMethod("decodePointVar", byte[].class, int.class, boolean.class, pointExtClass);
decodePointVarMethod.setAccessible(true);
pointExtCtor = pointExtClass.getDeclaredConstructors()[0];
pointExtCtor.setAccessible(true);
yField = pointExtClass.getDeclaredField("y");
yField.setAccessible(true);
} catch (NoSuchMethodException | SecurityException | IllegalArgumentException | NoSuchFieldException | ClassNotFoundException e) {
throw new RuntimeException("Can't initialize BouncyCastle25519 shim", e);
}
}
private static int[] obtainYFromPublicKey(byte[] ed25519PublicKey) {
try {
Object pA = pointExtCtor.newInstance();
Boolean result = (Boolean) decodePointVarMethod.invoke(null, ed25519PublicKey, 0, true, pA);
if (result == null || !result)
return null;
return (int[]) yField.get(pA);
} catch (SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
throw new RuntimeException("Can't reflect into BouncyCastle", e);
}
}
public static byte[] toX25519PublicKey(byte[] ed25519PublicKey) {
int[] one = new int[X25519Field.SIZE];
X25519Field.one(one);
int[] y = obtainYFromPublicKey(ed25519PublicKey);
int[] oneMinusY = new int[X25519Field.SIZE];
X25519Field.sub(one, y, oneMinusY);
int[] onePlusY = new int[X25519Field.SIZE];
X25519Field.add(one, y, onePlusY);
int[] oneMinusYInverted = new int[X25519Field.SIZE];
X25519Field.inv(oneMinusY, oneMinusYInverted);
int[] u = new int[X25519Field.SIZE];
X25519Field.mul(onePlusY, oneMinusYInverted, u);
byte[] x25519PublicKey = new byte[X25519.SCALAR_SIZE];
X25519Field.encode(u, x25519PublicKey, 0);
return x25519PublicKey;
}
public static byte[] toX25519PrivateKey(byte[] ed25519PrivateKey) {
Digest d = Ed25519.createPrehash();
byte[] h = new byte[d.getDigestSize()];
d.update(ed25519PrivateKey, 0, ed25519PrivateKey.length);
d.doFinal(h, 0);
byte[] s = new byte[X25519.SCALAR_SIZE];
System.arraycopy(h, 0, s, 0, X25519.SCALAR_SIZE);
s[0] &= 0xF8;
s[X25519.SCALAR_SIZE - 1] &= 0x7F;
s[X25519.SCALAR_SIZE - 1] |= 0x40;
return s;
}
}