NIFI-1324:

Changed Maven dependencies for BouncyCastle bcprov and bcpg from jdk16:1.46 to jdk15on:1.53 (kept nifi-web-security on jdk16:1.46 because jdk15on:1.53 splits OCSP logic into new module bcpkix).
Added individual unit tests for PGP public keyring validation.
Passes all legacy unit tests.
Added TODOs for customizable brick encryption and refactoring shared code.
Cleaned up magic numbers to constants.
Added unit tests for OpenPGPPasswordBasedEncryptor (internal consistency and legacy file decrypt).
Began refactoring shared encrypt code from OpenPGP* implementations.
Extracted encrypt utility method from OpenPGPPasswordBasedEncryptor to PGPUtil class.
Added test resources (signed and unsigned key-encrypted files).
Added unit tests for OpenPGPKeyBasedEncryptor (internal consistency and external file decrypt).
Changed BC dependency for nifi-web-security to bcprov-jdk15on:1.53 and bcpkix-jdk15on:1.53.
Updated OCSPValidator to use new BC logic for OCSP validation. This code compiles but should be fully audited, as the legacy OCSP validation was not completely implemented.
Added skeleton of OCSP validator unit tests with successful keypair and certificate generation and signing code.
Added further unit tests for issued certificates.
Annotated unimplemented unit tests with note about Groovy integration.
Refactored Jersey call in OCSPCertificateValidator to internal method.
Added toString() to NiFi local OcspRequest.
Implemented positive & negative unit tests with cache injection for valid/revoked OCSP certificate.
Resolved contrib-check issues.
Removed ignored code in unit test.

Signed-off-by: Matt Gilman <matt.c.gilman@gmail.com>
This commit is contained in:
Andy LoPresto 2015-12-22 19:03:09 -08:00 committed by Matt Gilman
parent 6b54753dbb
commit ffbfffce6d
18 changed files with 1128 additions and 233 deletions

View File

@ -85,7 +85,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId> <artifactId>bcprov-jdk15on</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.sun.jersey</groupId> <groupId>com.sun.jersey</groupId>

View File

@ -84,7 +84,11 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId> <artifactId>bcprov-jdk15on</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.google.guava</groupId> <groupId>com.google.guava</groupId>

View File

@ -27,40 +27,51 @@ import com.sun.jersey.api.client.UniformInterfaceException;
import com.sun.jersey.api.client.WebResource; import com.sun.jersey.api.client.WebResource;
import com.sun.jersey.api.client.config.ClientConfig; import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig; import com.sun.jersey.api.client.config.DefaultClientConfig;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.framework.security.util.SslContextFactory;
import org.apache.nifi.util.FormatUtils;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.security.x509.ocsp.OcspStatus.ValidationStatus;
import org.apache.nifi.web.security.x509.ocsp.OcspStatus.VerificationStatus;
import org.apache.nifi.web.util.WebUtils;
import org.bouncycastle.asn1.DEROctetString;
import org.bouncycastle.asn1.ocsp.OCSPObjectIdentifiers;
import org.bouncycastle.asn1.x509.Extension;
import org.bouncycastle.asn1.x509.Extensions;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.CertificateStatus;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.OCSPRespBuilder;
import org.bouncycastle.cert.ocsp.RevokedStatus;
import org.bouncycastle.cert.ocsp.SingleResp;
import org.bouncycastle.operator.DigestCalculatorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.security.auth.x500.X500Principal;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger; import java.math.BigInteger;
import java.net.URI; import java.net.URI;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.NoSuchProviderException; import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory; import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import javax.security.auth.x500.X500Principal;
import org.apache.nifi.framework.security.util.SslContextFactory;
import org.apache.nifi.web.security.x509.ocsp.OcspStatus.ValidationStatus;
import org.apache.nifi.web.security.x509.ocsp.OcspStatus.VerificationStatus;
import org.apache.nifi.util.FormatUtils;
import org.apache.nifi.util.NiFiProperties;
import org.apache.nifi.web.util.WebUtils;
import org.apache.commons.lang3.StringUtils;
import org.bouncycastle.ocsp.BasicOCSPResp;
import org.bouncycastle.ocsp.CertificateID;
import org.bouncycastle.ocsp.CertificateStatus;
import org.bouncycastle.ocsp.OCSPException;
import org.bouncycastle.ocsp.OCSPReq;
import org.bouncycastle.ocsp.OCSPReqGenerator;
import org.bouncycastle.ocsp.OCSPResp;
import org.bouncycastle.ocsp.OCSPRespStatus;
import org.bouncycastle.ocsp.RevokedStatus;
import org.bouncycastle.ocsp.SingleResp;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class OcspCertificateValidator { public class OcspCertificateValidator {
@ -89,7 +100,7 @@ public class OcspCertificateValidator {
// attempt to parse the specified va url // attempt to parse the specified va url
validationAuthorityURI = URI.create(rawValidationAuthorityUrl); validationAuthorityURI = URI.create(rawValidationAuthorityUrl);
// connection detials // connection details
final ClientConfig config = new DefaultClientConfig(); final ClientConfig config = new DefaultClientConfig();
config.getProperties().put(ClientConfig.PROPERTY_READ_TIMEOUT, READ_TIMEOUT); config.getProperties().put(ClientConfig.PROPERTY_READ_TIMEOUT, READ_TIMEOUT);
config.getProperties().put(ClientConfig.PROPERTY_CONNECT_TIMEOUT, CONNECT_TIMEOUT); config.getProperties().put(ClientConfig.PROPERTY_CONNECT_TIMEOUT, CONNECT_TIMEOUT);
@ -283,16 +294,24 @@ public class OcspCertificateValidator {
try { try {
// prepare the request // prepare the request
final BigInteger subjectSerialNumber = subjectCertificate.getSerialNumber(); final BigInteger subjectSerialNumber = subjectCertificate.getSerialNumber();
final CertificateID certificateId = new CertificateID(CertificateID.HASH_SHA1, issuerCertificate, subjectSerialNumber); final DigestCalculatorProvider calculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder().setProvider("BC").build();
final CertificateID certificateId = new CertificateID(calculatorProviderBuilder.get(CertificateID.HASH_SHA1),
new X509CertificateHolder(issuerCertificate.getEncoded()),
subjectSerialNumber);
// generate the request // generate the request
final OCSPReqGenerator requestGenerator = new OCSPReqGenerator(); final OCSPReqBuilder requestGenerator = new OCSPReqBuilder();
requestGenerator.addRequest(certificateId); requestGenerator.addRequest(certificateId);
final OCSPReq ocspRequest = requestGenerator.generate();
// Create a nonce to avoid replay attack
BigInteger nonce = BigInteger.valueOf(System.currentTimeMillis());
Extension ext = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, true, new DEROctetString(nonce.toByteArray()));
requestGenerator.setRequestExtensions(new Extensions(new Extension[]{ext}));
final OCSPReq ocspRequest = requestGenerator.build();
// perform the request // perform the request
final WebResource resource = client.resource(validationAuthorityURI); final ClientResponse response = getClientResponse(ocspRequest);
final ClientResponse response = resource.header(CONTENT_TYPE_HEADER, OCSP_REQUEST_CONTENT_TYPE).post(ClientResponse.class, ocspRequest.getEncoded());
// ensure the request was completed successfully // ensure the request was completed successfully
if (ClientResponse.Status.OK.getStatusCode() != response.getStatusInfo().getStatusCode()) { if (ClientResponse.Status.OK.getStatusCode() != response.getStatusInfo().getStatusCode()) {
@ -305,22 +324,22 @@ public class OcspCertificateValidator {
// verify the response status // verify the response status
switch (ocspResponse.getStatus()) { switch (ocspResponse.getStatus()) {
case OCSPRespStatus.SUCCESSFUL: case OCSPRespBuilder.SUCCESSFUL:
ocspStatus.setResponseStatus(OcspStatus.ResponseStatus.Successful); ocspStatus.setResponseStatus(OcspStatus.ResponseStatus.Successful);
break; break;
case OCSPRespStatus.INTERNAL_ERROR: case OCSPRespBuilder.INTERNAL_ERROR:
ocspStatus.setResponseStatus(OcspStatus.ResponseStatus.InternalError); ocspStatus.setResponseStatus(OcspStatus.ResponseStatus.InternalError);
break; break;
case OCSPRespStatus.MALFORMED_REQUEST: case OCSPRespBuilder.MALFORMED_REQUEST:
ocspStatus.setResponseStatus(OcspStatus.ResponseStatus.MalformedRequest); ocspStatus.setResponseStatus(OcspStatus.ResponseStatus.MalformedRequest);
break; break;
case OCSPRespStatus.SIGREQUIRED: case OCSPRespBuilder.SIG_REQUIRED:
ocspStatus.setResponseStatus(OcspStatus.ResponseStatus.SignatureRequired); ocspStatus.setResponseStatus(OcspStatus.ResponseStatus.SignatureRequired);
break; break;
case OCSPRespStatus.TRY_LATER: case OCSPRespBuilder.TRY_LATER:
ocspStatus.setResponseStatus(OcspStatus.ResponseStatus.TryLater); ocspStatus.setResponseStatus(OcspStatus.ResponseStatus.TryLater);
break; break;
case OCSPRespStatus.UNAUTHORIZED: case OCSPRespBuilder.UNAUTHORIZED:
ocspStatus.setResponseStatus(OcspStatus.ResponseStatus.Unauthorized); ocspStatus.setResponseStatus(OcspStatus.ResponseStatus.Unauthorized);
break; break;
default: default:
@ -329,7 +348,7 @@ public class OcspCertificateValidator {
} }
// only proceed if the response was successful // only proceed if the response was successful
if (ocspResponse.getStatus() != OCSPRespStatus.SUCCESSFUL) { if (ocspResponse.getStatus() != OCSPRespBuilder.SUCCESSFUL) {
logger.warn(String.format("OCSP request was unsuccessful (%s).", ocspStatus.getResponseStatus().toString())); logger.warn(String.format("OCSP request was unsuccessful (%s).", ocspStatus.getResponseStatus().toString()));
return ocspStatus; return ocspStatus;
} }
@ -337,7 +356,7 @@ public class OcspCertificateValidator {
// ensure the appropriate response object // ensure the appropriate response object
final Object ocspResponseObject = ocspResponse.getResponseObject(); final Object ocspResponseObject = ocspResponse.getResponseObject();
if (ocspResponseObject == null || !(ocspResponseObject instanceof BasicOCSPResp)) { if (ocspResponseObject == null || !(ocspResponseObject instanceof BasicOCSPResp)) {
logger.warn(String.format("Unexcepted OCSP response object: %s", ocspResponseObject)); logger.warn(String.format("Unexpected OCSP response object: %s", ocspResponseObject));
return ocspStatus; return ocspStatus;
} }
@ -345,9 +364,9 @@ public class OcspCertificateValidator {
final BasicOCSPResp basicOcspResponse = (BasicOCSPResp) ocspResponse.getResponseObject(); final BasicOCSPResp basicOcspResponse = (BasicOCSPResp) ocspResponse.getResponseObject();
// attempt to locate the responder certificate // attempt to locate the responder certificate
final X509Certificate[] responderCertificates = basicOcspResponse.getCerts(null); final X509CertificateHolder[] responderCertificates = basicOcspResponse.getCerts();
if (responderCertificates.length != 1) { if (responderCertificates.length != 1) {
logger.warn(String.format("Unexcepted number of OCSP responder certificates: %s", responderCertificates.length)); logger.warn(String.format("Unexpected number of OCSP responder certificates: %s", responderCertificates.length));
return ocspStatus; return ocspStatus;
} }
@ -355,7 +374,7 @@ public class OcspCertificateValidator {
final X509Certificate trustedResponderCertificate = getTrustedResponderCertificate(responderCertificates[0], issuerCertificate); final X509Certificate trustedResponderCertificate = getTrustedResponderCertificate(responderCertificates[0], issuerCertificate);
if (trustedResponderCertificate != null) { if (trustedResponderCertificate != null) {
// verify the response // verify the response
if (basicOcspResponse.verify(trustedResponderCertificate.getPublicKey(), null)) { if (basicOcspResponse.isSignatureValid(new JcaContentVerifierProviderBuilder().setProvider("BC").build(trustedResponderCertificate.getPublicKey()))) {
ocspStatus.setVerificationStatus(VerificationStatus.Verified); ocspStatus.setVerificationStatus(VerificationStatus.Verified);
} else { } else {
ocspStatus.setVerificationStatus(VerificationStatus.Unverified); ocspStatus.setVerificationStatus(VerificationStatus.Unverified);
@ -383,30 +402,40 @@ public class OcspCertificateValidator {
} }
} }
} }
} catch (final OCSPException | IOException | UniformInterfaceException | ClientHandlerException | NoSuchProviderException e) { } catch (final OCSPException | IOException | UniformInterfaceException | ClientHandlerException | OperatorCreationException e) {
logger.error(e.getMessage(), e); logger.error(e.getMessage(), e);
} catch (CertificateException e) {
e.printStackTrace();
} }
return ocspStatus; return ocspStatus;
} }
private ClientResponse getClientResponse(OCSPReq ocspRequest) throws IOException {
final WebResource resource = client.resource(validationAuthorityURI);
return resource.header(CONTENT_TYPE_HEADER, OCSP_REQUEST_CONTENT_TYPE).post(ClientResponse.class, ocspRequest.getEncoded());
}
/** /**
* Gets the trusted responder certificate. The response contains the responder certificate, however we cannot blindly trust it. Instead, we use a configured trusted CA. If the responder * Gets the trusted responder certificate. The response contains the responder certificate, however we cannot blindly trust it. Instead, we use a configured trusted CA. If the responder
* certificate is a trusted CA, then we can use it. If the responder certificate is not directly trusted, we still may be able to trust it if it was issued by the same CA that issued the subject * certificate is a trusted CA, then we can use it. If the responder certificate is not directly trusted, we still may be able to trust it if it was issued by the same CA that issued the subject
* certificate. Other various checks may be required (this portion is currently not implemented). * certificate. Other various checks may be required (this portion is currently not implemented).
* *
* @param responderCertificate cert * @param responderCertificateHolder cert
* @param issuerCertificate cert * @param issuerCertificate cert
* @return cert * @return cert
*/ */
private X509Certificate getTrustedResponderCertificate(final X509Certificate responderCertificate, final X509Certificate issuerCertificate) { private X509Certificate getTrustedResponderCertificate(final X509CertificateHolder responderCertificateHolder, final X509Certificate issuerCertificate) throws CertificateException {
// look for the responder's certificate specifically // look for the responder's certificate specifically
if (trustedCAs.containsKey(responderCertificate.getSubjectX500Principal().getName())) { final X509Certificate responderCertificate = new JcaX509CertificateConverter().setProvider("BC").getCertificate(responderCertificateHolder);
return trustedCAs.get(responderCertificate.getSubjectX500Principal().getName()); final String trustedCAName = responderCertificate.getSubjectX500Principal().getName();
if (trustedCAs.containsKey(trustedCAName)) {
return trustedCAs.get(trustedCAName);
} }
// if the responder certificate was issued by the same CA that issued the subject certificate we may be able to use that... // if the responder certificate was issued by the same CA that issued the subject certificate we may be able to use that...
if (responderCertificate.getIssuerX500Principal().equals(issuerCertificate.getSubjectX500Principal())) { final X500Principal issuerCA = issuerCertificate.getSubjectX500Principal();
if (responderCertificate.getIssuerX500Principal().equals(issuerCA)) {
// perform a number of verification steps... TODO... from sun.security.provider.certpath.OCSPResponse.java... currently incomplete... // perform a number of verification steps... TODO... from sun.security.provider.certpath.OCSPResponse.java... currently incomplete...
// try { // try {
// // ensure appropriate key usage // // ensure appropriate key usage

View File

@ -66,4 +66,13 @@ public class OcspRequest {
return true; return true;
} }
@Override
public String toString() {
return new StringBuilder("NiFi OCSP Request: ")
.append("Subject DN: ").append(subjectCertificate != null ? subjectCertificate.getSubjectDN().getName() : "<null>")
.append(" issued by ")
.append("Issuer DN: ").append(issuerCertificate != null ? issuerCertificate.getSubjectDN().getName() : "<null>").toString();
}
} }

View File

@ -0,0 +1,311 @@
/*
* 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.web.security.x509.ocsp;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x509.ExtendedKeyUsage;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import org.bouncycastle.asn1.x509.KeyUsage;
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
import org.bouncycastle.asn1.x509.X509Extension;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.math.BigInteger;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Security;
import java.security.SignatureException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Vector;
public class OcspCertificateValidatorTest {
private static final Logger logger = LoggerFactory.getLogger(OcspCertificateValidatorTest.class);
private static final int KEY_SIZE = 2048;
private static final long YESTERDAY = System.currentTimeMillis() - 24 * 60 * 60 * 1000;
private static final long ONE_YEAR_FROM_NOW = System.currentTimeMillis() + 365 * 24 * 60 * 60 * 1000;
private static final String SIGNATURE_ALGORITHM = "SHA256withRSA";
private static final String PROVIDER = "BC";
private static final String ISSUER_DN = "CN=NiFi Test CA,OU=Security,O=Apache,ST=CA,C=US";
private static X509Certificate ISSUER_CERTIFICATE;
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider());
// ISSUER_CERTIFICATE = generateCertificate(ISSUER_DN);
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
/**
* Generates a public/private RSA keypair using the default key size.
*
* @return the keypair
* @throws NoSuchAlgorithmException if the RSA algorithm is not available
*/
private static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(KEY_SIZE);
return keyPairGenerator.generateKeyPair();
}
/**
* Generates a signed certificate using an on-demand keypair.
*
* @param dn the DN
* @return the certificate
* @throws IOException if an exception occurs
* @throws NoSuchAlgorithmException if an exception occurs
* @throws CertificateException if an exception occurs
* @throws NoSuchProviderException if an exception occurs
* @throws SignatureException if an exception occurs
* @throws InvalidKeyException if an exception occurs
* @throws OperatorCreationException if an exception occurs
*/
private static X509Certificate generateCertificate(String dn) throws IOException, NoSuchAlgorithmException, CertificateException, NoSuchProviderException, SignatureException,
InvalidKeyException, OperatorCreationException {
KeyPair keyPair = generateKeyPair();
return generateCertificate(dn, keyPair);
}
/**
* Generates a signed certificate with a specific keypair.
*
* @param dn the DN
* @param keyPair the public key will be included in the certificate and the the private key is used to sign the certificate
* @return the certificate
* @throws IOException if an exception occurs
* @throws NoSuchAlgorithmException if an exception occurs
* @throws CertificateException if an exception occurs
* @throws NoSuchProviderException if an exception occurs
* @throws SignatureException if an exception occurs
* @throws InvalidKeyException if an exception occurs
* @throws OperatorCreationException if an exception occurs
*/
private static X509Certificate generateCertificate(String dn, KeyPair keyPair) throws IOException, NoSuchAlgorithmException, CertificateException, NoSuchProviderException, SignatureException,
InvalidKeyException, OperatorCreationException {
PrivateKey privateKey = keyPair.getPrivate();
ContentSigner sigGen = new JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(PROVIDER).build(privateKey);
SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(keyPair.getPublic().getEncoded());
Date startDate = new Date(YESTERDAY);
Date endDate = new Date(ONE_YEAR_FROM_NOW);
X509v3CertificateBuilder certBuilder = new X509v3CertificateBuilder(
new X500Name(dn),
BigInteger.valueOf(System.currentTimeMillis()),
startDate, endDate,
new X500Name(dn),
subPubKeyInfo);
// Set certificate extensions
// (1) digitalSignature extension
certBuilder.addExtension(X509Extension.keyUsage, true,
new KeyUsage(KeyUsage.digitalSignature | KeyUsage.keyEncipherment | KeyUsage.dataEncipherment | KeyUsage.keyAgreement));
// (2) extendedKeyUsage extension
Vector<KeyPurposeId> ekUsages = new Vector<>();
ekUsages.add(KeyPurposeId.id_kp_clientAuth);
ekUsages.add(KeyPurposeId.id_kp_serverAuth);
certBuilder.addExtension(X509Extension.extendedKeyUsage, false, new ExtendedKeyUsage(ekUsages));
// Sign the certificate
X509CertificateHolder certificateHolder = certBuilder.build(sigGen);
return new JcaX509CertificateConverter().setProvider(PROVIDER)
.getCertificate(certificateHolder);
}
/**
* Generates a certificate signed by the issuer key.
*
* @param dn the subject DN
* @param issuerDn the issuer DN
* @param issuerKey the issuer private key
* @return the certificate
* @throws IOException if an exception occurs
* @throws NoSuchAlgorithmException if an exception occurs
* @throws CertificateException if an exception occurs
* @throws NoSuchProviderException if an exception occurs
* @throws SignatureException if an exception occurs
* @throws InvalidKeyException if an exception occurs
* @throws OperatorCreationException if an exception occurs
*/
private static X509Certificate generateIssuedCertificate(String dn, String issuerDn, PrivateKey issuerKey) throws IOException, NoSuchAlgorithmException, CertificateException,
NoSuchProviderException, SignatureException, InvalidKeyException, OperatorCreationException {
KeyPair keyPair = generateKeyPair();
return generateIssuedCertificate(dn, keyPair.getPublic(), issuerDn, issuerKey);
}
/**
* Generates a certificate with a specific public key signed by the issuer key.
*
* @param dn the subject DN
* @param publicKey the subject public key
* @param issuerDn the issuer DN
* @param issuerKey the issuer private key
* @return the certificate
* @throws IOException if an exception occurs
* @throws NoSuchAlgorithmException if an exception occurs
* @throws CertificateException if an exception occurs
* @throws NoSuchProviderException if an exception occurs
* @throws SignatureException if an exception occurs
* @throws InvalidKeyException if an exception occurs
* @throws OperatorCreationException if an exception occurs
*/
private static X509Certificate generateIssuedCertificate(String dn, PublicKey publicKey, String issuerDn, PrivateKey issuerKey) throws IOException, NoSuchAlgorithmException,
CertificateException, NoSuchProviderException, SignatureException, InvalidKeyException, OperatorCreationException {
ContentSigner sigGen = new JcaContentSignerBuilder(SIGNATURE_ALGORITHM).setProvider(PROVIDER).build(issuerKey);
SubjectPublicKeyInfo subPubKeyInfo = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded());
Date startDate = new Date(YESTERDAY);
Date endDate = new Date(ONE_YEAR_FROM_NOW);
X509v3CertificateBuilder v3CertGen = new X509v3CertificateBuilder(
new X500Name(issuerDn),
BigInteger.valueOf(System.currentTimeMillis()),
startDate, endDate,
new X500Name(dn),
subPubKeyInfo);
X509CertificateHolder certificateHolder = v3CertGen.build(sigGen);
return new JcaX509CertificateConverter().setProvider(PROVIDER)
.getCertificate(certificateHolder);
}
@Test
public void testShouldGenerateCertificate() throws Exception {
// Arrange
final String testDn = "CN=This is a test";
// Act
X509Certificate certificate = generateCertificate(testDn);
logger.info("Generated certificate: \n{}", certificate);
// Assert
assert certificate.getSubjectDN().getName().equals(testDn);
assert certificate.getIssuerDN().getName().equals(testDn);
certificate.verify(certificate.getPublicKey());
}
@Test
public void testShouldGenerateCertificateFromKeyPair() throws Exception {
// Arrange
final String testDn = "CN=This is a test";
final KeyPair keyPair = generateKeyPair();
// Act
X509Certificate certificate = generateCertificate(testDn, keyPair);
logger.info("Generated certificate: \n{}", certificate);
// Assert
assert certificate.getPublicKey().equals(keyPair.getPublic());
assert certificate.getSubjectDN().getName().equals(testDn);
assert certificate.getIssuerDN().getName().equals(testDn);
certificate.verify(certificate.getPublicKey());
}
@Test
public void testShouldGenerateIssuedCertificate() throws Exception {
// Arrange
final String testDn = "CN=This is a signed test";
final String issuerDn = "CN=Issuer CA";
final KeyPair issuerKeyPair = generateKeyPair();
final PrivateKey issuerPrivateKey = issuerKeyPair.getPrivate();
final X509Certificate issuerCertificate = generateCertificate(issuerDn, issuerKeyPair);
logger.info("Generated issuer certificate: \n{}", issuerCertificate);
// Act
X509Certificate certificate = generateIssuedCertificate(testDn, issuerDn, issuerPrivateKey);
logger.info("Generated signed certificate: \n{}", certificate);
// Assert
assert issuerCertificate.getPublicKey().equals(issuerKeyPair.getPublic());
assert certificate.getSubjectX500Principal().getName().equals(testDn);
assert certificate.getIssuerX500Principal().getName().equals(issuerDn);
certificate.verify(issuerCertificate.getPublicKey());
try {
certificate.verify(certificate.getPublicKey());
Assert.fail("Should have thrown exception");
} catch (Exception e) {
assert e instanceof SignatureException;
assert e.getMessage().contains("certificate does not verify with supplied key");
}
}
@Ignore("To be implemented with Groovy test")
@Test
public void testShouldValidateCertificate() throws Exception {
}
@Ignore("To be implemented with Groovy test")
@Test
public void testShouldNotValidateEmptyCertificate() throws Exception {
}
@Ignore("To be implemented with Groovy test")
@Test
public void testShouldNotValidateInvalidCertificate() throws Exception {
}
@Ignore("To be implemented with Groovy test")
@Test
public void testValidateShouldHandleUnsignedResponse() throws Exception {
}
@Ignore("To be implemented with Groovy test")
@Test
public void testValidateShouldHandleResponseWithIncorrectNonce() throws Exception {
}
}

View File

@ -74,11 +74,11 @@ language governing permissions and limitations under the License. -->
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId> <artifactId>bcprov-jdk15on</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>bcpg-jdk16</artifactId> <artifactId>bcpg-jdk15on</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>commons-codec</groupId> <groupId>commons-codec</groupId>

View File

@ -16,16 +16,6 @@
*/ */
package org.apache.nifi.processors.standard; package org.apache.nifi.processors.standard;
import java.security.Security;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.annotation.behavior.EventDriven; import org.apache.nifi.annotation.behavior.EventDriven;
import org.apache.nifi.annotation.behavior.InputRequirement; import org.apache.nifi.annotation.behavior.InputRequirement;
@ -56,6 +46,16 @@ import org.apache.nifi.security.util.KeyDerivationFunction;
import org.apache.nifi.util.StopWatch; import org.apache.nifi.util.StopWatch;
import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.security.Security;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@EventDriven @EventDriven
@SideEffectFree @SideEffectFree
@SupportsBatching @SupportsBatching

View File

@ -16,17 +16,6 @@
*/ */
package org.apache.nifi.processors.standard.util; package org.apache.nifi.processors.standard.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
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.exception.ProcessException;
import org.apache.nifi.processor.io.StreamCallback; import org.apache.nifi.processor.io.StreamCallback;
import org.apache.nifi.processors.standard.EncryptContent; import org.apache.nifi.processors.standard.EncryptContent;
@ -41,6 +30,7 @@ import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData; import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator; import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPObjectFactory; import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPPrivateKey; import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKey; import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData; import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
@ -50,18 +40,41 @@ import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing; import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection; import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPUtil; 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;
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 static org.apache.nifi.processors.standard.util.PGPUtil.BLOCK_SIZE;
import static org.apache.nifi.processors.standard.util.PGPUtil.BUFFER_SIZE;
public class OpenPGPKeyBasedEncryptor implements Encryptor { public class OpenPGPKeyBasedEncryptor implements Encryptor {
private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptor.class);
private String algorithm; private String algorithm;
private String provider; private String provider;
// TODO: This can hold either the secret or public keyring path
private String keyring; private String keyring;
private String userId; private String userId;
private char[] passphrase; private char[] passphrase;
private String filename; private String filename;
public static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG";
public OpenPGPKeyBasedEncryptor(final String algorithm, final String provider, final String keyring, final String userId, final char[] passphrase, final 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.algorithm = algorithm;
this.provider = provider; this.provider = provider;
@ -81,75 +94,137 @@ public class OpenPGPKeyBasedEncryptor implements Encryptor {
return new OpenPGPDecryptCallback(provider, keyring, passphrase); return new OpenPGPDecryptCallback(provider, keyring, passphrase);
} }
/* /**
* Validate secret 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 { public static boolean validateKeyring(String provider, String secretKeyringFile, char[] passphrase) throws IOException, PGPException, NoSuchProviderException {
try (InputStream fin = Files.newInputStream(Paths.get(secretKeyringFile)); InputStream pin = PGPUtil.getDecoderStream(fin)) { try {
PGPSecretKeyRingCollection pgpsec = new PGPSecretKeyRingCollection(pin); getDecryptedPrivateKey(provider, secretKeyringFile, passphrase);
Iterator ringit = pgpsec.getKeyRings();
while (ringit.hasNext()) {
PGPSecretKeyRing secretkeyring = (PGPSecretKeyRing) ringit.next();
PGPSecretKey secretkey = secretkeyring.getSecretKey();
secretkey.extractPrivateKey(passphrase, provider);
return true; return true;
} } catch (Exception e) {
// If this point is reached, no private key could be extracted with the given passphrase
return false; 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. * Get the public key for a specific user id from a keyring.
*/ */
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
public static PGPPublicKey getPublicKey(String userId, String publicKeyring) throws IOException, PGPException { public static PGPPublicKey getPublicKey(String userId, String publicKeyringFile) throws IOException, PGPException {
PGPPublicKey pubkey = null; // TODO: Reevaluate the mechanism for executing this task as performance can suffer here and only a specific key needs to be validated
try (InputStream fin = Files.newInputStream(Paths.get(publicKeyring)); InputStream pin = PGPUtil.getDecoderStream(fin)) {
PGPPublicKeyRingCollection pgppub = new PGPPublicKeyRingCollection(pin);
Iterator ringit = pgppub.getKeyRings(); // Read in from the public keyring file
while (ringit.hasNext()) { try (FileInputStream keyInputStream = new FileInputStream(publicKeyringFile)) {
PGPPublicKeyRing kring = (PGPPublicKeyRing) ringit.next();
Iterator keyit = kring.getPublicKeys(); // Form the PublicKeyRing collection (1.53 way with fingerprint calculator)
while (keyit.hasNext()) { PGPPublicKeyRingCollection pgpPublicKeyRingCollection = new PGPPublicKeyRingCollection(keyInputStream, new BcKeyFingerprintCalculator());
pubkey = (PGPPublicKey) keyit.next();
boolean userIdMatch = false;
Iterator userit = pubkey.getUserIDs(); // Iterate over all public keyrings
while (userit.hasNext()) { Iterator<PGPPublicKeyRing> iter = pgpPublicKeyRingCollection.getKeyRings();
String id = userit.next().toString(); PGPPublicKeyRing keyRing;
if (id.contains(userId)) { while (iter.hasNext()) {
userIdMatch = true; keyRing = iter.next();
break;
} // Iterate over each public key in this keyring
} Iterator<PGPPublicKey> keyIter = keyRing.getPublicKeys();
if (pubkey.isEncryptionKey() && userIdMatch) { while (keyIter.hasNext()) {
return pubkey; 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;
} }
} }
} }
} }
return null; }
// 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 static class OpenPGPDecryptCallback implements StreamCallback {
private String provider; private String provider;
private String secretKeyring; private String secretKeyringFile;
private char[] passphrase; private char[] passphrase;
OpenPGPDecryptCallback(final String provider, final String keyring, final char[] passphrase) { OpenPGPDecryptCallback(final String provider, final String secretKeyringFile, final char[] passphrase) {
this.provider = provider; this.provider = provider;
this.secretKeyring = keyring; this.secretKeyringFile = secretKeyringFile;
this.passphrase = passphrase; this.passphrase = passphrase;
} }
@Override @Override
public void process(InputStream in, OutputStream out) throws IOException { public void process(InputStream in, OutputStream out) throws IOException {
try (InputStream pgpin = PGPUtil.getDecoderStream(in)) { try (InputStream pgpin = PGPUtil.getDecoderStream(in)) {
PGPObjectFactory pgpFactory = new PGPObjectFactory(pgpin); PGPObjectFactory pgpFactory = new PGPObjectFactory(pgpin, new BcKeyFingerprintCalculator());
Object obj = pgpFactory.nextObject(); Object obj = pgpFactory.nextObject();
if (!(obj instanceof PGPEncryptedDataList)) { if (!(obj instanceof PGPEncryptedDataList)) {
@ -160,19 +235,11 @@ public class OpenPGPKeyBasedEncryptor implements Encryptor {
} }
PGPEncryptedDataList encList = (PGPEncryptedDataList) obj; PGPEncryptedDataList encList = (PGPEncryptedDataList) obj;
PGPSecretKeyRingCollection pgpSecretKeyring;
try (InputStream secretKeyringIS = Files.newInputStream(Paths.get(secretKeyring)); InputStream pgpIS = PGPUtil.getDecoderStream(secretKeyringIS)) {
// open secret keyring file
pgpSecretKeyring = new PGPSecretKeyRingCollection(pgpIS);
} catch (Exception e) {
throw new ProcessException("Invalid secret keyring - " + e.getMessage());
}
try { try {
PGPPrivateKey privateKey = null; PGPPrivateKey privateKey = null;
PGPPublicKeyEncryptedData encData = null; PGPPublicKeyEncryptedData encData = null;
// find the secret key in the encrypted data // Find the secret key in the encrypted data
Iterator it = encList.getEncryptedDataObjects(); Iterator it = encList.getEncryptedDataObjects();
while (privateKey == null && it.hasNext()) { while (privateKey == null && it.hasNext()) {
obj = it.next(); obj = it.next();
@ -180,33 +247,61 @@ public class OpenPGPKeyBasedEncryptor implements Encryptor {
throw new ProcessException("Invalid OpenPGP data"); throw new ProcessException("Invalid OpenPGP data");
} }
encData = (PGPPublicKeyEncryptedData) obj; encData = (PGPPublicKeyEncryptedData) obj;
PGPSecretKey secretkey = pgpSecretKeyring.getSecretKey(encData.getKeyID());
if (secretkey != null) { // Check each encrypted data object to see if it contains the key ID for the secret key -> private key
privateKey = secretkey.extractPrivateKey(passphrase, provider); try {
privateKey = getDecryptedPrivateKey(provider, secretKeyringFile, encData.getKeyID(), passphrase);
} catch (PGPException e) {
// TODO: Log (expected) exception?
} }
} }
if (privateKey == null) { if (privateKey == null) {
throw new ProcessException("Secret keyring does not contain the key required to decrypt"); throw new ProcessException("Secret keyring does not contain the key required to decrypt");
} }
try (InputStream clearData = encData.getDataStream(privateKey, provider)) { // Read in the encrypted data stream and decrypt it
PGPObjectFactory clearFactory = new PGPObjectFactory(clearData); 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);
obj = clearFactory.nextObject(); Object message = plainFact.nextObject();
if (obj instanceof PGPCompressedData) {
PGPCompressedData compData = (PGPCompressedData) obj; // Check the message type and act accordingly
clearFactory = new PGPObjectFactory(compData.getDataStream());
obj = clearFactory.nextObject(); // If compressed, decompress
if (message instanceof PGPCompressedData) {
PGPCompressedData cData = (PGPCompressedData) message;
JcaPGPObjectFactory pgpFact = new JcaPGPObjectFactory(cData.getDataStream());
message = pgpFact.nextObject();
} }
PGPLiteralData literal = (PGPLiteralData) obj;
try (InputStream lis = literal.getInputStream()) { // If the message is literal data, read it and process to the out stream
final byte[] buffer = new byte[4096]; if (message instanceof PGPLiteralData) {
PGPLiteralData literalData = (PGPLiteralData) message;
try (InputStream lis = literalData.getInputStream()) {
final byte[] buffer = new byte[BLOCK_SIZE];
int len; int len;
while ((len = lis.read(buffer)) >= 0) { while ((len = lis.read(buffer)) >= 0) {
out.write(buffer, 0, len); 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) { } catch (Exception e) {
throw new ProcessException(e.getMessage()); throw new ProcessException(e.getMessage());
@ -235,6 +330,8 @@ public class OpenPGPKeyBasedEncryptor implements Encryptor {
@Override @Override
public void process(InputStream in, OutputStream out) throws IOException { public void process(InputStream in, OutputStream out) throws IOException {
PGPPublicKey publicKey; PGPPublicKey publicKey;
final boolean isArmored = EncryptContent.isPGPArmoredAlgorithm(algorithm);
try { try {
publicKey = getPublicKey(userId, publicKeyring); publicKey = getPublicKey(userId, publicKeyring);
} catch (Exception e) { } catch (Exception e) {
@ -242,35 +339,35 @@ public class OpenPGPKeyBasedEncryptor implements Encryptor {
} }
try { try {
SecureRandom secureRandom = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM);
OutputStream output = out; OutputStream output = out;
if (EncryptContent.isPGPArmoredAlgorithm(algorithm)) { if (isArmored) {
output = new ArmoredOutputStream(out); output = new ArmoredOutputStream(out);
} }
try { try {
PGPEncryptedDataGenerator encGenerator = new PGPEncryptedDataGenerator(PGPEncryptedData.CAST5, false, secureRandom, provider); // TODO: Refactor internal symmetric encryption algorithm to be customizable
encGenerator.addMethod(publicKey); PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(
try (OutputStream encOut = encGenerator.open(output, new byte[65536])) { new JcePGPDataEncryptorBuilder(PGPEncryptedData.AES_128).setWithIntegrityPacket(true).setSecureRandom(new SecureRandom()).setProvider(provider));
PGPCompressedDataGenerator compData = new PGPCompressedDataGenerator(PGPCompressedData.ZIP, Deflater.BEST_SPEED); encryptedDataGenerator.addMethod(new JcePublicKeyKeyEncryptionMethodGenerator(publicKey).setProvider(provider));
try (OutputStream compOut = compData.open(encOut, new byte[65536])) {
PGPLiteralDataGenerator literal = new PGPLiteralDataGenerator(); // TODO: Refactor shared encryption code to utility
try (OutputStream literalOut = literal.open(compOut, PGPLiteralData.BINARY, filename, new Date(), new byte[65536])) { 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[4096]; final byte[] buffer = new byte[BLOCK_SIZE];
int len; int len;
while ((len = in.read(buffer)) >= 0) { while ((len = in.read(buffer)) >= 0) {
literalOut.write(buffer, 0, len); literalOut.write(buffer, 0, len);
} }
} }
} }
} }
} finally { } finally {
if (EncryptContent.isPGPArmoredAlgorithm(algorithm)) { if (isArmored) {
output.close(); output.close();
} }
} }
@ -278,6 +375,5 @@ public class OpenPGPKeyBasedEncryptor implements Encryptor {
throw new ProcessException(e.getMessage()); throw new ProcessException(e.getMessage());
} }
} }
} }
} }

View File

@ -16,38 +16,39 @@
*/ */
package org.apache.nifi.processors.standard.util; package org.apache.nifi.processors.standard.util;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.io.StreamCallback;
import org.apache.nifi.processors.standard.EncryptContent.Encryptor;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPEncryptedData;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPPBEEncryptedData;
import org.bouncycastle.openpgp.jcajce.JcaPGPObjectFactory;
import org.bouncycastle.openpgp.operator.PBEDataDecryptorFactory;
import org.bouncycastle.openpgp.operator.PGPDigestCalculatorProvider;
import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator;
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPDigestCalculatorProviderBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBEDataDecryptorFactoryBuilder;
import org.bouncycastle.openpgp.operator.jcajce.JcePBEKeyEncryptionMethodGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.security.SecureRandom;
import java.util.Date;
import java.util.zip.Deflater;
import org.apache.nifi.processor.exception.ProcessException; import static org.bouncycastle.openpgp.PGPUtil.getDecoderStream;
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.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPPBEEncryptedData;
import org.bouncycastle.openpgp.PGPUtil;
public class OpenPGPPasswordBasedEncryptor implements Encryptor { public class OpenPGPPasswordBasedEncryptor implements Encryptor {
private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptor.class);
private String algorithm; private String algorithm;
private String provider; private String provider;
private char[] password; private char[] password;
private String filename; private String filename;
public static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG";
public OpenPGPPasswordBasedEncryptor(final String algorithm, final String provider, final char[] passphrase, final String filename) { public OpenPGPPasswordBasedEncryptor(final String algorithm, final String provider, final char[] passphrase, final String filename) {
this.algorithm = algorithm; this.algorithm = algorithm;
this.provider = provider; this.provider = provider;
@ -77,8 +78,8 @@ public class OpenPGPPasswordBasedEncryptor implements Encryptor {
@Override @Override
public void process(InputStream in, OutputStream out) throws IOException { public void process(InputStream in, OutputStream out) throws IOException {
InputStream pgpin = PGPUtil.getDecoderStream(in); InputStream pgpin = getDecoderStream(in);
PGPObjectFactory pgpFactory = new PGPObjectFactory(pgpin); JcaPGPObjectFactory pgpFactory = new JcaPGPObjectFactory(pgpin);
Object obj = pgpFactory.nextObject(); Object obj = pgpFactory.nextObject();
if (!(obj instanceof PGPEncryptedDataList)) { if (!(obj instanceof PGPEncryptedDataList)) {
@ -93,31 +94,41 @@ public class OpenPGPPasswordBasedEncryptor implements Encryptor {
if (!(obj instanceof PGPPBEEncryptedData)) { if (!(obj instanceof PGPPBEEncryptedData)) {
throw new ProcessException("Invalid OpenPGP data"); throw new ProcessException("Invalid OpenPGP data");
} }
PGPPBEEncryptedData encData = (PGPPBEEncryptedData) obj; PGPPBEEncryptedData encryptedData = (PGPPBEEncryptedData) obj;
try { try {
InputStream clearData = encData.getDataStream(password, provider); final PGPDigestCalculatorProvider digestCalculatorProvider = new JcaPGPDigestCalculatorProviderBuilder().setProvider(provider).build();
PGPObjectFactory clearFactory = new PGPObjectFactory(clearData); final PBEDataDecryptorFactory decryptorFactory = new JcePBEDataDecryptorFactoryBuilder(digestCalculatorProvider).setProvider(provider).build(password);
InputStream clear = encryptedData.getDataStream(decryptorFactory);
obj = clearFactory.nextObject(); JcaPGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(clear);
obj = pgpObjectFactory.nextObject();
if (obj instanceof PGPCompressedData) { if (obj instanceof PGPCompressedData) {
PGPCompressedData compData = (PGPCompressedData) obj; PGPCompressedData compressedData = (PGPCompressedData) obj;
clearFactory = new PGPObjectFactory(compData.getDataStream()); pgpObjectFactory = new JcaPGPObjectFactory(compressedData.getDataStream());
obj = clearFactory.nextObject(); obj = pgpObjectFactory.nextObject();
} }
PGPLiteralData literal = (PGPLiteralData) obj;
InputStream lis = literal.getInputStream(); PGPLiteralData literalData = (PGPLiteralData) obj;
final byte[] buffer = new byte[4096]; InputStream plainIn = literalData.getInputStream();
final byte[] buffer = new byte[org.apache.nifi.processors.standard.util.PGPUtil.BLOCK_SIZE];
int len; int len;
while ((len = lis.read(buffer)) >= 0) { while ((len = plainIn.read(buffer)) >= 0) {
out.write(buffer, 0, len); out.write(buffer, 0, len);
} }
if (encryptedData.isIntegrityProtected()) {
if (!encryptedData.verify()) {
throw new PGPException("Integrity check failed");
}
} else {
logger.warn("No message integrity check");
}
} catch (Exception e) { } catch (Exception e) {
throw new ProcessException(e.getMessage()); throw new ProcessException(e.getMessage());
} }
} }
} }
private static class OpenPGPEncryptCallback implements StreamCallback { private static class OpenPGPEncryptCallback implements StreamCallback {
@ -137,39 +148,11 @@ public class OpenPGPPasswordBasedEncryptor implements Encryptor {
@Override @Override
public void process(InputStream in, OutputStream out) throws IOException { public void process(InputStream in, OutputStream out) throws IOException {
try { try {
SecureRandom secureRandom = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM); PGPKeyEncryptionMethodGenerator encryptionMethodGenerator = new JcePBEKeyEncryptionMethodGenerator(password).setProvider(provider);
org.apache.nifi.processors.standard.util.PGPUtil.encrypt(in, out, algorithm, provider, PGPEncryptedData.AES_128, filename, encryptionMethodGenerator);
OutputStream output = out;
if (EncryptContent.isPGPArmoredAlgorithm(algorithm)) {
output = new ArmoredOutputStream(out);
}
PGPEncryptedDataGenerator encGenerator = new PGPEncryptedDataGenerator(PGPEncryptedData.CAST5, false,
secureRandom, provider);
encGenerator.addMethod(password);
OutputStream encOut = encGenerator.open(output, new byte[65536]);
PGPCompressedDataGenerator compData = new PGPCompressedDataGenerator(PGPCompressedData.ZIP, Deflater.BEST_SPEED);
OutputStream compOut = compData.open(encOut, new byte[65536]);
PGPLiteralDataGenerator literal = new PGPLiteralDataGenerator();
OutputStream literalOut = literal.open(compOut, PGPLiteralData.BINARY, filename, new Date(), new byte[65536]);
final byte[] buffer = new byte[4096];
int len;
while ((len = in.read(buffer)) >= 0) {
literalOut.write(buffer, 0, len);
}
literalOut.close();
compOut.close();
encOut.close();
output.close();
} catch (Exception e) { } catch (Exception e) {
throw new ProcessException(e.getMessage()); throw new ProcessException(e.getMessage());
} }
}
}
} }
} }

View File

@ -0,0 +1,89 @@
/*
* 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.processors.standard.util;
import org.apache.nifi.processors.standard.EncryptContent;
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.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator;
import org.bouncycastle.openpgp.operator.jcajce.JcePGPDataEncryptorBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.SecureRandom;
import java.util.Date;
import java.util.zip.Deflater;
/**
* This class contains static utility methods to assist with common PGP operations.
*/
public class PGPUtil {
private static final Logger logger = LoggerFactory.getLogger(PGPUtil.class);
public static final int BUFFER_SIZE = 65536;
public static final int BLOCK_SIZE = 4096;
public static void encrypt(InputStream in, OutputStream out, String algorithm, String provider, int cipher, String filename, PGPKeyEncryptionMethodGenerator encryptionMethodGenerator) throws IOException, PGPException {
final boolean isArmored = EncryptContent.isPGPArmoredAlgorithm(algorithm);
OutputStream output = out;
if (isArmored) {
output = new ArmoredOutputStream(out);
}
// Default value, do not allow null encryption
if (cipher == PGPEncryptedData.NULL) {
logger.warn("Null encryption not allowed; defaulting to AES-128");
cipher = PGPEncryptedData.AES_128;
}
try {
// TODO: Can probably hardcode provider to BC and remove one method parameter
PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(
new JcePGPDataEncryptorBuilder(cipher).setWithIntegrityPacket(true).setSecureRandom(new SecureRandom()).setProvider(provider));
encryptedDataGenerator.addMethod(encryptionMethodGenerator);
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();
}
}
}
}

View File

@ -16,12 +16,6 @@
*/ */
package org.apache.nifi.processors.standard; package org.apache.nifi.processors.standard;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.security.Security;
import java.util.Collection;
import org.apache.commons.codec.binary.Hex; import org.apache.commons.codec.binary.Hex;
import org.apache.nifi.components.ValidationResult; import org.apache.nifi.components.ValidationResult;
import org.apache.nifi.processors.standard.util.PasswordBasedEncryptor; import org.apache.nifi.processors.standard.util.PasswordBasedEncryptor;
@ -39,6 +33,12 @@ import org.junit.Test;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Paths;
import java.security.Security;
import java.util.Collection;
public class TestEncryptContent { public class TestEncryptContent {
private static final Logger logger = LoggerFactory.getLogger(TestEncryptContent.class); private static final Logger logger = LoggerFactory.getLogger(TestEncryptContent.class);
@ -203,6 +203,128 @@ public class TestEncryptContent {
flowFile.assertContentEquals(Paths.get("src/test/resources/TestEncryptContent/text.txt")); flowFile.assertContentEquals(Paths.get("src/test/resources/TestEncryptContent/text.txt"));
} }
@Test
public void testShouldValidatePGPPublicKeyringRequiresUserId() {
// Arrange
final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class);
Collection<ValidationResult> results;
MockProcessContext pc;
runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE);
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name());
runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/pubring.gpg");
runner.enqueue(new byte[0]);
pc = (MockProcessContext) runner.getProcessContext();
// Act
results = pc.validate();
// Assert
Assert.assertEquals(1, results.size());
ValidationResult vr = (ValidationResult) results.toArray()[0];
String expectedResult = " encryption without a " + EncryptContent.PASSWORD.getDisplayName() + " requires both "
+ EncryptContent.PUBLIC_KEYRING.getDisplayName() + " and "
+ EncryptContent.PUBLIC_KEY_USERID.getDisplayName();
String message = "'" + vr.toString() + "' contains '" + expectedResult + "'";
Assert.assertTrue(message, vr.toString().contains(expectedResult));
}
@Test
public void testShouldValidatePGPPublicKeyringExists() {
// Arrange
final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class);
Collection<ValidationResult> results;
MockProcessContext pc;
runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE);
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name());
runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/pubring.gpg.missing");
runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "USERID");
runner.enqueue(new byte[0]);
pc = (MockProcessContext) runner.getProcessContext();
// Act
results = pc.validate();
// Assert
Assert.assertEquals(1, results.size());
ValidationResult vr = (ValidationResult) results.toArray()[0];
String expectedResult = " (No such file or directory)";
String message = "'" + vr.toString() + "' contains '" + expectedResult + "'";
Assert.assertTrue(message, vr.toString().contains(expectedResult));
}
@Test
public void testShouldValidatePGPPublicKeyringIsProperFormat() {
// Arrange
final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class);
Collection<ValidationResult> results;
MockProcessContext pc;
runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE);
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name());
runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/text.txt");
runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "USERID");
runner.enqueue(new byte[0]);
pc = (MockProcessContext) runner.getProcessContext();
// Act
results = pc.validate();
// Assert
Assert.assertEquals(1, results.size());
ValidationResult vr = (ValidationResult) results.toArray()[0];
String expectedResult = " java.io.IOException: invalid header encountered";
String message = "'" + vr.toString() + "' contains '" + expectedResult + "'";
Assert.assertTrue(message, vr.toString().contains(expectedResult));
}
@Test
public void testShouldValidatePGPPublicKeyringContainsUserId() {
// Arrange
final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class);
Collection<ValidationResult> results;
MockProcessContext pc;
runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE);
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name());
runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/pubring.gpg");
runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "USERID");
runner.enqueue(new byte[0]);
pc = (MockProcessContext) runner.getProcessContext();
// Act
results = pc.validate();
// Assert
Assert.assertEquals(1, results.size());
ValidationResult vr = (ValidationResult) results.toArray()[0];
String expectedResult = "PGPException: Could not find a public key with the given userId";
String message = "'" + vr.toString() + "' contains '" + expectedResult + "'";
Assert.assertTrue(message, vr.toString().contains(expectedResult));
}
@Test
public void testShouldExtractPGPPublicKeyFromKeyring() {
// Arrange
final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class);
Collection<ValidationResult> results;
MockProcessContext pc;
runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE);
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.PGP.name());
runner.setProperty(EncryptContent.PUBLIC_KEYRING, "src/test/resources/TestEncryptContent/pubring.gpg");
runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "NiFi PGP Test Key (Short test key for NiFi PGP unit tests) <alopresto.apache+test@gmail.com>");
runner.enqueue(new byte[0]);
pc = (MockProcessContext) runner.getProcessContext();
// Act
results = pc.validate();
// Assert
Assert.assertEquals(0, results.size());
}
@Test @Test
public void testValidation() { public void testValidation() {
final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class); final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class);
@ -249,20 +371,15 @@ public class TestEncryptContent {
+ EncryptContent.PUBLIC_KEY_USERID.getDisplayName())); + EncryptContent.PUBLIC_KEY_USERID.getDisplayName()));
} }
runner.setProperty(EncryptContent.PUBLIC_KEY_USERID, "USERID"); // Legacy tests moved to individual tests to comply with new library
runner.enqueue(new byte[0]);
pc = (MockProcessContext) runner.getProcessContext(); // TODO: Move secring tests out to individual as well
results = pc.validate();
Assert.assertEquals(1, results.size());
for (final ValidationResult vr : results) {
Assert.assertTrue(vr.toString().contains("does not contain user id USERID"));
}
runner.removeProperty(EncryptContent.PUBLIC_KEYRING); runner.removeProperty(EncryptContent.PUBLIC_KEYRING);
runner.removeProperty(EncryptContent.PUBLIC_KEY_USERID); runner.removeProperty(EncryptContent.PUBLIC_KEY_USERID);
runner.setProperty(EncryptContent.MODE, EncryptContent.DECRYPT_MODE); runner.setProperty(EncryptContent.MODE, EncryptContent.DECRYPT_MODE);
runner.setProperty(EncryptContent.PRIVATE_KEYRING, "src/test/resources/TestEncryptContent/text.txt"); runner.setProperty(EncryptContent.PRIVATE_KEYRING, "src/test/resources/TestEncryptContent/secring.gpg");
runner.enqueue(new byte[0]); runner.enqueue(new byte[0]);
pc = (MockProcessContext) runner.getProcessContext(); pc = (MockProcessContext) runner.getProcessContext();
results = pc.validate(); results = pc.validate();

View File

@ -0,0 +1,129 @@
/*
* 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.processors.standard.util;
import org.apache.commons.codec.binary.Hex;
import org.apache.nifi.processor.io.StreamCallback;
import org.apache.nifi.security.util.EncryptionMethod;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.Security;
public class OpenPGPKeyBasedEncryptorTest {
private static final Logger logger = LoggerFactory.getLogger(OpenPGPKeyBasedEncryptorTest.class);
private final File plainFile = new File("src/test/resources/TestEncryptContent/text.txt");
private final File unsignedFile = new File("src/test/resources/TestEncryptContent/text.txt.unsigned.gpg");
private final File encryptedFile = new File("src/test/resources/TestEncryptContent/text.txt.gpg");
private static final String SECRET_KEYRING_PATH = "src/test/resources/TestEncryptContent/secring.gpg";
private static final String PUBLIC_KEYRING_PATH = "src/test/resources/TestEncryptContent/pubring.gpg";
private static final String USER_ID = "NiFi PGP Test Key (Short test key for NiFi PGP unit tests) <alopresto.apache+test@gmail.com>";
private static final String PASSWORD = "thisIsABadPassword";
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider());
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void testShouldEncryptAndDecrypt() throws Exception {
// Arrange
final String PLAINTEXT = "This is a plaintext message.";
logger.info("Plaintext: {}", PLAINTEXT);
InputStream plainStream = new ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8"));
OutputStream cipherStream = new ByteArrayOutputStream();
OutputStream recoveredStream = new ByteArrayOutputStream();
// No file, just streams
String filename = "tempFile.txt";
// Encryptor does not require password
OpenPGPKeyBasedEncryptor encryptor = new OpenPGPKeyBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), EncryptionMethod.PGP.getProvider(), PUBLIC_KEYRING_PATH, USER_ID, new char[0], filename);
StreamCallback encryptionCallback = encryptor.getEncryptionCallback();
OpenPGPKeyBasedEncryptor decryptor = new OpenPGPKeyBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID, PASSWORD.toCharArray(), filename);
StreamCallback decryptionCallback = decryptor.getDecryptionCallback();
// Act
encryptionCallback.process(plainStream, cipherStream);
final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray();
logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes));
InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes);
decryptionCallback.process(cipherInputStream, recoveredStream);
// Assert
byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray();
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: {}", recovered);
assert PLAINTEXT.equals(recovered);
}
@Test
public void testShouldDecryptExternalFile() throws Exception {
// Arrange
byte[] plainBytes = Files.readAllBytes(Paths.get(plainFile.getPath()));
final String PLAINTEXT = new String(plainBytes, "UTF-8");
InputStream cipherStream = new FileInputStream(unsignedFile);
OutputStream recoveredStream = new ByteArrayOutputStream();
// No file, just streams
String filename = unsignedFile.getName();
OpenPGPKeyBasedEncryptor encryptor = new OpenPGPKeyBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), EncryptionMethod.PGP.getProvider(), SECRET_KEYRING_PATH, USER_ID, PASSWORD.toCharArray(), filename);
StreamCallback decryptionCallback = encryptor.getDecryptionCallback();
// Act
decryptionCallback.process(cipherStream, recoveredStream);
// Assert
byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray();
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: {}", recovered);
Assert.assertEquals("Recovered text", PLAINTEXT, recovered);
}
}

View File

@ -0,0 +1,123 @@
/*
* 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.processors.standard.util;
import org.apache.commons.codec.binary.Hex;
import org.apache.nifi.processor.io.StreamCallback;
import org.apache.nifi.security.util.EncryptionMethod;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.Security;
public class OpenPGPPasswordBasedEncryptorTest {
private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptorTest.class);
private final File plainFile = new File("src/test/resources/TestEncryptContent/text.txt");
private final File encryptedFile = new File("src/test/resources/TestEncryptContent/text.txt.asc");
private static final String PASSWORD = "thisIsABadPassword";
private static final String LEGACY_PASSWORD = "Hello, World!";
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider());
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void testShouldEncryptAndDecrypt() throws Exception {
// Arrange
final String PLAINTEXT = "This is a plaintext message.";
logger.info("Plaintext: {}", PLAINTEXT);
InputStream plainStream = new java.io.ByteArrayInputStream(PLAINTEXT.getBytes("UTF-8"));
OutputStream cipherStream = new ByteArrayOutputStream();
OutputStream recoveredStream = new ByteArrayOutputStream();
// No file, just streams
String filename = "tempFile.txt";
OpenPGPPasswordBasedEncryptor encryptor = new OpenPGPPasswordBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), EncryptionMethod.PGP.getProvider(), PASSWORD.toCharArray(), filename);
StreamCallback encryptionCallback = encryptor.getEncryptionCallback();
StreamCallback decryptionCallback = encryptor.getDecryptionCallback();
// Act
encryptionCallback.process(plainStream, cipherStream);
final byte[] cipherBytes = ((ByteArrayOutputStream) cipherStream).toByteArray();
logger.info("Encrypted: {}", Hex.encodeHexString(cipherBytes));
InputStream cipherInputStream = new ByteArrayInputStream(cipherBytes);
decryptionCallback.process(cipherInputStream, recoveredStream);
// Assert
byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray();
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: {}", recovered);
assert PLAINTEXT.equals(recovered);
}
@Test
public void testShouldDecryptExternalFile() throws Exception {
// Arrange
byte[] plainBytes = Files.readAllBytes(Paths.get(plainFile.getPath()));
final String PLAINTEXT = new String(plainBytes, "UTF-8");
InputStream cipherStream = new FileInputStream(encryptedFile);
OutputStream recoveredStream = new ByteArrayOutputStream();
// No file, just streams
String filename = encryptedFile.getName();
OpenPGPPasswordBasedEncryptor encryptor = new OpenPGPPasswordBasedEncryptor(EncryptionMethod.PGP.getAlgorithm(), EncryptionMethod.PGP.getProvider(), LEGACY_PASSWORD.toCharArray(), filename);
StreamCallback decryptionCallback = encryptor.getDecryptionCallback();
// Act
decryptionCallback.process(cipherStream, recoveredStream);
// Assert
byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray();
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: {}", recovered);
Assert.assertEquals("Recovered text", PLAINTEXT, recovered);
}
}

13
pom.xml
View File

@ -241,13 +241,18 @@ language governing permissions and limitations under the License. -->
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId> <artifactId>bcprov-jdk15on</artifactId>
<version>1.46</version> <version>1.53</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.bouncycastle</groupId> <groupId>org.bouncycastle</groupId>
<artifactId>bcpg-jdk16</artifactId> <artifactId>bcpg-jdk15on</artifactId>
<version>1.46</version> <version>1.53</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.53</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.jcraft</groupId> <groupId>com.jcraft</groupId>