/*
* Copyright (c) 2004, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.ssl.jdbc4;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.net.Socket;
import java.security.AlgorithmParameters;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Collection;
import javax.crypto.Cipher;
import javax.crypto.EncryptedPrivateKeyInfo;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.net.ssl.X509KeyManager;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.x500.X500Principal;
/**
* A Key manager that only loads the keys, if necessary.
*/
public class LazyKeyManager implements X509KeyManager {
private X509Certificate[] cert = null;
private PrivateKey key = null;
private String certfile;
private String keyfile;
private CallbackHandler cbh;
private boolean defaultfile;
private PSQLException error = null;
/**
* Constructor. certfile and keyfile can be null, in that case no certificate is presented to the
* server.
*
* @param certfile certfile
* @param keyfile key file
* @param cbh callback handler
* @param defaultfile default file
*/
public LazyKeyManager(String certfile, String keyfile, CallbackHandler cbh, boolean defaultfile) {
this.certfile = certfile;
this.keyfile = keyfile;
this.cbh = cbh;
this.defaultfile = defaultfile;
}
/**
* getCertificateChain and getPrivateKey cannot throw exeptions, therefore any exception is stored
* in {@link #error} and can be raised by this method
*
* @throws PSQLException if any exception is stored in {@link #error} and can be raised
*/
public void throwKeyManagerException() throws PSQLException {
if (error != null) {
throw error;
}
}
@Override
public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
if (certfile == null) {
return null;
} else {
if (issuers == null || issuers.length == 0) {
// Postgres 8.4 and earlier do not send the list of accepted certificate authorities
// to the client. See BUG #5468. We only hope, that our certificate will be accepted.
return "user";
} else {
// Sending a wrong certificate makes the connection rejected, even, if clientcert=0 in
// pg_hba.conf.
// therefore we only send our certificate, if the issuer is listed in issuers
X509Certificate[] certchain = getCertificateChain("user");
if (certchain == null) {
return null;
} else {
X500Principal ourissuer = certchain[certchain.length - 1].getIssuerX500Principal();
boolean found = false;
for (Principal issuer : issuers) {
if (ourissuer.equals(issuer)) {
found = true;
}
}
return (found ? "user" : null);
}
}
}
}
@Override
public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
return null; // We are not a server
}
@Override
public X509Certificate[] getCertificateChain(String alias) {
if (cert == null && certfile != null) {
// If certfile is null, we do not load the certificate
// The certificate must be loaded
CertificateFactory cf;
try {
cf = CertificateFactory.getInstance("X.509");
} catch (CertificateException ex) {
// For some strange reason it throws CertificateException instead of
// NoSuchAlgorithmException...
error = new PSQLException(GT.tr(
"Could not find a java cryptographic algorithm: X.509 CertificateFactory not available."),
PSQLState.CONNECTION_FAILURE, ex);
return null;
}
Collection<? extends Certificate> certs;
try {
certs = cf.generateCertificates(new FileInputStream(certfile));
} catch (FileNotFoundException ioex) {
if (!defaultfile) { // It is not an error if there is no file at the default location
error = new PSQLException(
GT.tr("Could not open SSL certificate file {0}.", certfile),
PSQLState.CONNECTION_FAILURE, ioex);
}
return null;
} catch (CertificateException gsex) {
error = new PSQLException(GT.tr("Loading the SSL certificate {0} into a KeyManager failed.",
certfile), PSQLState.CONNECTION_FAILURE, gsex);
return null;
}
cert = certs.toArray(new X509Certificate[certs.size()]);
}
return cert;
}
@Override
public String[] getClientAliases(String keyType, Principal[] issuers) {
String alias = chooseClientAlias(new String[]{keyType}, issuers, (Socket) null);
return (alias == null ? new String[]{} : new String[]{alias});
}
@Override
public PrivateKey getPrivateKey(String alias) {
RandomAccessFile raf = null;
try {
if (key == null && keyfile != null) {
// If keyfile is null, we do not load the key
// The private key must be loaded
if (cert == null) { // We need the certificate for the algorithm
if (getCertificateChain("user") == null) {
return null; // getCertificateChain failed...
}
}
try {
raf = new RandomAccessFile(new File(keyfile), "r"); // NOSONAR
} catch (FileNotFoundException ex) {
if (!defaultfile) {
// It is not an error if there is no file at the default location
throw ex;
}
return null;
}
byte[] keydata = new byte[(int) raf.length()];
raf.readFully(keydata);
raf.close();
raf = null;
KeyFactory kf = KeyFactory.getInstance(cert[0].getPublicKey().getAlgorithm());
try {
KeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keydata);
key = kf.generatePrivate(pkcs8KeySpec);
} catch (InvalidKeySpecException ex) {
// The key might be password protected
EncryptedPrivateKeyInfo ePKInfo = new EncryptedPrivateKeyInfo(keydata);
Cipher cipher;
try {
cipher = Cipher.getInstance(ePKInfo.getAlgName());
} catch (NoSuchPaddingException npex) {
// Why is it not a subclass of NoSuchAlgorithmException?
throw new NoSuchAlgorithmException(npex.getMessage(), npex);
}
// We call back for the password
PasswordCallback pwdcb = new PasswordCallback(GT.tr("Enter SSL password: "), false);
try {
cbh.handle(new Callback[]{pwdcb});
} catch (UnsupportedCallbackException ucex) {
if ((cbh instanceof LibPQFactory.ConsoleCallbackHandler)
&& ("Console is not available".equals(ucex.getMessage()))) {
error = new PSQLException(GT
.tr("Could not read password for SSL key file, console is not available."),
PSQLState.CONNECTION_FAILURE, ucex);
} else {
error =
new PSQLException(
GT.tr("Could not read password for SSL key file by callbackhandler {0}.",
cbh.getClass().getName()),
PSQLState.CONNECTION_FAILURE, ucex);
}
return null;
}
try {
PBEKeySpec pbeKeySpec = new PBEKeySpec(pwdcb.getPassword());
// Now create the Key from the PBEKeySpec
SecretKeyFactory skFac = SecretKeyFactory.getInstance(ePKInfo.getAlgName());
Key pbeKey = skFac.generateSecret(pbeKeySpec);
// Extract the iteration count and the salt
AlgorithmParameters algParams = ePKInfo.getAlgParameters();
cipher.init(Cipher.DECRYPT_MODE, pbeKey, algParams);
// Decrypt the encryped private key into a PKCS8EncodedKeySpec
KeySpec pkcs8KeySpec = ePKInfo.getKeySpec(cipher);
key = kf.generatePrivate(pkcs8KeySpec);
} catch (GeneralSecurityException ikex) {
error = new PSQLException(
GT.tr("Could not decrypt SSL key file {0}.", keyfile),
PSQLState.CONNECTION_FAILURE, ikex);
return null;
}
}
}
} catch (IOException ioex) {
if (raf != null) {
try {
raf.close();
} catch (IOException ex) {
}
}
error = new PSQLException(GT.tr("Could not read SSL key file {0}.", keyfile),
PSQLState.CONNECTION_FAILURE, ioex);
} catch (NoSuchAlgorithmException ex) {
error = new PSQLException(GT.tr("Could not find a java cryptographic algorithm: {0}.",
ex.getMessage()), PSQLState.CONNECTION_FAILURE, ex);
return null;
}
return key;
}
@Override
public String[] getServerAliases(String keyType, Principal[] issuers) {
return new String[]{};
}
}