Skip to content

Commit

Permalink
Add experimental Elliptic Curves key support
Browse files Browse the repository at this point in the history
EC keys are always accepted, generation is disabled by default.

Fix contact display being stuck with empty lists.
  • Loading branch information
zapek committed Nov 17, 2024
1 parent a2bc5ca commit 0223e77
Show file tree
Hide file tree
Showing 9 changed files with 167 additions and 32 deletions.
50 changes: 50 additions & 0 deletions app/src/main/java/io/xeres/app/crypto/ec/Ed25519.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) 2024 by David Gerber - https://zapek.com
*
* This file is part of Xeres.
*
* Xeres is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Xeres is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xeres. If not, see <http://www.gnu.org/licenses/>.
*/

package io.xeres.app.crypto.ec;

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;

public final class Ed25519
{
private static final String KEY_ALGORITHM = "Ed25519";

private Ed25519()
{
throw new UnsupportedOperationException("Utility class");
}

public static KeyPair generateKeys(int size)
{
try
{
var keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM);

keyPairGenerator.initialize(size);

return keyPairGenerator.generateKeyPair();
}
catch (NoSuchAlgorithmException e)
{
throw new IllegalArgumentException("Algorithm not supported");
}
}
}
64 changes: 47 additions & 17 deletions app/src/main/java/io/xeres/app/crypto/pgp/PGP.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,23 @@
import org.bouncycastle.bcpg.PublicKeyPacket;
import org.bouncycastle.bcpg.SignaturePacket;
import org.bouncycastle.openpgp.*;
import org.bouncycastle.openpgp.operator.PGPContentSignerBuilder;
import org.bouncycastle.openpgp.operator.jcajce.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.SignatureException;
import java.util.Date;
import java.util.List;

import static org.bouncycastle.bcpg.HashAlgorithmTags.SHA1;
import static org.bouncycastle.bcpg.HashAlgorithmTags.SHA512;
import static io.xeres.common.Features.EXPERIMENTAL_EC;
import static org.bouncycastle.bcpg.HashAlgorithmTags.*;
import static org.bouncycastle.bcpg.PublicKeyAlgorithmTags.DSA;
import static org.bouncycastle.bcpg.PublicKeyAlgorithmTags.Ed25519;
import static org.bouncycastle.bcpg.SymmetricKeyAlgorithmTags.AES_128;
import static org.bouncycastle.openpgp.PGPPublicKey.RSA_GENERAL;
import static org.bouncycastle.openpgp.PGPSignature.BINARY_DOCUMENT;
Expand Down Expand Up @@ -189,22 +192,42 @@ public static PGPPublicKey getPGPPublicKey(byte[] data) throws InvalidKeyExcepti
*/
public static PGPSecretKey generateSecretKey(String id, String suffix, int size) throws PGPException
{
var keyPair = RSA.generateKeys(size);
KeyPair keyPair;

PGPKeyPair pgpKeyPair = new JcaPGPKeyPair(PublicKeyPacket.VERSION_4, RSA_GENERAL, keyPair, new Date());
if (EXPERIMENTAL_EC)
{
keyPair = io.xeres.app.crypto.ec.Ed25519.generateKeys(size);
}
else
{
keyPair = RSA.generateKeys(size);
}

PGPKeyPair pgpKeyPair = new JcaPGPKeyPair(EXPERIMENTAL_EC ? PublicKeyPacket.VERSION_6 : PublicKeyPacket.VERSION_4, EXPERIMENTAL_EC ? Ed25519 : RSA_GENERAL, keyPair, new Date());

return encryptKeyPair(pgpKeyPair, suffix != null ? (id + " " + suffix) : id);
}

public static PGPSecretKey encryptKeyPair(PGPKeyPair pgpKeyPair, String id) throws PGPException
{
var sha1Calc = new JcaPGPDigestCalculatorProviderBuilder().build().get(SHA1);
var shaCalc = new JcaPGPDigestCalculatorProviderBuilder().build().get(SHA1);
var signer = new JcaPGPContentSignerBuilder(pgpKeyPair.getPublicKey().getAlgorithm(), SHA256);
var encryptor = new JcePBESecretKeyEncryptorBuilder(AES_128, shaCalc).setSecureRandom(SecureRandomUtils.getGenerator()).build("".toCharArray());

return new PGPSecretKey(DEFAULT_CERTIFICATION, pgpKeyPair, id, sha1Calc, null, null,
new JcaPGPContentSignerBuilder(pgpKeyPair.getPublicKey().getAlgorithm(), SHA256),
new JcePBESecretKeyEncryptorBuilder(AES_128, sha1Calc)
.setSecureRandom(SecureRandomUtils.getGenerator())
.build("".toCharArray()));
return new PGPSecretKey(pgpKeyPair.getPrivateKey(), certifiedPublicKey(pgpKeyPair, id, signer), shaCalc, true, encryptor);
}

