|
|
|
@ -20,68 +20,57 @@ import io.netty.handler.ssl.SslContext; |
|
|
|
import io.netty.handler.ssl.SslContextBuilder; |
|
|
|
import lombok.Data; |
|
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
|
import org.apache.commons.codec.binary.Base64; |
|
|
|
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; |
|
|
|
import org.bouncycastle.cert.X509CertificateHolder; |
|
|
|
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; |
|
|
|
import org.bouncycastle.jce.provider.BouncyCastleProvider; |
|
|
|
import org.bouncycastle.openssl.PEMDecryptorProvider; |
|
|
|
import org.bouncycastle.openssl.PEMEncryptedKeyPair; |
|
|
|
import org.bouncycastle.openssl.PEMKeyPair; |
|
|
|
import org.bouncycastle.openssl.PEMParser; |
|
|
|
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; |
|
|
|
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; |
|
|
|
import org.bouncycastle.util.encoders.Hex; |
|
|
|
import org.bouncycastle.operator.InputDecryptorProvider; |
|
|
|
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo; |
|
|
|
import org.bouncycastle.pkcs.PKCSException; |
|
|
|
import org.bouncycastle.pkcs.jcajce.JcePKCSPBEInputDecryptorProviderBuilder; |
|
|
|
import org.thingsboard.server.common.data.StringUtils; |
|
|
|
|
|
|
|
import javax.crypto.Cipher; |
|
|
|
import javax.crypto.EncryptedPrivateKeyInfo; |
|
|
|
import javax.crypto.SecretKey; |
|
|
|
import javax.crypto.SecretKeyFactory; |
|
|
|
import javax.crypto.spec.IvParameterSpec; |
|
|
|
import javax.crypto.spec.PBEKeySpec; |
|
|
|
import javax.crypto.spec.SecretKeySpec; |
|
|
|
import javax.net.ssl.KeyManagerFactory; |
|
|
|
import javax.net.ssl.TrustManagerFactory; |
|
|
|
import java.io.ByteArrayInputStream; |
|
|
|
import java.io.InputStream; |
|
|
|
import java.math.BigInteger; |
|
|
|
import java.nio.ByteBuffer; |
|
|
|
import java.security.AlgorithmParameters; |
|
|
|
import java.security.Key; |
|
|
|
import java.security.KeyFactory; |
|
|
|
import java.security.KeyPair; |
|
|
|
import java.io.IOException; |
|
|
|
import java.io.StringReader; |
|
|
|
import java.security.GeneralSecurityException; |
|
|
|
import java.security.KeyStore; |
|
|
|
import java.security.MessageDigest; |
|
|
|
import java.security.PrivateKey; |
|
|
|
import java.security.Security; |
|
|
|
import java.security.cert.CertPath; |
|
|
|
import java.security.cert.Certificate; |
|
|
|
import java.security.cert.CertificateFactory; |
|
|
|
import java.security.cert.X509Certificate; |
|
|
|
import java.security.spec.KeySpec; |
|
|
|
import java.security.spec.PKCS8EncodedKeySpec; |
|
|
|
import java.security.spec.RSAPrivateCrtKeySpec; |
|
|
|
import java.util.ArrayList; |
|
|
|
import java.util.Collections; |
|
|
|
import java.util.List; |
|
|
|
import java.util.regex.Matcher; |
|
|
|
import java.util.regex.Pattern; |
|
|
|
import java.util.stream.Collectors; |
|
|
|
|
|
|
|
@Data |
|
|
|
@Slf4j |
|
|
|
@JsonIgnoreProperties(ignoreUnknown = true) |
|
|
|
public class CertPemCredentials implements ClientCredentials { |
|
|
|
private static final String TLS_VERSION = "TLSv1.2"; |
|
|
|
|
|
|
|
public static final String PRIVATE_KEY_ALIAS = "private-key"; |
|
|
|
public static final String X_509 = "X.509"; |
|
|
|
public static final String CERT_ALIAS_PREFIX = "cert-"; |
|
|
|
public static final String CA_CERT_CERT_ALIAS_PREFIX = "caCert-cert-"; |
|
|
|
protected String caCert; |
|
|
|
private String cert; |
|
|
|
private String privateKey; |
|
|
|
private String password; |
|
|
|
|
|
|
|
static final String OPENSSL_ENCRYPTED_RSA_PRIVATEKEY_REGEX = "\\s*" |
|
|
|
+ "-----BEGIN RSA PRIVATE KEY-----" + "\\s*" |
|
|
|
+ "Proc-Type: 4,ENCRYPTED" + "\\s*" |
|
|
|
+ "DEK-Info:" + "\\s*([^\\s]+)" + "\\s+" |
|
|
|
+ "([\\s\\S]*)" |
|
|
|
+ "-----END RSA PRIVATE KEY-----" + "\\s*"; |
|
|
|
|
|
|
|
static final Pattern OPENSSL_ENCRYPTED_RSA_PRIVATEKEY_PATTERN = Pattern.compile(OPENSSL_ENCRYPTED_RSA_PRIVATEKEY_REGEX); |
|
|
|
public CertPemCredentials() { |
|
|
|
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { |
|
|
|
Security.addProvider(new BouncyCastleProvider()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@Override |
|
|
|
public CredentialsType getType() { |
|
|
|
@ -91,7 +80,6 @@ public class CertPemCredentials implements ClientCredentials { |
|
|
|
@Override |
|
|
|
public SslContext initSslContext() { |
|
|
|
try { |
|
|
|
Security.addProvider(new BouncyCastleProvider()); |
|
|
|
SslContextBuilder builder = SslContextBuilder.forClient(); |
|
|
|
if (StringUtils.hasLength(caCert)) { |
|
|
|
builder.trustManager(createAndInitTrustManagerFactory()); |
|
|
|
@ -106,51 +94,13 @@ public class CertPemCredentials implements ClientCredentials { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private KeyManagerFactory createAndInitKeyManagerFactory() throws Exception { |
|
|
|
List<X509Certificate> certHolders = readCertFile(cert); |
|
|
|
Object keyObject = readPrivateKeyFile(privateKey); |
|
|
|
char[] passwordCharArray = "".toCharArray(); |
|
|
|
if (!StringUtils.isEmpty(password)) { |
|
|
|
passwordCharArray = password.toCharArray(); |
|
|
|
} |
|
|
|
|
|
|
|
JcaPEMKeyConverter keyConverter = new JcaPEMKeyConverter().setProvider("BC"); |
|
|
|
|
|
|
|
PrivateKey privateKey; |
|
|
|
if (keyObject instanceof PEMEncryptedKeyPair) { |
|
|
|
PEMDecryptorProvider provider = new JcePEMDecryptorProviderBuilder().build(passwordCharArray); |
|
|
|
KeyPair key = keyConverter.getKeyPair(((PEMEncryptedKeyPair) keyObject).decryptKeyPair(provider)); |
|
|
|
privateKey = key.getPrivate(); |
|
|
|
} else if (keyObject instanceof PEMKeyPair) { |
|
|
|
KeyPair key = keyConverter.getKeyPair((PEMKeyPair) keyObject); |
|
|
|
privateKey = key.getPrivate(); |
|
|
|
} else if (keyObject instanceof PrivateKey) { |
|
|
|
privateKey = (PrivateKey) keyObject; |
|
|
|
} else { |
|
|
|
throw new RuntimeException("Unable to get private key from object: " + keyObject.getClass()); |
|
|
|
} |
|
|
|
|
|
|
|
KeyStore clientKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
|
|
|
clientKeyStore.load(null, null); |
|
|
|
for (X509Certificate certHolder : certHolders) { |
|
|
|
clientKeyStore.setCertificateEntry("cert-" + certHolder.getSubjectDN().getName(), certHolder); |
|
|
|
} |
|
|
|
clientKeyStore.setKeyEntry("private-key", |
|
|
|
privateKey, |
|
|
|
passwordCharArray, |
|
|
|
certHolders.toArray(new Certificate[]{})); |
|
|
|
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); |
|
|
|
keyManagerFactory.init(clientKeyStore, passwordCharArray); |
|
|
|
return keyManagerFactory; |
|
|
|
} |
|
|
|
|
|
|
|
protected TrustManagerFactory createAndInitTrustManagerFactory() throws Exception { |
|
|
|
List<X509Certificate> caCertHolders = readCertFile(caCert); |
|
|
|
List<X509Certificate> caCerts = readCertFile(caCert); |
|
|
|
|
|
|
|
KeyStore caKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
|
|
|
caKeyStore.load(null, null); |
|
|
|
for (X509Certificate caCertHolder : caCertHolders) { |
|
|
|
caKeyStore.setCertificateEntry("caCert-cert-" + caCertHolder.getSubjectDN().getName(), caCertHolder); |
|
|
|
for (X509Certificate caCert : caCerts) { |
|
|
|
caKeyStore.setCertificateEntry(CA_CERT_CERT_ALIAS_PREFIX + caCert.getSubjectDN().getName(), caCert); |
|
|
|
} |
|
|
|
|
|
|
|
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); |
|
|
|
@ -158,170 +108,74 @@ public class CertPemCredentials implements ClientCredentials { |
|
|
|
return trustManagerFactory; |
|
|
|
} |
|
|
|
|
|
|
|
List<X509Certificate> readCertFile(String fileContent) throws Exception { |
|
|
|
if (fileContent == null || fileContent.trim().isEmpty()) { |
|
|
|
return Collections.emptyList(); |
|
|
|
protected KeyManagerFactory createAndInitKeyManagerFactory() throws Exception { |
|
|
|
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); |
|
|
|
kmf.init(loadKeyStore(), password.toCharArray()); |
|
|
|
return kmf; |
|
|
|
} |
|
|
|
|
|
|
|
private KeyStore loadKeyStore() throws Exception { |
|
|
|
List<X509Certificate> certificates = readCertFile(this.cert); |
|
|
|
PrivateKey privateKey = readPrivateKey(this.privateKey, this.password); |
|
|
|
|
|
|
|
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); |
|
|
|
keyStore.load(null); |
|
|
|
List<X509Certificate> unique = certificates.stream().distinct().collect(Collectors.toList()); |
|
|
|
for (X509Certificate cert : unique) { |
|
|
|
keyStore.setCertificateEntry(CERT_ALIAS_PREFIX + cert.getSubjectDN().getName(), cert); |
|
|
|
} |
|
|
|
|
|
|
|
if (privateKey != null) { |
|
|
|
CertificateFactory factory = CertificateFactory.getInstance(X_509); |
|
|
|
CertPath certPath = factory.generateCertPath(certificates); |
|
|
|
List<? extends Certificate> path = certPath.getCertificates(); |
|
|
|
Certificate[] x509Certificates = path.toArray(new Certificate[0]); |
|
|
|
keyStore.setKeyEntry(PRIVATE_KEY_ALIAS, privateKey, password.toCharArray(), x509Certificates); |
|
|
|
} |
|
|
|
return keyStore; |
|
|
|
} |
|
|
|
|
|
|
|
protected List<X509Certificate> readCertFile(String fileContent) throws IOException, GeneralSecurityException { |
|
|
|
List<X509Certificate> certificates = new ArrayList<>(); |
|
|
|
String[] pems = fileContent.trim().split("-----END CERTIFICATE-----"); |
|
|
|
for (String pem : pems) { |
|
|
|
if (pem.trim().isEmpty()) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
pem = pem.replace("-----BEGIN CERTIFICATE-----", "") |
|
|
|
.replace("-----END CERTIFICATE-----", "") |
|
|
|
.replaceAll("\\s", ""); |
|
|
|
byte[] decoded = Base64.decodeBase64(pem); |
|
|
|
CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); |
|
|
|
try (InputStream inStream = new ByteArrayInputStream(decoded)) { |
|
|
|
certificates.add((X509Certificate) certFactory.generateCertificate(inStream)); |
|
|
|
JcaX509CertificateConverter certConverter = new JcaX509CertificateConverter(); |
|
|
|
try (PEMParser pemParser = new PEMParser(new StringReader(fileContent))) { |
|
|
|
Object object; |
|
|
|
while ((object = pemParser.readObject()) != null) { |
|
|
|
if (object instanceof X509CertificateHolder) { |
|
|
|
X509Certificate x509Cert = certConverter.getCertificate((X509CertificateHolder) object); |
|
|
|
certificates.add(x509Cert); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
return certificates; |
|
|
|
} |
|
|
|
|
|
|
|
private PrivateKey readPrivateKeyFile(String fileContent) throws Exception { |
|
|
|
protected PrivateKey readPrivateKey(String fileContent, String password) throws IOException, PKCSException { |
|
|
|
PrivateKey privateKey = null; |
|
|
|
if (fileContent != null && !fileContent.isEmpty()) { |
|
|
|
KeyFactory keyFactory = KeyFactory.getInstance("RSA"); |
|
|
|
KeySpec keySpec = getKeySpec(fileContent); |
|
|
|
privateKey = keyFactory.generatePrivate(keySpec); |
|
|
|
} |
|
|
|
return privateKey; |
|
|
|
} |
|
|
|
|
|
|
|
private KeySpec getKeySpec(String encodedKey) throws Exception { |
|
|
|
KeySpec keySpec = null; |
|
|
|
Matcher matcher = OPENSSL_ENCRYPTED_RSA_PRIVATEKEY_PATTERN.matcher(encodedKey); |
|
|
|
if (matcher.matches()) { |
|
|
|
String encryptionDetails = matcher.group(1).trim(); |
|
|
|
String encryptedKey = matcher.group(2).replaceAll("\\s", ""); |
|
|
|
byte[] encryptedBinaryKey = java.util.Base64.getDecoder().decode(encryptedKey); |
|
|
|
String[] encryptionDetailsParts = encryptionDetails.split(","); |
|
|
|
if (encryptionDetailsParts.length == 2) { |
|
|
|
String encryptionAlgorithm = encryptionDetailsParts[0]; |
|
|
|
String encryptedAlgorithmParams = encryptionDetailsParts[1]; |
|
|
|
byte[] pw = password.getBytes(); |
|
|
|
byte[] iv = Hex.decode(encryptedAlgorithmParams); |
|
|
|
|
|
|
|
MessageDigest digest = MessageDigest.getInstance("MD5"); |
|
|
|
digest.update(pw); |
|
|
|
digest.update(iv, 0, 8); |
|
|
|
|
|
|
|
byte[] round1Digest = digest.digest(); |
|
|
|
digest.update(round1Digest); |
|
|
|
digest.update(pw); |
|
|
|
digest.update(iv, 0, 8); |
|
|
|
|
|
|
|
byte[] round2Digest = digest.digest(); |
|
|
|
Cipher cipher = null; |
|
|
|
SecretKey secretKey = null; |
|
|
|
byte[] key = null; |
|
|
|
|
|
|
|
switch(encryptionAlgorithm) { |
|
|
|
case "AES-256-CBC": |
|
|
|
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); |
|
|
|
key = new byte[32]; |
|
|
|
System.arraycopy(round1Digest, 0, key, 0, 16); |
|
|
|
System.arraycopy(round2Digest, 0, key, 16, 16); |
|
|
|
secretKey = new SecretKeySpec(key, "AES"); |
|
|
|
break; |
|
|
|
case "AES-192-CBC": |
|
|
|
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); |
|
|
|
key = new byte[24]; |
|
|
|
System.arraycopy(round1Digest, 0, key, 0, 16); |
|
|
|
System.arraycopy(round2Digest, 0, key, 16, 8); |
|
|
|
secretKey = new SecretKeySpec(key, "AES"); |
|
|
|
JcaPEMKeyConverter keyConverter = new JcaPEMKeyConverter(); |
|
|
|
|
|
|
|
if (StringUtils.isNotEmpty(fileContent)) { |
|
|
|
try (PEMParser pemParser = new PEMParser(new StringReader(fileContent))) { |
|
|
|
Object object; |
|
|
|
while ((object = pemParser.readObject()) != null) { |
|
|
|
if (object instanceof PEMEncryptedKeyPair) { |
|
|
|
PEMDecryptorProvider decProv = new JcePEMDecryptorProviderBuilder().build(password.toCharArray()); |
|
|
|
privateKey = keyConverter.getKeyPair(((PEMEncryptedKeyPair) object).decryptKeyPair(decProv)).getPrivate(); |
|
|
|
break; |
|
|
|
case "AES-128-CBC": |
|
|
|
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); |
|
|
|
key = new byte[16]; |
|
|
|
System.arraycopy(round1Digest, 0, key, 0, 16); |
|
|
|
secretKey = new SecretKeySpec(key, "AES"); |
|
|
|
} else if (object instanceof PKCS8EncryptedPrivateKeyInfo) { |
|
|
|
InputDecryptorProvider decProv = |
|
|
|
new JcePKCSPBEInputDecryptorProviderBuilder().setProvider(new BouncyCastleProvider()).build(password.toCharArray()); |
|
|
|
privateKey = keyConverter.getPrivateKey(((PKCS8EncryptedPrivateKeyInfo) object).decryptPrivateKeyInfo(decProv)); |
|
|
|
break; |
|
|
|
case "DES-EDE3-CBC": |
|
|
|
cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding"); |
|
|
|
key = new byte[24]; |
|
|
|
System.arraycopy(round1Digest, 0, key, 0, 16); |
|
|
|
System.arraycopy(round2Digest, 0, key, 16, 8); |
|
|
|
secretKey = new SecretKeySpec(key, "DESede"); |
|
|
|
break; |
|
|
|
case "DES-CBC": |
|
|
|
cipher = Cipher.getInstance("DES/CBC/PKCS5Padding"); |
|
|
|
key = new byte[8]; |
|
|
|
System.arraycopy(round1Digest, 0, key, 0, 8); |
|
|
|
secretKey = new SecretKeySpec(key, "DES"); |
|
|
|
} else if (object instanceof PEMKeyPair) { |
|
|
|
privateKey = keyConverter.getKeyPair((PEMKeyPair) object).getPrivate(); |
|
|
|
break; |
|
|
|
} else if (object instanceof PrivateKeyInfo) { |
|
|
|
privateKey = keyConverter.getPrivateKey((PrivateKeyInfo) object); |
|
|
|
} |
|
|
|
if (cipher != null) { |
|
|
|
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); |
|
|
|
byte[] pkcs1 = cipher.doFinal(encryptedBinaryKey); |
|
|
|
keySpec = decodeRSAPrivatePKCS1(pkcs1); |
|
|
|
} else { |
|
|
|
throw new RuntimeException("Unknown Encryption algorithm!"); |
|
|
|
} |
|
|
|
} else { |
|
|
|
throw new RuntimeException("Wrong encryption details!"); |
|
|
|
} |
|
|
|
} else { |
|
|
|
encodedKey = encodedKey.replaceAll(".*BEGIN.*PRIVATE KEY.*", "") |
|
|
|
.replaceAll(".*END.*PRIVATE KEY.*", "") |
|
|
|
.replaceAll("\\s", ""); |
|
|
|
byte[] decoded = Base64.decodeBase64(encodedKey); |
|
|
|
if (password == null || password.isEmpty()) { |
|
|
|
keySpec = new PKCS8EncodedKeySpec(decoded); |
|
|
|
} else { |
|
|
|
PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray()); |
|
|
|
|
|
|
|
EncryptedPrivateKeyInfo privateKeyInfo = new EncryptedPrivateKeyInfo(decoded); |
|
|
|
String algorithmName = privateKeyInfo.getAlgName(); |
|
|
|
Cipher cipher = Cipher.getInstance(algorithmName); |
|
|
|
SecretKeyFactory secretKeyFactory = SecretKeyFactory.getInstance(algorithmName); |
|
|
|
|
|
|
|
Key pbeKey = secretKeyFactory.generateSecret(pbeKeySpec); |
|
|
|
AlgorithmParameters algParams = privateKeyInfo.getAlgParameters(); |
|
|
|
cipher.init(Cipher.DECRYPT_MODE, pbeKey, algParams); |
|
|
|
keySpec = privateKeyInfo.getKeySpec(cipher); |
|
|
|
} |
|
|
|
} |
|
|
|
return keySpec; |
|
|
|
} |
|
|
|
|
|
|
|
private static BigInteger derint(ByteBuffer input) { |
|
|
|
int len = der(input, 0x02); |
|
|
|
byte[] value = new byte[len]; |
|
|
|
input.get(value); |
|
|
|
return new BigInteger(+1, value); |
|
|
|
} |
|
|
|
|
|
|
|
private static int der(ByteBuffer input, int exp) { |
|
|
|
int tag = input.get() & 0xFF; |
|
|
|
if (tag != exp) throw new IllegalArgumentException("Unexpected tag"); |
|
|
|
int n = input.get() & 0xFF; |
|
|
|
if (n < 128) return n; |
|
|
|
n &= 0x7F; |
|
|
|
if ((n < 1) || (n > 2)) throw new IllegalArgumentException("Invalid length"); |
|
|
|
int len = 0; |
|
|
|
while (n-- > 0) { |
|
|
|
len <<= 8; |
|
|
|
len |= input.get() & 0xFF; |
|
|
|
} |
|
|
|
return len; |
|
|
|
} |
|
|
|
|
|
|
|
static RSAPrivateCrtKeySpec decodeRSAPrivatePKCS1(byte[] encoded) { |
|
|
|
ByteBuffer input = ByteBuffer.wrap(encoded); |
|
|
|
if (der(input, 0x30) != input.remaining()) throw new IllegalArgumentException("Excess data"); |
|
|
|
if (!BigInteger.ZERO.equals(derint(input))) throw new IllegalArgumentException("Unsupported version"); |
|
|
|
BigInteger n = derint(input); |
|
|
|
BigInteger e = derint(input); |
|
|
|
BigInteger d = derint(input); |
|
|
|
BigInteger p = derint(input); |
|
|
|
BigInteger q = derint(input); |
|
|
|
BigInteger ep = derint(input); |
|
|
|
BigInteger eq = derint(input); |
|
|
|
BigInteger c = derint(input); |
|
|
|
return new RSAPrivateCrtKeySpec(n, e, d, p, q, ep, eq, c); |
|
|
|
return privateKey; |
|
|
|
} |
|
|
|
} |
|
|
|
|