/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.nifi.security.util.crypto; import static org.apache.nifi.processors.standard.util.PGPUtil.BLOCK_SIZE; import static org.apache.nifi.processors.standard.util.PGPUtil.BUFFER_SIZE; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.security.NoSuchProviderException; import java.security.SecureRandom; import java.util.Date; import java.util.Iterator; import java.util.zip.Deflater; import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processor.io.StreamCallback; import org.apache.nifi.processors.standard.EncryptContent; import org.apache.nifi.processors.standard.EncryptContent.Encryptor; import org.bouncycastle.bcpg.ArmoredOutputStream; import org.bouncycastle.openpgp.PGPCompressedData; import org.bouncycastle.openpgp.PGPCompressedDataGenerator; import org.bouncycastle.openpgp.PGPEncryptedData; import org.bouncycastle.openpgp.PGPEncryptedDataGenerator; import org.bouncycastle.openpgp.PGPEncryptedDataList; import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPLiteralDataGenerator; import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPOnePassSignatureList; import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyRing; import org.bouncycastle.openpgp.PGPPublicKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKey; import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPUtil; import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory; import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory; import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyDataDecryptorFactoryBuilder; import org.bouncycastle.openpgp.operator.jcajce.JcePublicKeyKeyEncryptionMethodGenerator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class OpenPGPKeyBasedEncryptor implements Encryptor { private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptor.class); private String algorithm; private String provider; // TODO: This can hold either the secret or public keyring path private String keyring; private String userId; private char[] passphrase; private String filename; public OpenPGPKeyBasedEncryptor(final String algorithm, final String provider, final String keyring, final String userId, final char[] passphrase, final String filename) { this.algorithm = algorithm; this.provider = provider; this.keyring = keyring; this.userId = userId; this.passphrase = passphrase; this.filename = filename; } @Override public StreamCallback getEncryptionCallback() throws Exception { return new OpenPGPEncryptCallback(algorithm, provider, keyring, userId, filename); } @Override public StreamCallback getDecryptionCallback() throws Exception { return new OpenPGPDecryptCallback(provider, keyring, passphrase); } /** * Returns true if the passphrase is valid. * <p> * This is used in the EncryptContent custom validation to check if the passphrase can extract a private key from the secret key ring. After BC was upgraded from 1.46 to 1.53, the API changed * so this is performed differently but the functionality is equivalent. * * @param provider the provider name * @param secretKeyringFile the file path to the keyring * @param passphrase the passphrase * @return true if the passphrase can successfully extract any private key * @throws IOException if there is a problem reading the keyring file * @throws PGPException if there is a problem parsing/extracting the private key * @throws NoSuchProviderException if the provider is not available */ public static boolean validateKeyring(String provider, String secretKeyringFile, char[] passphrase) throws IOException, PGPException, NoSuchProviderException { try { getDecryptedPrivateKey(provider, secretKeyringFile, passphrase); return true; } catch (Exception e) { // If this point is reached, no private key could be extracted with the given passphrase return false; } } private static PGPPrivateKey getDecryptedPrivateKey(String provider, String secretKeyringFile, char[] passphrase) throws IOException, PGPException { // TODO: Verify that key IDs cannot be 0 return getDecryptedPrivateKey(provider, secretKeyringFile, 0L, passphrase); } private static PGPPrivateKey getDecryptedPrivateKey(String provider, String secretKeyringFile, long keyId, char[] passphrase) throws IOException, PGPException { // TODO: Reevaluate the mechanism for executing this task as performance can suffer here and only a specific key needs to be validated // Read in from the secret keyring file try (FileInputStream keyInputStream = new FileInputStream(secretKeyringFile)) { // Form the SecretKeyRing collection (1.53 way with fingerprint calculator) PGPSecretKeyRingCollection pgpSecretKeyRingCollection = new PGPSecretKeyRingCollection(keyInputStream, new BcKeyFingerprintCalculator()); // The decryptor is identical for all keys final PBESecretKeyDecryptor decryptor = new JcePBESecretKeyDecryptorBuilder().setProvider(provider).build(passphrase); // Iterate over all secret keyrings Iterator<PGPSecretKeyRing> keyringIterator = pgpSecretKeyRingCollection.getKeyRings(); PGPSecretKeyRing keyRing; PGPSecretKey secretKey; while (keyringIterator.hasNext()) { keyRing = keyringIterator.next(); // If keyId exists, get a specific secret key; else, iterate over all if (keyId != 0) { secretKey = keyRing.getSecretKey(keyId); try { return secretKey.extractPrivateKey(decryptor); } catch (Exception e) { throw new PGPException("No private key available using passphrase", e); } } else { Iterator<PGPSecretKey> keyIterator = keyRing.getSecretKeys(); while (keyIterator.hasNext()) { secretKey = keyIterator.next(); try { return secretKey.extractPrivateKey(decryptor); } catch (Exception e) { // TODO: Log (expected) failures? } } } } } // If this point is reached, no private key could be extracted with the given passphrase throw new PGPException("No private key available using passphrase"); } /* * Get the public key for a specific user id from a keyring. */ @SuppressWarnings("rawtypes") public static PGPPublicKey getPublicKey(String userId, String publicKeyringFile) throws IOException, PGPException { // TODO: Reevaluate the mechanism for executing this task as performance can suffer here and only a specific key needs to be validated // Read in from the public keyring file try (FileInputStream keyInputStream = new FileInputStream(publicKeyringFile)) { // Form the PublicKeyRing collection (1.53 way with fingerprint calculator) PGPPublicKeyRingCollection pgpPublicKeyRingCollection = new PGPPublicKeyRingCollection(keyInputStream, new BcKeyFingerprintCalculator()); // Iterate over all public keyrings Iterator<PGPPublicKeyRing> iter = pgpPublicKeyRingCollection.getKeyRings(); PGPPublicKeyRing keyRing; while (iter.hasNext()) { keyRing = iter.next(); // Iterate over each public key in this keyring Iterator<PGPPublicKey> keyIter = keyRing.getPublicKeys(); while (keyIter.hasNext()) { PGPPublicKey publicKey = keyIter.next(); // Iterate over each userId attached to the public key Iterator userIdIterator = publicKey.getUserIDs(); while (userIdIterator.hasNext()) { String id = (String) userIdIterator.next(); if (userId.equalsIgnoreCase(id)) { return publicKey; } } } } } // If this point is reached, no public key could be extracted with the given userId throw new PGPException("Could not find a public key with the given userId"); } private static class OpenPGPDecryptCallback implements StreamCallback { private String provider; private String secretKeyringFile; private char[] passphrase; OpenPGPDecryptCallback(final String provider, final String secretKeyringFile, final char[] passphrase) { this.provider = provider; this.secretKeyringFile = secretKeyringFile; this.passphrase = passphrase; } @Override public void process(InputStream in, OutputStream out) throws IOException { try (InputStream pgpin = PGPUtil.getDecoderStream(in)) { PGPObjectFactory pgpFactory = new PGPObjectFactory(pgpin, new BcKeyFingerprintCalculator()); Object obj = pgpFactory.nextObject(); if (!(obj instanceof PGPEncryptedDataList)) { obj = pgpFactory.nextObject(); if (!(obj instanceof PGPEncryptedDataList)) { throw new ProcessException("Invalid OpenPGP data"); } } PGPEncryptedDataList encList = (PGPEncryptedDataList) obj; try { PGPPrivateKey privateKey = null; PGPPublicKeyEncryptedData encData = null; // Find the secret key in the encrypted data Iterator it = encList.getEncryptedDataObjects(); while (privateKey == null && it.hasNext()) { obj = it.next(); if (!(obj instanceof PGPPublicKeyEncryptedData)) { throw new ProcessException("Invalid OpenPGP data"); } encData = (PGPPublicKeyEncryptedData) obj; // Check each encrypted data object to see if it contains the key ID for the secret key -> private key try { privateKey = getDecryptedPrivateKey(provider, secretKeyringFile, encData.getKeyID(), passphrase); } catch (PGPException e) { // TODO: Log (expected) exception? } } if (privateKey == null) { throw new ProcessException("Secret keyring does not contain the key required to decrypt"); } // Read in the encrypted data stream and decrypt it final PublicKeyDataDecryptorFactory dataDecryptor = new JcePublicKeyDataDecryptorFactoryBuilder().setProvider(provider).build(privateKey); try (InputStream clear = encData.getDataStream(dataDecryptor)) { // Create a plain object factory JcaPGPObjectFactory plainFact = new JcaPGPObjectFactory(clear); Object message = plainFact.nextObject(); // Check the message type and act accordingly // If compressed, decompress if (message instanceof PGPCompressedData) { PGPCompressedData cData = (PGPCompressedData) message; JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(cData.getDataStream()); message = pgpFact.nextObject(); } // If the message is literal data, read it and process to the out stream if (message instanceof PGPLiteralData) { PGPLiteralData literalData = (PGPLiteralData) message; try (InputStream lis = literalData.getInputStream()) { final byte[] buffer = new byte[BLOCK_SIZE]; int len; while ((len = lis.read(buffer)) >= 0) { out.write(buffer, 0, len); } } } else if (message instanceof PGPOnePassSignatureList) { // TODO: This is legacy code but should verify signature list here throw new PGPException("encrypted message contains a signed message - not literal data."); } else { throw new PGPException("message is not a simple encrypted file - type unknown."); } if (encData.isIntegrityProtected()) { if (!encData.verify()) { throw new PGPException("Failed message integrity check"); } } else { logger.warn("No message integrity check"); } } } catch (Exception e) { throw new ProcessException(e.getMessage()); } } } } private static class OpenPGPEncryptCallback implements StreamCallback { private String algorithm; private String provider; private String publicKeyring; private String userId; private String filename; OpenPGPEncryptCallback(final String algorithm, final String provider, final String keyring, final String userId, final String filename) { this.algorithm = algorithm; this.provider = provider; this.publicKeyring = keyring; this.userId = userId; this.filename = filename; } @Override public void process(InputStream in, OutputStream out) throws IOException { PGPPublicKey publicKey; final boolean isArmored = EncryptContent.isPGPArmoredAlgorithm(algorithm); try { publicKey = getPublicKey(userId, publicKeyring); } catch (Exception e) { throw new ProcessException("Invalid public keyring - " + e.getMessage()); } try { OutputStream output = out; if (isArmored) { output = new ArmoredOutputStream(out); } try { // TODO: Refactor internal symmetric encryption algorithm to be customizable PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator( new JcePGPDataEncryptorBuilder(PGPEncryptedData.AES_128).setWithIntegrityPacket(true).setSecureRandom(new SecureRandom()).setProvider(provider)); encryptedDataGenerator.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(publicKey).setProvider(provider)); // TODO: Refactor shared encryption code to utility try (OutputStream encryptedOut = encryptedDataGenerator.open(output, new byte[BUFFER_SIZE])) { PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(PGPCompressedData.ZIP, Deflater.BEST_SPEED); try (OutputStream compressedOut = compressedDataGenerator.open(encryptedOut, new byte[BUFFER_SIZE])) { PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator(); try (OutputStream literalOut = literalDataGenerator.open(compressedOut, PGPLiteralData.BINARY, filename, new Date(), new byte[BUFFER_SIZE])) { final byte[] buffer = new byte[BLOCK_SIZE]; int len; while ((len = in.read(buffer)) >= 0) { literalOut.write(buffer, 0, len); } } } } } finally { if (isArmored) { output.close(); } } } catch (Exception e) { throw new ProcessException(e.getMessage()); } } } }