private static PGPPublicKey certifiedPublicKey(PGPKeyPair keyPair, String id, PGPContentSignerBuilder certificationSignerBuilder) throws PGPException
{
var signatureGenerator = new PGPSignatureGenerator(certificationSignerBuilder, keyPair.getPublicKey(), EXPERIMENTAL_EC ? SignaturePacket.VERSION_6 : SignaturePacket.VERSION_4);

signatureGenerator.init(DEFAULT_CERTIFICATION, keyPair.getPrivateKey());

signatureGenerator.setHashedSubpackets(null);
signatureGenerator.setUnhashedSubpackets(null);

var certification = signatureGenerator.generateCertification(id, keyPair.getPublicKey());
return PGPPublicKey.addCertification(keyPair.getPublicKey(), id, certification);
}

/**
Expand All @@ -227,7 +250,7 @@ public static void sign(PGPSecretKey pgpSecretKey, InputStream in, OutputStream
var pgpPrivateKey = pgpSecretKey.extractPrivateKey(new JcePBESecretKeyDecryptorBuilder()
.build("".toCharArray()));

var signatureGenerator = new PGPSignatureGenerator(new JcaPGPContentSignerBuilder(pgpSecretKey.getPublicKey().getAlgorithm(), SHA256), pgpSecretKey.getPublicKey(), SignaturePacket.VERSION_4);
var signatureGenerator = new PGPSignatureGenerator(new JcaPGPContentSignerBuilder(pgpSecretKey.getPublicKey().getAlgorithm(), SHA256), pgpSecretKey.getPublicKey(), EXPERIMENTAL_EC ? SignaturePacket.VERSION_6 : SignaturePacket.VERSION_4);

signatureGenerator.init(BINARY_DOCUMENT, pgpPrivateKey);

Expand Down Expand Up @@ -303,17 +326,17 @@ private static PGPSignature getSignature(byte[] signature) throws SignatureExcep
throw new SignatureException("Signature is not of BINARY_DOCUMENT (" + pgpSignature.getSignatureType() + ")");
}

if (pgpSignature.getVersion() != 4)
if (pgpSignature.getVersion() != 4 && pgpSignature.getVersion() != 6)
{
throw new SignatureException("Signature is not PGP version 4 (" + pgpSignature.getVersion() + ")");
throw new SignatureException("Signature is not PGP version 4 or 6 (" + pgpSignature.getVersion() + ")");
}

if (!List.of(RSA_GENERAL, 3 /* RSA_SIGN */, DSA).contains(pgpSignature.getKeyAlgorithm()))
if (!List.of(RSA_GENERAL, 3 /* RSA_SIGN */, DSA, Ed25519).contains(pgpSignature.getKeyAlgorithm()))
{
throw new SignatureException("Signature key algorithm is not of RSA or DSA (" + pgpSignature.getSignatureType() + ")");
throw new SignatureException("Signature key algorithm is not of RSA, DSA or Ed25519 (" + pgpSignature.getSignatureType() + ")");
}

if (!List.of(SHA1, SHA256, SHA512).contains(pgpSignature.getHashAlgorithm()))
if (!List.of(SHA1, SHA256, SHA384, SHA512).contains(pgpSignature.getHashAlgorithm()))
{
throw new SignatureException("Signature hash algorithm is not of SHA family (" + pgpSignature.getHashAlgorithm() + ")");
}
Expand All @@ -328,7 +351,14 @@ private static PGPSignature getSignature(byte[] signature) throws SignatureExcep
public static long getPGPIdentifierFromFingerprint(byte[] fingerprint)
{
var buf = ByteBuffer.allocate(Long.BYTES);
buf.put(fingerprint, 12, 8);
if (fingerprint.length == 20)
{
buf.put(fingerprint, 12, 8);
}
else
{
buf.put(fingerprint, 0, 8);
}
buf.flip();
return buf.getLong();
}
Expand Down
5 changes: 3 additions & 2 deletions app/src/main/java/io/xeres/app/service/ProfileService.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,14 @@
import java.util.stream.Collectors;

import static io.xeres.app.service.ResourceCreationState.*;
import static io.xeres.common.Features.EXPERIMENTAL_EC;

@Service
public class ProfileService
{
private static final Logger log = LoggerFactory.getLogger(ProfileService.class);

private static final int KEY_SIZE = 3072;
private static final int KEY_SIZE = EXPERIMENTAL_EC ? 256 : 3072;
private static final int KEY_ID_LENGTH_MIN = ProfileConstants.NAME_LENGTH_MIN;
private static final int KEY_ID_LENGTH_MAX = ProfileConstants.NAME_LENGTH_MAX;
private static final String KEY_ID_SUFFIX = "(Generated by " + AppName.NAME + ")";
Expand Down Expand Up @@ -96,7 +97,7 @@ public ResourceCreationState generateProfileKeys(String name)
throw new IllegalArgumentException("Profile name is too long, maximum is " + KEY_ID_LENGTH_MAX);
}

log.info("Generating PGP keys, algorithm: RSA, bits: {} ...", KEY_SIZE);
log.info("Generating PGP keys, algorithm: {}, bits: {} ...", EXPERIMENTAL_EC ? "EdDSA" : "RSA", KEY_SIZE);

