mirror of
https://github.com/cryptomator/cryptomator.git
synced 2026-05-21 20:21:27 +00:00
add PKCS12 support for on-demand creation and storage of an EC keypair
This commit is contained in:
@@ -23,6 +23,8 @@ module org.cryptomator.desktop {
|
||||
requires org.apache.commons.lang3;
|
||||
requires dagger;
|
||||
requires com.auth0.jwt;
|
||||
requires org.bouncycastle.provider;
|
||||
requires org.bouncycastle.pkix;
|
||||
|
||||
/* TODO: filename-based modules: */
|
||||
requires static javax.inject; /* ugly dagger/guava crap */
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import org.cryptomator.cryptolib.api.InvalidPassphraseException;
|
||||
import org.cryptomator.cryptolib.api.MasterkeyLoadingFailedException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.ECGenParameterSpec;
|
||||
|
||||
class P12AccessHelper {
|
||||
|
||||
private static final String EC_ALG = "EC";
|
||||
private static final String EC_CURVE_NAME = "secp256r1";
|
||||
private static final String SIGNATURE_ALG = "SHA256withECDSA";
|
||||
private static final String KEYSTORE_ALIAS_KEY = "key";
|
||||
private static final String KEYSTORE_ALIAS_CERT = "crt";
|
||||
|
||||
private P12AccessHelper() {}
|
||||
|
||||
/**
|
||||
* Creates a new key pair and stores it in PKCS#12 format at the given path.
|
||||
*
|
||||
* @param p12File The path of the .p12 file
|
||||
* @param pw The password to protect the key material
|
||||
* @throws IOException In case of I/O errors
|
||||
* @throws MasterkeyLoadingFailedException If any cryptographic operation fails
|
||||
*/
|
||||
public static KeyPair createNew(Path p12File, char[] pw) throws IOException, MasterkeyLoadingFailedException {
|
||||
try {
|
||||
var keyPair = getKeyPairGenerator().generateKeyPair();
|
||||
var keyStore = getKeyStore();
|
||||
keyStore.load(null, pw);
|
||||
var cert = X509Helper.createSelfSignedCert(keyPair, SIGNATURE_ALG);
|
||||
var chain = new X509Certificate[]{cert};
|
||||
keyStore.setKeyEntry(KEYSTORE_ALIAS_KEY, keyPair.getPrivate(), pw, chain);
|
||||
keyStore.setCertificateEntry(KEYSTORE_ALIAS_CERT, cert);
|
||||
var tmpFile = p12File.resolveSibling(p12File.getFileName().toString() + ".tmp");
|
||||
try (var out = Files.newOutputStream(tmpFile, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
keyStore.store(out, pw);
|
||||
}
|
||||
Files.move(tmpFile, p12File, StandardCopyOption.REPLACE_EXISTING);
|
||||
return keyPair;
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new MasterkeyLoadingFailedException("Failed to store PKCS12 file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a key pair from a PKCS#12 file located at the given path.
|
||||
*
|
||||
* @param p12File The path of the .p12 file
|
||||
* @param pw The password to protect the key material
|
||||
* @throws IOException In case of I/O errors
|
||||
* @throws InvalidPassphraseException If the supplied password is incorrect
|
||||
* @throws MasterkeyLoadingFailedException If any cryptographic operation fails
|
||||
*/
|
||||
public static KeyPair loadExisting(Path p12File, char[] pw) throws IOException, InvalidPassphraseException, MasterkeyLoadingFailedException {
|
||||
try (var in = Files.newInputStream(p12File, StandardOpenOption.READ)) {
|
||||
var keyStore = getKeyStore();
|
||||
keyStore.load(in, pw);
|
||||
var sk = (PrivateKey) keyStore.getKey(KEYSTORE_ALIAS_KEY, pw);
|
||||
var pk = keyStore.getCertificate(KEYSTORE_ALIAS_CERT).getPublicKey();
|
||||
return new KeyPair(pk, sk);
|
||||
} catch (UnrecoverableKeyException e) {
|
||||
throw new InvalidPassphraseException();
|
||||
} catch (IOException e) {
|
||||
if (e.getCause() instanceof UnrecoverableKeyException) {
|
||||
throw new InvalidPassphraseException();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
} catch (GeneralSecurityException e) {
|
||||
throw new MasterkeyLoadingFailedException("Failed to load PKCS12 file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyPairGenerator getKeyPairGenerator() {
|
||||
try {
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(EC_ALG);
|
||||
keyGen.initialize(new ECGenParameterSpec(EC_CURVE_NAME));
|
||||
return keyGen;
|
||||
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException e) {
|
||||
throw new IllegalStateException("secp256r1 curve not supported");
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyStore getKeyStore() {
|
||||
try {
|
||||
return KeyStore.getInstance("PKCS12");
|
||||
} catch (KeyStoreException e) {
|
||||
throw new IllegalStateException("Every implementation of the Java platform is required to support PKCS12.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package org.cryptomator.ui.keyloading.hub;
|
||||
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.bouncycastle.asn1.x500.X500Name;
|
||||
import org.bouncycastle.cert.X509v3CertificateBuilder;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||
import org.bouncycastle.operator.OperatorCreationException;
|
||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.sql.Date;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.UUID;
|
||||
|
||||
class X509Helper {
|
||||
|
||||
private static final X500Name ISSUER = new X500Name("CN=Cryptomator");
|
||||
private static final X500Name SUBJECT = new X500Name("CN=Self Signed Cert");
|
||||
private static final ASN1ObjectIdentifier ASN1_SUBJECT_KEY_ID = new ASN1ObjectIdentifier("2.5.29.14");
|
||||
|
||||
private X509Helper() {}
|
||||
|
||||
/**
|
||||
* Creates a self-signed X509Certificate containing the public key and signed with the private key of a given key pair.
|
||||
*
|
||||
* @param keyPair A key pair
|
||||
* @param signatureAlg A signature algorithm suited for the given key pair (see <a href="https://docs.oracle.com/en/java/javase/16/docs/specs/security/standard-names.html#signature-algorithms">available algorithms</a>)
|
||||
* @return A self-signed X509Certificate
|
||||
* @throws CertificateException If certificate generation failed, e.g. because of unsupported algorithms
|
||||
*/
|
||||
public static X509Certificate createSelfSignedCert(KeyPair keyPair, String signatureAlg) throws CertificateException {
|
||||
try {
|
||||
X509v3CertificateBuilder certificateBuilder = new JcaX509v3CertificateBuilder( //
|
||||
ISSUER, //
|
||||
randomSerialNo(), //
|
||||
Date.from(Instant.now()), //
|
||||
Date.from(Instant.now().plus(3650, ChronoUnit.DAYS)), //
|
||||
SUBJECT, //
|
||||
keyPair.getPublic());
|
||||
certificateBuilder.addExtension(ASN1_SUBJECT_KEY_ID, false, getX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic()));
|
||||
var signer = new JcaContentSignerBuilder(signatureAlg).build(keyPair.getPrivate());
|
||||
var cert = certificateBuilder.build(signer);
|
||||
try (InputStream in = new ByteArrayInputStream(cert.getEncoded())) {
|
||||
return (X509Certificate) getCertFactory().generateCertificate(in);
|
||||
}
|
||||
} catch (IOException | OperatorCreationException e) {
|
||||
throw new CertificateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static BigInteger randomSerialNo() {
|
||||
return BigInteger.valueOf(UUID.randomUUID().getMostSignificantBits());
|
||||
}
|
||||
|
||||
private static JcaX509ExtensionUtils getX509ExtensionUtils() {
|
||||
try {
|
||||
return new JcaX509ExtensionUtils();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Every implementation of the Java platform is required to support SHA-1.");
|
||||
}
|
||||
}
|
||||
|
||||
private static CertificateFactory getCertFactory() {
|
||||
try {
|
||||
return CertificateFactory.getInstance("X.509");
|
||||
} catch (CertificateException e) {
|
||||
throw new IllegalStateException("Every implementation of the Java platform is required to support X.509.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user