/* * Copyright (C) 2017-2020 HERE Europe B.V. * * 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. * * SPDX-License-Identifier: Apache-2.0 * License-Filename: LICENSE */ package com.here.xyz.connectors.decryptors; import static java.nio.charset.StandardCharsets.UTF_8; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.nio.file.Files; import java.nio.file.Paths; import java.security.Key; import java.security.KeyFactory; import java.security.PrivateKey; import java.security.spec.MGF1ParameterSpec; import java.security.spec.PKCS8EncodedKeySpec; import java.util.Base64; import java.util.Iterator; import java.util.stream.Stream; import javax.crypto.Cipher; import javax.crypto.EncryptedPrivateKeyInfo; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.OAEPParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.PSource.PSpecified; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class PrivateKeyEventDecryptor extends EventDecryptor { /** * Logger for the class */ private static final Logger logger = LogManager.getLogger(); /** * Environment variable for the file path to the private key in PKCS#8 PEM format. */ public static final String ENV_PRIVATE_KEY = "PRIVATE_KEY_FILE"; /** * The optional (but recommended) passphrase to decode the private key. */ public static final String ENV_PRIVATE_KEY_PASSPHRASE = "PRIVATE_KEY_PASSPHRASE"; /** * The algorithm used for decrypting the secrets. */ public static final String ALGORITHM = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding"; /** * The algorithm for creating the private key. */ public static final String RSA = "RSA"; /** * Prefix for loading files from classpath. */ private static final String CLASSPATH_PREFIX = "classpath:"; /** * OAEP Padding specs for decrypting secrets. */ private static final OAEPParameterSpec OAEP_PARAMS = new OAEPParameterSpec("SHA-256", "MGF1", new MGF1ParameterSpec("SHA-256"), PSpecified.DEFAULT); /** * The private key for decrypting secrets. */ final private PrivateKey privateKey; /** * Default constructor to create a new EventDecryptor that takes * a private key for decrypting the secrets. * * For security reasons should the private key be encrypted * using PKCS#8 scheme. * This line converts an openssl private key to an usable, encrypted * PKCS#8 key: * openssl pkcs8 -topk8 -in private_key.pem -v1 PBE-SHA1-3DES -out private_pkcs8.pem * * This is how you can encrypt a secret using openssl and the public key: * openssl pkeyutl -encrypt \ * -inkey public_key.pem \ * -pubin \ * -in secret.txt \ * -pkeyopt rsa_padding_mode:oaep \ * -pkeyopt rsa_oaep_md:sha256 \ * -pkeyopt rsa_mgf1_md:sha256 * * Following environment variables need to be set to use this class: * - {@link com.here.xyz.connectors.AbstractConnectorHandler#ENV_DECRYPTOR} * - {@link #ENV_PRIVATE_KEY} * - {@link #ENV_PRIVATE_KEY_PASSPHRASE} */ PrivateKeyEventDecryptor() { PrivateKey tmp; try { tmp = loadPrivateKey(System.getenv(ENV_PRIVATE_KEY), System.getenv(ENV_PRIVATE_KEY_PASSPHRASE)); } catch (Exception e) { tmp = null; logger.error("Could not load the private key. Params will not be decrypted!", e); } privateKey = tmp; } /** * {@inheritDoc} */ @Override public String decryptAsymmetric(final String encoded) { if (!isEncrypted(encoded)) { return encoded; } // we cannot decode the secret if no private key is set. if (privateKey == null) { return encoded; } String tmp = encoded.substring(2, encoded.length() - 2); try { Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, privateKey, OAEP_PARAMS); byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(tmp)); return new String(decryptedBytes, UTF_8); } catch (IllegalArgumentException e) { logger.warn("Could not Base64 decode value", e); } catch(Exception e) { logger.warn("Could not decrypt string.", e); } return encoded; } /** * Load the private key. * * @param filePath The file path to the private key in PKCS#8 PEM format * @param passphrase The optional passphrase to decode the private key (recommended). * @return Returns the {@link PrivateKey} or null if there a problem. */ public static PrivateKey loadPrivateKey(final String filePath, final String passphrase) { if (filePath == null || filePath.isEmpty()) { return null; } Stream<String> lines; try { if (filePath.startsWith(CLASSPATH_PREFIX)) { InputStream is = PrivateKeyEventDecryptor.class.getResourceAsStream(filePath.substring(CLASSPATH_PREFIX.length())); lines = new LineNumberReader(new InputStreamReader(is)).lines(); } else { lines = Files.newBufferedReader(Paths.get(filePath), UTF_8).lines(); } } catch (NullPointerException | IOException e) { logger.error("Could not load private key from file path '" + filePath + "'", e); return null; } final StringBuilder privateKeyPEM = new StringBuilder(); final Iterator<String> i = lines.iterator(); if (i.hasNext()) { String header = i.next(); if ("-----BEGIN PRIVATE KEY-----".equals(header)) { i.forEachRemaining(s -> { if (!"-----END PRIVATE KEY-----".equals(s)) { privateKeyPEM.append(s); } }); return createPrivateKey(privateKeyPEM.toString()); } else if("-----BEGIN ENCRYPTED PRIVATE KEY-----".equals(header)) { i.forEachRemaining(s -> { if (!"-----END ENCRYPTED PRIVATE KEY-----".equals(s)) { privateKeyPEM.append(s); } }); return decryptPrivateKey(privateKeyPEM.toString(), passphrase); } else{ logger.error("Unknown key format. Params will not be decrypted."); return null; } } logger.error("Empty key file. Params will not be decrypted."); return null; } /** * This method decrypts the private key that was encrypted using PKCS#8 scheme. * * @param pkcs8Data The private key in PEM format without header and footer. * @param passphrase The passphrase for decrypting the private key. * @return Returns the {@link PrivateKey} or null if there a problem. */ public static PrivateKey decryptPrivateKey(final String pkcs8Data, final String passphrase) { if (passphrase == null || pkcs8Data == null) { logger.error("Could not create private key because passphrase or key is null"); return null; } try { PBEKeySpec pbeSpec = new PBEKeySpec(passphrase.toCharArray()); EncryptedPrivateKeyInfo pkinfo = new EncryptedPrivateKeyInfo(Base64.getDecoder().decode(pkcs8Data.getBytes(UTF_8))); SecretKeyFactory skf = SecretKeyFactory.getInstance(pkinfo.getAlgName()); Key secret = skf.generateSecret(pbeSpec); PKCS8EncodedKeySpec keySpec = pkinfo.getKeySpec(secret); KeyFactory keyFactory = KeyFactory.getInstance(RSA); return keyFactory.generatePrivate(keySpec); } catch (Exception e) { logger.error("Could not create encrypted private key from environment variable", e); return null; } } /** * Try to create a RSA private key from a PKCS#8 PEM without header and footer. * * @param pkcs8Data The private key in PKCS#8 PEM format without header and footer. * @return Returns the {@link PrivateKey} or null if there was a problem. */ public static PrivateKey createPrivateKey(final String pkcs8Data) { try { KeyFactory keyFactory = KeyFactory.getInstance(RSA); return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(pkcs8Data.getBytes(UTF_8)))); } catch (Exception e) { logger.error("Could not create unencrypted private key from environment variable", e); return null; } } }