try
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
--
-- Extend fingerprints to 32 bytes
--
ALTER TABLE profile ALTER COLUMN pgp_fingerprint VARBINARY(32);
33 changes: 33 additions & 0 deletions common/src/main/java/io/xeres/common/Features.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2024 by David Gerber - https://zapek.com
*
* This file is part of Xeres.
*
* Xeres is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Xeres is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Xeres. If not, see <http://www.gnu.org/licenses/>.
*/

package io.xeres.common;

public final class Features
{
/**
* Enable experimental generation of Elliptic Curve keys.
*/
public static final boolean EXPERIMENTAL_EC = false;

private Features()
{
throw new UnsupportedOperationException("Utility class");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public record ProfileDTO(

Instant created,

@Size(min = ProfileFingerprint.LENGTH, max = ProfileFingerprint.LENGTH)
@Size(min = ProfileFingerprint.V4_LENGTH, max = ProfileFingerprint.LENGTH)
@Schema(example = "nhgF6ITwm/LLqchhpwJ91KFfAxg=")
byte[] pgpFingerprint,

Expand Down
12 changes: 7 additions & 5 deletions common/src/main/java/io/xeres/common/id/ProfileFingerprint.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
@Embeddable
public class ProfileFingerprint implements Identifier
{
public static final int LENGTH = 20;
public static final int V4_LENGTH = 20;
public static final int LENGTH = 32;

private byte[] identifier;

Expand All @@ -40,9 +41,9 @@ public ProfileFingerprint()
public ProfileFingerprint(byte[] identifier)
{
Objects.requireNonNull(identifier, "Null identifier");
if (identifier.length != LENGTH)
if (identifier.length != V4_LENGTH && identifier.length != LENGTH)
{
throw new IllegalArgumentException("Bad identifier length, expected " + LENGTH + ", got " + identifier.length);
throw new IllegalArgumentException("Bad identifier length, expected " + V4_LENGTH + " or " + LENGTH + ", got " + identifier.length);
}
this.identifier = identifier;
}
Expand All @@ -63,7 +64,7 @@ public void setBytes(byte[] identifier)
@Override
public int getLength()
{
return LENGTH;
return identifier.length;
}

@Override
Expand Down Expand Up @@ -92,8 +93,9 @@ public String toString()
{
var s = Id.toString(identifier);
var out = new StringBuilder();
var length = identifier.length * 2;

for (var i = 0; i < 40; i += 4)
for (var i = 0; i < length; i += 4)
{
if (i > 0)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,7 +410,10 @@ private void setupContactTreeTableView()
// updated.
sortedList.addListener((InvalidationListener) c -> {
log.debug("Sorting invalidated, selected index: {}", contactTreeTableView.getSelectionModel().getSelectedIndex());
contactListLocked = true;
if (!sortedList.isEmpty()) // Empty lists don't get passed on to filtered list and would get locked forever
{
contactListLocked = true;
}
selectedItem = contactTreeTableView.getSelectionModel().getSelectedItem();
});

Expand Down Expand Up @@ -1007,11 +1010,22 @@ private void showProfileKeyInformation(Profile profile, Label node)

private String getKeyInformation(ProfileKeyAttributes profileKeyAttributes)
{
return MessageFormat.format(bundle.getString("contact-view.information.key-information"),
profileKeyAttributes.version(),
PublicKeyUtils.getKeyAlgorithmName(profileKeyAttributes.keyAlgorithm()),
profileKeyAttributes.keyBits(),
PublicKeyUtils.getSignatureHash(profileKeyAttributes.signatureHash()));
// EC keys don't return the length for some reason
if (profileKeyAttributes.keyBits() > 0)
{
return MessageFormat.format(bundle.getString("contact-view.information.key-information-with-length"),
profileKeyAttributes.version(),
PublicKeyUtils.getKeyAlgorithmName(profileKeyAttributes.keyAlgorithm()),
profileKeyAttributes.keyBits(),
PublicKeyUtils.getSignatureHash(profileKeyAttributes.signatureHash()));
}
else
{
return MessageFormat.format(bundle.getString("contact-view.information.key-information"),
profileKeyAttributes.version(),
PublicKeyUtils.getKeyAlgorithmName(profileKeyAttributes.keyAlgorithm()),
PublicKeyUtils.getSignatureHash(profileKeyAttributes.signatureHash()));
}
}

private void showBadges(Profile profile)
Expand Down
3 changes: 2 additions & 1 deletion ui/src/main/resources/i18n/messages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,8 @@ contact-view.information.type=Type
contact-view.information.created=Created
contact-view.information.updated=Updated
contact-view.information.created-unknown=unknown
contact-view.information.key-information=Key specifications\nVersion: {0}\nAlgorithm: {1}\nLength: {2} bits\nSignature hash: {3}
contact-view.information.key-information-with-length=Version: {0}\nAlgorithm: {1}\nLength: {2} bits\nSignature hash: {3}
contact-view.information.key-information=Version: {0}\nAlgorithm: {1}\nSignature hash: {2}
contact-view.open.identity-not-found=Identity not found
contact-view.information.location.id=Location ID:
contact-view.information.location.version=Version:
Expand Down

0 comments on commit 0223e77

Please sign in to comment.