http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/NiFiLegacyCipherProvider.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/NiFiLegacyCipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/NiFiLegacyCipherProvider.java new file mode 100644 index 0000000..6918fe2 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/NiFiLegacyCipherProvider.java @@ -0,0 +1,112 @@ +/* + * 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.crypto; + +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.security.util.EncryptionMethod; +import org.apache.nifi.stream.io.StreamUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Provides a cipher initialized with the original NiFi key derivation process for password-based encryption (MD5 @ 1000 iterations). This is not a secure + * {@link org.apache.nifi.security.util.KeyDerivationFunction} (KDF) and should no longer be used. + * It is provided only for backward-compatibility with legacy data. A strong KDF should be selected for any future use. + * + * @see BcryptCipherProvider + * @see ScryptCipherProvider + * @see PBKDF2CipherProvider + */ +@Deprecated +public class NiFiLegacyCipherProvider extends OpenSSLPKCS5CipherProvider implements PBECipherProvider { + private static final Logger logger = LoggerFactory.getLogger(NiFiLegacyCipherProvider.class); + + // Legacy magic number value + private static final int ITERATION_COUNT = 1000; + + /** + * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived using the NiFi legacy code, based on @see org.apache.nifi.processors.standard.util.crypto + * .OpenSSLPKCS5CipherProvider#getCipher(java.lang.String, java.lang.String, java.lang.String, boolean) [essentially {@code MD5(password || salt) * 1000 }]. + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param keyLength the desired key length in bits (ignored because OpenSSL ciphers provide key length in algorithm name) + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, int keyLength, boolean encryptMode) throws Exception { + return getCipher(encryptionMethod, password, new byte[0], keyLength, encryptMode); + } + + /** + * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived using the NiFi legacy code, based on @see org.apache.nifi.processors.standard.util.crypto + * .OpenSSLPKCS5CipherProvider#getCipher(java.lang.String, java.lang.String, java.lang.String, byte[], boolean) [essentially {@code MD5(password || salt) * 1000 }]. + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param salt the salt + * @param keyLength the desired key length in bits (ignored because OpenSSL ciphers provide key length in algorithm name) + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, int keyLength, boolean encryptMode) throws Exception { + try { + // This method is defined in the OpenSSL implementation and just uses a locally-overridden iteration count + return getInitializedCipher(encryptionMethod, password, salt, encryptMode); + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new ProcessException("Error initializing the cipher", e); + } + } + + @Override + public byte[] readSalt(InputStream in) throws IOException, ProcessException { + if (in == null) { + throw new IllegalArgumentException("Cannot read salt from null InputStream"); + } + + // The first 16 bytes of the input stream are the salt + if (in.available() < getDefaultSaltLength()) { + throw new ProcessException("The cipher stream is too small to contain the salt"); + } + byte[] salt = new byte[getDefaultSaltLength()]; + StreamUtils.fillBuffer(in, salt); + return salt; + } + + @Override + public void writeSalt(byte[] salt, OutputStream out) throws IOException { + if (out == null) { + throw new IllegalArgumentException("Cannot write salt to null OutputStream"); + } + out.write(salt); + } + + protected int getIterationCount() { + return ITERATION_COUNT; + } +}
http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/OpenPGPKeyBasedEncryptor.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/OpenPGPKeyBasedEncryptor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/OpenPGPKeyBasedEncryptor.java new file mode 100644 index 0000000..f0f8631 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/OpenPGPKeyBasedEncryptor.java @@ -0,0 +1,380 @@ +/* + * 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.crypto; + +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; + +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 { + 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()); + } + } + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/OpenPGPPasswordBasedEncryptor.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/OpenPGPPasswordBasedEncryptor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/OpenPGPPasswordBasedEncryptor.java new file mode 100644 index 0000000..93e565a --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/OpenPGPPasswordBasedEncryptor.java @@ -0,0 +1,158 @@ +/* + * 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.crypto; + +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.InputStream; +import java.io.OutputStream; + +import static org.bouncycastle.openpgp.PGPUtil.getDecoderStream; + +public class OpenPGPPasswordBasedEncryptor implements Encryptor { + private static final Logger logger = LoggerFactory.getLogger(OpenPGPPasswordBasedEncryptor.class); + + private String algorithm; + private String provider; + private char[] password; + private String filename; + + public OpenPGPPasswordBasedEncryptor(final String algorithm, final String provider, final char[] passphrase, final String filename) { + this.algorithm = algorithm; + this.provider = provider; + this.password = passphrase; + this.filename = filename; + } + + @Override + public StreamCallback getEncryptionCallback() throws Exception { + return new OpenPGPEncryptCallback(algorithm, provider, password, filename); + } + + @Override + public StreamCallback getDecryptionCallback() throws Exception { + return new OpenPGPDecryptCallback(provider, password); + } + + private static class OpenPGPDecryptCallback implements StreamCallback { + + private String provider; + private char[] password; + + OpenPGPDecryptCallback(final String provider, final char[] password) { + this.provider = provider; + this.password = password; + } + + @Override + public void process(InputStream in, OutputStream out) throws IOException { + InputStream pgpin = getDecoderStream(in); + JcaPGPObjectFactory pgpFactory = new JcaPGPObjectFactory(pgpin); + + 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; + + obj = encList.get(0); + if (!(obj instanceof PGPPBEEncryptedData)) { + throw new ProcessException("Invalid OpenPGP data"); + } + PGPPBEEncryptedData encryptedData = (PGPPBEEncryptedData) obj; + + try { + final PGPDigestCalculatorProvider digestCalculatorProvider = new JcaPGPDigestCalculatorProviderBuilder().setProvider(provider).build(); + final PBEDataDecryptorFactory decryptorFactory = new JcePBEDataDecryptorFactoryBuilder(digestCalculatorProvider).setProvider(provider).build(password); + InputStream clear = encryptedData.getDataStream(decryptorFactory); + + JcaPGPObjectFactory pgpObjectFactory = new JcaPGPObjectFactory(clear); + + obj = pgpObjectFactory.nextObject(); + if (obj instanceof PGPCompressedData) { + PGPCompressedData compressedData = (PGPCompressedData) obj; + pgpObjectFactory = new JcaPGPObjectFactory(compressedData.getDataStream()); + obj = pgpObjectFactory.nextObject(); + } + + PGPLiteralData literalData = (PGPLiteralData) obj; + InputStream plainIn = literalData.getInputStream(); + final byte[] buffer = new byte[org.apache.nifi.processors.standard.util.PGPUtil.BLOCK_SIZE]; + int len; + while ((len = plainIn.read(buffer)) >= 0) { + 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) { + throw new ProcessException(e.getMessage()); + } + } + } + + private static class OpenPGPEncryptCallback implements StreamCallback { + + private String algorithm; + private String provider; + private char[] password; + private String filename; + + OpenPGPEncryptCallback(final String algorithm, final String provider, final char[] password, final String filename) { + this.algorithm = algorithm; + this.provider = provider; + this.password = password; + this.filename = filename; + } + + @Override + public void process(InputStream in, OutputStream out) throws IOException { + try { + PGPKeyEncryptionMethodGenerator encryptionMethodGenerator = new JcePBEKeyEncryptionMethodGenerator(password).setProvider(provider); + org.apache.nifi.processors.standard.util.PGPUtil.encrypt(in, out, algorithm, provider, PGPEncryptedData.AES_128, filename, encryptionMethodGenerator); + } catch (Exception e) { + throw new ProcessException(e.getMessage()); + } + } + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/OpenSSLPKCS5CipherProvider.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/OpenSSLPKCS5CipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/OpenSSLPKCS5CipherProvider.java new file mode 100644 index 0000000..049dbd8 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/OpenSSLPKCS5CipherProvider.java @@ -0,0 +1,211 @@ +/* + * 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.crypto; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.security.util.EncryptionMethod; +import org.apache.nifi.stream.io.StreamUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.PBEParameterSpec; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.util.Arrays; + +public class OpenSSLPKCS5CipherProvider implements PBECipherProvider { + private static final Logger logger = LoggerFactory.getLogger(OpenSSLPKCS5CipherProvider.class); + + // Legacy magic number value + private static final int ITERATION_COUNT = 0; + private static final int DEFAULT_SALT_LENGTH = 8; + private static final byte[] EMPTY_SALT = new byte[8]; + + private static final String OPENSSL_EVP_HEADER_MARKER = "Salted__"; + private static final int OPENSSL_EVP_HEADER_SIZE = 8; + + /** + * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived using the + * <a href="https://www.openssl.org/docs/manmaster/crypto/EVP_BytesToKey.html">OpenSSL EVP_BytesToKey proprietary KDF</a> [essentially {@code MD5(password || salt) }]. + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param keyLength the desired key length in bits (ignored because OpenSSL ciphers provide key length in algorithm name) + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, int keyLength, boolean encryptMode) throws Exception { + return getCipher(encryptionMethod, password, new byte[0], keyLength, encryptMode); + } + + /** + * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived using the + * <a href="https://www.openssl.org/docs/manmaster/crypto/EVP_BytesToKey.html">OpenSSL EVP_BytesToKey proprietary KDF</a> [essentially {@code MD5(password || salt) }]. + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param salt the salt + * @param keyLength the desired key length in bits (ignored because OpenSSL ciphers provide key length in algorithm name) + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, int keyLength, boolean encryptMode) throws Exception { + try { + return getInitializedCipher(encryptionMethod, password, salt, encryptMode); + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new ProcessException("Error initializing the cipher", e); + } + } + + /** + * Convenience method without key length parameter. See {@link OpenSSLPKCS5CipherProvider#getCipher(EncryptionMethod, String, int, boolean)} + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, boolean encryptMode) throws Exception { + return getCipher(encryptionMethod, password, new byte[0], -1, encryptMode); + } + + /** + * Convenience method without key length parameter. See {@link OpenSSLPKCS5CipherProvider#getCipher(EncryptionMethod, String, byte[], int, boolean)} + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param salt the salt + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, boolean encryptMode) throws Exception { + return getCipher(encryptionMethod, password, salt, -1, encryptMode); + } + + protected Cipher getInitializedCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, boolean encryptMode) + throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, + InvalidAlgorithmParameterException { + if (encryptionMethod == null) { + throw new IllegalArgumentException("The encryption method must be specified"); + } + + if (StringUtils.isEmpty(password)) { + throw new IllegalArgumentException("Encryption with an empty password is not supported"); + } + + if (salt.length != DEFAULT_SALT_LENGTH && salt.length != 0) { + // This does not enforce ASCII encoding, just length + throw new IllegalArgumentException("Salt must be 8 bytes US-ASCII encoded or empty"); + } + + String algorithm = encryptionMethod.getAlgorithm(); + String provider = encryptionMethod.getProvider(); + + // Initialize secret key from password + final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray()); + final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, provider); + SecretKey tempKey = factory.generateSecret(pbeKeySpec); + + final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, getIterationCount()); + Cipher cipher = Cipher.getInstance(algorithm, provider); + cipher.init(encryptMode ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, tempKey, parameterSpec); + return cipher; + } + + protected int getIterationCount() { + return ITERATION_COUNT; + } + + @Override + public byte[] generateSalt() { + byte[] salt = new byte[getDefaultSaltLength()]; + new SecureRandom().nextBytes(salt); + return salt; + } + + @Override + public int getDefaultSaltLength() { + return DEFAULT_SALT_LENGTH; + } + + /** + * Returns the salt provided as part of the cipher stream, or throws an exception if one cannot be detected. + * + * @param in the cipher InputStream + * @return the salt + */ + @Override + public byte[] readSalt(InputStream in) throws IOException { + if (in == null) { + throw new IllegalArgumentException("Cannot read salt from null InputStream"); + } + + // The header and salt format is "Salted__salt x8b" in ASCII + byte[] salt = new byte[DEFAULT_SALT_LENGTH]; + + // Try to read the header and salt from the input + byte[] header = new byte[OPENSSL_EVP_HEADER_SIZE]; + + // Mark the stream in case there is no salt + in.mark(OPENSSL_EVP_HEADER_SIZE + 1); + StreamUtils.fillBuffer(in, header); + + final byte[] headerMarkerBytes = OPENSSL_EVP_HEADER_MARKER.getBytes(StandardCharsets.US_ASCII); + + if (!Arrays.equals(headerMarkerBytes, header)) { + // No salt present + salt = new byte[0]; + // Reset the stream because we skipped 8 bytes of cipher text + in.reset(); + } + + StreamUtils.fillBuffer(in, salt); + return salt; + } + + @Override + public void writeSalt(byte[] salt, OutputStream out) throws IOException { + if (out == null) { + throw new IllegalArgumentException("Cannot write salt to null OutputStream"); + } + + out.write(OPENSSL_EVP_HEADER_MARKER.getBytes(StandardCharsets.US_ASCII)); + out.write(salt); + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/PBECipherProvider.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/PBECipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/PBECipherProvider.java new file mode 100644 index 0000000..8677656 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/PBECipherProvider.java @@ -0,0 +1,84 @@ +/* + * 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.crypto; + +import org.apache.nifi.security.util.EncryptionMethod; + +import javax.crypto.Cipher; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public interface PBECipherProvider extends CipherProvider { + /** + * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived by the KDF of the implementation. + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param keyLength the desired key length in bits + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + Cipher getCipher(EncryptionMethod encryptionMethod, String password, int keyLength, boolean encryptMode) throws Exception; + + /** + * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived by the KDF of the implementation. + * <p/> + * The IV can be retrieved by the calling method using {@link Cipher#getIV()}. + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param salt the salt + * @param keyLength the desired key length in bits + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, int keyLength, boolean encryptMode) throws Exception; + + /** + * Returns a random salt suitable for this cipher provider. + * + * @return a random salt + * @see PBECipherProvider#getDefaultSaltLength() + */ + byte[] generateSalt(); + + /** + * Returns the default salt length for this implementation. + * + * @return the default salt length in bytes + */ + int getDefaultSaltLength(); + + /** + * Returns the salt provided as part of the cipher stream, or throws an exception if one cannot be detected. + * + * @param in the cipher InputStream + * @return the salt + */ + byte[] readSalt(InputStream in) throws IOException; + + /** + * Writes the salt provided as part of the cipher stream, or throws an exception if it cannot be written. + * + * @param salt the salt + * @param out the cipher OutputStream + */ + void writeSalt(byte[] salt, OutputStream out) throws IOException; +} http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/PBKDF2CipherProvider.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/PBKDF2CipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/PBKDF2CipherProvider.java new file mode 100644 index 0000000..748d77f --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/PBKDF2CipherProvider.java @@ -0,0 +1,218 @@ +/* + * 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.crypto; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.security.util.EncryptionMethod; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.MD5Digest; +import org.bouncycastle.crypto.digests.SHA1Digest; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.digests.SHA384Digest; +import org.bouncycastle.crypto.digests.SHA512Digest; +import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator; +import org.bouncycastle.crypto.params.KeyParameter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; + +public class PBKDF2CipherProvider extends RandomIVPBECipherProvider { + private static final Logger logger = LoggerFactory.getLogger(PBKDF2CipherProvider.class); + private static final int DEFAULT_SALT_LENGTH = 16; + + private final int iterationCount; + private final Digest prf; + + private static final String DEFAULT_PRF = "SHA-512"; + /** + * This can be calculated automatically using the code {@see PBKDF2CipherProviderGroovyTest#calculateMinimumIterationCount} or manually updated by a maintainer + */ + private static final int DEFAULT_ITERATION_COUNT = 160_000; + + /** + * Instantiates a PBKDF2 cipher provider with the default number of iterations and the default PRF. Currently 128,000 iterations and SHA-512. + */ + public PBKDF2CipherProvider() { + this(DEFAULT_PRF, DEFAULT_ITERATION_COUNT); + } + + /** + * Instantiates a PBKDF2 cipher provider with the specified number of iterations and the specified PRF. Currently supports MD5, SHA1, SHA256, SHA384, and SHA512. Unknown PRFs will default to + * SHA512. + * + * @param prf a String representation of the PRF name, e.g. "SHA256", "SHA-384" "sha_512" + * @param iterationCount the number of iterations + */ + public PBKDF2CipherProvider(String prf, int iterationCount) { + this.iterationCount = iterationCount; + if (iterationCount < DEFAULT_ITERATION_COUNT) { + logger.warn("The provided iteration count {} is below the recommended minimum {}", iterationCount, DEFAULT_ITERATION_COUNT); + } + this.prf = resolvePRF(prf); + } + + /** + * Returns an initialized cipher for the specified algorithm. The key is derived by the KDF of the implementation. The IV is provided externally to allow for non-deterministic IVs, as IVs + * deterministically derived from the password are a potential vulnerability and compromise semantic security. See + * <a href="http://crypto.stackexchange.com/a/3970/12569">Ilmari Karonen's answer on Crypto Stack Exchange</a> + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param salt the salt + * @param iv the IV + * @param keyLength the desired key length in bits + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception { + try { + return getInitializedCipher(encryptionMethod, password, salt, iv, keyLength, encryptMode); + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new ProcessException("Error initializing the cipher", e); + } + } + + @Override + Logger getLogger() { + return logger; + } + + /** + * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived by the KDF of the implementation. + * + * The IV can be retrieved by the calling method using {@link Cipher#getIV()}. + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param keyLength the desired key length in bits + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, int keyLength, boolean encryptMode) throws Exception { + return getCipher(encryptionMethod, password, new byte[0], new byte[0], keyLength, encryptMode); + } + + /** + * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived by the KDF of the implementation. + * + * The IV can be retrieved by the calling method using {@link Cipher#getIV()}. + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param salt the salt + * @param keyLength the desired key length in bits + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, int keyLength, boolean encryptMode) throws Exception { + return getCipher(encryptionMethod, password, salt, new byte[0], keyLength, encryptMode); + } + + protected Cipher getInitializedCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception { + if (encryptionMethod == null) { + throw new IllegalArgumentException("The encryption method must be specified"); + } + + if (!encryptionMethod.isCompatibleWithStrongKDFs()) { + throw new IllegalArgumentException(encryptionMethod.name() + " is not compatible with PBKDF2"); + } + + String algorithm = encryptionMethod.getAlgorithm(); + + final String cipherName = CipherUtility.parseCipherFromAlgorithm(algorithm); + if (!CipherUtility.isValidKeyLength(keyLength, cipherName)) { + throw new IllegalArgumentException(String.valueOf(keyLength) + " is not a valid key length for " + cipherName); + } + + if (StringUtils.isEmpty(password)) { + throw new IllegalArgumentException("Encryption with an empty password is not supported"); + } + + if (salt == null || salt.length < DEFAULT_SALT_LENGTH) { + throw new IllegalArgumentException("The salt must be at least " + DEFAULT_SALT_LENGTH + " bytes. To generate a salt, use PBKDF2CipherProvider#generateSalt()"); + } + + PKCS5S2ParametersGenerator gen = new PKCS5S2ParametersGenerator(this.prf); + gen.init(password.getBytes(StandardCharsets.UTF_8), salt, getIterationCount()); + byte[] dk = ((KeyParameter) gen.generateDerivedParameters(keyLength)).getKey(); + SecretKey tempKey = new SecretKeySpec(dk, algorithm); + + KeyedCipherProvider keyedCipherProvider = new AESKeyedCipherProvider(); + return keyedCipherProvider.getCipher(encryptionMethod, tempKey, iv, encryptMode); + } + + @Override + public byte[] generateSalt() { + byte[] salt = new byte[DEFAULT_SALT_LENGTH]; + new SecureRandom().nextBytes(salt); + return salt; + } + + @Override + public int getDefaultSaltLength() { + return DEFAULT_SALT_LENGTH; + } + + protected int getIterationCount() { + return iterationCount; + } + + protected String getPRFName() { + if (prf != null) { + return prf.getAlgorithmName(); + } else { + return "No PRF enabled"; + } + } + + private Digest resolvePRF(final String prf) { + if (StringUtils.isEmpty(prf)) { + throw new IllegalArgumentException("Cannot resolve empty PRF"); + } + String formattedPRF = prf.toLowerCase().replaceAll("[\\W]+", ""); + logger.debug("Resolved PRF {} to {}", prf, formattedPRF); + switch (formattedPRF) { + case "md5": + return new MD5Digest(); + case "sha1": + return new SHA1Digest(); + case "sha384": + return new SHA384Digest(); + case "sha256": + return new SHA256Digest(); + case "sha512": + return new SHA512Digest(); + default: + logger.warn("Could not resolve PRF {}. Using default PRF {} instead", prf, DEFAULT_PRF); + return new SHA512Digest(); + } + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/PasswordBasedEncryptor.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/PasswordBasedEncryptor.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/PasswordBasedEncryptor.java new file mode 100644 index 0000000..ce81f07 --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/PasswordBasedEncryptor.java @@ -0,0 +1,182 @@ +/* + * 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.crypto; + +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processor.io.StreamCallback; +import org.apache.nifi.processors.standard.EncryptContent.Encryptor; +import org.apache.nifi.security.util.EncryptionMethod; +import org.apache.nifi.security.util.KeyDerivationFunction; + +import javax.crypto.Cipher; +import javax.crypto.spec.PBEKeySpec; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.NoSuchAlgorithmException; + +public class PasswordBasedEncryptor implements Encryptor { + + private EncryptionMethod encryptionMethod; + private PBEKeySpec password; + private KeyDerivationFunction kdf; + + private static final int DEFAULT_MAX_ALLOWED_KEY_LENGTH = 128; + private static final int MINIMUM_SAFE_PASSWORD_LENGTH = 10; + + private static boolean isUnlimitedStrengthCryptographyEnabled; + + // Evaluate an unlimited strength algorithm to determine if we support the capability we have on the system + static { + try { + isUnlimitedStrengthCryptographyEnabled = (Cipher.getMaxAllowedKeyLength("AES") > DEFAULT_MAX_ALLOWED_KEY_LENGTH); + } catch (NoSuchAlgorithmException e) { + // if there are issues with this, we default back to the value established + isUnlimitedStrengthCryptographyEnabled = false; + } + } + + public PasswordBasedEncryptor(final EncryptionMethod encryptionMethod, final char[] password, KeyDerivationFunction kdf) { + super(); + try { + if (encryptionMethod == null) { + throw new IllegalArgumentException("Cannot initialize password-based encryptor with null encryption method"); + } + this.encryptionMethod = encryptionMethod; + if (kdf == null || kdf.equals(KeyDerivationFunction.NONE)) { + throw new IllegalArgumentException("Cannot initialize password-based encryptor with null KDF"); + } + this.kdf = kdf; + if (password == null || password.length == 0) { + throw new IllegalArgumentException("Cannot initialize password-based encryptor with empty password"); + } + this.password = new PBEKeySpec(password); + } catch (Exception e) { + throw new ProcessException(e); + } + } + + public static int getMaxAllowedKeyLength(final String algorithm) { + if (StringUtils.isEmpty(algorithm)) { + return DEFAULT_MAX_ALLOWED_KEY_LENGTH; + } + String parsedCipher = CipherUtility.parseCipherFromAlgorithm(algorithm); + try { + return Cipher.getMaxAllowedKeyLength(parsedCipher); + } catch (NoSuchAlgorithmException e) { + // Default algorithm max key length on unmodified JRE + return DEFAULT_MAX_ALLOWED_KEY_LENGTH; + } + } + + /** + * Returns a recommended minimum length for passwords. This can be modified over time and does not take full entropy calculations (patterns, character space, etc.) into account. + * + * @return the minimum safe password length + */ + public static int getMinimumSafePasswordLength() { + return MINIMUM_SAFE_PASSWORD_LENGTH; + } + + public static boolean supportsUnlimitedStrength() { + return isUnlimitedStrengthCryptographyEnabled; + } + + @Override + public StreamCallback getEncryptionCallback() throws ProcessException { + return new EncryptCallback(); + } + + @Override + public StreamCallback getDecryptionCallback() throws ProcessException { + return new DecryptCallback(); + } + + private class DecryptCallback implements StreamCallback { + + public DecryptCallback() { + } + + @Override + public void process(final InputStream in, final OutputStream out) throws IOException { + // Initialize cipher provider + PBECipherProvider cipherProvider = (PBECipherProvider) CipherProviderFactory.getCipherProvider(kdf); + + // Read salt + byte[] salt; + try { + salt = cipherProvider.readSalt(in); + } catch (final EOFException e) { + throw new ProcessException("Cannot decrypt because file size is smaller than salt size", e); + } + + // Determine necessary key length + int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(encryptionMethod.getAlgorithm()); + + // Generate cipher + try { + Cipher cipher; + // Read IV if necessary + if (cipherProvider instanceof RandomIVPBECipherProvider) { + RandomIVPBECipherProvider rivpcp = (RandomIVPBECipherProvider) cipherProvider; + byte[] iv = rivpcp.readIV(in); + cipher = rivpcp.getCipher(encryptionMethod, new String(password.getPassword()), salt, iv, keyLength, false); + } else { + cipher = cipherProvider.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, false); + } + CipherUtility.processStreams(cipher, in, out); + } catch (Exception e) { + throw new ProcessException(e); + } + } + } + + private class EncryptCallback implements StreamCallback { + + public EncryptCallback() { + } + + @Override + public void process(final InputStream in, final OutputStream out) throws IOException { + // Initialize cipher provider + PBECipherProvider cipherProvider = (PBECipherProvider) CipherProviderFactory.getCipherProvider(kdf); + + // Generate salt + byte[] salt = cipherProvider.generateSalt(); + + // Write to output stream + cipherProvider.writeSalt(salt, out); + + // Determine necessary key length + int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(encryptionMethod.getAlgorithm()); + + // Generate cipher + try { + Cipher cipher = cipherProvider.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, true); + // Write IV if necessary + if (cipherProvider instanceof RandomIVPBECipherProvider) { + ((RandomIVPBECipherProvider) cipherProvider).writeIV(cipher.getIV(), out); + } + CipherUtility.processStreams(cipher, in, out); + } catch (Exception e) { + throw new ProcessException(e); + } + } + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/RandomIVPBECipherProvider.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/RandomIVPBECipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/RandomIVPBECipherProvider.java new file mode 100644 index 0000000..903dfac --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/RandomIVPBECipherProvider.java @@ -0,0 +1,71 @@ +/* + * 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.crypto; + +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.security.util.EncryptionMethod; +import org.slf4j.Logger; + +import javax.crypto.Cipher; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +public abstract class RandomIVPBECipherProvider implements PBECipherProvider { + static final byte[] SALT_DELIMITER = "NiFiSALT".getBytes(StandardCharsets.UTF_8); + static final int MAX_SALT_LIMIT = 128; + static final byte[] IV_DELIMITER = "NiFiIV".getBytes(StandardCharsets.UTF_8); + // This is 16 bytes for AES but can vary for other ciphers + static final int MAX_IV_LIMIT = 16; + + /** + * Returns an initialized cipher for the specified algorithm. The key is derived by the KDF of the implementation. The IV is provided externally to allow for non-deterministic IVs, as IVs + * deterministically derived from the password are a potential vulnerability and compromise semantic security. See + * <a href="http://crypto.stackexchange.com/a/3970/12569">Ilmari Karonen's answer on Crypto Stack Exchange</a> + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param salt the salt + * @param iv the IV + * @param keyLength the desired key length in bits + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + abstract Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception; + + abstract Logger getLogger(); + + @Override + public byte[] readSalt(InputStream in) throws IOException, ProcessException { + return CipherUtility.readBytesFromInputStream(in, "salt", MAX_SALT_LIMIT, SALT_DELIMITER); + } + + @Override + public void writeSalt(byte[] salt, OutputStream out) throws IOException { + CipherUtility.writeBytesToOutputStream(out, salt, "salt", SALT_DELIMITER); + } + + public byte[] readIV(InputStream in) throws IOException, ProcessException { + return CipherUtility.readBytesFromInputStream(in, "IV", MAX_IV_LIMIT, IV_DELIMITER); + } + + public void writeIV(byte[] iv, OutputStream out) throws IOException { + CipherUtility.writeBytesToOutputStream(out, iv, "IV", IV_DELIMITER); + } +} http://git-wip-us.apache.org/repos/asf/nifi/blob/498b5023/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/ScryptCipherProvider.java ---------------------------------------------------------------------- diff --git a/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/ScryptCipherProvider.java b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/ScryptCipherProvider.java new file mode 100644 index 0000000..635b4ef --- /dev/null +++ b/nifi-nar-bundles/nifi-standard-bundle/nifi-standard-processors/src/main/java/org/apache/nifi/processors/standard/util/crypto/ScryptCipherProvider.java @@ -0,0 +1,306 @@ +/* + * 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.crypto; + +import org.apache.commons.codec.DecoderException; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.codec.binary.Hex; +import org.apache.commons.lang3.StringUtils; +import org.apache.nifi.processor.exception.ProcessException; +import org.apache.nifi.processors.standard.util.crypto.scrypt.Scrypt; +import org.apache.nifi.security.util.EncryptionMethod; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ScryptCipherProvider extends RandomIVPBECipherProvider { + private static final Logger logger = LoggerFactory.getLogger(ScryptCipherProvider.class); + + private final int n; + private final int r; + private final int p; + /** + * These values can be calculated automatically using the code {@see ScryptCipherProviderGroovyTest#calculateMinimumParameters} or manually updated by a maintainer + */ + private static final int DEFAULT_N = Double.valueOf(Math.pow(2, 14)).intValue(); + private static final int DEFAULT_R = 8; + private static final int DEFAULT_P = 1; + + private static final Pattern SCRYPT_SALT_FORMAT = Pattern.compile("^\\$s0\\$[a-f0-9]{5,16}\\$[\\w\\/\\.]{12,44}"); + private static final Pattern MCRYPT_SALT_FORMAT = Pattern.compile("^\\$\\d+\\$\\d+\\$\\d+\\$[a-f0-9]{16,64}"); + + /** + * Instantiates a Scrypt cipher provider with the default parameters N=2^14, r=8, p=1. + */ + public ScryptCipherProvider() { + this(DEFAULT_N, DEFAULT_R, DEFAULT_P); + } + + /** + * Instantiates a Scrypt cipher provider with the specified N, r, p values. + * + * @param n the number of iterations + * @param r the block size in bytes + * @param p the parallelization factor + */ + public ScryptCipherProvider(int n, int r, int p) { + this.n = n; + this.r = r; + this.p = p; + if (n < DEFAULT_N) { + logger.warn("The provided iteration count {} is below the recommended minimum {}", n, DEFAULT_N); + } + if (r < DEFAULT_R) { + logger.warn("The provided block size {} is below the recommended minimum {}", r, DEFAULT_R); + } + if (p < DEFAULT_P) { + logger.warn("The provided parallelization factor {} is below the recommended minimum {}", p, DEFAULT_P); + } + } + + /** + * Returns an initialized cipher for the specified algorithm. The key is derived by the KDF of the implementation. The IV is provided externally to allow for non-deterministic IVs, as IVs + * deterministically derived from the password are a potential vulnerability and compromise semantic security. See + * <a href="http://crypto.stackexchange.com/a/3970/12569">Ilmari Karonen's answer on Crypto Stack Exchange</a> + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param salt the complete salt (e.g. {@code "$2a$10$gUVbkVzp79H8YaCOsCVZNu".getBytes(StandardCharsets.UTF_8)}) + * @param iv the IV + * @param keyLength the desired key length in bits + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception { + try { + return getInitializedCipher(encryptionMethod, password, salt, iv, keyLength, encryptMode); + } catch (IllegalArgumentException e) { + throw e; + } catch (Exception e) { + throw new ProcessException("Error initializing the cipher", e); + } + } + + @Override + Logger getLogger() { + return logger; + } + + /** + * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived by the KDF of the implementation. + * <p/> + * This method is deprecated because while Scrypt could generate a random salt to use, it would not be returned to the caller of this method and future derivations would fail. Provide a valid + * salt generated by {@link ScryptCipherProvider#generateSalt()}. + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param keyLength the desired key length in bits + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + * @deprecated Provide a salt parameter using {@link ScryptCipherProvider#getCipher(EncryptionMethod, String, byte[], int, boolean)} + */ + @Deprecated + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, int keyLength, boolean encryptMode) throws Exception { + throw new UnsupportedOperationException("The cipher cannot be initialized without a valid salt. Use ScryptCipherProvider#generateSalt() to generate a valid salt"); + } + + /** + * Returns an initialized cipher for the specified algorithm. The key (and IV if necessary) are derived by the KDF of the implementation. + * + * The IV can be retrieved by the calling method using {@link Cipher#getIV()}. + * + * @param encryptionMethod the {@link EncryptionMethod} + * @param password the secret input + * @param salt the complete salt (e.g. {@code "$s0$20101$gUVbkVzp79H8YaCOsCVZNu".getBytes(StandardCharsets.UTF_8)}) + * @param keyLength the desired key length in bits + * @param encryptMode true for encrypt, false for decrypt + * @return the initialized cipher + * @throws Exception if there is a problem initializing the cipher + */ + @Override + public Cipher getCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, int keyLength, boolean encryptMode) throws Exception { + return getCipher(encryptionMethod, password, salt, new byte[0], keyLength, encryptMode); + } + + protected Cipher getInitializedCipher(EncryptionMethod encryptionMethod, String password, byte[] salt, byte[] iv, int keyLength, boolean encryptMode) throws Exception { + if (encryptionMethod == null) { + throw new IllegalArgumentException("The encryption method must be specified"); + } + if (!encryptionMethod.isCompatibleWithStrongKDFs()) { + throw new IllegalArgumentException(encryptionMethod.name() + " is not compatible with Scrypt"); + } + + if (StringUtils.isEmpty(password)) { + throw new IllegalArgumentException("Encryption with an empty password is not supported"); + } + + String algorithm = encryptionMethod.getAlgorithm(); + + final String cipherName = CipherUtility.parseCipherFromAlgorithm(algorithm); + if (!CipherUtility.isValidKeyLength(keyLength, cipherName)) { + throw new IllegalArgumentException(String.valueOf(keyLength) + " is not a valid key length for " + cipherName); + } + + String scryptSalt = formatSaltForScrypt(salt); + List<Integer> params = new ArrayList<>(3); + byte[] rawSalt = new byte[Scrypt.getDefaultSaltLength()]; + + parseSalt(scryptSalt, rawSalt, params); + + String hash = Scrypt.scrypt(password, rawSalt, params.get(0), params.get(1), params.get(2), keyLength); + + // Split out the derived key from the hash and form a key object + final String[] hashComponents = hash.split("\\$"); + final int HASH_INDEX = 4; + if (hashComponents.length < HASH_INDEX) { + throw new ProcessException("There was an error generating a scrypt hash -- the resulting hash was not properly formatted"); + } + byte[] keyBytes = Base64.decodeBase64(hashComponents[HASH_INDEX]); + SecretKey tempKey = new SecretKeySpec(keyBytes, algorithm); + + KeyedCipherProvider keyedCipherProvider = new AESKeyedCipherProvider(); + return keyedCipherProvider.getCipher(encryptionMethod, tempKey, iv, encryptMode); + } + + private void parseSalt(String scryptSalt, byte[] rawSalt, List<Integer> params) { + if (StringUtils.isEmpty(scryptSalt)) { + throw new IllegalArgumentException("Cannot parse empty salt"); + } + + /** Salt format is $s0$params$saltB64 where params is encoded according to + * {@link Scrypt#parseParameters(String)}*/ + final String[] saltComponents = scryptSalt.split("\\$"); + if (saltComponents.length < 4) { + throw new IllegalArgumentException("Could not parse salt"); + } + byte[] salt = Base64.decodeBase64(saltComponents[3]); + if (rawSalt.length < salt.length) { + byte[] tempBytes = new byte[salt.length]; + System.arraycopy(rawSalt, 0, tempBytes, 0, rawSalt.length); + rawSalt = tempBytes; + } + System.arraycopy(salt, 0, rawSalt, 0, salt.length); + + if (params == null) { + params = new ArrayList<>(3); + } + params.addAll(Scrypt.parseParameters(saltComponents[2])); + } + + /** + * Formats the salt into a string which Scrypt can understand containing the N, r, p values along with the salt value. If the provided salt contains all values, the response will be unchanged. + * If it only contains the raw salt value, the resulting return value will also include the current instance version, N, r, and p. + * + * @param salt the provided salt + * @return the properly-formatted and complete salt + */ + private String formatSaltForScrypt(byte[] salt) { + if (salt == null || salt.length == 0) { + throw new IllegalArgumentException("The salt cannot be empty. To generate a salt, use ScryptCipherProvider#generateSalt()"); + } + + String saltString = new String(salt, StandardCharsets.UTF_8); + Matcher matcher = SCRYPT_SALT_FORMAT.matcher(saltString); + + if (matcher.find()) { + return saltString; + } else { + if (saltString.startsWith("$")) { + logger.warn("Salt starts with $ but is not valid scrypt salt"); + matcher = MCRYPT_SALT_FORMAT.matcher(saltString); + if (matcher.find()) { + logger.warn("The salt appears to be of the modified mcrypt format. Use ScryptCipherProvider#translateSalt(mcryptSalt) to form a valid salt"); + return translateSalt(saltString); + } + + logger.info("Salt is not modified mcrypt format"); + } + logger.info("Treating as raw salt bytes"); + + // Ensure the length of the salt + int saltLength = salt.length; + if (saltLength < 8 || saltLength > 32) { + throw new IllegalArgumentException("The raw salt must be between 8 and 32 bytes"); + } + return Scrypt.formatSalt(salt, n, r, p); + } + } + + /** + * Translates a salt from the mcrypt format {@code $n$r$p$salt_hex} to the Java scrypt format {@code $s0$params$saltBase64}. + * + * @param mcryptSalt the mcrypt-formatted salt string + * @return the formatted salt to use with Java Scrypt + */ + public String translateSalt(String mcryptSalt) { + if (StringUtils.isEmpty(mcryptSalt)) { + throw new IllegalArgumentException("Cannot translate empty salt"); + } + + // Format should be $n$r$p$saltHex + Matcher matcher = MCRYPT_SALT_FORMAT.matcher(mcryptSalt); + if (!matcher.matches()) { + throw new IllegalArgumentException("Salt is not valid mcrypt format of $n$r$p$saltHex"); + } + + String[] components = mcryptSalt.split("\\$"); + try { + return Scrypt.formatSalt(Hex.decodeHex(components[4].toCharArray()), Integer.valueOf(components[1]), Integer.valueOf(components[2]), Integer.valueOf(components[3])); + } catch (DecoderException e) { + final String msg = "Mcrypt salt was not properly hex-encoded"; + logger.warn(msg); + throw new IllegalArgumentException(msg); + } + } + + @Override + public byte[] generateSalt() { + byte[] salt = new byte[Scrypt.getDefaultSaltLength()]; + new SecureRandom().nextBytes(salt); + return Scrypt.formatSalt(salt, n, r, p).getBytes(StandardCharsets.UTF_8); + } + + @Override + public int getDefaultSaltLength() { + return Scrypt.getDefaultSaltLength(); + } + + protected int getN() { + return n; + } + + protected int getR() { + return r; + } + + protected int getP() { + return p; + } +}
