Java компактное представление ECC PublicKey
java.security.PublicKey#getEncoded()
возвращает представление ключа в X509, которое в случае ECC добавляет много служебной информации по сравнению с необработанными значениями ECC.
Я хотел бы иметь возможность преобразовывать PublicKey в байтовый массив (и наоборот) в наиболее компактном представлении (то есть как можно меньшем байтовом блоке).
KeyType (ECC) и тип конкретной кривой известны заранее, поэтому информацию о них не нужно кодировать.
Решение может использовать Java API, BouncyCastle или любой другой пользовательский код / библиотеку (если лицензия не требует открытого исходного кода, в котором он будет использоваться).
6 ответов
Эта функциональность также присутствует в Bouncy Castle, но я покажу, как пройти через это, используя только Java на случай, если кому-то это понадобится:
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.util.Arrays;
public class Curvy {
private static final byte UNCOMPRESSED_POINT_INDICATOR = 0x04;
public static ECPublicKey fromUncompressedPoint(
final byte[] uncompressedPoint, final ECParameterSpec params)
throws Exception {
int offset = 0;
if (uncompressedPoint[offset++] != UNCOMPRESSED_POINT_INDICATOR) {
throw new IllegalArgumentException(
"Invalid uncompressedPoint encoding, no uncompressed point indicator");
}
int keySizeBytes = (params.getOrder().bitLength() + Byte.SIZE - 1)
/ Byte.SIZE;
if (uncompressedPoint.length != 1 + 2 * keySizeBytes) {
throw new IllegalArgumentException(
"Invalid uncompressedPoint encoding, not the correct size");
}
final BigInteger x = new BigInteger(1, Arrays.copyOfRange(
uncompressedPoint, offset, offset + keySizeBytes));
offset += keySizeBytes;
final BigInteger y = new BigInteger(1, Arrays.copyOfRange(
uncompressedPoint, offset, offset + keySizeBytes));
final ECPoint w = new ECPoint(x, y);
final ECPublicKeySpec ecPublicKeySpec = new ECPublicKeySpec(w, params);
final KeyFactory keyFactory = KeyFactory.getInstance("EC");
return (ECPublicKey) keyFactory.generatePublic(ecPublicKeySpec);
}
public static byte[] toUncompressedPoint(final ECPublicKey publicKey) {
int keySizeBytes = (publicKey.getParams().getOrder().bitLength() + Byte.SIZE - 1)
/ Byte.SIZE;
final byte[] uncompressedPoint = new byte[1 + 2 * keySizeBytes];
int offset = 0;
uncompressedPoint[offset++] = 0x04;
final byte[] x = publicKey.getW().getAffineX().toByteArray();
if (x.length <= keySizeBytes) {
System.arraycopy(x, 0, uncompressedPoint, offset + keySizeBytes
- x.length, x.length);
} else if (x.length == keySizeBytes + 1 && x[0] == 0) {
System.arraycopy(x, 1, uncompressedPoint, offset, keySizeBytes);
} else {
throw new IllegalStateException("x value is too large");
}
offset += keySizeBytes;
final byte[] y = publicKey.getW().getAffineY().toByteArray();
if (y.length <= keySizeBytes) {
System.arraycopy(y, 0, uncompressedPoint, offset + keySizeBytes
- y.length, y.length);
} else if (y.length == keySizeBytes + 1 && y[0] == 0) {
System.arraycopy(y, 1, uncompressedPoint, offset, keySizeBytes);
} else {
throw new IllegalStateException("y value is too large");
}
return uncompressedPoint;
}
public static void main(final String[] args) throws Exception {
// just for testing
final KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
kpg.initialize(163);
for (int i = 0; i < 1_000; i++) {
final KeyPair ecKeyPair = kpg.generateKeyPair();
final ECPublicKey ecPublicKey = (ECPublicKey) ecKeyPair.getPublic();
final ECPublicKey retrievedEcPublicKey = fromUncompressedPoint(
toUncompressedPoint(ecPublicKey), ecPublicKey.getParams());
if (!Arrays.equals(retrievedEcPublicKey.getEncoded(),
ecPublicKey.getEncoded())) {
throw new IllegalArgumentException("Whoops");
}
}
}
}
Попытка создать несжатое представление в Java почти убила меня! Жаль, что я нашел бы это (особенно превосходный ответ Мартена Бодьюса) ранее. Я хотел бы указать на проблему в ответе и предложить улучшение:
if (x.length <= keySizeBytes) {
System.arraycopy(x, 0, uncompressedPoint, offset + keySizeBytes
- x.length, x.length);
} else if (x.length == keySizeBytes + 1 && x[0] == 0) {
System.arraycopy(x, 1, uncompressedPoint, offset, keySizeBytes);
} else {
throw new IllegalStateException("x value is too large");
}
Этот уродливый бит необходим из-за способа BigInteger
выплевывает представления массива байтов: "Массив будет содержать минимальное количество байтов, необходимое для представления этогоBigInteger
, включая, по крайней мере, один бит знака "( toByteArray javadoc). Это означает, что.) если старший бит x
или же y
установлен, 0x00
будет добавлен к массиву и б.) ведущий 0x00
будет урезан. Первая ветка занимается обрезкой 0x00
и второй с префендом 0x00
,
"Обрезанный ведущий ноль" приводит к проблеме в коде, определяющем ожидаемую длину x
а также y
:
int keySizeBytes = (publicKey.getParams().getOrder().bitLength() + Byte.SIZE - 1)
/ Byte.SIZE;
Если order
кривой имеет ведущий 0x00
оно усекается и не рассматривается bitLength
, Полученная длина ключа слишком коротка. Невероятно запутанный (но правильный?) Способ добраться до битов p
было бы:
int keySizeBits = publicKey.getParams().getCurve().getField().getFieldSize();
int keySizeBytes = (keySizeBits + 7) >>> 3;
(The +7
должен компенсировать битовые длины, которые не являются степенями 2.)
Эта проблема затрагивает как минимум одну кривую, поставляемую со стандартным JCA (X9_62_c2tnb431r1
) который имеет порядок с ведущим нулем:
000340340340340 34034034034034034
034034034034034 0340340340323c313
fab50589703b5ec 68d3587fec60d161c
c149c1ad4a91
Вот подход BouncyCastle, который я использовал для распаковки открытого ключа:
public static byte[] extractData(final @NonNull PublicKey publicKey) {
final SubjectPublicKeyInfo subjectPublicKeyInfo =
SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
final byte[] encodedBytes = subjectPublicKeyInfo.getPublicKeyData().getBytes();
final byte[] publicKeyData = new byte[encodedBytes.length - 1];
System.arraycopy(encodedBytes, 1, publicKeyData, 0, encodedBytes.length - 1);
return publicKeyData;
}
С BouncyCastle, ECPoint.getEncoded(true)
возвращает сжатое представление точки. В основном, аффинная X-координата со знаковым битом для аффинного Y.
Однострочный подход BC:
EC5Util.convertPoint(ecPublicKey.getParams(), ecPublicKey.getW()).getEncoded(true);
ecPublicKey
это JavaECPublicKey
.
Примечание. Использование сжатых точек в 2022 году совершенно нормально. Срок действия забавных патентов истек. См. раздел Криптография StackExchange .
В 2021 году просто воспользуйтесь библиотекой Tink
public static byte[] pointEncode(EllipticCurves.CurveType curveType,
EllipticCurves.PointFormatType format,
ECPoint point)
throws GeneralSecurityException