org.neo4j.driver.internal.pki.PemFormats Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of neo4j-java-driver Show documentation
Show all versions of neo4j-java-driver Show documentation
Access to the Neo4j graph database through Java
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [https://neo4j.com]
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.neo4j.driver.internal.pki;
import static java.lang.String.format;
import static org.neo4j.driver.internal.pki.DerUtils.beginDerSequence;
import static org.neo4j.driver.internal.pki.DerUtils.getDerContext;
import static org.neo4j.driver.internal.pki.DerUtils.readDerInteger;
import static org.neo4j.driver.internal.pki.DerUtils.readDerOctetString;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.AlgorithmParameters;
import java.security.KeyException;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.DSAPrivateKeySpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPrivateKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateCrtKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HexFormat;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.function.Function;
import javax.crypto.Cipher;
import javax.crypto.EncryptedPrivateKeyInfo;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Supported PEM format
*/
final class PemFormats {
private static final KeyFactory RSA_KEY_FACTORY;
private static final KeyFactory DSA_KEY_FACTORY;
private static final KeyFactory EC_KEY_FACTORY;
private static final Function ALL_KEY_FACTORIES;
static {
try {
// All exists as part of openJDK
RSA_KEY_FACTORY = KeyFactory.getInstance("RSA");
DSA_KEY_FACTORY = KeyFactory.getInstance("DSA");
EC_KEY_FACTORY = KeyFactory.getInstance("EC");
ALL_KEY_FACTORIES = keySpec -> {
try {
return RSA_KEY_FACTORY.generatePrivate(keySpec);
} catch (InvalidKeySpecException e) {
try {
return DSA_KEY_FACTORY.generatePrivate(keySpec);
} catch (InvalidKeySpecException ex) {
try {
return EC_KEY_FACTORY.generatePrivate(keySpec);
} catch (InvalidKeySpecException exc) {
// We tried...
e.addSuppressed(ex);
e.addSuppressed(exc);
throw new IllegalStateException("Key does not match RSA, DSA or EC spec.", e);
}
}
}
};
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("Non-conforming JDK implementation.", e);
}
}
private PemFormats() {}
interface PemFormat {
PrivateKey decodePrivate(byte[] der, Map headers, String password) throws KeyException;
PublicKey decodePublicKey(byte[] der) throws KeyException;
}
/**
* PEM format at described by RFC7468 section 10.
*/
static class Pkcs8 implements PemFormat {
static final String PRIVATE_LABEL = "PRIVATE KEY";
static final String PUBLIC_LABEL = "PUBLIC KEY";
@Override
public PrivateKey decodePrivate(byte[] der, Map headers, String password) throws KeyException {
assertNoPassword(password);
return ALL_KEY_FACTORIES.apply(new PKCS8EncodedKeySpec(der));
}
@Override
public PublicKey decodePublicKey(byte[] der) throws KeyException {
KeySpec encodedKeySpec = new X509EncodedKeySpec(der);
try {
return RSA_KEY_FACTORY.generatePublic(encodedKeySpec);
} catch (InvalidKeySpecException e) {
try {
return DSA_KEY_FACTORY.generatePublic(encodedKeySpec);
} catch (InvalidKeySpecException ex) {
try {
return EC_KEY_FACTORY.generatePublic(encodedKeySpec);
} catch (InvalidKeySpecException exc) {
// We tried...
e.addSuppressed(ex);
e.addSuppressed(exc);
throw new KeyException("Public key does not match RSA, DSA or EC spec.", e);
}
}
}
}
}
/**
* PEM format at described by RFC7468 section 11.
*/
static class Pkcs8Encrypted extends Pkcs8 {
static final String ENCRYPTED_LABEL = "ENCRYPTED PRIVATE KEY";
@Override
public PrivateKey decodePrivate(byte[] der, Map headers, String password) throws KeyException {
assertPassword(password);
try {
var keyInfo = new EncryptedPrivateKeyInfo(der);
var pbeKey = getSecretKey(keyInfo, password);
var cipher = getCipher(keyInfo);
cipher.init(Cipher.DECRYPT_MODE, pbeKey, keyInfo.getAlgParameters());
return ALL_KEY_FACTORIES.apply(keyInfo.getKeySpec(cipher));
} catch (Exception e) {
throw new KeyException("Unable to decrypt private key.", e);
}
}
private static SecretKey getSecretKey(EncryptedPrivateKeyInfo keyInfo, String password)
throws InvalidKeySpecException, NoSuchAlgorithmException {
SecretKeyFactory keyFactory;
try {
// Try to find by algorithm name first
keyFactory = SecretKeyFactory.getInstance(keyInfo.getAlgName());
} catch (NoSuchAlgorithmException e) {
// Maybe the algorithm parameter have a descent toString()?
keyFactory =
SecretKeyFactory.getInstance(keyInfo.getAlgParameters().toString());
}
return keyFactory.generateSecret(new PBEKeySpec(password.toCharArray()));
}
private static Cipher getCipher(EncryptedPrivateKeyInfo keyInfo)
throws NoSuchPaddingException, NoSuchAlgorithmException {
try {
return Cipher.getInstance(keyInfo.getAlgName());
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
return Cipher.getInstance(keyInfo.getAlgParameters().toString());
}
}
}
/**
* PEM Legacy described by RFC1421.
* Main difference is that the type of key is described in the label, and not as PKCS#8 as part
* of the structure. Encryption is supported and is defined in the header.
*/
abstract static class PemLegacy implements PemFormat {
@Override
public PrivateKey decodePrivate(byte[] der, Map headers, String password) throws KeyException {
// Handle decryption
var procType = headers.get("Proc-Type");
if (procType != null && procType.equals("4,ENCRYPTED")) {
assertPassword(password);
var deckInfo = headers.get("DEK-Info");
if (deckInfo == null) {
throw new KeyException("Missing 'DEK-Info' in encrypted PRIVATE KEY.");
}
var tokenizer = new StringTokenizer(deckInfo, ",");
var algorithm = tokenizer.nextToken();
var iv = HexFormat.of().parseHex(tokenizer.nextToken());
der = decryptLegacyPem(der, algorithm, iv, password);
} else {
assertNoPassword(password);
}
// Here we have an un-encrypted DER that can be parsed by the corresponding algorithm
var buffer = ByteBuffer.wrap(der);
if (beginDerSequence(buffer) != buffer.remaining()) {
throw new IllegalArgumentException("Malformed ASN.1 input.");
}
if (!version().equals(readDerInteger(buffer))) {
throw new IllegalArgumentException("PrivateKey version mismatch.");
}
try {
return decodePrivate0(buffer);
} catch (InvalidKeySpecException e) {
throw new KeyException(e);
}
}
protected abstract PrivateKey decodePrivate0(ByteBuffer buffer) throws InvalidKeySpecException;
protected abstract BigInteger version();
}
/**
* Parser for PKCS1 encoded keys as described by
* RFC3447
*
* Unlike PKCS8, the type of the key is not encoded in the structure, so
* we need to parse the ASN.1 structures directly.
*/
static class PemPKCS1Rsa extends PemLegacy {
static final String PRIVATE_LABEL = "RSA PRIVATE KEY";
static final String PUBLIC_LABEL = "RSA PUBLIC KEY";
/**
*
* RSAPublicKey ::= SEQUENCE {
* modulus INTEGER, -- n
* publicExponent INTEGER -- e
* }
*
*/
@Override
public PublicKey decodePublicKey(byte[] der) throws KeyException {
var input = ByteBuffer.wrap(der);
if (beginDerSequence(input) != input.remaining()) {
throw new IllegalArgumentException("Malformed RSAPublicKey");
}
var n = readDerInteger(input); // INTEGER modulus
var e = readDerInteger(input); // INTEGER publicExponent
try {
return RSA_KEY_FACTORY.generatePublic(new RSAPublicKeySpec(n, e));
} catch (InvalidKeySpecException ex) {
throw new KeyException(ex);
}
}
/**
*
* RSAPrivateKey ::= SEQUENCE {
* version Version,
* modulus INTEGER, -- n
* publicExponent INTEGER, -- e
* privateExponent INTEGER, -- d
* prime1 INTEGER, -- p
* prime2 INTEGER, -- q
* exponent1 INTEGER, -- d mod (p-1)
* exponent2 INTEGER, -- d mod (q-1)
* coefficient INTEGER, -- (inverse of q) mod p
* otherPrimeInfos OtherPrimeInfos OPTIONAL
* }
*
*/
@Override
protected PrivateKey decodePrivate0(ByteBuffer buffer) throws InvalidKeySpecException {
var n = readDerInteger(buffer); // INTEGER modulus
var e = readDerInteger(buffer); // INTEGER publicExponent
var d = readDerInteger(buffer); // INTEGER privateExponent
var p = readDerInteger(buffer); // INTEGER prime1
var q = readDerInteger(buffer); // INTEGER prime2
var ep = readDerInteger(buffer); // INTEGER exponent1
var eq = readDerInteger(buffer); // INTEGER exponent2
var c = readDerInteger(buffer); // INTEGER coefficient
return RSA_KEY_FACTORY.generatePrivate(new RSAPrivateCrtKeySpec(n, e, d, p, q, ep, eq, c));
}
@Override
protected BigInteger version() {
return BigInteger.ZERO;
}
}
/**
* Parser for DSA keys, no available standard but exists as a de facto standard as implemented by openSSL
*/
static class PemPKCS1Dsa extends PemLegacy {
static final String PRIVATE_LABEL = "DSA PRIVATE KEY";
@Override
public PublicKey decodePublicKey(byte[] der) {
throw new UnsupportedOperationException();
}
/**
*
* DSSPrivatKey_OpenSSL ::= SEQUENCE
* version INTEGER,
* p INTEGER,
* q INTEGER,
* g INTEGER,
* y INTEGER,
* x INTEGER
* }
*
*/
@Override
protected PrivateKey decodePrivate0(ByteBuffer buffer) throws InvalidKeySpecException {
var p = readDerInteger(buffer); // INTEGER p
var q = readDerInteger(buffer); // INTEGER q
var g = readDerInteger(buffer); // INTEGER g
readDerInteger(buffer); // public key 'y' is not used in the private key
var x = readDerInteger(buffer); // INTEGER x
return DSA_KEY_FACTORY.generatePrivate(new DSAPrivateKeySpec(x, p, q, g));
}
@Override
protected BigInteger version() {
return BigInteger.ZERO;
}
}
/**
* Parser of Elliptic Curve keys as described by RFC5915
*/
static class PemPKCS1Ec extends PemLegacy {
static final String PRIVATE_LABEL = "EC PRIVATE KEY";
@Override
public PublicKey decodePublicKey(byte[] der) {
throw new UnsupportedOperationException();
}
/**
*
* ECPrivateKey ::= SEQUENCE {
* version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1),
* privateKey OCTET STRING,
* parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
* publicKey [1] BIT STRING OPTIONAL
* }
*
*/
@Override
protected PrivateKey decodePrivate0(ByteBuffer buffer) throws InvalidKeySpecException {
try {
var s = new BigInteger(1, readDerOctetString(buffer));
var parameters = getDerContext(buffer, (byte) 0);
var ecParams = AlgorithmParameters.getInstance("EC");
ecParams.init(parameters);
var parameterSpec = ecParams.getParameterSpec(ECParameterSpec.class);
return EC_KEY_FACTORY.generatePrivate(new ECPrivateKeySpec(s, parameterSpec));
} catch (NoSuchAlgorithmException | IOException | InvalidParameterSpecException e) {
throw new IllegalArgumentException("Failed to decode EC private key", e);
}
}
@Override
protected BigInteger version() {
return BigInteger.ONE;
}
}
private static void assertNoPassword(String password) throws KeyException {
if (password != null) {
throw new KeyException("Passphrase was provided but found un-encrypted private key.");
}
}
private static void assertPassword(String password) throws KeyException {
if (password == null) {
throw new KeyException("Found encrypted private key but no passphrase was provided.");
}
}
/**
* Supported encryption schemas for legacy PEM.
*/
private enum DecryptSchema {
DES_CBC(8, "DES", "DES/CBC/PKCS5Padding"),
DES_EDE3_CBC(24, "DESede", "DESede/CBC/PKCS5Padding"),
AES_128_CBC(16, "AES", "AES/CBC/PKCS5Padding"),
AES_192_CBC(24, "AES", "AES/CBC/PKCS5Padding"),
AES_256_CBC(32, "AES", "AES/CBC/PKCS5Padding");
final int keySize;
final String family;
final String cipher;
DecryptSchema(int keySize, String family, String cipher) {
this.keySize = keySize;
this.family = family;
this.cipher = cipher;
}
}
private static byte[] decryptLegacyPem(byte[] der, String algorithm, byte[] iv, String password)
throws KeyException {
try {
DecryptSchema decryptSchema;
try {
decryptSchema = DecryptSchema.valueOf(algorithm.replace("-", "_"));
} catch (IllegalArgumentException e) {
throw new KeyException(format("Encryption scheme %s is not supported.", algorithm));
}
// Take as many bytes from the digest as needed
var kdf = keyDerivationFunction(iv, password);
var key = new byte[decryptSchema.keySize];
System.arraycopy(kdf, 0, key, 0, decryptSchema.keySize);
var secret = new SecretKeySpec(key, decryptSchema.family);
// Decrypt
var cipher = Cipher.getInstance(decryptSchema.cipher);
cipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv));
return cipher.doFinal(der);
} catch (Exception e) {
throw new KeyException("Failed to decrypt PEM file.", e);
}
}
/**
* OpenSSL de facto standard for generating keys for symmetric cyphers for PEM file encryption.
* It's basically PBKDF1 as described in PKCS#5 v1.5 with MD5 hash and iteration count of 1.
*
* @param iv initialization vector.
* @param password user password.
* @return an array of 32 bytes that should be used as keys for symmetric cyphers.
* @throws NoSuchAlgorithmException if MD5 message digest is not available.
*/
private static byte[] keyDerivationFunction(byte[] iv, String password) throws NoSuchAlgorithmException {
// https://github.com/openssl/openssl/blob/e4fd3fc379d76d9cd33ea6699268485606447737/crypto/pem/pem_lib.c#L378
// https://github.com/openssl/openssl/blob/e4fd3fc379d76d9cd33ea6699268485606447737/crypto/evp/evp_key.c#L78
var pw = password.getBytes(StandardCharsets.UTF_8);
var md5 = MessageDigest.getInstance("MD5");
md5.update(pw);
md5.update(iv, 0, 8);
var d0 = md5.digest();
md5.update(d0);
md5.update(pw);
md5.update(iv, 0, 8);
var d1 = md5.digest();
var kdf = new byte[32];
System.arraycopy(d0, 0, kdf, 0, 16);
System.arraycopy(d1, 0, kdf, 16, 16);
return kdf;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy