/* * ProActive Parallel Suite(TM): * The Open Source library for parallel and distributed * Workflows & Scheduling, Orchestration, Cloud Automation * and Big Data Analysis on Enterprise Grids & Clouds. * * Copyright (c) 2007 - 2017 ActiveEon * Contact: contact@activeeon.com * * This library is free software: you can redistribute it and/or * modify it under the terms of the GNU Affero General Public License * as published by the Free Software Foundation: version 3 of * the License. * * This program 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. * * If needed, contact us to obtain a release under GPL Version 2 or 3 * or a different license than the AGPL. */ package org.ow2.proactive.authentication.crypto; import java.io.*; import java.security.*; import java.security.interfaces.DSAPublicKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; import javax.crypto.interfaces.DHPublicKey; import org.apache.commons.codec.binary.Base64; import org.objectweb.proactive.annotation.PublicAPI; import org.objectweb.proactive.core.util.converter.ByteToObjectConverter; import org.objectweb.proactive.core.util.converter.ObjectToByteConverter; /** * Encapsulates encrypted Scheduler credentials * <p> * Stores encapsulated Scheduler credentials as well as metadata used to * determine which method should be used for decryption: key generation * algorithm, key size, and cipher parameters. * <p> * The credentials are encrypted with a symmetric AES key. * The AES key is encrypted using an asymmetric public key: * the corresponding private key is required to decrypt the secret AES * key, and then decrypt the data. * <p> * Extensive documentation for these parameters can be found in the * <a href="http://java.sun.com/j2se/1.5.0/docs/guide/security/jce/JCERefGuide.html">Java Cryptography Extension Reference Guide</a>. * * @see org.ow2.proactive.authentication.crypto.KeyPairUtil * @author The ProActive Team * @since ProActive Scheduling 1.1 */ @PublicAPI public class Credentials implements Serializable { // fix for #2456 : Credential Data and TaskLogs contain serialVersionUID based on scheduler server version private static final long serialVersionUID = 1L; /** Default credentials location */ private static final String DEFAULT_CREDS = System.getProperty("user.home") + File.separator + ".proactive" + File.separator + "security" + File.separator + "creds.enc"; /** Java properly describing the path to the encrypted credentials on the local drive */ public static final String credentialsPathProperty = "pa.common.auth.credentials"; /** Default pubkey location */ private static final String DEFAULT_PUBKEY = System.getProperty("user.home") + File.separator + ".proactive" + File.separator + "security" + File.separator + "pub.key"; /** Java property describing the path to the public key on the local drive */ public static final String pubkeyPathProperty = "pa.common.auth.pubkey"; static { File home = new File(DEFAULT_CREDS).getParentFile(); if (!home.isDirectory()) { home.mkdirs(); } home = new File(DEFAULT_PUBKEY).getParentFile(); if (!home.isDirectory()) { home.mkdirs(); } } /** key generation algorithm */ private String algorithm; /** key size */ private int size; /** cipher initialization parameters */ private String cipher; /** encrypted data with AES cipher */ private byte[] data; /** AES key encrypted with RSA */ private byte[] aes; /** * Default constructor * <p> * Constructor is kept private, use {@link org.ow2.proactive.authentication.crypto.Credentials#getCredentials()} or * {@link org.ow2.proactive.authentication.crypto.Credentials#createCredentials(CredData, String)} to get instances * * @param algo Key generation algorithm * @param size Key size in bits * @param cipher Cipher parameters * @param aes Encrypted AES key * @param data raw encrypted credentials */ private Credentials(String algo, int size, String cipher, byte[] aes, byte[] data) { this.algorithm = algo; this.size = size; this.cipher = cipher; this.aes = aes; this.data = data; } /** * Write the contents of a Credentials object to the disk * <p> * Use the current value of the {@link org.ow2.proactive.authentication.crypto.Credentials#credentialsPathProperty} * property to determine the file to which the data will be written * <p> * Credentials are written to disk in base64 encoded form. * <p> * See {@link org.ow2.proactive.authentication.crypto.Credentials#getCredentials()} for the inverse operation * * @param path file path where the credentials will be written on the disk * @throws KeyException Unable to locate or open file, IO error */ public void writeToDisk(String path) throws KeyException { File f = new File(path); FileOutputStream fs; try { fs = new FileOutputStream(f); fs.write(getBase64()); fs.close(); } catch (Exception e) { throw new KeyException("Could not write credentials to " + path, e); } } /** * Retrieves a public key stored in a local file * <p> * * @param pubPath path to the public key on the local filesystem * @return the key encapsulated in a regular JCE container * @throws KeyException the key could not be retrieved or is malformed */ public static PublicKey getPublicKey(String pubPath) throws KeyException { byte[] bytes; File f = new File(pubPath); FileInputStream fin; String algo = "", tmp = ""; // recover public key bytes try { fin = new FileInputStream(f); DataInputStream in = new DataInputStream(fin); int read, tot = 0; while ((read = in.read()) != '\n') { algo += (char) read; tot++; } tot++; while ((read = in.read()) != '\n') { tmp += (char) read; tot++; } tot++; bytes = new byte[(int) f.length() - tot]; in.readFully(bytes); in.close(); } catch (Exception e) { throw new KeyException("Could not retrieve public key from " + pubPath, e); } // reconstruct public key X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(bytes); PublicKey pubKey; KeyFactory keyFactory; try { keyFactory = KeyFactory.getInstance(algo); } catch (NoSuchAlgorithmException e) { throw new KeyException("Cannot initialize key factory", e); } try { pubKey = keyFactory.generatePublic(pubKeySpec); } catch (InvalidKeySpecException e) { throw new KeyException("Cannot re-generate public key", e); } return pubKey; } /** * Retrieves a private key stored in a local file * <p> * Tries to guess the algorithm used for keypair generation which * is not included in the file. According to <a href="http://download.oracle.com/javase/1.5.0/docs/guide/security/CryptoSpec.html#AppA">Java Cryptography Specification</a>, * the algorithm can be only one of "RSA" or "DSA", so this method will try using both. * If the algorithm used to generate the key is neither RSA or DSA * (highly unlikely), this method cannot recreate the private key, but {@link #decrypt(String)} * maybe will. * * @param privPath * path to the private key on the local filesystem * @return the key encapsulated in a regular JCE container * @throws KeyException * the key could not be retrieved or is malformed, or the algorithm used * for generation is different from the ones used by this method */ public static PrivateKey getPrivateKey(String privPath) throws KeyException { return getPrivateKey(privPath, new String[] { "RSA", "DSA" }); } /** * Retrieves a private key stored in a local file * <p> * Tries to guess the algorithm used for keypair generation which * is not included in the file. According to <a href="http://download.oracle.com/javase/1.5.0/docs/guide/security/CryptoSpec.html#AppA">Java Cryptography Specification</a>, * the algorithm can be only one of "RSA" or "DSA", so we can just try both using the * <code>algorithms</code> param. If the algorithm used to generate the key is neither RSA or DSA * (highly unlikely), this method cannot recreate the private key, but {@link #decrypt(String)} * maybe will. * * @param privPath * path to the private key on the local filesystem * @param algorithms a list of algorithms to try for creating the PK. Recommanded value: * {"RSA","DSA"} * @return the key encapsulated in a regular JCE container * @throws KeyException * the key could not be retrieved or is malformed, or the algorithm used for generation * is not one of <code>algorithms</code> */ public static PrivateKey getPrivateKey(String privPath, String[] algorithms) throws KeyException { PrivateKey privKey = null; for (String algo : algorithms) { try { KeyFactory keyFactory; keyFactory = KeyFactory.getInstance(algo); // recover private key bytes byte[] bytes; try { File pkFile = new File(privPath); DataInputStream pkStream = new DataInputStream(new FileInputStream(pkFile)); bytes = new byte[(int) pkFile.length()]; pkStream.readFully(bytes); pkStream.close(); } catch (Exception e) { throw new KeyException("Could not recover private key (algo=" + algo + ")", e); } // reconstruct private key PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(bytes); try { privKey = keyFactory.generatePrivate(privKeySpec); } catch (InvalidKeySpecException e) { throw new KeyException("Cannot re-generate private key (algo=" + algo + ")", e); } } catch (Exception e) { } } if (privKey == null) { String str = "Could not generate Private Key (algorithms: "; for (String algo : algorithms) { str += algo + " "; } str += ")"; throw new KeyException(str); } return privKey; } /** * Retrieves a credentials from disk * <p> * See {@link org.ow2.proactive.authentication.crypto.Credentials#writeToDisk(String)} for details on how information is * stored on disk. * * @return the Credentials object represented by the file saved at the file * described by the property {@link org.ow2.proactive.authentication.crypto.Credentials#credentialsPathProperty} * @throws KeyException Credentials could not be recovered */ public static Credentials getCredentials() throws KeyException { return getCredentials(getCredentialsPath()); } /** * Retrieves a credentials from disk * <p> * See {@link org.ow2.proactive.authentication.crypto.Credentials#writeToDisk(String)} for details on how information is * stored on disk. * * @param path to the file in which credentials are stored * @return the Credentials object represented by the file located at <code>path</code> * @throws KeyException Credentials could not be recovered */ public static Credentials getCredentials(String path) throws KeyException { File f = new File(path); byte[] bytes = new byte[(int) f.length()]; FileInputStream fin; try { fin = new FileInputStream(f); fin.read(bytes); fin.close(); } catch (Exception e) { throw new KeyException("Could not read credentials from " + path, e); } return getCredentialsBase64(bytes); } /** * Constructs a Credentials given an InputStream * * @param is contains the base64 representation of a Credentials upon read * @return the Credentials object contained in the InputStream * @throws KeyException the Credentials data was read but could not be reconstructed * @throws IOException the Credentials data could not be read from the stream */ public static Credentials getCredentials(InputStream is) throws KeyException, IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); byte[] buf = new byte[1024]; int len; while (true) { len = is.read(buf); if (len > 0) { out.write(buf, 0, len); } else { break; } } byte[] bytes = out.toByteArray(); out.close(); return Credentials.getCredentialsBase64(bytes); } /** * Creates a Credentials given its base64 encoded representation * * @param base64enc the Credentials representation as a base64 encoded byte array, * as returned by {@link Credentials#getBase64()} * @return the Credentials object corresponding the <code>base64en</code> representation * @throws KeyException */ public static Credentials getCredentialsBase64(byte[] base64enc) throws KeyException { String algo = "", cipher = "", tmp = ""; byte[] data; byte[] aes; int size; byte[] asciiEnc; try { asciiEnc = Base64.decodeBase64(base64enc); } catch (Exception e) { throw new KeyException("Unable to decode base64 credentials", e); } try { DataInputStream in = new DataInputStream(new ByteArrayInputStream(asciiEnc)); int read, tot = 0; while ((read = in.read()) != '\n') { if (read == -1) throw new KeyException("Failed to parse malformed credentials"); algo += (char) read; tot++; } tot++; while ((read = in.read()) != '\n') { if (read == -1) throw new KeyException("Failed to parse malformed credentials"); tmp += (char) read; tot++; } tot++; size = Integer.parseInt(tmp); while ((read = in.read()) != '\n') { if (read == -1) throw new KeyException("Failed to parse malformed credentials"); cipher += (char) read; tot++; } tot++; aes = new byte[size / 8]; for (int i = 0; i < size / 8; i++) { aes[i] = (byte) in.read(); tot++; } data = new byte[asciiEnc.length - tot]; in.readFully(data); } catch (Exception e) { throw new KeyException("Could not decode credentials", e); } return new Credentials(algo, size, cipher, aes, data); } /** * Returns a representation of this credentials as a base64 encoded byte array * <p> * Prior to base64 encoding, format is the following: * <ul> * <li>The key generation algorithm, in human readable format, on a single * line * <li>The key size, in human readable format, on a single line * <li>The cipher parameters, in human readable format, on a single line * <li>The encrypted AES key, which should be exactly <code>size / 8</code> bytes * <li>The encrypted data, which can be of arbitrary length, should occupy the rest of the file * </ul> * @throws KeyException */ public byte[] getBase64() throws KeyException { ByteArrayOutputStream b = new ByteArrayOutputStream(); try { b.write((algorithm + '\n').getBytes()); b.write(("" + size + '\n').getBytes()); b.write((cipher + '\n').getBytes()); b.write(this.aes); b.write(this.data); } catch (IOException e) { } byte[] ret; try { ret = Base64.encodeBase64(b.toByteArray()); } catch (Exception e) { throw new KeyException("Unable to encode credentials to base64", e); } return ret; } /** * * @return the path to the Credentials to use in this runtime */ public static String getCredentialsPath() { String path = System.getProperty(credentialsPathProperty); if (path == null) { path = DEFAULT_CREDS; } return path; } /** * * @return the path to the public key to use in this runtime */ public static String getPubKeyPath() { String path = System.getProperty(pubkeyPathProperty); if (path == null) { path = DEFAULT_PUBKEY; } return path; } /** * Creates new encrypted credentials. * <p> * See {@link org.ow2.proactive.authentication.crypto.Credentials#createCredentials(CredData, PublicKey)} * * @param cc the data to be encrypted * @param pubPath path to the public key * @return the Credentials object containing the encrypted data * @throws KeyException key generation or encryption failed */ public static Credentials createCredentials(final CredData cc, final String pubPath) throws KeyException { PublicKey pubKey = getPublicKey(pubPath); return createCredentials(cc, pubKey); } /** * Creates new encrypted credentials. * <p> * See {@link org.ow2.proactive.authentication.crypto.Credentials#createCredentials(CredData, PublicKey, String)} * * @param cc the data to be encrypted * @param pubKey the public key * @return the Credentials object containing the encrypted data * @throws KeyException key generation or encryption failed */ public static Credentials createCredentials(final CredData cc, final PublicKey pubKey) throws KeyException { return createCredentials(cc, pubKey, "RSA/ECB/PKCS1Padding"); } /** * Creates new encrypted credentials * <p> * Encrypts the message '<code>credData</code>' using the * public key <code>pubKey</code> and <code>cipher</code> * and store it in a new Credentials object. * * @see KeyPairUtil#encrypt(PublicKey, String, byte[]) * @param cc, the class containing the data to be crypted * @param pubKey public key used for encryption * @param cipher cipher parameters: combination of transformations * @return the Credentials object containing the encrypted data * @throws KeyException key generation or encryption failed */ public static Credentials createCredentials(final CredData cc, final PublicKey pubKey, final String cipher) throws KeyException { // serialize clear credentials to byte array byte[] clearCred; try { clearCred = ObjectToByteConverter.ObjectStream.convert(cc); } catch (IOException e1) { throw new KeyException(e1.getMessage()); } HybridEncryptionUtil.HybridEncryptedData encryptedData = HybridEncryptionUtil.encrypt(pubKey, cipher, clearCred); byte[] encAes = encryptedData.getEncryptedSymmetricKey(); byte[] encData = encryptedData.getEncryptedData(); int size = keySize(pubKey); return new Credentials(pubKey.getAlgorithm(), size, cipher, encAes, encData); } /** * Decrypts the encapsulated credentials * * @see org.ow2.proactive.authentication.crypto.KeyPairUtil#decrypt(PrivateKey, String, byte[]) * @param privPath path to the private key file * @return the credential data containing the clear data:login, password and key * @throws KeyException decryption failure, malformed data */ public CredData decrypt(String privPath) throws KeyException { PrivateKey privKey = Credentials.getPrivateKey(privPath, new String[] { algorithm }); return decrypt(privKey); } /** * Decrypts the encapsulated credentials * * @see org.ow2.proactive.authentication.crypto.KeyPairUtil#decrypt(PrivateKey, String, byte[]) * @param privKey the private key * @return the credential data containing the clear data:login, password and key * @throws KeyException decryption failure, malformed data */ public CredData decrypt(PrivateKey privKey) throws KeyException { byte[] decryptedData = HybridEncryptionUtil.decrypt(privKey, this.cipher, new HybridEncryptionUtil.HybridEncryptedData(aes, data)); // deserialize clear credentials and obtain login & password try { return (CredData) ByteToObjectConverter.ObjectStream.convert(decryptedData); } catch (Exception e) { throw new KeyException(e.getMessage()); } } /** * {@inheritDoc} */ @Override public String toString() { return "[" + algorithm + " " + size + "b " + cipher + "]"; } /** * Creates new encrypted credentials * <p> * See {@link org.ow2.proactive.authentication.crypto.Credentials#createCredentials(String, String, String, String)} * * @param login the login to encrypt * @param password the corresponding password to encrypt * @param pubPath path to the public key * @return the Credentials object containing the encrypted data * @throws KeyException key generation or encryption failed */ @Deprecated public static Credentials createCredentials(String login, String password, String pubPath) throws KeyException { return createCredentials(login, password, pubPath, "RSA/ECB/PKCS1Padding"); } /** * Creates new encrypted credentials * <p> * See {@link org.ow2.proactive.authentication.crypto.Credentials#createCredentials(String, String, String, String)} * * @param login the login to encrypt * @param password the corresponding password to encrypt * @param pubKey the public key * @return the Credentials object containing the encrypted data * @throws KeyException key generation or encryption failed */ @Deprecated public static Credentials createCredentials(String login, String password, PublicKey pubKey) throws KeyException { return createCredentials(login, password, null, pubKey, "RSA/ECB/PKCS1Padding"); } /** * Creates new encrypted credentials * <p> * Encrypts the message '<code>login</code>:<code>password</code>' using the * public key at <code>pubPath</code> and <code>cipher</code> * and store it in a new Credentials object. * * @see KeyPairUtil#encrypt(PublicKey, String, byte[]) * @param login the login to encrypt * @param password the corresponding password to encrypt * @param pubPath path to the public key used for encryption * @param cipher cipher parameters: combination of transformations * @return the Credentials object containing the encrypted data * @throws KeyException key generation or encryption failed */ @Deprecated public static Credentials createCredentials(String login, String password, String pubPath, String cipher) throws KeyException { PublicKey pubKey = getPublicKey(pubPath); return createCredentials(login, password, null, pubKey, cipher); } /** * Creates new encrypted credentials * <p> * Encrypts the message '<code>login</code>:<code>password</code>' using the * public key <code>pubKey</code> and <code>cipher</code> * and store it in a new Credentials object. * * @see KeyPairUtil#encrypt(PublicKey, String, byte[]) * @param login the login to encrypt * @param password the corresponding password to encrypt * @param pubKey public key used for encryption * @param cipher cipher parameters: combination of transformations * @return the Credentials object containing the encrypted data * @throws KeyException key generation or encryption failed */ @Deprecated public static Credentials createCredentials(String login, String password, byte[] datakey, PublicKey pubKey, String cipher) throws KeyException { CredData cc = new CredData(); cc.setLogin(CredData.parseLogin(login)); cc.setDomain(CredData.parseDomain(login)); cc.setPassword(password); cc.setKey(datakey); // serialize clear credentials to byte array byte[] clearCred; try { clearCred = ObjectToByteConverter.ObjectStream.convert(cc); } catch (IOException e1) { throw new KeyException(e1.getMessage()); } int size = keySize(pubKey); HybridEncryptionUtil.HybridEncryptedData encryptedData = HybridEncryptionUtil.encrypt(pubKey, cipher, clearCred); byte[] encAes = encryptedData.getEncryptedSymmetricKey(); byte[] encData = encryptedData.getEncryptedData(); return new Credentials(pubKey.getAlgorithm(), size, cipher, encAes, encData); } private static int keySize(PublicKey pubKey) { int size = -1; if (pubKey instanceof RSAPublicKey) { size = ((RSAPublicKey) pubKey).getModulus().bitLength(); } else if (pubKey instanceof DSAPublicKey) { size = ((DSAPublicKey) pubKey).getParams().getP().bitLength(); } else if (pubKey instanceof DHPublicKey) { size = ((DHPublicKey) pubKey).getParams().getP().bitLength(); } return size; } }