NIFI-1257 NIFI-1259

Added utility method to return the maximum acceptable password length for PBE ciphers on JVM with limited strength crypto because BC implementation is undocumented (based on empirical evidence).
Updated EncryptionMethod definitions to accurately reflect need for unlimited strength crypto according to algorithm key length.
Added processor logic to invoke keyed cipher.
Added EncryptContent processor property for raw hex key (always visible until NIFI-1121).
Added validations for KDF (keyed and PBE) and hex key.
Added utility method to return list of valid key lengths for algorithm.
Added description to allowable values for KDF and encryption method in EncryptContent processor.
Added IV read/write to KeyedCipherProvider and changed from interface to abstract class.
Added salt read/write logic to NifiLegacy and OpenSSL cipher providers.
Changed RandomIVPBECipherProvider from interface to abstract class.
Updated strong KDF implementations.
Renamed CipherFactory to CipherProviderFactory.
Added unit test for registered KDF resolution from factory.
Updated default iteration count for PBKDF2 cipher provider.
Implemented Scrypt cipher provider.
Added salt translator from mcrypt format to Java format.
Added unit tests for salt formatting and validation.
Added surefire block to groovy unit test profile to enforce 3072 MB heap for Scrypt test.
Added local Java implementation of Scrypt KDF (and underlying PBKDF2 KDF) from Will Glozer.
Defined interface for KeyedCipherProvider.
Implemented AES implementation for KeyedCipherProvider.
Added Ruby script to test/resources for external compatibility check.
Added key length check to PBKDF2 cipher provider.
Changed default PRF to SHA-512.
Added salt and key length check to PBKDF2 cipher provider.
Added utility method to check key length validity for cipher families.
Added Bcrypt implementation.
Implemented PBKDF2 cipher provider.
Added default constructor with strong choices for PBKDF2 cipher provider.
Implemented NiFiLegacyCipherProvider and added unit tests.
Added key length parameter to PBKDF2 cipher provider.
Added PRF resolution to PBKDF2 cipher provider.
Added RandomIVPBECipherProvider to allow for non-deterministic IVs.
Added new keyed encryption methods and added boolean field for compatibility with new KDFs.
Added CipherFactory.
Improved Javadoc in NiFi legacy cipher provider and OpenSSL cipher provider.
Added KeyedCipherProvider interface.
Added OpenSSL PKCS#5 v1.5 EVP_BytesToKey cipher provider and unit test.

This closes #201.

Signed-off-by: Aldrin Piri <aldrin@apache.org>
This commit is contained in:
Andy LoPresto 2015-12-22 10:12:02 -08:00 committed by Aldrin Piri
parent 0690aee452
commit 498b5023ce
56 changed files with 8533 additions and 461 deletions

16
LICENSE
View File

@ -500,3 +500,19 @@ This product bundles HexViewJS available under an MIT License
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This product bundles 'jBCrypt' which is available under a BSD license.
For details see https://github.com/svenkubiak/jBCrypt/blob/0.4.1/LICENSE
Copyright (c) 2006 Damien Miller <djm@mindrot.org>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@ -1086,7 +1086,6 @@ information can be found here: http://www.adobe.com/devnet/xmp/library/eula-xmp-
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
This product bundles 'Jsoup' which is available under "The MIT license". More
information can be found here: http://jsoup.org/license
@ -1133,4 +1132,21 @@ information can be found here:
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
THE SOFTWARE.
This product bundles 'jBCrypt' which is available under a BSD license.
For details see https://github.com/svenkubiak/jBCrypt/blob/0.4.1/LICENSE
Copyright (c) 2006 Damien Miller <djm@mindrot.org>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@ -26,37 +26,43 @@ import org.apache.commons.lang3.builder.ToStringStyle;
*/
public enum EncryptionMethod {
MD5_128AES("PBEWITHMD5AND128BITAES-CBC-OPENSSL", "BC", false),
MD5_192AES("PBEWITHMD5AND192BITAES-CBC-OPENSSL", "BC", false),
MD5_256AES("PBEWITHMD5AND256BITAES-CBC-OPENSSL", "BC", false),
MD5_DES("PBEWITHMD5ANDDES", "BC", false),
MD5_RC2("PBEWITHMD5ANDRC2", "BC", false),
SHA1_RC2("PBEWITHSHA1ANDRC2", "BC", false),
SHA1_DES("PBEWITHSHA1ANDDES", "BC", false),
SHA_128AES("PBEWITHSHAAND128BITAES-CBC-BC", "BC", true),
SHA_192AES("PBEWITHSHAAND192BITAES-CBC-BC", "BC", true),
SHA_256AES("PBEWITHSHAAND256BITAES-CBC-BC", "BC", true),
SHA_40RC2("PBEWITHSHAAND40BITRC2-CBC", "BC", true),
SHA_128RC2("PBEWITHSHAAND128BITRC2-CBC", "BC", true),
SHA_40RC4("PBEWITHSHAAND40BITRC4", "BC", true),
SHA_128RC4("PBEWITHSHAAND128BITRC4", "BC", true),
SHA256_128AES("PBEWITHSHA256AND128BITAES-CBC-BC", "BC", true),
SHA256_192AES("PBEWITHSHA256AND192BITAES-CBC-BC", "BC", true),
SHA256_256AES("PBEWITHSHA256AND256BITAES-CBC-BC", "BC", true),
SHA_2KEYTRIPLEDES("PBEWITHSHAAND2-KEYTRIPLEDES-CBC", "BC", true),
SHA_3KEYTRIPLEDES("PBEWITHSHAAND3-KEYTRIPLEDES-CBC", "BC", true),
SHA_TWOFISH("PBEWITHSHAANDTWOFISH-CBC", "BC", true),
PGP("PGP", "BC", false),
PGP_ASCII_ARMOR("PGP-ASCII-ARMOR", "BC", false);
MD5_128AES("PBEWITHMD5AND128BITAES-CBC-OPENSSL", "BC", false, false),
MD5_192AES("PBEWITHMD5AND192BITAES-CBC-OPENSSL", "BC", true, false),
MD5_256AES("PBEWITHMD5AND256BITAES-CBC-OPENSSL", "BC", true, false),
MD5_DES("PBEWITHMD5ANDDES", "BC", false, false),
MD5_RC2("PBEWITHMD5ANDRC2", "BC", false, false),
SHA1_RC2("PBEWITHSHA1ANDRC2", "BC", false, false),
SHA1_DES("PBEWITHSHA1ANDDES", "BC", false, false),
SHA_128AES("PBEWITHSHAAND128BITAES-CBC-BC", "BC", false, false),
SHA_192AES("PBEWITHSHAAND192BITAES-CBC-BC", "BC", true, false),
SHA_256AES("PBEWITHSHAAND256BITAES-CBC-BC", "BC", true, false),
SHA_40RC2("PBEWITHSHAAND40BITRC2-CBC", "BC", false, false),
SHA_128RC2("PBEWITHSHAAND128BITRC2-CBC", "BC", false, false),
SHA_40RC4("PBEWITHSHAAND40BITRC4", "BC", false, false),
SHA_128RC4("PBEWITHSHAAND128BITRC4", "BC", false, false),
SHA256_128AES("PBEWITHSHA256AND128BITAES-CBC-BC", "BC", false, false),
SHA256_192AES("PBEWITHSHA256AND192BITAES-CBC-BC", "BC", true, false),
SHA256_256AES("PBEWITHSHA256AND256BITAES-CBC-BC", "BC", true, false),
SHA_2KEYTRIPLEDES("PBEWITHSHAAND2-KEYTRIPLEDES-CBC", "BC", false, false),
SHA_3KEYTRIPLEDES("PBEWITHSHAAND3-KEYTRIPLEDES-CBC", "BC", false, false),
SHA_TWOFISH("PBEWITHSHAANDTWOFISH-CBC", "BC", false, false),
PGP("PGP", "BC", false, false),
PGP_ASCII_ARMOR("PGP-ASCII-ARMOR", "BC", false, false),
// New encryption methods which used keyed encryption
AES_CBC("AES/CBC/PKCS7Padding", "BC", false, true),
AES_CTR("AES/CTR/NoPadding", "BC", false, true),
AES_GCM("AES/GCM/NoPadding", "BC", false, true);
private final String algorithm;
private final String provider;
private final boolean unlimitedStrength;
private final boolean compatibleWithStrongKDFs;
EncryptionMethod(String algorithm, String provider, boolean unlimitedStrength) {
EncryptionMethod(String algorithm, String provider, boolean unlimitedStrength, boolean compatibleWithStrongKDFs) {
this.algorithm = algorithm;
this.provider = provider;
this.unlimitedStrength = unlimitedStrength;
this.compatibleWithStrongKDFs = compatibleWithStrongKDFs;
}
public String getProvider() {
@ -74,13 +80,29 @@ public enum EncryptionMethod {
return unlimitedStrength;
}
/**
* @return true if algorithm is compatible with strong {@link KeyDerivationFunction}s
*/
public boolean isCompatibleWithStrongKDFs() {
return compatibleWithStrongKDFs;
}
/**
* @return true if this algorithm does not rely on its own internal key derivation process
*/
public boolean isKeyedCipher() {
return !algorithm.startsWith("PBE") && !algorithm.startsWith("PGP");
}
@Override
public String toString() {
final ToStringBuilder builder = new ToStringBuilder(this);
ToStringBuilder.setDefaultStyle(ToStringStyle.SHORT_PREFIX_STYLE);
builder.append("algorithm name", algorithm);
builder.append("Algorithm name", algorithm);
builder.append("Requires unlimited strength JCE policy", unlimitedStrength);
builder.append("Algorithm Provider", provider);
builder.append("Compatible with strong KDFs", compatibleWithStrongKDFs);
builder.append("Keyed cipher", isKeyedCipher());
return builder.toString();
}
}

View File

@ -24,9 +24,12 @@ import org.apache.commons.lang3.builder.ToStringStyle;
*/
public enum KeyDerivationFunction {
NIFI_LEGACY("NiFi legacy KDF", "MD5 @ 1000 iterations"),
OPENSSL_EVP_BYTES_TO_KEY("OpenSSL EVP_BytesToKey", "Single iteration MD5 compatible with PKCS#5 v1.5");
// TODO: Implement bcrypt, scrypt, and PBKDF2
NIFI_LEGACY("NiFi Legacy KDF", "MD5 @ 1000 iterations"),
OPENSSL_EVP_BYTES_TO_KEY("OpenSSL EVP_BytesToKey", "Single iteration MD5 compatible with PKCS#5 v1.5"),
BCRYPT("Bcrypt", "Bcrypt with configurable work factor. See Admin Guide"),
SCRYPT("Scrypt", "Scrypt with configurable cost parameters. See Admin Guide"),
PBKDF2("PBKDF2", "PBKDF2 with configurable hash function and iteration count. See Admin Guide"),
NONE("None", "The cipher is given a raw key conforming to the algorithm specifications");
private final String name;
private final String description;
@ -44,6 +47,10 @@ public enum KeyDerivationFunction {
return description;
}
public boolean isStrongKDF() {
return (name.equals(BCRYPT.name) || name.equals(SCRYPT.name) || name.equals(PBKDF2.name));
}
@Override
public String toString() {
final ToStringBuilder builder = new ToStringBuilder(this);

View File

@ -366,6 +366,199 @@ a Remote Process Group. In that scenario, all the nodes
in the remote cluster can be included in the same group. When the ADMIN wants to grant port access to the remote
cluster, s/he can grant it to the group and avoid having to grant it individually to each node in the cluster.
[[encryption]]
Encryption Configuration
------------------------
This section provides an overview of the capabilities of NiFi to encrypt and decrypt data.
The `EncryptContent` processor allows for the encryption and decryption of data, both internal to NiFi and integrated with external systems, such as `openssl` and other data sources and consumers.
[[key-derivation-functions]]
Key Derivation Functions
~~~~~~~~~~~~~~~~~~~~~~~~
Key Derivation Functions (KDF) are mechanisms by which human-readable information, usually a password or other secret information, is translated into a cryptographic key suitable for data protection. For further information, read https://en.wikipedia.org/wiki/Key_derivation_function[the Wikipedia entry on Key Derivation Functions].
Currently, KDFs are ingested by `CipherProvider` implementations and return a fully-initialized `Cipher` object to be used for encryption or decryption. Due to the use of a `CipherProviderFactory`, the KDFs are not customizable at this time. Future enhancements will include the ability to provide custom cost parameters to the KDF at initialization time. As a work-around, `CipherProvider` instances can be initialized with custom cost parameters in the constructor but this is not currently supported by the `CipherProviderFactory`.
Here are the KDFs currently supported by NiFi (primarily in the `EncryptContent` processor for password-based encryption (PBE)) and relevant notes:
* NiFi Legacy KDF
** The original KDF used by NiFi for internal key derivation for PBE, this is 1000 iterations of the MD5 digest over the concatenation of the password and 16 bytes of random salt.
** This KDF is *deprecated as of NiFi 0.5.0* and should only be used for backwards compatibility to decrypt data that was previously encrypted by a legacy version of NiFi.
* OpenSSL PKCS#5 v1.5 EVP_BytesToKey
** This KDF was added in v0.4.0.
** This KDF is provided for compatibility with data encrypted using OpenSSL's default PBE, known as `EVP_BytesToKey`. This is a single iteration of MD5 over the concatenation of the password and 8 bytes of random ASCII salt. OpenSSL recommends using `PBKDF2` for key derivation but does not expose the library method necessary to the command-line tool, so this KDF is still the de facto default for command-line encryption.
* Bcrypt
** This KDF was added in v0.5.0.
** https://en.wikipedia.org/wiki/Bcrypt[Bcrypt] is an adaptive function based on the https://en.wikipedia.org/wiki/Blowfish_(cipher)[Blowfish] cipher. This KDF is strongly recommended as it automatically incorporates a random 16 byte salt, configurable cost parameter (or "work factor"), and is hardened against brute-force attacks using https://en.wikipedia.org/wiki/General-purpose_computing_on_graphics_processing_units[GPGPU] (which share memory between cores) by requiring access to "large" blocks of memory during the key derivation. It is less resistant to https://en.wikipedia.org/wiki/Field-programmable_gate_array[FPGA] brute-force attacks where the gate arrays have access to individual embedded RAM blocks.
** Because the length of a Bcrypt-derived key is always 184 bits, the complete output is then fed to a `SHA-512` digest and truncated to the desired key length. This provides the benefit of the avalanche effect on the formatted input.
** The recommended minimum work factor is 12 (2^12^ key derivation rounds) (as of 2/1/2016 on commodity hardware) and should be increased to the threshold at which legitimate systems will encounter detrimental delays (see schedule below or use `BcryptCipherProviderGroovyTest#testDefaultConstructorShouldProvideStrongWorkFactor()` to calculate safe minimums).
** The salt format is `$2a$10$ABCDEFGHIJKLMNOPQRSTUV`. The salt is delimited by `$` and the three sections are as follows:
*** `2a` - the version of the format. An extensive explanation can be found http://blog.ircmaxell.com/2012/12/seven-ways-to-screw-up-bcrypt.html[here]. NiFi currently uses `2a` for all salts generated internally.
*** `10` - the work factor. This is actually the log~2~ value, so the total iteration count would be 2^10^ in this case.
*** `ABCDEFGHIJKLMNOPQRSTUV` - the 22 character, Base64-encoded, unpadded, raw salt value. This decodes to a 16 byte salt used in the key derivation.
* Scrypt
** This KDF was added in v0.5.0.
** https://en.wikipedia.org/wiki/Scrypt[Scrypt] is an adaptive function designed in response to `bcrypt`. This KDF is recommended as it requires relatively large amounts of memory for each derivation, making it resistant to hardware brute-force attacks.
** The recommended minimum cost is `N`=2^14^, `r`=8, `p`=1 (as of 2/1/2016 on commodity hardware) and should be increased to the threshold at which legitimate systems will encounter detrimental delays (see schedule below or use `ScryptCipherProviderGroovyTest#testDefaultConstructorShouldProvideStrongParameters()` to calculate safe minimums).
** The salt format is `$s0$e0101$ABCDEFGHIJKLMNOPQRSTUV`. The salt is delimited by `$` and the three sections are as follows:
*** `s0` - the version of the format. NiFi currently uses `s0` for all salts generated internally.
*** `e0101` - the cost parameters. This is actually a hexadecimal encoding of `N`, `r`, `p` using shifts. This can be formed/parsed using `Scrypt#encodeParams()` and `Scrypt#parseParameters()`.
**** Some external libraries encode `N`, `r`, and `p` separately in the form `$400$1$1$`. A utility method is available at `ScryptCipherProvider#translateSalt()` which will convert the external form to the internal form.
*** `ABCDEFGHIJKLMNOPQRSTUV` - the 11-44 character, Base64-encoded, unpadded, raw salt value. This decodes to a 8-32 byte salt used in the key derivation.
* PBKDF2
** This KDF was added in v0.5.0.
** https://en.wikipedia.org/wiki/PBKDF2[Password-Based Key Derivation Function 2] is an adaptive derivation function which uses an internal pseudorandom function (PRF) and iterates it many times over a password and salt (at least 16 bytes).
** The PRF is recommended to be `HMAC/SHA-256` or `HMAC/SHA-512`. The use of an HMAC cryptographic hash function mitigates a length extension attack.
** The recommended minimum number of iterations is 160,000 (as of 2/1/2016 on commodity hardware). This number should be doubled every two years (see schedule below or use `PBKDF2CipherProviderGroovyTest#testDefaultConstructorShouldProvideStrongIterationCount()` to calculate safe minimums).
** This KDF is not memory-hard (can be parallelized massively with commodity hardware) but is still recommended as sufficient by http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf[NIST SP 800-132 (PDF)] and many cryptographers (when used with a proper iteration count and HMAC cryptographic hash function).
* None
** This KDF was added in v0.5.0.
** This KDF performs no operation on the input and is a marker to indicate the raw key is provided to the cipher. The key must be provided in hexadecimal encoding and be of a valid length for the associated cipher/algorithm.
Additional Resources
^^^^^^^^^^^^^^^^^^^^
* http://stackoverflow.com/a/30308723/70465[Explanation of optimal scrypt cost parameters and relationships]
* http://csrc.nist.gov/publications/nistpubs/800-132/nist-sp800-132.pdf[NIST Special Publication 800-132]
* https://www.owasp.org/index.php/Password_Storage_Cheat_Sheet#Work_Factor[OWASP Password Storage Work Factor Calculations]
* http://security.stackexchange.com/a/3993/16485[PBKDF2 rounds calculations]
* http://blog.ircmaxell.com/2014/03/why-i-dont-recommend-scrypt.html[Scrypt as KDF vs password storage vulnerabilities]
* http://security.stackexchange.com/a/26253/16485[Scrypt vs. Bcrypt (as of 2010)]
* http://security.stackexchange.com/a/6415/16485[Bcrypt vs PBKDF2]
* http://wildlyinaccurate.com/bcrypt-choosing-a-work-factor/[Choosing a work factor for Bcrypt]
* https://docs.spring.io/spring-security/site/docs/current/apidocs/org/springframework/security/crypto/bcrypt/BCrypt.html[Spring Security Bcrypt]
* https://www.openssl.org/docs/manmaster/crypto/EVP_BytesToKey.html[OpenSSL EVP BytesToKey PKCS#1v1.5]
* https://www.openssl.org/docs/manmaster/crypto/PKCS5_PBKDF2_HMAC.html[OpenSSL PBKDF2 KDF]
* http://security.stackexchange.com/a/29139/16485[OpenSSL KDF flaws description]
Salt and IV Encoding
~~~~~~~~~~~~~~~~~~~~
Initially, the `EncryptContent` processor had a single method of deriving the encryption key from a user-provided password. This is now referred to as `NiFiLegacy` mode, effectively `MD5 digest, 1000 iterations`. In v0.4.0, another method of deriving the key, `OpenSSL PKCS#5 v1.5 EVP_BytesToKey` was added for compatibility with content encrypted outside of NiFi using the `openssl` command-line tool. Both of these <<key-derivation-functions, Key Derivation Functions>> (KDF) had hard-coded digest functions and iteration counts, and the salt format was also hard-coded. With v0.5.0, additional KDFs are introduced with variable iteration counts, work factors, and salt formats. In addition, _raw keyed encryption_ was also introduced. This required the capacity to encode arbitrary salts and Initialization Vectors (IV) into the cipher stream in order to be recovered by NiFi or a follow-on system to decrypt these messages.
For the existing KDFs, the salt format has not changed.
NiFi Legacy
^^^^^^^^^^^
The first 16 bytes of the input are the salt. On decryption, the salt is read in and combined with the password to derive the encryption key and IV.
image:nifi-legacy-salt.png["NiFi Legacy Salt Encoding"]
OpenSSL PKCS#5 v1.5 EVP_BytesToKey
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
OpenSSL allows for salted or unsalted key derivation. _*Unsalted key derivation is a security risk and is not recommended.*_ If a salt is present, the first 8 bytes of the input are the ASCII string "`Salted__`" (`0x53 61 6C 74 65 64 5F 5F`) and the next 8 bytes are the ASCII-encoded salt. On decryption, the salt is read in and combined with the password to derive the encryption key and IV. If there is no salt header, the entire input is considered to be the cipher text.
image:openssl-salt.png["OpenSSL Salt Encoding"]
For new KDFs, each of which allow for non-deterministic IVs, the IV must be stored alongside the cipher text. This is not a vulnerability, as the IV is not required to be secret, but simply to be unique for messages encrypted using the same key to reduce the success of cryptographic attacks. For these KDFs, the output consists of the salt, followed by the salt delimiter, UTF-8 string "`NiFiSALT`" (`0x4E 69 46 69 53 41 4C 54`) and then the IV, followed by the IV delimiter, UTF-8 string "`NiFiIV`" (`0x4E 69 46 69 49 56`), followed by the cipher text.
Bcrypt, Scrypt, PBKDF2
^^^^^^^^^^^^^^^^^^^^^^
image:bcrypt-salt.png["Bcrypt Salt & IV Encoding"]
image:scrypt-salt.png["Scrypt Salt & IV Encoding"]
image:pbkdf2-salt.png["PBKDF2 Salt & IV Encoding"]
Java Cryptography Extension (JCE) Limited Strength Jurisdiction Policies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Because of US export regulations, default JVMs have http://docs.oracle.com/javase/7/docs/technotes/guides/security/SunProviders.html#importlimits[limits imposed on the strength of cryptographic operations] available to them. For example, AES operations are limited to `128 bit keys` by default. While `AES-128` is cryptographically safe, this can have unintended consequences, specifically on Password-based Encryption (PBE).
PBE is the process of deriving a cryptographic key for encryption or decryption from _user-provided secret material_, usually a password. Rather than a human remembering a (random-appearing) 32 or 64 character hexadecimal string, a password or passphrase is used.
A number of PBE algorithms provided by NiFi impose strict limits on the length of the password due to the underlying key length checks. Below is a table listing the maximum password length on a JVM with limited cryptographic strength.
.Maximum Password Length on Limited Cryptographic Strength JVM
|===
|Algorithm |Max Password Length
|`PBEWITHMD5AND128BITAES-CBC-OPENSSL`
|16
|`PBEWITHMD5AND192BITAES-CBC-OPENSSL`
|16
|`PBEWITHMD5AND256BITAES-CBC-OPENSSL`
|16
|`PBEWITHMD5ANDDES`
|16
|`PBEWITHMD5ANDRC2`
|16
|`PBEWITHSHA1ANDRC2`
|16
|`PBEWITHSHA1ANDDES`
|16
|`PBEWITHSHAAND128BITAES-CBC-BC`
|7
|`PBEWITHSHAAND192BITAES-CBC-BC`
|7
|`PBEWITHSHAAND256BITAES-CBC-BC`
|7
|`PBEWITHSHAAND40BITRC2-CBC`
|7
|`PBEWITHSHAAND128BITRC2-CBC`
|7
|`PBEWITHSHAAND40BITRC4`
|7
|`PBEWITHSHAAND128BITRC4`
|7
|`PBEWITHSHA256AND128BITAES-CBC-BC`
|7
|`PBEWITHSHA256AND192BITAES-CBC-BC`
|7
|`PBEWITHSHA256AND256BITAES-CBC-BC`
|7
|`PBEWITHSHAAND2-KEYTRIPLEDES-CBC`
|7
|`PBEWITHSHAAND3-KEYTRIPLEDES-CBC`
|7
|`PBEWITHSHAANDTWOFISH-CBC`
|7
|===
Allow Insecure Cryptographic Modes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
By default, the `Allow Insecure Cryptographic Modes` property in `EncryptContent` processor settings is set to `not-allowed`. This means that if a password of fewer than `10` characters is provided, a validation error will occur. 10 characters is a conservative estimate and does not take into consideration full entropy calculations, patterns, etc.
image:allow-weak-crypto.png["Allow Insecure Cryptographic Modes", width=940]
On a JVM with limited strength cryptography, some PBE algorithms limit the maximum password length to 7, and in this case it will not be possible to provide a "safe" password. It is recommended to install the JCE Unlimited Strength Jurisdiction Policy files for the JVM to mitigate this issue.
* http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html[JCE Unlimited Strength Jurisdiction Policy files for Java 7]
* http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html[JCE Unlimited Strength Jurisdiction Policy files for Java 8]
If on a system where the unlimited strength policies cannot be installed, it is recommended to switch to an algorithm that supports longer passwords (see table above).
[WARNING]
.Allowing Weak Crypto
=====================
If it is not possible to install the unlimited strength jurisdiction policies, the `Allow Weak Crypto` setting can be changed to `allowed`, but *this is _not_ recommended*. Changing this setting explicitly acknowledges the inherent risk in using weak cryptographic configurations.
=====================
It is preferable to request upstream/downstream systems to switch to https://cwiki.apache.org/confluence/display/NIFI/Encryption+Information[keyed encryption] or use a "strong" https://cwiki.apache.org/confluence/display/NIFI/Key+Derivation+Function+Explanations[Key Derivation Function (KDF) supported by NiFi].
[[clustering]]
Clustering Configuration
@ -806,8 +999,8 @@ Security Configuration section of this Administrator's Guide.
|====
|*Property*|*Description*
|nifi.sensitive.props.key|This is the password used to encrypt any sensitive property values that are configured in processors. By default, it is blank, but the system administrator should provide a value for it. It can be a string of any length. Be aware that once this password is set and one or more sensitive processor properties have been configured, this password should not be changed.
|nifi.sensitive.props.algorithm|The algorithm used to encrypt sensitive properties. The default value is PBEWITHMD5AND256BITAES-CBC-OPENSSL.
|nifi.sensitive.props.key|This is the password used to encrypt any sensitive property values that are configured in processors. By default, it is blank, but the system administrator should provide a value for it. It can be a string of any length, although the recommended minimum length is 10 characters. Be aware that once this password is set and one or more sensitive processor properties have been configured, this password should not be changed.
|nifi.sensitive.props.algorithm|The algorithm used to encrypt sensitive properties. The default value is `PBEWITHMD5AND256BITAES-CBC-OPENSSL`.
|nifi.sensitive.props.provider|The sensitive property provider. The default value is BC.
|nifi.security.keystore*|The full path and name of the keystore. It is blank by default.
|nifi.security.keystoreType|The keystore type. It is blank by default.

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 KiB

View File

@ -53,7 +53,7 @@
<configuration>
<excludes>**/authentication/generated/*.java,</excludes>
</configuration>
</plugin>
</plugin>
</plugins>
</build>
<dependencies>

View File

@ -319,3 +319,19 @@ For details see http://asm.ow2.org/asmdex-license.html
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
The binary distribution of this product bundles 'jBCrypt' which is available under a BSD license. For details see https://github.com/svenkubiak/jBCrypt/blob/0.4.1/LICENSE
Copyright (c) 2006 Damien Miller <djm@mindrot.org>
Permission to use, copy, modify, and distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

View File

@ -180,6 +180,11 @@ language governing permissions and limitations under the License. -->
<version>0.6</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>de.svenkubiak</groupId>
<artifactId>jBcrypt</artifactId>
<version>0.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.nifi</groupId>
<artifactId>nifi-mock</artifactId>
@ -315,9 +320,35 @@ language governing permissions and limitations under the License. -->
<exclude>src/test/resources/TestEncryptContent/plain.txt</exclude>
<exclude>src/test/resources/TestEncryptContent/salted_raw.enc</exclude>
<exclude>src/test/resources/TestEncryptContent/unsalted_raw.enc</exclude>
<!-- This file is copied from https://github.com/jeremyh/jBCrypt because the binary is compiled for Java 8 and we must support Java 7 -->
<exclude>src/main/java/org/apache/nifi/processors/standard/util/crypto/bcrypt/BCrypt.java</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<profiles>
<profile>
<!-- Custom profile for tests requiring large heap operations -->
<id>expensive-heap-tests</id>
<activation>
<property>
<name>heap</name>
<value>expensive</value>
</property>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.18</version>
<configuration>
<argLine>-Xmx3072M</argLine>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>

View File

@ -16,6 +16,8 @@
*/
package org.apache.nifi.processors.standard;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.annotation.behavior.EventDriven;
import org.apache.nifi.annotation.behavior.InputRequirement;
@ -24,6 +26,7 @@ import org.apache.nifi.annotation.behavior.SideEffectFree;
import org.apache.nifi.annotation.behavior.SupportsBatching;
import org.apache.nifi.annotation.documentation.CapabilityDescription;
import org.apache.nifi.annotation.documentation.Tags;
import org.apache.nifi.components.AllowableValue;
import org.apache.nifi.components.PropertyDescriptor;
import org.apache.nifi.components.ValidationContext;
import org.apache.nifi.components.ValidationResult;
@ -38,14 +41,17 @@ import org.apache.nifi.processor.Relationship;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.io.StreamCallback;
import org.apache.nifi.processor.util.StandardValidators;
import org.apache.nifi.processors.standard.util.OpenPGPKeyBasedEncryptor;
import org.apache.nifi.processors.standard.util.OpenPGPPasswordBasedEncryptor;
import org.apache.nifi.processors.standard.util.PasswordBasedEncryptor;
import org.apache.nifi.processors.standard.util.crypto.CipherUtility;
import org.apache.nifi.processors.standard.util.crypto.KeyedEncryptor;
import org.apache.nifi.processors.standard.util.crypto.OpenPGPKeyBasedEncryptor;
import org.apache.nifi.processors.standard.util.crypto.OpenPGPPasswordBasedEncryptor;
import org.apache.nifi.processors.standard.util.crypto.PasswordBasedEncryptor;
import org.apache.nifi.security.util.EncryptionMethod;
import org.apache.nifi.security.util.KeyDerivationFunction;
import org.apache.nifi.util.StopWatch;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.text.Normalizer;
import java.util.ArrayList;
@ -67,71 +73,91 @@ public class EncryptContent extends AbstractProcessor {
public static final String ENCRYPT_MODE = "Encrypt";
public static final String DECRYPT_MODE = "Decrypt";
private static final String WEAK_CRYPTO_ALLOWED_NAME = "allowed";
private static final String WEAK_CRYPTO_NOT_ALLOWED_NAME = "not-allowed";
public static final PropertyDescriptor MODE = new PropertyDescriptor.Builder()
.name("Mode")
.description("Specifies whether the content should be encrypted or decrypted")
.required(true)
.allowableValues(ENCRYPT_MODE, DECRYPT_MODE)
.defaultValue(ENCRYPT_MODE)
.build();
.name("Mode")
.description("Specifies whether the content should be encrypted or decrypted")
.required(true)
.allowableValues(ENCRYPT_MODE, DECRYPT_MODE)
.defaultValue(ENCRYPT_MODE)
.build();
public static final PropertyDescriptor KEY_DERIVATION_FUNCTION = new PropertyDescriptor.Builder()
.name("key-derivation-function")
.displayName("Key Derivation Function")
.description("Specifies the key derivation function to generate the key from the password (and salt)")
.required(true)
.allowableValues(KeyDerivationFunction.values())
.defaultValue(KeyDerivationFunction.NIFI_LEGACY.name())
.build();
.name("key-derivation-function")
.displayName("Key Derivation Function")
.description("Specifies the key derivation function to generate the key from the password (and salt)")
.required(true)
.allowableValues(buildKeyDerivationFunctionAllowableValues())
.defaultValue(KeyDerivationFunction.NIFI_LEGACY.name())
.build();
public static final PropertyDescriptor ENCRYPTION_ALGORITHM = new PropertyDescriptor.Builder()
.name("Encryption Algorithm")
.description("The Encryption Algorithm to use")
.required(true)
.allowableValues(EncryptionMethod.values())
.defaultValue(EncryptionMethod.MD5_128AES.name())
.build();
.name("Encryption Algorithm")
.description("The Encryption Algorithm to use")
.required(true)
.allowableValues(buildEncryptionMethodAllowableValues())
.defaultValue(EncryptionMethod.MD5_128AES.name())
.build();
public static final PropertyDescriptor PASSWORD = new PropertyDescriptor.Builder()
.name("Password")
.description("The Password to use for encrypting or decrypting the data")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.sensitive(true)
.build();
.name("Password")
.description("The Password to use for encrypting or decrypting the data")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.sensitive(true)
.build();
public static final PropertyDescriptor PUBLIC_KEYRING = new PropertyDescriptor.Builder()
.name("public-keyring-file")
.displayName("Public Keyring File")
.description("In a PGP encrypt mode, this keyring contains the public key of the recipient")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
.name("public-keyring-file")
.displayName("Public Keyring File")
.description("In a PGP encrypt mode, this keyring contains the public key of the recipient")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
public static final PropertyDescriptor PUBLIC_KEY_USERID = new PropertyDescriptor.Builder()
.name("public-key-user-id")
.displayName("Public Key User Id")
.description("In a PGP encrypt mode, this user id of the recipient")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
.name("public-key-user-id")
.displayName("Public Key User Id")
.description("In a PGP encrypt mode, this user id of the recipient")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
public static final PropertyDescriptor PRIVATE_KEYRING = new PropertyDescriptor.Builder()
.name("private-keyring-file")
.displayName("Private Keyring File")
.description("In a PGP decrypt mode, this keyring contains the private key of the recipient")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
.name("private-keyring-file")
.displayName("Private Keyring File")
.description("In a PGP decrypt mode, this keyring contains the private key of the recipient")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.build();
public static final PropertyDescriptor PRIVATE_KEYRING_PASSPHRASE = new PropertyDescriptor.Builder()
.name("private-keyring-passphrase")
.displayName("Private Keyring Passphrase")
.description("In a PGP decrypt mode, this is the private keyring passphrase")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.sensitive(true)
.build();
.name("private-keyring-passphrase")
.displayName("Private Keyring Passphrase")
.description("In a PGP decrypt mode, this is the private keyring passphrase")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.sensitive(true)
.build();
public static final PropertyDescriptor RAW_KEY_HEX = new PropertyDescriptor.Builder()
.name("raw-key-hex")
.displayName("Raw Key (hexadecimal)")
.description("In keyed encryption, this is the raw key, encoded in hexadecimal")
.required(false)
.addValidator(StandardValidators.NON_EMPTY_VALIDATOR)
.sensitive(true)
.build();
public static final PropertyDescriptor ALLOW_WEAK_CRYPTO = new PropertyDescriptor.Builder()
.name("allow-weak-crypto")
.displayName("Allow insecure cryptographic modes")
.description("Overrides the default behavior to prevent unsafe combinations of encryption algorithms and short passwords on JVMs with limited strength cryptographic jurisdiction policies")
.required(true)
.allowableValues(buildWeakCryptoAllowableValues())
.defaultValue(buildDefaultWeakCryptoAllowableValue().getValue())
.build();
public static final Relationship REL_SUCCESS = new Relationship.Builder().name("success")
.description("Any FlowFile that is successfully encrypted or decrypted will be routed to success").build();
public static final Relationship REL_FAILURE = new Relationship.Builder().name("failure")
.description("Any FlowFile that cannot be encrypted or decrypted will be routed to failure").build();
.description("Any FlowFile that is successfully encrypted or decrypted will be routed to success").build();
public static final Relationship REL_FAILURE = new Relationship.Builder().name("failure")
.description("Any FlowFile that cannot be encrypted or decrypted will be routed to failure").build();
private List<PropertyDescriptor> properties;
private Set<Relationship> relationships;
static {
@ -139,13 +165,48 @@ public class EncryptContent extends AbstractProcessor {
Security.addProvider(new BouncyCastleProvider());
}
private static AllowableValue[] buildKeyDerivationFunctionAllowableValues() {
final KeyDerivationFunction[] keyDerivationFunctions = KeyDerivationFunction.values();
List<AllowableValue> allowableValues = new ArrayList<>(keyDerivationFunctions.length);
for (KeyDerivationFunction kdf : keyDerivationFunctions) {
allowableValues.add(new AllowableValue(kdf.name(), kdf.getName(), kdf.getDescription()));
}
return allowableValues.toArray(new AllowableValue[0]);
}
private static AllowableValue[] buildEncryptionMethodAllowableValues() {
final EncryptionMethod[] encryptionMethods = EncryptionMethod.values();
List<AllowableValue> allowableValues = new ArrayList<>(encryptionMethods.length);
for (EncryptionMethod em : encryptionMethods) {
allowableValues.add(new AllowableValue(em.name(), em.name(), em.toString()));
}
return allowableValues.toArray(new AllowableValue[0]);
}
private static AllowableValue[] buildWeakCryptoAllowableValues() {
List<AllowableValue> allowableValues = new ArrayList<>();
allowableValues.add(new AllowableValue(WEAK_CRYPTO_ALLOWED_NAME, "Allowed", "Operation will not be blocked and no alerts will be presented " +
"when unsafe combinations of encryption algorithms and passwords are provided"));
allowableValues.add(buildDefaultWeakCryptoAllowableValue());
return allowableValues.toArray(new AllowableValue[0]);
}
private static AllowableValue buildDefaultWeakCryptoAllowableValue() {
return new AllowableValue(WEAK_CRYPTO_NOT_ALLOWED_NAME, "Not Allowed", "When set, operation will be blocked and alerts will be presented to the user " +
"if unsafe combinations of encryption algorithms and passwords are provided on a JVM with limited strength crypto. To fix this, see the Admin Guide.");
}
@Override
protected void init(final ProcessorInitializationContext context) {
final List<PropertyDescriptor> properties = new ArrayList<>();
properties.add(MODE);
properties.add(KEY_DERIVATION_FUNCTION);
properties.add(ENCRYPTION_ALGORITHM);
properties.add(ALLOW_WEAK_CRYPTO);
properties.add(PASSWORD);
properties.add(RAW_KEY_HEX);
properties.add(PUBLIC_KEYRING);
properties.add(PUBLIC_KEY_USERID);
properties.add(PRIVATE_KEYRING);
@ -180,97 +241,212 @@ public class EncryptContent extends AbstractProcessor {
protected Collection<ValidationResult> customValidate(final ValidationContext context) {
final List<ValidationResult> validationResults = new ArrayList<>(super.customValidate(context));
final String methodValue = context.getProperty(ENCRYPTION_ALGORITHM).getValue();
final EncryptionMethod method = EncryptionMethod.valueOf(methodValue);
final String algorithm = method.getAlgorithm();
final EncryptionMethod encryptionMethod = EncryptionMethod.valueOf(methodValue);
final String algorithm = encryptionMethod.getAlgorithm();
final String password = context.getProperty(PASSWORD).getValue();
final String kdf = context.getProperty(KEY_DERIVATION_FUNCTION).getValue();
final KeyDerivationFunction kdf = KeyDerivationFunction.valueOf(context.getProperty(KEY_DERIVATION_FUNCTION).getValue());
final String keyHex = context.getProperty(RAW_KEY_HEX).getValue();
if (isPGPAlgorithm(algorithm)) {
if (password == null) {
final boolean encrypt = context.getProperty(MODE).getValue().equalsIgnoreCase(ENCRYPT_MODE);
if (encrypt) {
// need both public-keyring-file and public-key-user-id set
final String publicKeyring = context.getProperty(PUBLIC_KEYRING).getValue();
final String publicUserId = context.getProperty(PUBLIC_KEY_USERID).getValue();
if (publicKeyring == null || publicUserId == null) {
validationResults.add(new ValidationResult.Builder().subject(PUBLIC_KEYRING.getDisplayName())
.explanation(algorithm + " encryption without a " + PASSWORD.getDisplayName() + " requires both "
+ PUBLIC_KEYRING.getDisplayName() + " and " + PUBLIC_KEY_USERID.getDisplayName())
.build());
} else {
// verify the public keyring contains the user id
try {
if (OpenPGPKeyBasedEncryptor.getPublicKey(publicUserId, publicKeyring) == null) {
validationResults.add(new ValidationResult.Builder().subject(PUBLIC_KEYRING.getDisplayName())
.explanation(PUBLIC_KEYRING.getDisplayName() + " " + publicKeyring
+ " does not contain user id " + publicUserId)
.build());
}
} catch (final Exception e) {
validationResults.add(new ValidationResult.Builder().subject(PUBLIC_KEYRING.getDisplayName())
.explanation("Invalid " + PUBLIC_KEYRING.getDisplayName() + " " + publicKeyring
+ " because " + e.toString())
.build());
}
}
} else {
// need both private-keyring-file and private-keyring-passphrase set
final String privateKeyring = context.getProperty(PRIVATE_KEYRING).getValue();
final String keyringPassphrase = context.getProperty(PRIVATE_KEYRING_PASSPHRASE).getValue();
if (privateKeyring == null || keyringPassphrase == null) {
validationResults.add(new ValidationResult.Builder().subject(PRIVATE_KEYRING.getName())
.explanation(algorithm + " decryption without a " + PASSWORD.getDisplayName() + " requires both "
+ PRIVATE_KEYRING.getDisplayName() + " and " + PRIVATE_KEYRING_PASSPHRASE.getDisplayName())
.build());
} else {
final String providerName = EncryptionMethod.valueOf(methodValue).getProvider();
// verify the passphrase works on the private keyring
try {
if (!OpenPGPKeyBasedEncryptor.validateKeyring(providerName, privateKeyring, keyringPassphrase.toCharArray())) {
validationResults.add(new ValidationResult.Builder().subject(PRIVATE_KEYRING.getDisplayName())
.explanation(PRIVATE_KEYRING.getDisplayName() + " " + privateKeyring
+ " could not be opened with the provided " + PRIVATE_KEYRING_PASSPHRASE.getDisplayName())
.build());
}
} catch (final Exception e) {
validationResults.add(new ValidationResult.Builder().subject(PRIVATE_KEYRING.getDisplayName())
.explanation("Invalid " + PRIVATE_KEYRING.getDisplayName() + " " + privateKeyring
+ " because " + e.toString())
.build());
}
}
}
}
} else { // PBE
if (!PasswordBasedEncryptor.supportsUnlimitedStrength()) {
if (method.isUnlimitedStrength()) {
validationResults.add(new ValidationResult.Builder().subject(ENCRYPTION_ALGORITHM.getName())
.explanation(methodValue + " (" + algorithm + ") is not supported by this JVM due to lacking JCE Unlimited " +
"Strength Jurisdiction Policy files.").build());
}
}
int allowedKeyLength = PasswordBasedEncryptor.getMaxAllowedKeyLength(ENCRYPTION_ALGORITHM.getName());
if (StringUtils.isEmpty(password)) {
validationResults.add(new ValidationResult.Builder().subject(PASSWORD.getName())
.explanation(PASSWORD.getDisplayName() + " is required when using algorithm " + algorithm).build());
} else {
if (password.getBytes().length * 8 > allowedKeyLength) {
validationResults.add(new ValidationResult.Builder().subject(PASSWORD.getName())
.explanation("Password length greater than " + allowedKeyLength + " bits is not supported by this JVM" +
" due to lacking JCE Unlimited Strength Jurisdiction Policy files.").build());
}
}
// Perform some analysis on the selected encryption algorithm to ensure the JVM can support it and the associated key
if (StringUtils.isEmpty(kdf)) {
validationResults.add(new ValidationResult.Builder().subject(KEY_DERIVATION_FUNCTION.getName())
.explanation(KEY_DERIVATION_FUNCTION.getDisplayName() + " is required when using algorithm " + algorithm).build());
final boolean encrypt = context.getProperty(MODE).getValue().equalsIgnoreCase(ENCRYPT_MODE);
final String publicKeyring = context.getProperty(PUBLIC_KEYRING).getValue();
final String publicUserId = context.getProperty(PUBLIC_KEY_USERID).getValue();
final String privateKeyring = context.getProperty(PRIVATE_KEYRING).getValue();
final String privateKeyringPassphrase = context.getProperty(PRIVATE_KEYRING_PASSPHRASE).getValue();
validationResults.addAll(validatePGP(encryptionMethod, password, encrypt, publicKeyring, publicUserId, privateKeyring, privateKeyringPassphrase));
} else { // Not PGP
if (encryptionMethod.isKeyedCipher()) { // Raw key
validationResults.addAll(validateKeyed(encryptionMethod, kdf, keyHex));
} else { // PBE
boolean allowWeakCrypto = context.getProperty(ALLOW_WEAK_CRYPTO).getValue().equalsIgnoreCase(WEAK_CRYPTO_ALLOWED_NAME);
validationResults.addAll(validatePBE(encryptionMethod, kdf, password, allowWeakCrypto));
}
}
return validationResults;
}
private List<ValidationResult> validatePGP(EncryptionMethod encryptionMethod, String password, boolean encrypt, String publicKeyring, String publicUserId, String privateKeyring,
String privateKeyringPassphrase) {
List<ValidationResult> validationResults = new ArrayList<>();
if (password == null) {
if (encrypt) {
// If encrypting without a password, require both public-keyring-file and public-key-user-id
if (publicKeyring == null || publicUserId == null) {
validationResults.add(new ValidationResult.Builder().subject(PUBLIC_KEYRING.getDisplayName())
.explanation(encryptionMethod.getAlgorithm() + " encryption without a " + PASSWORD.getDisplayName() + " requires both "
+ PUBLIC_KEYRING.getDisplayName() + " and " + PUBLIC_KEY_USERID.getDisplayName())
.build());
} else {
// Verify the public keyring contains the user id
try {
if (OpenPGPKeyBasedEncryptor.getPublicKey(publicUserId, publicKeyring) == null) {
validationResults.add(new ValidationResult.Builder().subject(PUBLIC_KEYRING.getDisplayName())
.explanation(PUBLIC_KEYRING.getDisplayName() + " " + publicKeyring
+ " does not contain user id " + publicUserId)
.build());
}
} catch (final Exception e) {
validationResults.add(new ValidationResult.Builder().subject(PUBLIC_KEYRING.getDisplayName())
.explanation("Invalid " + PUBLIC_KEYRING.getDisplayName() + " " + publicKeyring
+ " because " + e.toString())
.build());
}
}
} else { // Decrypt
// Require both private-keyring-file and private-keyring-passphrase
if (privateKeyring == null || privateKeyringPassphrase == null) {
validationResults.add(new ValidationResult.Builder().subject(PRIVATE_KEYRING.getName())
.explanation(encryptionMethod.getAlgorithm() + " decryption without a " + PASSWORD.getDisplayName() + " requires both "
+ PRIVATE_KEYRING.getDisplayName() + " and " + PRIVATE_KEYRING_PASSPHRASE.getDisplayName())
.build());
} else {
final String providerName = encryptionMethod.getProvider();
// Verify the passphrase works on the private keyring
try {
if (!OpenPGPKeyBasedEncryptor.validateKeyring(providerName, privateKeyring, privateKeyringPassphrase.toCharArray())) {
validationResults.add(new ValidationResult.Builder().subject(PRIVATE_KEYRING.getDisplayName())
.explanation(PRIVATE_KEYRING.getDisplayName() + " " + privateKeyring
+ " could not be opened with the provided " + PRIVATE_KEYRING_PASSPHRASE.getDisplayName())
.build());
}
} catch (final Exception e) {
validationResults.add(new ValidationResult.Builder().subject(PRIVATE_KEYRING.getDisplayName())
.explanation("Invalid " + PRIVATE_KEYRING.getDisplayName() + " " + privateKeyring
+ " because " + e.toString())
.build());
}
}
}
}
return validationResults;
}
private List<ValidationResult> validatePBE(EncryptionMethod encryptionMethod, KeyDerivationFunction kdf, String password, boolean allowWeakCrypto) {
List<ValidationResult> validationResults = new ArrayList<>();
boolean limitedStrengthCrypto = !PasswordBasedEncryptor.supportsUnlimitedStrength();
// Password required (short circuits validation because other conditions depend on password presence)
if (StringUtils.isEmpty(password)) {
validationResults.add(new ValidationResult.Builder().subject(PASSWORD.getName())
.explanation(PASSWORD.getDisplayName() + " is required when using algorithm " + encryptionMethod.getAlgorithm()).build());
return validationResults;
}
// If weak crypto is not explicitly allowed via override, check the password length and algorithm
final int passwordBytesLength = password.getBytes(StandardCharsets.UTF_8).length;
if (!allowWeakCrypto) {
final int minimumSafePasswordLength = PasswordBasedEncryptor.getMinimumSafePasswordLength();
if (passwordBytesLength < minimumSafePasswordLength) {
validationResults.add(new ValidationResult.Builder().subject(PASSWORD.getName())
.explanation("Password length less than " + minimumSafePasswordLength + " characters is potentially unsafe. See Admin Guide.").build());
}
}
// Multiple checks on machine with limited strength crypto
if (limitedStrengthCrypto) {
// Cannot use unlimited strength ciphers on machine that lacks policies
if (encryptionMethod.isUnlimitedStrength()) {
validationResults.add(new ValidationResult.Builder().subject(ENCRYPTION_ALGORITHM.getName())
.explanation(encryptionMethod.name() + " (" + encryptionMethod.getAlgorithm() + ") is not supported by this JVM due to lacking JCE Unlimited " +
"Strength Jurisdiction Policy files. See Admin Guide.").build());
}
// Check if the password exceeds the limit
final boolean passwordLongerThanLimit = !CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(passwordBytesLength, encryptionMethod);
if (passwordLongerThanLimit) {
int maxPasswordLength = CipherUtility.getMaximumPasswordLengthForAlgorithmOnLimitedStrengthCrypto(encryptionMethod);
validationResults.add(new ValidationResult.Builder().subject(PASSWORD.getName())
.explanation("Password length greater than " + maxPasswordLength + " characters is not supported by this JVM" +
" due to lacking JCE Unlimited Strength Jurisdiction Policy files. See Admin Guide.").build());
}
}
// Check the KDF for compatibility with this algorithm
List<String> kdfsForPBECipher = getKDFsForPBECipher(encryptionMethod);
if (kdf == null || !kdfsForPBECipher.contains(kdf.name())) {
final String displayName = KEY_DERIVATION_FUNCTION.getDisplayName();
validationResults.add(new ValidationResult.Builder().subject(displayName)
.explanation(displayName + " is required to be " + StringUtils.join(kdfsForPBECipher,
", ") + " when using algorithm " + encryptionMethod.getAlgorithm() + ". See Admin Guide.").build());
}
return validationResults;
}
private List<ValidationResult> validateKeyed(EncryptionMethod encryptionMethod, KeyDerivationFunction kdf, String keyHex) {
List<ValidationResult> validationResults = new ArrayList<>();
boolean limitedStrengthCrypto = !PasswordBasedEncryptor.supportsUnlimitedStrength();
if (limitedStrengthCrypto) {
if (encryptionMethod.isUnlimitedStrength()) {
validationResults.add(new ValidationResult.Builder().subject(ENCRYPTION_ALGORITHM.getName())
.explanation(encryptionMethod.name() + " (" + encryptionMethod.getAlgorithm() + ") is not supported by this JVM due to lacking JCE Unlimited " +
"Strength Jurisdiction Policy files. See Admin Guide.").build());
}
}
int allowedKeyLength = PasswordBasedEncryptor.getMaxAllowedKeyLength(ENCRYPTION_ALGORITHM.getName());
if (StringUtils.isEmpty(keyHex)) {
validationResults.add(new ValidationResult.Builder().subject(RAW_KEY_HEX.getName())
.explanation(RAW_KEY_HEX.getDisplayName() + " is required when using algorithm " + encryptionMethod.getAlgorithm() + ". See Admin Guide.").build());
} else {
byte[] keyBytes = new byte[0];
try {
keyBytes = Hex.decodeHex(keyHex.toCharArray());
} catch (DecoderException e) {
validationResults.add(new ValidationResult.Builder().subject(RAW_KEY_HEX.getName())
.explanation("Key must be valid hexadecimal string. See Admin Guide.").build());
}
if (keyBytes.length * 8 > allowedKeyLength) {
validationResults.add(new ValidationResult.Builder().subject(RAW_KEY_HEX.getName())
.explanation("Key length greater than " + allowedKeyLength + " bits is not supported by this JVM" +
" due to lacking JCE Unlimited Strength Jurisdiction Policy files. See Admin Guide.").build());
}
if (!CipherUtility.isValidKeyLengthForAlgorithm(keyBytes.length * 8, encryptionMethod.getAlgorithm())) {
List<Integer> validKeyLengths = CipherUtility.getValidKeyLengthsForAlgorithm(encryptionMethod.getAlgorithm());
validationResults.add(new ValidationResult.Builder().subject(RAW_KEY_HEX.getName())
.explanation("Key must be valid length [" + StringUtils.join(validKeyLengths, ", ") + "]. See Admin Guide.").build());
}
}
// Perform some analysis on the selected encryption algorithm to ensure the JVM can support it and the associated key
List<String> kdfsForKeyedCipher = getKDFsForKeyedCipher();
if (kdf == null || !kdfsForKeyedCipher.contains(kdf.name())) {
validationResults.add(new ValidationResult.Builder().subject(KEY_DERIVATION_FUNCTION.getName())
.explanation(KEY_DERIVATION_FUNCTION.getDisplayName() + " is required to be " + StringUtils.join(kdfsForKeyedCipher, ", ") + " when using algorithm " +
encryptionMethod.getAlgorithm()).build());
}
return validationResults;
}
private List<String> getKDFsForKeyedCipher() {
List<String> kdfsForKeyedCipher = new ArrayList<>();
kdfsForKeyedCipher.add(KeyDerivationFunction.NONE.name());
for (KeyDerivationFunction k : KeyDerivationFunction.values()) {
if (k.isStrongKDF()) {
kdfsForKeyedCipher.add(k.name());
}
}
return kdfsForKeyedCipher;
}
private List<String> getKDFsForPBECipher(EncryptionMethod encryptionMethod) {
List<String> kdfsForPBECipher = new ArrayList<>();
for (KeyDerivationFunction k : KeyDerivationFunction.values()) {
// Add all weak (legacy) KDFs except NONE
if (!k.isStrongKDF() && !k.equals(KeyDerivationFunction.NONE)) {
kdfsForPBECipher.add(k.name());
// If this algorithm supports strong KDFs, add them as well
} else if ((encryptionMethod.isCompatibleWithStrongKDFs() && k.isStrongKDF())) {
kdfsForPBECipher.add(k.name());
}
}
return kdfsForPBECipher;
}
@Override
public void onTrigger(final ProcessContext context, final ProcessSession session) {
FlowFile flowFile = session.get();
@ -300,14 +476,17 @@ public class EncryptContent extends AbstractProcessor {
} else if (!encrypt && privateKeyring != null) {
final char[] keyringPassphrase = context.getProperty(PRIVATE_KEYRING_PASSPHRASE).getValue().toCharArray();
encryptor = new OpenPGPKeyBasedEncryptor(algorithm, providerName, privateKeyring, null, keyringPassphrase,
filename);
filename);
} else {
final char[] passphrase = Normalizer.normalize(password, Normalizer.Form.NFC).toCharArray();
encryptor = new OpenPGPPasswordBasedEncryptor(algorithm, providerName, passphrase, filename);
}
} else if (kdf.equals(KeyDerivationFunction.NONE)) { // Raw key
final String keyHex = context.getProperty(RAW_KEY_HEX).getValue();
encryptor = new KeyedEncryptor(encryptionMethod, Hex.decodeHex(keyHex.toCharArray()));
} else { // PBE
final char[] passphrase = Normalizer.normalize(password, Normalizer.Form.NFC).toCharArray();
encryptor = new PasswordBasedEncryptor(algorithm, providerName, passphrase, kdf);
encryptor = new PasswordBasedEncryptor(encryptionMethod, passphrase, kdf);
}
if (encrypt) {

View File

@ -1,266 +0,0 @@
/*
* 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 java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.PBEParameterSpec;
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.KeyDerivationFunction;
import org.apache.nifi.stream.io.StreamUtils;
public class PasswordBasedEncryptor implements Encryptor {
private Cipher cipher;
private int saltSize;
private SecretKey secretKey;
private KeyDerivationFunction kdf;
private int iterationsCount = LEGACY_KDF_ITERATIONS;
@Deprecated
private static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG";
private static final int DEFAULT_SALT_SIZE = 8;
// TODO: Eventually KDF-specific values should be refactored into injectable interface impls
private static final int LEGACY_KDF_ITERATIONS = 1000;
private static final int OPENSSL_EVP_HEADER_SIZE = 8;
private static final int OPENSSL_EVP_SALT_SIZE = 8;
private static final String OPENSSL_EVP_HEADER_MARKER = "Salted__";
private static final int OPENSSL_EVP_KDF_ITERATIONS = 0;
private static final int DEFAULT_MAX_ALLOWED_KEY_LENGTH = 128;
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 String algorithm, final String providerName, final char[] password, KeyDerivationFunction kdf) {
super();
try {
// initialize cipher
this.cipher = Cipher.getInstance(algorithm, providerName);
this.kdf = kdf;
if (isOpenSSLKDF()) {
this.saltSize = OPENSSL_EVP_SALT_SIZE;
this.iterationsCount = OPENSSL_EVP_KDF_ITERATIONS;
} else {
int algorithmBlockSize = cipher.getBlockSize();
this.saltSize = (algorithmBlockSize > 0) ? algorithmBlockSize : DEFAULT_SALT_SIZE;
}
// initialize SecretKey from password
final PBEKeySpec pbeKeySpec = new PBEKeySpec(password);
final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, providerName);
this.secretKey = factory.generateSecret(pbeKeySpec);
} 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 = parseCipherFromAlgorithm(algorithm);
try {
return Cipher.getMaxAllowedKeyLength(parsedCipher);
} catch (NoSuchAlgorithmException e) {
// Default algorithm max key length on unmodified JRE
return DEFAULT_MAX_ALLOWED_KEY_LENGTH;
}
}
private static String parseCipherFromAlgorithm(final String algorithm) {
// This is not optimal but the algorithms do not have a standard format
final String AES = "AES";
final String TDES = "TRIPLEDES";
final String DES = "DES";
final String RC4 = "RC4";
final String RC2 = "RC2";
final String TWOFISH = "TWOFISH";
final List<String> SYMMETRIC_CIPHERS = Arrays.asList(AES, TDES, DES, RC4, RC2, TWOFISH);
// The algorithms contain "TRIPLEDES" but the cipher name is "DESede"
final String ACTUAL_TDES_CIPHER = "DESede";
for (String cipher : SYMMETRIC_CIPHERS) {
if (algorithm.contains(cipher)) {
if (cipher.equals(TDES)) {
return ACTUAL_TDES_CIPHER;
} else {
return cipher;
}
}
}
return algorithm;
}
public static boolean supportsUnlimitedStrength() {
return isUnlimitedStrengthCryptographyEnabled;
}
@Override
public StreamCallback getEncryptionCallback() throws ProcessException {
try {
byte[] salt = new byte[saltSize];
SecureRandom secureRandom = new SecureRandom();
secureRandom.nextBytes(salt);
return new EncryptCallback(salt);
} catch (Exception e) {
throw new ProcessException(e);
}
}
@Override
public StreamCallback getDecryptionCallback() throws ProcessException {
return new DecryptCallback();
}
private int getIterationsCount() {
return iterationsCount;
}
private boolean isOpenSSLKDF() {
return KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY.equals(kdf);
}
private class DecryptCallback implements StreamCallback {
public DecryptCallback() {
}
@Override
public void process(final InputStream in, final OutputStream out) throws IOException {
byte[] salt = new byte[saltSize];
try {
// If the KDF is OpenSSL, try to read the salt from the input stream
if (isOpenSSLKDF()) {
// The header and salt format is "Salted__salt x8b" in ASCII
// Try to read the header and salt from the input
byte[] header = new byte[PasswordBasedEncryptor.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);
} catch (final EOFException e) {
throw new ProcessException("Cannot decrypt because file size is smaller than salt size", e);
}
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, getIterationsCount());
try {
cipher.init(Cipher.DECRYPT_MODE, secretKey, parameterSpec);
} catch (final Exception e) {
throw new ProcessException(e);
}
final byte[] buffer = new byte[65536];
int len;
while ((len = in.read(buffer)) > 0) {
final byte[] decryptedBytes = cipher.update(buffer, 0, len);
if (decryptedBytes != null) {
out.write(decryptedBytes);
}
}
try {
out.write(cipher.doFinal());
} catch (final Exception e) {
throw new ProcessException(e);
}
}
}
private class EncryptCallback implements StreamCallback {
private final byte[] salt;
public EncryptCallback(final byte[] salt) {
this.salt = salt;
}
@Override
public void process(final InputStream in, final OutputStream out) throws IOException {
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, getIterationsCount());
try {
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
} catch (final Exception e) {
throw new ProcessException(e);
}
// If this is OpenSSL EVP, the salt must be preceded by the header
if (isOpenSSLKDF()) {
out.write(OPENSSL_EVP_HEADER_MARKER.getBytes(StandardCharsets.US_ASCII));
}
out.write(salt);
final byte[] buffer = new byte[65536];
int len;
while ((len = in.read(buffer)) > 0) {
final byte[] encryptedBytes = cipher.update(buffer, 0, len);
if (encryptedBytes != null) {
out.write(encryptedBytes);
}
}
try {
out.write(cipher.doFinal());
} catch (final IllegalBlockSizeException | BadPaddingException e) {
throw new ProcessException(e);
}
}
}
}

View File

@ -0,0 +1,153 @@
/*
* 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.io.UnsupportedEncodingException;
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;
import java.util.List;
/**
* This is a standard implementation of {@link KeyedCipherProvider} which supports {@code AES} cipher families with arbitrary modes of operation (currently only {@code CBC}, {@code CTR}, and {@code
* GCM} are supported as {@link EncryptionMethod}s.
*/
public class AESKeyedCipherProvider extends KeyedCipherProvider {
private static final Logger logger = LoggerFactory.getLogger(AESKeyedCipherProvider.class);
private static final int IV_LENGTH = 16;
private static final List<Integer> VALID_KEY_LENGTHS = Arrays.asList(128, 192, 256);
/**
* Returns an initialized cipher for the specified algorithm. 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 key the key
* @param iv the IV or nonce (cannot be all 0x00)
* @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, SecretKey key, byte[] iv, boolean encryptMode) throws Exception {
try {
return getInitializedCipher(encryptionMethod, key, iv, encryptMode);
} catch (IllegalArgumentException e) {
throw e;
} catch (Exception e) {
throw new ProcessException("Error initializing the cipher", e);
}
}
/**
* Returns an initialized cipher for the specified algorithm. The IV will be generated internally (for encryption). If decryption is requested, it will throw an exception.
*
* @param encryptionMethod the {@link EncryptionMethod}
* @param key the key
* @param encryptMode true for encrypt, false for decrypt
* @return the initialized cipher
* @throws Exception if there is a problem initializing the cipher or if decryption is requested
*/
@Override
public Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, boolean encryptMode) throws Exception {
return getCipher(encryptionMethod, key, new byte[0], encryptMode);
}
protected Cipher getInitializedCipher(EncryptionMethod encryptionMethod, SecretKey key, byte[] iv,
boolean encryptMode) throws NoSuchAlgorithmException, NoSuchProviderException,
InvalidKeySpecException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, UnsupportedEncodingException {
if (encryptionMethod == null) {
throw new IllegalArgumentException("The encryption method must be specified");
}
if (!encryptionMethod.isKeyedCipher()) {
throw new IllegalArgumentException(encryptionMethod.name() + " requires a PBECipherProvider");
}
String algorithm = encryptionMethod.getAlgorithm();
String provider = encryptionMethod.getProvider();
if (key == null) {
throw new IllegalArgumentException("The key must be specified");
}
if (!isValidKeyLength(key)) {
throw new IllegalArgumentException("The key must be of length [" + StringUtils.join(VALID_KEY_LENGTHS, ", ") + "]");
}
Cipher cipher = Cipher.getInstance(algorithm, provider);
final String operation = encryptMode ? "encrypt" : "decrypt";
boolean ivIsInvalid = false;
// If an IV was not provided already, generate a random IV and inject it in the cipher
int ivLength = cipher.getBlockSize();
if (iv.length != ivLength) {
logger.warn("An IV was provided of length {} bytes for {}ion but should be {} bytes", iv.length, operation, ivLength);
ivIsInvalid = true;
}
final byte[] emptyIv = new byte[ivLength];
if (Arrays.equals(iv, emptyIv)) {
logger.warn("An empty IV was provided of length {} for {}ion", iv.length, operation);
ivIsInvalid = true;
}
if (ivIsInvalid) {
if (encryptMode) {
logger.warn("Generating new IV. The value can be obtained in the calling code by invoking 'cipher.getIV()';");
iv = generateIV();
} else {
// Can't decrypt without an IV
throw new IllegalArgumentException("Cannot decrypt without a valid IV");
}
}
cipher.init(encryptMode ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
return cipher;
}
private boolean isValidKeyLength(SecretKey key) {
return VALID_KEY_LENGTHS.contains(key.getEncoded().length * 8);
}
/**
* Generates a new random IV of 16 bytes using {@link java.security.SecureRandom}.
*
* @return the IV
*/
public byte[] generateIV() {
byte[] iv = new byte[IV_LENGTH];
new SecureRandom().nextBytes(iv);
return iv;
}
}

View File

@ -0,0 +1,198 @@
/*
* 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.processors.standard.util.crypto.bcrypt.BCrypt;
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.MessageDigest;
import java.util.Arrays;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class BcryptCipherProvider extends RandomIVPBECipherProvider {
private static final Logger logger = LoggerFactory.getLogger(BcryptCipherProvider.class);
private final int workFactor;
/**
* This can be calculated automatically using the code {@see BcryptCipherProviderGroovyTest#calculateMinimumWorkFactor} or manually updated by a maintainer
*/
private static final int DEFAULT_WORK_FACTOR = 12;
private static final int DEFAULT_SALT_LENGTH = 16;
private static final Pattern BCRYPT_SALT_FORMAT = Pattern.compile("^\\$\\d\\w\\$\\d{2}\\$[\\w\\/\\.]{22}");
/**
* Instantiates a Bcrypt cipher provider with the default work factor 12 (2^12 key expansion rounds).
*/
public BcryptCipherProvider() {
this(DEFAULT_WORK_FACTOR);
}
/**
* Instantiates a Bcrypt cipher provider with the specified work factor w (2^w key expansion rounds).
*
* @param workFactor the (log) number of key expansion rounds [4..30]
*/
public BcryptCipherProvider(int workFactor) {
this.workFactor = workFactor;
if (workFactor < DEFAULT_WORK_FACTOR) {
logger.warn("The provided work factor {} is below the recommended minimum {}", workFactor, DEFAULT_WORK_FACTOR);
}
}
/**
* 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 Bcrypt 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 BcryptCipherProvider#generateSalt()}.
* </p>
*
* @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 BcryptCipherProvider#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 BcryptCipherProvider#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 "$2a$10$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 Bcrypt");
}
if (StringUtils.isEmpty(password)) {
throw new IllegalArgumentException("Encryption with an empty password is not supported");
}
String algorithm = encryptionMethod.getAlgorithm();
String provider = encryptionMethod.getProvider();
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 bcryptSalt = formatSaltForBcrypt(salt);
String hash = BCrypt.hashpw(password, bcryptSalt);
/* The SHA-512 hash is required in order to derive a key longer than 184 bits (the resulting size of the Bcrypt hash) and ensuring the avalanche effect causes higher key entropy (if all
derived keys follow a consistent pattern, it weakens the strength of the encryption) */
MessageDigest digest = MessageDigest.getInstance("SHA-512", provider);
byte[] dk = digest.digest(hash.getBytes(StandardCharsets.UTF_8));
dk = Arrays.copyOf(dk, keyLength / 8);
SecretKey tempKey = new SecretKeySpec(dk, algorithm);
KeyedCipherProvider keyedCipherProvider = new AESKeyedCipherProvider();
return keyedCipherProvider.getCipher(encryptionMethod, tempKey, iv, encryptMode);
}
private String formatSaltForBcrypt(byte[] salt) {
if (salt == null || salt.length == 0) {
throw new IllegalArgumentException("The salt cannot be empty. To generate a salt, use BcryptCipherProvider#generateSalt()");
}
String rawSalt = new String(salt, StandardCharsets.UTF_8);
Matcher matcher = BCRYPT_SALT_FORMAT.matcher(rawSalt);
if (matcher.find()) {
return rawSalt;
} else {
throw new IllegalArgumentException("The salt must be of the format $2a$10$gUVbkVzp79H8YaCOsCVZNu. To generate a salt, use BcryptCipherProvider#generateSalt()");
}
}
@Override
public byte[] generateSalt() {
return BCrypt.gensalt(workFactor).getBytes(StandardCharsets.UTF_8);
}
@Override
public int getDefaultSaltLength() {
return DEFAULT_SALT_LENGTH;
}
protected int getWorkFactor() {
return workFactor;
}
}

View File

@ -0,0 +1,23 @@
/*
* 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;
/**
* Marker interface for cipher providers.
*/
public interface CipherProvider {
}

View File

@ -0,0 +1,57 @@
/*
* 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.KeyDerivationFunction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
public class CipherProviderFactory {
private static final Logger logger = LoggerFactory.getLogger(CipherProviderFactory.class);
private static Map<KeyDerivationFunction, Class<? extends CipherProvider>> registeredCipherProviders;
static {
registeredCipherProviders = new HashMap<>();
registeredCipherProviders.put(KeyDerivationFunction.NIFI_LEGACY, NiFiLegacyCipherProvider.class);
registeredCipherProviders.put(KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY, OpenSSLPKCS5CipherProvider.class);
registeredCipherProviders.put(KeyDerivationFunction.PBKDF2, PBKDF2CipherProvider.class);
registeredCipherProviders.put(KeyDerivationFunction.BCRYPT, BcryptCipherProvider.class);
registeredCipherProviders.put(KeyDerivationFunction.SCRYPT, ScryptCipherProvider.class);
registeredCipherProviders.put(KeyDerivationFunction.NONE, AESKeyedCipherProvider.class);
}
public static CipherProvider getCipherProvider(KeyDerivationFunction kdf) {
logger.debug("{} KDFs registered", registeredCipherProviders.size());
if (registeredCipherProviders.containsKey(kdf)) {
Class<? extends CipherProvider> clazz = registeredCipherProviders.get(kdf);
try {
return clazz.newInstance();
} catch (Exception e) {
logger.error("Error instantiating new {} with default parameters for {}", clazz.getName(), kdf.getName());
throw new ProcessException("Error instantiating cipher provider");
}
}
throw new IllegalArgumentException("No cipher provider registered for " + kdf.getName());
}
}

View File

@ -0,0 +1,320 @@
/*
* 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.binary.Base64;
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.ByteArrayOutputStream;
import org.apache.nifi.stream.io.StreamUtils;
import javax.crypto.Cipher;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CipherUtility {
public static final int BUFFER_SIZE = 65536;
private static final Pattern KEY_LENGTH_PATTERN = Pattern.compile("([\\d]+)BIT");
private static final Map<String, Integer> MAX_PASSWORD_LENGTH_BY_ALGORITHM;
static {
Map<String, Integer> aMap = new HashMap<>();
/**
* These values were determined empirically by running {@link NiFiLegacyCipherProviderGroovyTest#testShouldDetermineDependenceOnUnlimitedStrengthCrypto()}
*, which evaluates each algorithm in a try/catch harness with increasing password size until it throws an exception.
* This was performed on a JVM without the Unlimited Strength Jurisdiction cryptographic policy files installed.
*/
aMap.put("PBEWITHMD5AND128BITAES-CBC-OPENSSL", 16);
aMap.put("PBEWITHMD5AND192BITAES-CBC-OPENSSL", 16);
aMap.put("PBEWITHMD5AND256BITAES-CBC-OPENSSL", 16);
aMap.put("PBEWITHMD5ANDDES", 16);
aMap.put("PBEWITHMD5ANDRC2", 16);
aMap.put("PBEWITHSHA1ANDRC2", 16);
aMap.put("PBEWITHSHA1ANDDES", 16);
aMap.put("PBEWITHSHAAND128BITAES-CBC-BC", 7);
aMap.put("PBEWITHSHAAND192BITAES-CBC-BC", 7);
aMap.put("PBEWITHSHAAND256BITAES-CBC-BC", 7);
aMap.put("PBEWITHSHAAND40BITRC2-CBC", 7);
aMap.put("PBEWITHSHAAND128BITRC2-CBC", 7);
aMap.put("PBEWITHSHAAND40BITRC4", 7);
aMap.put("PBEWITHSHAAND128BITRC4", 7);
aMap.put("PBEWITHSHA256AND128BITAES-CBC-BC", 7);
aMap.put("PBEWITHSHA256AND192BITAES-CBC-BC", 7);
aMap.put("PBEWITHSHA256AND256BITAES-CBC-BC", 7);
aMap.put("PBEWITHSHAAND2-KEYTRIPLEDES-CBC", 7);
aMap.put("PBEWITHSHAAND3-KEYTRIPLEDES-CBC", 7);
aMap.put("PBEWITHSHAANDTWOFISH-CBC", 7);
MAX_PASSWORD_LENGTH_BY_ALGORITHM = Collections.unmodifiableMap(aMap);
}
/**
* Returns the cipher algorithm from the full algorithm name. Useful for getting key lengths, etc.
* <p/>
* Ex: PBEWITHMD5AND128BITAES-CBC-OPENSSL -> AES
*
* @param algorithm the full algorithm name
* @return the generic cipher name or the full algorithm if one cannot be extracted
*/
public static String parseCipherFromAlgorithm(final String algorithm) {
if (StringUtils.isEmpty(algorithm)) {
return algorithm;
}
String formattedAlgorithm = algorithm.toUpperCase();
// This is not optimal but the algorithms do not have a standard format
final String AES = "AES";
final String TDES = "TRIPLEDES";
final String TDES_ALTERNATE = "DESEDE";
final String DES = "DES";
final String RC4 = "RC4";
final String RC2 = "RC2";
final String TWOFISH = "TWOFISH";
final List<String> SYMMETRIC_CIPHERS = Arrays.asList(AES, TDES, TDES_ALTERNATE, DES, RC4, RC2, TWOFISH);
// The algorithms contain "TRIPLEDES" but the cipher name is "DESede"
final String ACTUAL_TDES_CIPHER = "DESede";
for (String cipher : SYMMETRIC_CIPHERS) {
if (formattedAlgorithm.contains(cipher)) {
if (cipher.equals(TDES) || cipher.equals(TDES_ALTERNATE)) {
return ACTUAL_TDES_CIPHER;
} else {
return cipher;
}
}
}
return algorithm;
}
/**
* Returns the cipher key length from the full algorithm name. Useful for getting key lengths, etc.
* <p/>
* Ex: PBEWITHMD5AND128BITAES-CBC-OPENSSL -> 128
*
* @param algorithm the full algorithm name
* @return the key length or -1 if one cannot be extracted
*/
public static int parseKeyLengthFromAlgorithm(final String algorithm) {
int keyLength = parseActualKeyLengthFromAlgorithm(algorithm);
if (keyLength != -1) {
return keyLength;
} else {
// Key length not explicitly named in algorithm
String cipher = parseCipherFromAlgorithm(algorithm);
return getDefaultKeyLengthForCipher(cipher);
}
}
private static int parseActualKeyLengthFromAlgorithm(final String algorithm) {
Matcher matcher = KEY_LENGTH_PATTERN.matcher(algorithm);
if (matcher.find()) {
return Integer.parseInt(matcher.group(1));
} else {
return -1;
}
}
/**
* Returns true if the provided key length is a valid key length for the provided cipher family. Does not reflect if the Unlimited Strength Cryptography Jurisdiction Policies are installed.
* Does not reflect if the key length is correct for a specific combination of cipher and PBE-derived key length.
* <p/>
* Ex:
* <p/>
* 256 is valid for {@code AES/CBC/PKCS7Padding} but not {@code PBEWITHMD5AND128BITAES-CBC-OPENSSL}. However, this method will return {@code true} for both because it only gets the cipher
* family, {@code AES}.
* <p/>
* 64, AES -> false
* [128, 192, 256], AES -> true
*
* @param keyLength the key length in bits
* @param cipher the cipher family
* @return true if this key length is valid
*/
public static boolean isValidKeyLength(int keyLength, final String cipher) {
if (StringUtils.isEmpty(cipher)) {
return false;
}
return getValidKeyLengthsForAlgorithm(cipher).contains(keyLength);
}
/**
* Returns true if the provided key length is a valid key length for the provided algorithm. Does not reflect if the Unlimited Strength Cryptography Jurisdiction Policies are installed.
* <p/>
* Ex:
* <p/>
* 256 is valid for {@code AES/CBC/PKCS7Padding} but not {@code PBEWITHMD5AND128BITAES-CBC-OPENSSL}.
* <p/>
* 64, AES/CBC/PKCS7Padding -> false
* [128, 192, 256], AES/CBC/PKCS7Padding -> true
* <p/>
* 128, PBEWITHMD5AND128BITAES-CBC-OPENSSL -> true
* [192, 256], PBEWITHMD5AND128BITAES-CBC-OPENSSL -> false
*
* @param keyLength the key length in bits
* @param algorithm the specific algorithm
* @return true if this key length is valid
*/
public static boolean isValidKeyLengthForAlgorithm(int keyLength, final String algorithm) {
if (StringUtils.isEmpty(algorithm)) {
return false;
}
return getValidKeyLengthsForAlgorithm(algorithm).contains(keyLength);
}
public static List<Integer> getValidKeyLengthsForAlgorithm(String algorithm) {
List<Integer> validKeyLengths = new ArrayList<>();
if (StringUtils.isEmpty(algorithm)) {
return validKeyLengths;
}
// Some algorithms specify a single key size
int keyLength = parseActualKeyLengthFromAlgorithm(algorithm);
if (keyLength != -1) {
validKeyLengths.add(keyLength);
return validKeyLengths;
}
// The algorithm does not specify a key size
String cipher = parseCipherFromAlgorithm(algorithm);
switch (cipher.toUpperCase()) {
case "DESEDE":
// 3DES keys have the cryptographic strength of 7/8 because of parity bits, but are often represented with n*8 bytes
return Arrays.asList(56, 64, 112, 128, 168, 192);
case "DES":
return Arrays.asList(56, 64);
case "RC2":
case "RC4":
case "RC5":
/** These ciphers can have arbitrary length keys but that's a really bad idea, {@see http://crypto.stackexchange.com/a/9963/12569}.
* Also, RC* is deprecated and should be considered insecure */
for (int i = 40; i <= 2048; i++) {
validKeyLengths.add(i);
}
return validKeyLengths;
case "AES":
case "TWOFISH":
return Arrays.asList(128, 192, 256);
default:
return validKeyLengths;
}
}
private static int getDefaultKeyLengthForCipher(String cipher) {
if (StringUtils.isEmpty(cipher)) {
return -1;
}
cipher = cipher.toUpperCase();
switch (cipher) {
case "DESEDE":
return 112;
case "DES":
return 64;
case "RC2":
case "RC4":
case "RC5":
default:
return 128;
}
}
public static void processStreams(Cipher cipher, InputStream in, OutputStream out) {
try {
final byte[] buffer = new byte[BUFFER_SIZE];
int len;
while ((len = in.read(buffer)) > 0) {
final byte[] decryptedBytes = cipher.update(buffer, 0, len);
if (decryptedBytes != null) {
out.write(decryptedBytes);
}
}
out.write(cipher.doFinal());
} catch (Exception e) {
throw new ProcessException(e);
}
}
public static byte[] readBytesFromInputStream(InputStream in, String label, int limit, byte[] delimiter) throws IOException, ProcessException {
if (in == null) {
throw new IllegalArgumentException("Cannot read " + label + " from null InputStream");
}
// If the value is not detected within the first n bytes, throw an exception
in.mark(limit);
// The first n bytes of the input stream contain the value up to the custom delimiter
ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
byte[] stoppedBy = StreamUtils.copyExclusive(in, bytesOut, limit + delimiter.length, delimiter);
if (stoppedBy != null) {
byte[] bytes = bytesOut.toByteArray();
return bytes;
}
// If no delimiter was found, reset the cursor
in.reset();
return null;
}
public static void writeBytesToOutputStream(OutputStream out, byte[] value, String label, byte[] delimiter) throws IOException {
if (out == null) {
throw new IllegalArgumentException("Cannot write " + label + " to null OutputStream");
}
out.write(value);
out.write(delimiter);
}
public static String encodeBase64NoPadding(final byte[] bytes) {
String base64UrlNoPadding = Base64.encodeBase64URLSafeString(bytes);
base64UrlNoPadding = base64UrlNoPadding.replaceAll("-", "+");
base64UrlNoPadding = base64UrlNoPadding.replaceAll("_", "/");
return base64UrlNoPadding;
}
public static boolean passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(final int passwordLength, EncryptionMethod encryptionMethod) {
if (encryptionMethod == null) {
throw new IllegalArgumentException("Cannot evaluate an empty encryption method algorithm");
}
return passwordLength <= getMaximumPasswordLengthForAlgorithmOnLimitedStrengthCrypto(encryptionMethod);
}
public static int getMaximumPasswordLengthForAlgorithmOnLimitedStrengthCrypto(EncryptionMethod encryptionMethod) {
if (encryptionMethod == null) {
throw new IllegalArgumentException("Cannot evaluate an empty encryption method algorithm");
}
if (MAX_PASSWORD_LENGTH_BY_ALGORITHM.containsKey(encryptionMethod.getAlgorithm())) {
return MAX_PASSWORD_LENGTH_BY_ALGORITHM.get(encryptionMethod.getAlgorithm());
} else {
return -1;
}
}
}

View File

@ -0,0 +1,73 @@
/*
* 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 javax.crypto.Cipher;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public abstract class KeyedCipherProvider implements CipherProvider {
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 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 key the key
* @param iv the IV or nonce
* @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, SecretKey key, byte[] iv, boolean encryptMode) throws Exception;
/**
* Returns an initialized cipher for the specified algorithm. The IV will be generated internally (for encryption). If decryption is requested, it will throw an exception.
*
* @param encryptionMethod the {@link EncryptionMethod}
* @param key the key
* @param encryptMode true for encrypt, false for decrypt
* @return the initialized cipher
* @throws Exception if there is a problem initializing the cipher or if decryption is requested
*/
abstract Cipher getCipher(EncryptionMethod encryptionMethod, SecretKey key, boolean encryptMode) throws Exception;
/**
* Generates a new random IV of the correct length.
*
* @return the IV
*/
abstract byte[] generateIV();
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);
}
}

View File

@ -0,0 +1,163 @@
/*
* 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.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.NoSuchAlgorithmException;
public class KeyedEncryptor implements Encryptor {
private EncryptionMethod encryptionMethod;
private SecretKey key;
private byte[] iv;
private static final int DEFAULT_MAX_ALLOWED_KEY_LENGTH = 128;
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 KeyedEncryptor(final EncryptionMethod encryptionMethod, final SecretKey key) {
this(encryptionMethod, key == null ? new byte[0] : key.getEncoded(), new byte[0]);
}
public KeyedEncryptor(final EncryptionMethod encryptionMethod, final SecretKey key, final byte[] iv) {
this(encryptionMethod, key == null ? new byte[0] : key.getEncoded(), iv);
}
public KeyedEncryptor(final EncryptionMethod encryptionMethod, final byte[] keyBytes) {
this(encryptionMethod, keyBytes, new byte[0]);
}
public KeyedEncryptor(final EncryptionMethod encryptionMethod, final byte[] keyBytes, final byte[] iv) {
super();
try {
if (encryptionMethod == null) {
throw new IllegalArgumentException("Cannot instantiate a keyed encryptor with null encryption method");
}
if (!encryptionMethod.isKeyedCipher()) {
throw new IllegalArgumentException("Cannot instantiate a keyed encryptor with encryption method " + encryptionMethod.name());
}
this.encryptionMethod = encryptionMethod;
if (keyBytes == null || keyBytes.length == 0) {
throw new IllegalArgumentException("Cannot instantiate a keyed encryptor with empty key");
}
if (!CipherUtility.isValidKeyLengthForAlgorithm(keyBytes.length * 8, encryptionMethod.getAlgorithm())) {
throw new IllegalArgumentException("Cannot instantiate a keyed encryptor with key of length " + keyBytes.length);
}
String cipherName = CipherUtility.parseCipherFromAlgorithm(encryptionMethod.getAlgorithm());
this.key = new SecretKeySpec(keyBytes, cipherName);
this.iv = iv;
} 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;
}
}
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
KeyedCipherProvider cipherProvider = (KeyedCipherProvider) CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE);
// Generate cipher
try {
Cipher cipher;
// The IV could have been set by the constructor, but if not, read from the cipher stream
if (iv.length == 0) {
iv = cipherProvider.readIV(in);
}
cipher = cipherProvider.getCipher(encryptionMethod, key, iv, 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
KeyedCipherProvider cipherProvider = (KeyedCipherProvider) CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE);
// Generate cipher
try {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, key, iv, true);
cipherProvider.writeIV(cipher.getIV(), out);
CipherUtility.processStreams(cipher, in, out);
} catch (Exception e) {
throw new ProcessException(e);
}
}
}
}

View File

@ -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;
}
}

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.processors.standard.util;
package org.apache.nifi.processors.standard.util.crypto;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.io.StreamCallback;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.processors.standard.util;
package org.apache.nifi.processors.standard.util.crypto;
import org.apache.nifi.processor.exception.ProcessException;
import org.apache.nifi.processor.io.StreamCallback;

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,789 @@
// Copyright (c) 2006 Damien Miller <djm@mindrot.org>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package org.apache.nifi.processors.standard.util.crypto.bcrypt;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
/**
* BCrypt implements OpenBSD-style Blowfish password hashing using
* the scheme described in "A Future-Adaptable Password Scheme" by
* Niels Provos and David Mazieres.
* <p/>
* This password hashing system tries to thwart off-line password
* cracking using a computationally-intensive hashing algorithm,
* based on Bruce Schneier's Blowfish cipher. The work factor of
* the algorithm is parameterised, so it can be increased as
* computers get faster.
* <p/>
* Usage is really simple. To hash a password for the first time,
* call the hashpw method with a random salt, like this:
* <p/>
* <code>
* String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());
* </code>
* <p/>
* To check whether a plaintext password matches one that has been
* hashed previously, use the checkpw method:
* <p/>
* <code>
* if (BCrypt.checkpw(candidate_password, stored_hash))
* &nbsp;&nbsp;&nbsp;&nbsp;System.out.println("It matches");
* else
* &nbsp;&nbsp;&nbsp;&nbsp;System.out.println("It does not match");
* </code>
* <p/>
* The gensalt() method takes an optional parameter (log_rounds)
* that determines the computational complexity of the hashing:
* <p/>
* <code>
* String strong_salt = BCrypt.gensalt(10)
* String stronger_salt = BCrypt.gensalt(12)
* </code>
* <p/>
* The amount of work increases exponentially (2**log_rounds), so
* each increment is twice as much work. The default log_rounds is
* 10, and the valid range is 4 to 30.
*
* @author Damien Miller
* @version 0.4
*/
public class BCrypt {
// BCrypt parameters
private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10;
private static final int BCRYPT_SALT_LEN = 16;
// Blowfish parameters
private static final int BLOWFISH_NUM_ROUNDS = 16;
// Initial contents of key schedule
private static final int P_orig[] = {
0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344,
0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89,
0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c,
0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917,
0x9216d5d9, 0x8979fb1b
};
private static final int S_orig[] = {
0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7,
0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99,
0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16,
0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e,
0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee,
0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013,
0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef,
0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e,
0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60,
0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440,
0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce,
0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a,
0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e,
0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677,
0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193,
0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032,
0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88,
0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239,
0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e,
0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0,
0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3,
0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98,
0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88,
0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe,
0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6,
0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d,
0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b,
0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7,
0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba,
0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463,
0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f,
0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09,
0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3,
0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb,
0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279,
0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8,
0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab,
0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82,
0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db,
0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573,
0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0,
0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b,
0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790,
0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8,
0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4,
0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0,
0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7,
0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c,
0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad,
0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1,
0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299,
0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9,
0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477,
0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf,
0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49,
0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af,
0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa,
0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5,
0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41,
0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915,
0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400,
0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915,
0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664,
0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a,
0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623,
0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266,
0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1,
0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e,
0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6,
0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1,
0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e,
0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1,
0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737,
0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8,
0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff,
0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd,
0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701,
0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7,
0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41,
0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331,
0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf,
0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af,
0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e,
0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87,
0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c,
0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2,
0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16,
0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd,
0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b,
0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509,
0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e,
0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3,
0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f,
0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a,
0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4,
0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960,
0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66,
0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28,
0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802,
0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84,
0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510,
0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf,
0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14,
0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e,
0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50,
0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7,
0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8,
0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281,
0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99,
0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696,
0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128,
0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73,
0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0,
0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0,
0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105,
0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250,
0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3,
0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285,
0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00,
0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061,
0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb,
0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e,
0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735,
0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc,
0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9,
0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340,
0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20,
0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7,
0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934,
0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068,
0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af,
0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840,
0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45,
0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504,
0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a,
0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb,
0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee,
0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6,
0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42,
0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b,
0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2,
0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb,
0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527,
0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b,
0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33,
0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c,
0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3,
0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc,
0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17,
0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564,
0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b,
0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115,
0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922,
0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728,
0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0,
0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e,
0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37,
0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d,
0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804,
0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b,
0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3,
0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb,
0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d,
0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c,
0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350,
0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9,
0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a,
0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe,
0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d,
0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc,
0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f,
0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61,
0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2,
0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9,
0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2,
0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c,
0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e,
0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633,
0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10,
0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169,
0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52,
0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027,
0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5,
0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62,
0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634,
0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76,
0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24,
0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc,
0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4,
0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c,
0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837,
0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0,
0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b,
0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe,
0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b,
0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4,
0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8,
0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6,
0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304,
0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22,
0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4,
0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6,
0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9,
0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59,
0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593,
0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51,
0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28,
0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c,
0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b,
0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28,
0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c,
0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd,
0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a,
0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319,
0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb,
0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f,
0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991,
0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32,
0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680,
0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166,
0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae,
0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb,
0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5,
0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47,
0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370,
0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d,
0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84,
0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048,
0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8,
0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd,
0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9,
0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7,
0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38,
0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f,
0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c,
0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525,
0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1,
0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442,
0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964,
0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e,
0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8,
0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d,
0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f,
0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299,
0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02,
0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc,
0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614,
0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a,
0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6,
0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b,
0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0,
0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060,
0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e,
0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9,
0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f,
0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6
};
// bcrypt IV: "OrpheanBeholderScryDoubt". The C implementation calls
// this "ciphertext", but it is really plaintext or an IV. We keep
// the name to make code comparison easier.
static private final int bf_crypt_ciphertext[] = {
0x4f727068, 0x65616e42, 0x65686f6c,
0x64657253, 0x63727944, 0x6f756274
};
// Table for Base64 encoding
static private final char base64_code[] = {
'.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V',
'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h',
'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5',
'6', '7', '8', '9'
};
// Table for Base64 decoding
static private final byte index_64[] = {
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, 0, 1, 54, 55,
56, 57, 58, 59, 60, 61, 62, 63, -1, -1,
-1, -1, -1, -1, -1, 2, 3, 4, 5, 6,
7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27,
-1, -1, -1, -1, -1, -1, 28, 29, 30,
31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50,
51, 52, 53, -1, -1, -1, -1, -1
};
// Expanded Blowfish key
private int P[];
private int S[];
/**
* Encode a byte array using bcrypt's slightly-modified base64
* encoding scheme. Note that this is *not* compatible with
* the standard MIME-base64 encoding.
*
* @param d the byte array to encode
* @param len the number of bytes to encode
* @throws IllegalArgumentException if the length is invalid
* @return base64-encoded string
*/
private static String encode_base64(byte d[], int len)
throws IllegalArgumentException {
int off = 0;
StringBuffer rs = new StringBuffer();
int c1, c2;
if (len <= 0 || len > d.length)
throw new IllegalArgumentException("Invalid len");
while (off < len) {
c1 = d[off++] & 0xff;
rs.append(base64_code[(c1 >> 2) & 0x3f]);
c1 = (c1 & 0x03) << 4;
if (off >= len) {
rs.append(base64_code[c1 & 0x3f]);
break;
}
c2 = d[off++] & 0xff;
c1 |= (c2 >> 4) & 0x0f;
rs.append(base64_code[c1 & 0x3f]);
c1 = (c2 & 0x0f) << 2;
if (off >= len) {
rs.append(base64_code[c1 & 0x3f]);
break;
}
c2 = d[off++] & 0xff;
c1 |= (c2 >> 6) & 0x03;
rs.append(base64_code[c1 & 0x3f]);
rs.append(base64_code[c2 & 0x3f]);
}
return rs.toString();
}
/**
* Look up the 3 bits base64-encoded by the specified character,
* range-checking againt conversion table
*
* @param x the base64-encoded value
* @return the decoded value of x
*/
private static byte char64(char x) {
if ((int) x < 0 || (int) x > index_64.length)
return -1;
return index_64[(int) x];
}
/**
* Decode a string encoded using bcrypt's base64 scheme to a
* byte array. Note that this is *not* compatible with
* the standard MIME-base64 encoding.
*
* @param s the string to decode
* @param maxolen the maximum number of bytes to decode
* @throws IllegalArgumentException if maxolen is invalid
* @return an array containing the decoded bytes
*/
private static byte[] decode_base64(String s, int maxolen)
throws IllegalArgumentException {
StringBuffer rs = new StringBuffer();
int off = 0, slen = s.length(), olen = 0;
byte ret[];
byte c1, c2, c3, c4, o;
if (maxolen <= 0)
throw new IllegalArgumentException("Invalid maxolen");
while (off < slen - 1 && olen < maxolen) {
c1 = char64(s.charAt(off++));
c2 = char64(s.charAt(off++));
if (c1 == -1 || c2 == -1)
break;
o = (byte) (c1 << 2);
o |= (c2 & 0x30) >> 4;
rs.append((char) o);
if (++olen >= maxolen || off >= slen)
break;
c3 = char64(s.charAt(off++));
if (c3 == -1)
break;
o = (byte) ((c2 & 0x0f) << 4);
o |= (c3 & 0x3c) >> 2;
rs.append((char) o);
if (++olen >= maxolen || off >= slen)
break;
c4 = char64(s.charAt(off++));
o = (byte) ((c3 & 0x03) << 6);
o |= c4;
rs.append((char) o);
++olen;
}
ret = new byte[olen];
for (off = 0; off < olen; off++)
ret[off] = (byte) rs.charAt(off);
return ret;
}
/**
* Blowfish encipher a single 64-bit block encoded as
* two 32-bit halves
*
* @param lr an array containing the two 32-bit half blocks
* @param off the position in the array of the blocks
*/
private final void encipher(int lr[], int off) {
int i, n, l = lr[off], r = lr[off + 1];
l ^= P[0];
for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2; ) {
// Feistel substitution on left word
n = S[(l >> 24) & 0xff];
n += S[0x100 | ((l >> 16) & 0xff)];
n ^= S[0x200 | ((l >> 8) & 0xff)];
n += S[0x300 | (l & 0xff)];
r ^= n ^ P[++i];
// Feistel substitution on right word
n = S[(r >> 24) & 0xff];
n += S[0x100 | ((r >> 16) & 0xff)];
n ^= S[0x200 | ((r >> 8) & 0xff)];
n += S[0x300 | (r & 0xff)];
l ^= n ^ P[++i];
}
lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1];
lr[off + 1] = l;
}
/**
* Cycically extract a word of key material
*
* @param data the string to extract the data from
* @param offp a "pointer" (as a one-entry array) to the
* current offset into data
* @return the next word of material from data
*/
private static int streamtoword(byte data[], int offp[]) {
int i;
int word = 0;
int off = offp[0];
for (i = 0; i < 4; i++) {
word = (word << 8) | (data[off] & 0xff);
off = (off + 1) % data.length;
}
offp[0] = off;
return word;
}
/**
* Initialise the Blowfish key schedule
*/
private void init_key() {
P = (int[]) P_orig.clone();
S = (int[]) S_orig.clone();
}
/**
* Key the Blowfish cipher
*
* @param key an array containing the key
*/
private void key(byte key[]) {
int i;
int koffp[] = {0};
int lr[] = {0, 0};
int plen = P.length, slen = S.length;
for (i = 0; i < plen; i++)
P[i] = P[i] ^ streamtoword(key, koffp);
for (i = 0; i < plen; i += 2) {
encipher(lr, 0);
P[i] = lr[0];
P[i + 1] = lr[1];
}
for (i = 0; i < slen; i += 2) {
encipher(lr, 0);
S[i] = lr[0];
S[i + 1] = lr[1];
}
}
/**
* Perform the "enhanced key schedule" step described by
* Provos and Mazieres in "A Future-Adaptable Password Scheme"
* http://www.openbsd.org/papers/bcrypt-paper.ps
*
* @param data salt information
* @param key password information
*/
private void ekskey(byte data[], byte key[]) {
int i;
int koffp[] = {0}, doffp[] = {0};
int lr[] = {0, 0};
int plen = P.length, slen = S.length;
for (i = 0; i < plen; i++)
P[i] = P[i] ^ streamtoword(key, koffp);
for (i = 0; i < plen; i += 2) {
lr[0] ^= streamtoword(data, doffp);
lr[1] ^= streamtoword(data, doffp);
encipher(lr, 0);
P[i] = lr[0];
P[i + 1] = lr[1];
}
for (i = 0; i < slen; i += 2) {
lr[0] ^= streamtoword(data, doffp);
lr[1] ^= streamtoword(data, doffp);
encipher(lr, 0);
S[i] = lr[0];
S[i + 1] = lr[1];
}
}
/**
* Perform the central password hashing step in the
* bcrypt scheme
*
* @param password the password to hash
* @param salt the binary salt to hash with the password
* @param log_rounds the binary logarithm of the number
* of rounds of hashing to apply
* @param cdata the plaintext to encrypt
* @return an array containing the binary hashed password
*/
public byte[] crypt_raw(byte password[], byte salt[], int log_rounds,
int cdata[]) {
int rounds, i, j;
int clen = cdata.length;
byte ret[];
if (log_rounds < 4 || log_rounds > 30)
throw new IllegalArgumentException("Bad number of rounds");
rounds = 1 << log_rounds;
if (salt.length != BCRYPT_SALT_LEN)
throw new IllegalArgumentException("Bad salt length");
init_key();
ekskey(salt, password);
for (i = 0; i != rounds; i++) {
key(password);
key(salt);
}
for (i = 0; i < 64; i++) {
for (j = 0; j < (clen >> 1); j++)
encipher(cdata, j << 1);
}
ret = new byte[clen * 4];
for (i = 0, j = 0; i < clen; i++) {
ret[j++] = (byte) ((cdata[i] >> 24) & 0xff);
ret[j++] = (byte) ((cdata[i] >> 16) & 0xff);
ret[j++] = (byte) ((cdata[i] >> 8) & 0xff);
ret[j++] = (byte) (cdata[i] & 0xff);
}
return ret;
}
/**
* Hash a password using the OpenBSD bcrypt scheme
*
* @param password the password to hash
* @param salt the salt to hash with (perhaps generated
* using BCrypt.gensalt)
* @return the hashed password
*/
public static String hashpw(String password, String salt) {
BCrypt B;
String real_salt;
byte passwordb[], saltb[], hashed[];
char minor = (char) 0;
int rounds, off = 0;
StringBuffer rs = new StringBuffer();
if (salt.charAt(0) != '$' || salt.charAt(1) != '2')
throw new IllegalArgumentException("Invalid salt version");
if (salt.charAt(2) == '$')
off = 3;
else {
minor = salt.charAt(2);
if (minor != 'a' || salt.charAt(3) != '$')
throw new IllegalArgumentException("Invalid salt revision");
off = 4;
}
// Extract number of rounds
if (salt.charAt(off + 2) > '$')
throw new IllegalArgumentException("Missing salt rounds");
rounds = Integer.parseInt(salt.substring(off, off + 2));
real_salt = salt.substring(off + 3, off + 25);
try {
passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8");
} catch (UnsupportedEncodingException uee) {
throw new AssertionError("UTF-8 is not supported");
}
saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
B = new BCrypt();
hashed = B.crypt_raw(passwordb, saltb, rounds,
(int[]) bf_crypt_ciphertext.clone());
rs.append("$2");
if (minor >= 'a')
rs.append(minor);
rs.append("$");
if (rounds < 10)
rs.append("0");
if (rounds > 30) {
throw new IllegalArgumentException(
"rounds exceeds maximum (30)");
}
rs.append(Integer.toString(rounds));
rs.append("$");
rs.append(encode_base64(saltb, saltb.length));
rs.append(encode_base64(hashed,
bf_crypt_ciphertext.length * 4 - 1));
return rs.toString();
}
/**
* Generate a salt for use with the BCrypt.hashpw() method
*
* @param log_rounds the log2 of the number of rounds of
* hashing to apply - the work factor therefore increases as
* 2**log_rounds.
* @param random an instance of SecureRandom to use
* @return an encoded salt value
*/
public static String gensalt(int log_rounds, SecureRandom random) {
StringBuffer rs = new StringBuffer();
byte rnd[] = new byte[BCRYPT_SALT_LEN];
random.nextBytes(rnd);
rs.append("$2a$");
if (log_rounds < 10)
rs.append("0");
if (log_rounds > 30) {
throw new IllegalArgumentException(
"log_rounds exceeds maximum (30)");
}
rs.append(Integer.toString(log_rounds));
rs.append("$");
rs.append(encode_base64(rnd, rnd.length));
return rs.toString();
}
/**
* Generate a salt for use with the BCrypt.hashpw() method
*
* @param log_rounds the log2 of the number of rounds of
* hashing to apply - the work factor therefore increases as
* 2**log_rounds.
* @return an encoded salt value
*/
public static String gensalt(int log_rounds) {
return gensalt(log_rounds, new SecureRandom());
}
/**
* Generate a salt for use with the BCrypt.hashpw() method,
* selecting a reasonable default for the number of hashing
* rounds to apply
*
* @return an encoded salt value
*/
public static String gensalt() {
return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS);
}
/**
* Check that a plaintext password matches a previously hashed
* one
*
* @param plaintext the plaintext password to verify
* @param hashed the previously-hashed password
* @return true if the passwords match, false otherwise
*/
public static boolean checkpw(String plaintext, String hashed) {
byte hashed_bytes[];
byte try_bytes[];
try {
String try_pw = hashpw(plaintext, hashed);
hashed_bytes = hashed.getBytes("UTF-8");
try_bytes = try_pw.getBytes("UTF-8");
} catch (UnsupportedEncodingException uee) {
return false;
}
if (hashed_bytes.length != try_bytes.length)
return false;
byte ret = 0;
for (int i = 0; i < try_bytes.length; i++)
ret |= hashed_bytes[i] ^ try_bytes[i];
return ret == 0;
}
}

View File

@ -0,0 +1,509 @@
/*
* 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.scrypt;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.processors.standard.util.crypto.CipherUtility;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import static java.lang.Integer.MAX_VALUE;
import static java.lang.System.arraycopy;
/**
* Copyright (C) 2011 - Will Glozer. All rights reserved.
* <p/>
* Taken from Will Glozer's port of Colin Percival's C implementation. Glozer's project located at <a href="https://github.com/wg/scrypt">https://github.com/wg/scrypt</a> was released under the ASF
* 2.0 license and has not been updated since May 25, 2013 and there are outstanding issues which have been patched in this version.
* <p/>
* An implementation of the <a href="http://www.tarsnap.com/scrypt/scrypt.pdf">scrypt</a>
* key derivation function.
* <p/>
* Allows for hashing passwords using the
* <a href="http://www.tarsnap.com/scrypt.html">scrypt</a> key derivation function
* and comparing a plain text password to a hashed one.
*/
public class Scrypt {
private static final Logger logger = LoggerFactory.getLogger(Scrypt.class);
private static final int DEFAULT_SALT_LENGTH = 16;
/**
* Hash the supplied plaintext password and generate output in the format described
* below:
* <p/>
* The hashed output is an
* extended implementation of the Modular Crypt Format that also includes the scrypt
* algorithm parameters.
* <p/>
* Format: <code>$s0$PARAMS$SALT$KEY</code>.
* <p/>
* <dl>
* <dd>PARAMS</dd><dt>32-bit hex integer containing log2(N) (16 bits), r (8 bits), and p (8 bits)</dt>
* <dd>SALT</dd><dt>base64-encoded salt</dt>
* <dd>KEY</dd><dt>base64-encoded derived key</dt>
* </dl>
* <p/>
* <code>s0</code> identifies version 0 of the scrypt format, using a 128-bit salt and 256-bit derived key.
* <p/>
* This method generates a 16 byte random salt internally.
*
* @param password password
* @param n CPU cost parameter
* @param r memory cost parameter
* @param p parallelization parameter
* @param dkLen the desired key length in bits
* @return the hashed password
*/
public static String scrypt(String password, int n, int r, int p, int dkLen) {
byte[] salt = new byte[DEFAULT_SALT_LENGTH];
new SecureRandom().nextBytes(salt);
return scrypt(password, salt, n, r, p, dkLen);
}
/**
* Hash the supplied plaintext password and generate output in the format described
* in {@link Scrypt#scrypt(String, int, int, int, int)}.
*
* @param password password
* @param salt the raw salt (16 bytes)
* @param n CPU cost parameter
* @param r memory cost parameter
* @param p parallelization parameter
* @param dkLen the desired key length in bits
* @return the hashed password
*/
public static String scrypt(String password, byte[] salt, int n, int r, int p, int dkLen) {
try {
byte[] derived = deriveScryptKey(password.getBytes(StandardCharsets.UTF_8), salt, n, r, p, dkLen);
return formatHash(salt, n, r, p, derived);
} catch (GeneralSecurityException e) {
throw new IllegalStateException("JVM doesn't support SHA1PRNG or HMAC_SHA256?");
}
}
public static String formatSalt(byte[] salt, int n, int r, int p) {
String params = encodeParams(n, r, p);
StringBuilder sb = new StringBuilder((salt.length) * 2);
sb.append("$s0$").append(params).append('$');
sb.append(CipherUtility.encodeBase64NoPadding(salt));
return sb.toString();
}
private static String encodeParams(int n, int r, int p) {
return Long.toString(log2(n) << 16L | r << 8 | p, 16);
}
private static String formatHash(byte[] salt, int n, int r, int p, byte[] derived) {
StringBuilder sb = new StringBuilder((salt.length + derived.length) * 2);
sb.append(formatSalt(salt, n, r, p)).append('$');
sb.append(CipherUtility.encodeBase64NoPadding(derived));
return sb.toString();
}
/**
* Returns the expected memory cost of the provided parameters in bytes.
*
* @param n the N value, iterations >= 2
* @param r the r value, block size >= 1
* @param p the p value, parallelization factor >= 1
* @return the memory cost in bytes
*/
public static int calculateExpectedMemory(int n, int r, int p) {
return 128 * r * n + 128 * r * p;
}
/**
* Compare the supplied plaintext password to a hashed password.
*
* @param password plaintext password
* @param hashed scrypt hashed password
* @return true if password matches hashed value
*/
public static boolean check(String password, String hashed) {
try {
if (StringUtils.isEmpty(password)) {
throw new IllegalArgumentException("Password cannot be empty");
}
if (StringUtils.isEmpty(hashed)) {
throw new IllegalArgumentException("Hash cannot be empty");
}
String[] parts = hashed.split("\\$");
if (parts.length != 5 || !parts[1].equals("s0")) {
throw new IllegalArgumentException("Hash is not properly formatted");
}
List<Integer> splitParams = parseParameters(parts[2]);
int n = splitParams.get(0);
int r = splitParams.get(1);
int p = splitParams.get(2);
byte[] salt = Base64.decodeBase64(parts[3]);
byte[] derived0 = Base64.decodeBase64(parts[4]);
// Previously this was hard-coded to 32 bits but the publicly-available scrypt methods accept arbitrary bit lengths
int hashLength = derived0.length * 8;
byte[] derived1 = deriveScryptKey(password.getBytes(StandardCharsets.UTF_8), salt, n, r, p, hashLength);
if (derived0.length != derived1.length) return false;
int result = 0;
for (int i = 0; i < derived0.length; i++) {
result |= derived0[i] ^ derived1[i];
}
return result == 0;
} catch (GeneralSecurityException e) {
throw new IllegalStateException("JVM doesn't support SHA1PRNG or HMAC_SHA256?");
}
}
/**
* Parses the individual values from the encoded params value in the modified-mcrypt format for the salt & hash.
* <p/>
* Example:
* <p/>
* Hash: $s0$e0801$epIxT/h6HbbwHaehFnh/bw$7H0vsXlY8UxxyW/BWx/9GuY7jEvGjT71GFd6O4SZND0
* Params: e0801
* <p/>
* N = 16384
* r = 8
* p = 1
*
* @param encodedParams the String representation of the second section of the mcrypt format hash
* @return a list containing N, r, p
*/
public static List<Integer> parseParameters(String encodedParams) {
long params = Long.parseLong(encodedParams, 16);
List<Integer> paramsList = new ArrayList<>(3);
// Parse N, r, p from encoded value and add to return list
paramsList.add((int) Math.pow(2, params >> 16 & 0xffff));
paramsList.add((int) params >> 8 & 0xff);
paramsList.add((int) params & 0xff);
return paramsList;
}
private static int log2(int n) {
int log = 0;
if ((n & 0xffff0000) != 0) {
n >>>= 16;
log = 16;
}
if (n >= 256) {
n >>>= 8;
log += 8;
}
if (n >= 16) {
n >>>= 4;
log += 4;
}
if (n >= 4) {
n >>>= 2;
log += 2;
}
return log + (n >>> 1);
}
/**
* Implementation of the <a href="http://www.tarsnap.com/scrypt/scrypt.pdf">scrypt KDF</a>.
*
* @param password password
* @param salt salt
* @param n CPU cost parameter
* @param r memory cost parameter
* @param p parallelization parameter
* @param dkLen intended length of the derived key in bits
* @return the derived key
* @throws GeneralSecurityException when HMAC_SHA256 is not available
*/
protected static byte[] deriveScryptKey(byte[] password, byte[] salt, int n, int r, int p, int dkLen) throws GeneralSecurityException {
if (n < 2 || (n & (n - 1)) != 0) {
throw new IllegalArgumentException("N must be a power of 2 greater than 1");
}
if (r < 1) {
throw new IllegalArgumentException("Parameter r must be 1 or greater");
}
if (p < 1) {
throw new IllegalArgumentException("Parameter p must be 1 or greater");
}
if (n > MAX_VALUE / 128 / r) {
throw new IllegalArgumentException("Parameter N is too large");
}
// Must be enforced before r check
if (p > MAX_VALUE / 128) {
throw new IllegalArgumentException("Parameter p is too large");
}
if (r > MAX_VALUE / 128 / p) {
throw new IllegalArgumentException("Parameter r is too large");
}
if (password == null || password.length == 0) {
throw new IllegalArgumentException("Password cannot be empty");
}
int saltLength = salt == null ? 0 : salt.length;
if (salt == null || saltLength == 0) {
// Do not enforce this check here. According to the scrypt spec, the salt can be empty. However, in the user-facing ScryptCipherProvider, enforce an arbitrary check to avoid empty salts
logger.warn("An empty salt was used for scrypt key derivation");
// throw new IllegalArgumentException("Salt cannot be empty");
}
if (saltLength < 8 || saltLength > 32) {
// Do not enforce this check here. According to the scrypt spec, the salt can be empty. However, in the user-facing ScryptCipherProvider, enforce an arbitrary check of [8..32] bytes
logger.warn("A salt of length {} was used for scrypt key derivation", saltLength);
// throw new IllegalArgumentException("Salt must be between 8 and 32 bytes");
}
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(password, "HmacSHA256"));
byte[] b = new byte[128 * r * p];
byte[] xy = new byte[256 * r];
byte[] v = new byte[128 * r * n];
int i;
pbkdf2(mac, salt, 1, b, p * 128 * r);
for (i = 0; i < p; i++) {
smix(b, i * 128 * r, r, n, v, xy);
}
byte[] dk = new byte[dkLen / 8];
pbkdf2(mac, b, 1, dk, dkLen / 8);
return dk;
}
/**
* Implementation of PBKDF2 (RFC2898).
*
* @param alg the HMAC algorithm to use
* @param p the password
* @param s the salt
* @param c the iteration count
* @param dkLen the intended length, in octets, of the derived key
* @return The derived key
*/
private static byte[] pbkdf2(String alg, byte[] p, byte[] s, int c, int dkLen) throws GeneralSecurityException {
Mac mac = Mac.getInstance(alg);
mac.init(new SecretKeySpec(p, alg));
byte[] dk = new byte[dkLen];
pbkdf2(mac, s, c, dk, dkLen);
return dk;
}
/**
* Implementation of PBKDF2 (RFC2898).
*
* @param mac the pre-initialized {@link Mac} instance to use
* @param s the salt
* @param c the iteration count
* @param dk the byte array that derived key will be placed in
* @param dkLen the intended length, in octets, of the derived key
* @throws GeneralSecurityException if the key length is too long
*/
private static void pbkdf2(Mac mac, byte[] s, int c, byte[] dk, int dkLen) throws GeneralSecurityException {
int hLen = mac.getMacLength();
if (dkLen > (Math.pow(2, 32) - 1) * hLen) {
throw new GeneralSecurityException("Requested key length too long");
}
byte[] U = new byte[hLen];
byte[] T = new byte[hLen];
byte[] block1 = new byte[s.length + 4];
int l = (int) Math.ceil((double) dkLen / hLen);
int r = dkLen - (l - 1) * hLen;
arraycopy(s, 0, block1, 0, s.length);
for (int i = 1; i <= l; i++) {
block1[s.length + 0] = (byte) (i >> 24 & 0xff);
block1[s.length + 1] = (byte) (i >> 16 & 0xff);
block1[s.length + 2] = (byte) (i >> 8 & 0xff);
block1[s.length + 3] = (byte) (i >> 0 & 0xff);
mac.update(block1);
mac.doFinal(U, 0);
arraycopy(U, 0, T, 0, hLen);
for (int j = 1; j < c; j++) {
mac.update(U);
mac.doFinal(U, 0);
for (int k = 0; k < hLen; k++) {
T[k] ^= U[k];
}
}
arraycopy(T, 0, dk, (i - 1) * hLen, (i == l ? r : hLen));
}
}
private static void smix(byte[] b, int bi, int r, int n, byte[] v, byte[] xy) {
int xi = 0;
int yi = 128 * r;
int i;
arraycopy(b, bi, xy, xi, 128 * r);
for (i = 0; i < n; i++) {
arraycopy(xy, xi, v, i * (128 * r), 128 * r);
blockmix_salsa8(xy, xi, yi, r);
}
for (i = 0; i < n; i++) {
int j = integerify(xy, xi, r) & (n - 1);
blockxor(v, j * (128 * r), xy, xi, 128 * r);
blockmix_salsa8(xy, xi, yi, r);
}
arraycopy(xy, xi, b, bi, 128 * r);
}
private static void blockmix_salsa8(byte[] by, int bi, int yi, int r) {
byte[] X = new byte[64];
int i;
arraycopy(by, bi + (2 * r - 1) * 64, X, 0, 64);
for (i = 0; i < 2 * r; i++) {
blockxor(by, i * 64, X, 0, 64);
salsa20_8(X);
arraycopy(X, 0, by, yi + (i * 64), 64);
}
for (i = 0; i < r; i++) {
arraycopy(by, yi + (i * 2) * 64, by, bi + (i * 64), 64);
}
for (i = 0; i < r; i++) {
arraycopy(by, yi + (i * 2 + 1) * 64, by, bi + (i + r) * 64, 64);
}
}
private static int r(int a, int b) {
return (a << b) | (a >>> (32 - b));
}
private static void salsa20_8(byte[] b) {
int[] b32 = new int[16];
int[] x = new int[16];
int i;
for (i = 0; i < 16; i++) {
b32[i] = (b[i * 4 + 0] & 0xff) << 0;
b32[i] |= (b[i * 4 + 1] & 0xff) << 8;
b32[i] |= (b[i * 4 + 2] & 0xff) << 16;
b32[i] |= (b[i * 4 + 3] & 0xff) << 24;
}
arraycopy(b32, 0, x, 0, 16);
for (i = 8; i > 0; i -= 2) {
x[4] ^= r(x[0] + x[12], 7);
x[8] ^= r(x[4] + x[0], 9);
x[12] ^= r(x[8] + x[4], 13);
x[0] ^= r(x[12] + x[8], 18);
x[9] ^= r(x[5] + x[1], 7);
x[13] ^= r(x[9] + x[5], 9);
x[1] ^= r(x[13] + x[9], 13);
x[5] ^= r(x[1] + x[13], 18);
x[14] ^= r(x[10] + x[6], 7);
x[2] ^= r(x[14] + x[10], 9);
x[6] ^= r(x[2] + x[14], 13);
x[10] ^= r(x[6] + x[2], 18);
x[3] ^= r(x[15] + x[11], 7);
x[7] ^= r(x[3] + x[15], 9);
x[11] ^= r(x[7] + x[3], 13);
x[15] ^= r(x[11] + x[7], 18);
x[1] ^= r(x[0] + x[3], 7);
x[2] ^= r(x[1] + x[0], 9);
x[3] ^= r(x[2] + x[1], 13);
x[0] ^= r(x[3] + x[2], 18);
x[6] ^= r(x[5] + x[4], 7);
x[7] ^= r(x[6] + x[5], 9);
x[4] ^= r(x[7] + x[6], 13);
x[5] ^= r(x[4] + x[7], 18);
x[11] ^= r(x[10] + x[9], 7);
x[8] ^= r(x[11] + x[10], 9);
x[9] ^= r(x[8] + x[11], 13);
x[10] ^= r(x[9] + x[8], 18);
x[12] ^= r(x[15] + x[14], 7);
x[13] ^= r(x[12] + x[15], 9);
x[14] ^= r(x[13] + x[12], 13);
x[15] ^= r(x[14] + x[13], 18);
}
for (i = 0; i < 16; ++i) b32[i] = x[i] + b32[i];
for (i = 0; i < 16; i++) {
b[i * 4 + 0] = (byte) (b32[i] >> 0 & 0xff);
b[i * 4 + 1] = (byte) (b32[i] >> 8 & 0xff);
b[i * 4 + 2] = (byte) (b32[i] >> 16 & 0xff);
b[i * 4 + 3] = (byte) (b32[i] >> 24 & 0xff);
}
}
private static void blockxor(byte[] s, int si, byte[] d, int di, int len) {
for (int i = 0; i < len; i++) {
d[di + i] ^= s[si + i];
}
}
private static int integerify(byte[] b, int bi, int r) {
int n;
bi += (2 * r - 1) * 64;
n = (b[bi + 0] & 0xff) << 0;
n |= (b[bi + 1] & 0xff) << 8;
n |= (b[bi + 2] & 0xff) << 16;
n |= (b[bi + 3] & 0xff) << 24;
return n;
}
public static int getDefaultSaltLength() {
return DEFAULT_SALT_LENGTH;
}
}

View File

@ -0,0 +1,479 @@
/*
* 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
import org.apache.nifi.components.ValidationResult
import org.apache.nifi.processors.standard.util.crypto.CipherUtility
import org.apache.nifi.processors.standard.util.crypto.PasswordBasedEncryptor
import org.apache.nifi.security.util.EncryptionMethod
import org.apache.nifi.security.util.KeyDerivationFunction
import org.apache.nifi.util.MockFlowFile
import org.apache.nifi.util.MockProcessContext
import org.apache.nifi.util.TestRunner
import org.apache.nifi.util.TestRunners
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Assert
import org.junit.Assume
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.nio.file.Paths
import java.security.Security
@RunWith(JUnit4.class)
public class TestEncryptContentGroovy {
private static final Logger logger = LoggerFactory.getLogger(TestEncryptContentGroovy.class)
private static final String WEAK_CRYPTO_ALLOWED = EncryptContent.WEAK_CRYPTO_ALLOWED_NAME
private static final String WEAK_CRYPTO_NOT_ALLOWED = EncryptContent.WEAK_CRYPTO_NOT_ALLOWED_NAME
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void testShouldValidateMaxKeySizeForAlgorithmsOnUnlimitedStrengthJVM() throws IOException {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
PasswordBasedEncryptor.supportsUnlimitedStrength());
final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class)
Collection<ValidationResult> results
MockProcessContext pc
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Integer.MAX_VALUE or 128, so use 256 or 128
final int MAX_KEY_LENGTH = [PasswordBasedEncryptor.getMaxAllowedKeyLength(encryptionMethod.algorithm), 256].min()
final String TOO_LONG_KEY_HEX = "ab" * (MAX_KEY_LENGTH / 8 + 1)
logger.info("Using key ${TOO_LONG_KEY_HEX} (${TOO_LONG_KEY_HEX.length() * 4} bits)")
runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE)
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, encryptionMethod.name())
runner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, KeyDerivationFunction.NONE.name())
runner.setProperty(EncryptContent.RAW_KEY_HEX, TOO_LONG_KEY_HEX)
runner.enqueue(new byte[0])
pc = (MockProcessContext) runner.getProcessContext()
// Act
results = pc.validate()
// Assert
Assert.assertEquals(1, results.size())
logger.expected(results)
ValidationResult vr = results.first()
String expectedResult = "'raw-key-hex' is invalid because Key must be valid length [128, 192, 256]"
String message = "'" + vr.toString() + "' contains '" + expectedResult + "'"
Assert.assertTrue(message, vr.toString().contains(expectedResult))
}
@Test
public void testShouldValidateMaxKeySizeForAlgorithmsOnLimitedStrengthJVM() throws IOException {
// Arrange
Assume.assumeTrue("Test is being skipped because this JVM supports unlimited strength crypto.",
!PasswordBasedEncryptor.supportsUnlimitedStrength());
final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class)
Collection<ValidationResult> results
MockProcessContext pc
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
final int MAX_KEY_LENGTH = 128
final String TOO_LONG_KEY_HEX = "ab" * (MAX_KEY_LENGTH / 8 + 1)
logger.info("Using key ${TOO_LONG_KEY_HEX} (${TOO_LONG_KEY_HEX.length() * 4} bits)")
runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE)
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, encryptionMethod.name())
runner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, KeyDerivationFunction.NONE.name())
runner.setProperty(EncryptContent.RAW_KEY_HEX, TOO_LONG_KEY_HEX)
runner.enqueue(new byte[0])
pc = (MockProcessContext) runner.getProcessContext()
// Act
results = pc.validate()
// Assert
// Two validation problems -- max key size and key length is invalid
Assert.assertEquals(2, results.size())
logger.expected(results)
ValidationResult maxKeyLengthVR = results.first()
String expectedResult = "'raw-key-hex' is invalid because Key length greater than ${MAX_KEY_LENGTH} bits is not supported"
String message = "'" + maxKeyLengthVR.toString() + "' contains '" + expectedResult + "'"
Assert.assertTrue(message, maxKeyLengthVR.toString().contains(expectedResult))
expectedResult = "'raw-key-hex' is invalid because Key must be valid length [128, 192, 256]"
ValidationResult keyLengthInvalidVR = results.last()
message = "'" + keyLengthInvalidVR.toString() + "' contains '" + expectedResult + "'"
Assert.assertTrue(message, keyLengthInvalidVR.toString().contains(expectedResult))
}
@Test
public void testShouldValidateKeyFormatAndSizeForAlgorithms() throws IOException {
// Arrange
final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class)
Collection<ValidationResult> results
MockProcessContext pc
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
final int INVALID_KEY_LENGTH = 120
final String INVALID_KEY_HEX = "ab" * (INVALID_KEY_LENGTH / 8)
logger.info("Using key ${INVALID_KEY_HEX} (${INVALID_KEY_HEX.length() * 4} bits)")
runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE)
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, encryptionMethod.name())
runner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, KeyDerivationFunction.NONE.name())
runner.setProperty(EncryptContent.RAW_KEY_HEX, INVALID_KEY_HEX)
runner.enqueue(new byte[0])
pc = (MockProcessContext) runner.getProcessContext()
// Act
results = pc.validate()
// Assert
Assert.assertEquals(1, results.size())
logger.expected(results)
ValidationResult keyLengthInvalidVR = results.first()
String expectedResult = "'raw-key-hex' is invalid because Key must be valid length [128, 192, 256]"
String message = "'" + keyLengthInvalidVR.toString() + "' contains '" + expectedResult + "'"
Assert.assertTrue(message, keyLengthInvalidVR.toString().contains(expectedResult))
}
@Test
public void testShouldValidateKDFWhenKeyedCipherSelected() {
// Arrange
final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class)
Collection<ValidationResult> results
MockProcessContext pc
def encryptionMethods = EncryptionMethod.values().findAll { it.isKeyedCipher() }
final int VALID_KEY_LENGTH = 128
final String VALID_KEY_HEX = "ab" * (VALID_KEY_LENGTH / 8)
logger.info("Using key ${VALID_KEY_HEX} (${VALID_KEY_HEX.length() * 4} bits)")
runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE)
encryptionMethods.each { EncryptionMethod encryptionMethod ->
logger.info("Trying encryption method ${encryptionMethod.name()}")
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, encryptionMethod.name())
final def INVALID_KDFS = [KeyDerivationFunction.NIFI_LEGACY, KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY]
INVALID_KDFS.each { KeyDerivationFunction invalidKDF ->
logger.info("Trying KDF ${invalidKDF.name()}")
runner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, invalidKDF.name())
runner.setProperty(EncryptContent.RAW_KEY_HEX, VALID_KEY_HEX)
runner.enqueue(new byte[0])
pc = (MockProcessContext) runner.getProcessContext()
// Act
results = pc.validate()
// Assert
Assert.assertEquals(1, results.size())
logger.expected(results)
ValidationResult keyLengthInvalidVR = results.first()
String expectedResult = "'key-derivation-function' is invalid because Key Derivation Function is required to be NONE, BCRYPT, SCRYPT, PBKDF2 when using " +
"algorithm ${encryptionMethod.algorithm}"
String message = "'" + keyLengthInvalidVR.toString() + "' contains '" + expectedResult + "'"
Assert.assertTrue(message, keyLengthInvalidVR.toString().contains(expectedResult))
}
final def VALID_KDFS = [KeyDerivationFunction.NONE, KeyDerivationFunction.BCRYPT, KeyDerivationFunction.SCRYPT, KeyDerivationFunction.PBKDF2]
VALID_KDFS.each { KeyDerivationFunction validKDF ->
logger.info("Trying KDF ${validKDF.name()}")
runner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, validKDF.name())
runner.enqueue(new byte[0])
pc = (MockProcessContext) runner.getProcessContext()
// Act
results = pc.validate()
// Assert
Assert.assertEquals(0, results.size())
}
}
}
@Test
public void testShouldValidateKDFWhenPBECipherSelected() {
// Arrange
final TestRunner runner = TestRunners.newTestRunner(EncryptContent.class)
Collection<ValidationResult> results
MockProcessContext pc
final String PASSWORD = "short"
def encryptionMethods = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") }
if (!PasswordBasedEncryptor.supportsUnlimitedStrength()) {
// Remove all unlimited strength algorithms
encryptionMethods.removeAll { it.unlimitedStrength }
}
runner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE)
runner.setProperty(EncryptContent.PASSWORD, PASSWORD)
runner.setProperty(EncryptContent.ALLOW_WEAK_CRYPTO, WEAK_CRYPTO_ALLOWED)
encryptionMethods.each { EncryptionMethod encryptionMethod ->
logger.info("Trying encryption method ${encryptionMethod.name()}")
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, encryptionMethod.name())
final def INVALID_KDFS = [KeyDerivationFunction.NONE, KeyDerivationFunction.BCRYPT, KeyDerivationFunction.SCRYPT, KeyDerivationFunction.PBKDF2]
INVALID_KDFS.each { KeyDerivationFunction invalidKDF ->
logger.info("Trying KDF ${invalidKDF.name()}")
runner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, invalidKDF.name())
runner.enqueue(new byte[0])
pc = (MockProcessContext) runner.getProcessContext()
// Act
results = pc.validate()
// Assert
logger.expected(results)
Assert.assertEquals(1, results.size())
ValidationResult keyLengthInvalidVR = results.first()
String expectedResult = "'Key Derivation Function' is invalid because Key Derivation Function is required to be NIFI_LEGACY, OPENSSL_EVP_BYTES_TO_KEY when using " +
"algorithm ${encryptionMethod.algorithm}"
String message = "'" + keyLengthInvalidVR.toString() + "' contains '" + expectedResult + "'"
Assert.assertTrue(message, keyLengthInvalidVR.toString().contains(expectedResult))
}
final def VALID_KDFS = [KeyDerivationFunction.NIFI_LEGACY, KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY]
VALID_KDFS.each { KeyDerivationFunction validKDF ->
logger.info("Trying KDF ${validKDF.name()}")
runner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, validKDF.name())
runner.enqueue(new byte[0])
pc = (MockProcessContext) runner.getProcessContext()
// Act
results = pc.validate()
// Assert
Assert.assertEquals(0, results.size())
}
}
}
@Test
public void testRoundTrip() throws IOException {
final TestRunner testRunner = TestRunners.newTestRunner(new EncryptContent())
final String RAW_KEY_HEX = "ab" * 16
testRunner.setProperty(EncryptContent.RAW_KEY_HEX, RAW_KEY_HEX)
testRunner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, KeyDerivationFunction.NONE.name())
def keyedCipherEMs = EncryptionMethod.values().findAll { it.isKeyedCipher() }
keyedCipherEMs.each { EncryptionMethod encryptionMethod ->
logger.info("Attempting {}", encryptionMethod.name())
testRunner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, encryptionMethod.name())
testRunner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE)
testRunner.enqueue(Paths.get("src/test/resources/hello.txt"))
testRunner.clearTransferState()
testRunner.run()
testRunner.assertAllFlowFilesTransferred(EncryptContent.REL_SUCCESS, 1)
MockFlowFile flowFile = testRunner.getFlowFilesForRelationship(EncryptContent.REL_SUCCESS).get(0)
testRunner.assertQueueEmpty()
testRunner.setProperty(EncryptContent.MODE, EncryptContent.DECRYPT_MODE)
testRunner.enqueue(flowFile)
testRunner.clearTransferState()
testRunner.run()
testRunner.assertAllFlowFilesTransferred(EncryptContent.REL_SUCCESS, 1)
logger.info("Successfully decrypted {}", encryptionMethod.name())
flowFile = testRunner.getFlowFilesForRelationship(EncryptContent.REL_SUCCESS).get(0)
flowFile.assertContentEquals(new File("src/test/resources/hello.txt"))
}
}
@Test
public void testShouldCheckMaximumLengthOfPasswordOnLimitedStrengthCryptoJVM() throws IOException {
// Arrange
Assume.assumeTrue("Only run on systems with limited strength crypto", !PasswordBasedEncryptor.supportsUnlimitedStrength())
final TestRunner testRunner = TestRunners.newTestRunner(new EncryptContent())
testRunner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, KeyDerivationFunction.NIFI_LEGACY.name())
testRunner.setProperty(EncryptContent.ALLOW_WEAK_CRYPTO, WEAK_CRYPTO_ALLOWED)
Collection<ValidationResult> results
MockProcessContext pc
def encryptionMethods = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") }
// Use .find instead of .each to allow "breaks" using return false
encryptionMethods.find { EncryptionMethod encryptionMethod ->
def invalidPasswordLength = CipherUtility.getMaximumPasswordLengthForAlgorithmOnLimitedStrengthCrypto(encryptionMethod) + 1
String tooLongPassword = "x" * invalidPasswordLength
if (encryptionMethod.isUnlimitedStrength() || encryptionMethod.isKeyedCipher()) {
return false // cannot test unlimited strength in unit tests because it's not enabled by the JVM by default.
}
testRunner.setProperty(EncryptContent.PASSWORD, tooLongPassword)
logger.info("Attempting ${encryptionMethod.algorithm} with password of length ${invalidPasswordLength}")
testRunner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, encryptionMethod.name())
testRunner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE)
testRunner.clearTransferState()
testRunner.enqueue(new byte[0])
pc = (MockProcessContext) testRunner.getProcessContext()
// Act
results = pc.validate()
// Assert
logger.expected(results)
Assert.assertEquals(1, results.size())
ValidationResult passwordLengthVR = results.first()
String expectedResult = "'Password' is invalid because Password length greater than ${invalidPasswordLength - 1} characters is not supported by" +
" this JVM due to lacking JCE Unlimited Strength Jurisdiction Policy files."
String message = "'" + passwordLengthVR.toString() + "' contains '" + expectedResult + "'"
Assert.assertTrue(message, passwordLengthVR.toString().contains(expectedResult))
}
}
@Test
public void testShouldCheckLengthOfPasswordWhenNotAllowed() throws IOException {
// Arrange
final TestRunner testRunner = TestRunners.newTestRunner(new EncryptContent())
testRunner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, KeyDerivationFunction.NIFI_LEGACY.name())
Collection<ValidationResult> results
MockProcessContext pc
def encryptionMethods = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") }
boolean limitedStrengthCrypto = !PasswordBasedEncryptor.supportsUnlimitedStrength()
boolean allowWeakCrypto = false
testRunner.setProperty(EncryptContent.ALLOW_WEAK_CRYPTO, WEAK_CRYPTO_NOT_ALLOWED)
// Use .find instead of .each to allow "breaks" using return false
encryptionMethods.find { EncryptionMethod encryptionMethod ->
// Determine the minimum of the algorithm-accepted length or the global safe minimum to ensure only one validation result
def shortPasswordLength = [PasswordBasedEncryptor.getMinimumSafePasswordLength() - 1, CipherUtility.getMaximumPasswordLengthForAlgorithmOnLimitedStrengthCrypto(encryptionMethod) - 1].min()
String shortPassword = "x" * shortPasswordLength
if (encryptionMethod.isUnlimitedStrength() || encryptionMethod.isKeyedCipher()) {
return false // cannot test unlimited strength in unit tests because it's not enabled by the JVM by default.
}
testRunner.setProperty(EncryptContent.PASSWORD, shortPassword)
logger.info("Attempting ${encryptionMethod.algorithm} with password of length ${shortPasswordLength}")
logger.state("Limited strength crypto ${limitedStrengthCrypto} and allow weak crypto: ${allowWeakCrypto}")
testRunner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, encryptionMethod.name())
testRunner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE)
testRunner.clearTransferState()
testRunner.enqueue(new byte[0])
pc = (MockProcessContext) testRunner.getProcessContext()
// Act
results = pc.validate()
// Assert
logger.expected(results)
Assert.assertEquals(1, results.size())
ValidationResult passwordLengthVR = results.first()
String expectedResult = "'Password' is invalid because Password length less than ${PasswordBasedEncryptor.getMinimumSafePasswordLength()} characters is potentially unsafe. " +
"See Admin Guide."
String message = "'" + passwordLengthVR.toString() + "' contains '" + expectedResult + "'"
Assert.assertTrue(message, passwordLengthVR.toString().contains(expectedResult))
}
}
@Test
public void testShouldNotCheckLengthOfPasswordWhenAllowed() throws IOException {
// Arrange
final TestRunner testRunner = TestRunners.newTestRunner(new EncryptContent())
testRunner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, KeyDerivationFunction.NIFI_LEGACY.name())
Collection<ValidationResult> results
MockProcessContext pc
def encryptionMethods = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") }
boolean limitedStrengthCrypto = !PasswordBasedEncryptor.supportsUnlimitedStrength()
boolean allowWeakCrypto = true
testRunner.setProperty(EncryptContent.ALLOW_WEAK_CRYPTO, WEAK_CRYPTO_ALLOWED)
// Use .find instead of .each to allow "breaks" using return false
encryptionMethods.find { EncryptionMethod encryptionMethod ->
// Determine the minimum of the algorithm-accepted length or the global safe minimum to ensure only one validation result
def shortPasswordLength = [PasswordBasedEncryptor.getMinimumSafePasswordLength() - 1, CipherUtility.getMaximumPasswordLengthForAlgorithmOnLimitedStrengthCrypto(encryptionMethod) - 1].min()
String shortPassword = "x" * shortPasswordLength
if (encryptionMethod.isUnlimitedStrength() || encryptionMethod.isKeyedCipher()) {
return false // cannot test unlimited strength in unit tests because it's not enabled by the JVM by default.
}
testRunner.setProperty(EncryptContent.PASSWORD, shortPassword)
logger.info("Attempting ${encryptionMethod.algorithm} with password of length ${shortPasswordLength}")
logger.state("Limited strength crypto ${limitedStrengthCrypto} and allow weak crypto: ${allowWeakCrypto}")
testRunner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, encryptionMethod.name())
testRunner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE)
testRunner.clearTransferState()
testRunner.enqueue(new byte[0])
pc = (MockProcessContext) testRunner.getProcessContext()
// Act
results = pc.validate()
// Assert
Assert.assertEquals(results.toString(), 0, results.size())
}
}
}

View File

@ -0,0 +1,340 @@
/*
* 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.binary.Hex
import org.apache.nifi.security.util.EncryptionMethod
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.*
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
import java.security.SecureRandom
import java.security.Security
import static groovy.test.GroovyAssert.shouldFail
@RunWith(JUnit4.class)
public class AESKeyedCipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(AESKeyedCipherProviderGroovyTest.class)
private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210"
private static final List<EncryptionMethod> keyedEncryptionMethods = EncryptionMethod.values().findAll { it.keyedCipher }
private static final SecretKey key = new SecretKeySpec(Hex.decodeHex(KEY_HEX as char[]), "AES")
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void testGetCipherShouldBeInternallyConsistent() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final String plaintext = "This is a plaintext message."
// Act
for (EncryptionMethod em : keyedEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, key, true)
byte[] iv = cipher.getIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, key, iv, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final String plaintext = "This is a plaintext message."
// Act
keyedEncryptionMethods.each { EncryptionMethod em ->
logger.info("Using algorithm: ${em.getAlgorithm()}")
byte[] iv = cipherProvider.generateIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, key, iv, true)
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, key, iv, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered)
}
}
@Test
public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
PasswordBasedEncryptor.supportsUnlimitedStrength())
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final List<Integer> LONG_KEY_LENGTHS = [192, 256]
final String plaintext = "This is a plaintext message."
SecureRandom secureRandom = new SecureRandom()
// Act
keyedEncryptionMethods.each { EncryptionMethod em ->
// Re-use the same IV for the different length keys to ensure the encryption is different
byte[] iv = cipherProvider.generateIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
LONG_KEY_LENGTHS.each { int keyLength ->
logger.info("Using algorithm: ${em.getAlgorithm()} with key length ${keyLength}")
// Generate a key
byte[] keyBytes = new byte[keyLength / 8]
secureRandom.nextBytes(keyBytes)
SecretKey localKey = new SecretKeySpec(keyBytes, "AES")
logger.info("Key: ${Hex.encodeHexString(keyBytes)} ${keyBytes.length}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, localKey, iv, true)
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
cipher = cipherProvider.getCipher(em, localKey, iv, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered)
}
}
}
@Test
public void testShouldRejectEmptyKey() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
def msg = shouldFail(IllegalArgumentException) {
cipherProvider.getCipher(encryptionMethod, null, true)
}
// Assert
assert msg =~ "The key must be specified"
}
@Test
public void testShouldRejectIncorrectLengthKey() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
SecretKey localKey = new SecretKeySpec(Hex.decodeHex("0123456789ABCDEF" as char[]), "AES")
assert ![128, 192, 256].contains(localKey.encoded.length)
final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
def msg = shouldFail(IllegalArgumentException) {
cipherProvider.getCipher(encryptionMethod, localKey, true)
}
// Assert
assert msg =~ "The key must be of length \\[128, 192, 256\\]"
}
@Test
public void testShouldRejectEmptyEncryptionMethod() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
// Act
def msg = shouldFail(IllegalArgumentException) {
cipherProvider.getCipher(null, key, true)
}
// Assert
assert msg =~ "The encryption method must be specified"
}
@Test
public void testShouldRejectUnsupportedEncryptionMethod() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
// Act
def msg = shouldFail(IllegalArgumentException) {
cipherProvider.getCipher(encryptionMethod, key, true)
}
// Assert
assert msg =~ " requires a PBECipherProvider"
}
@Test
public void testGetCipherShouldSupportExternalCompatibility() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final String PLAINTEXT = "This is a plaintext message."
// These values can be generated by running `$ ./openssl_aes.rb` in the terminal
final byte[] IV = Hex.decodeHex("e0bc8cc7fbc0bdfdc184dc22ce2fcb5b" as char[])
final byte[] LOCAL_KEY = Hex.decodeHex("c72943d27c3e5a276169c5998a779117" as char[])
final String CIPHER_TEXT = "a2725ea55c7dd717664d044cab0f0b5f763653e322c27df21954f5be394efb1b"
byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
SecretKey localKey = new SecretKeySpec(LOCAL_KEY, "AES")
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}")
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
// Act
Cipher cipher = cipherProvider.getCipher(encryptionMethod, localKey, IV, false)
byte[] recoveredBytes = cipher.doFinal(cipherBytes)
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered)
}
@Test
public void testGetCipherForDecryptShouldRequireIV() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final String plaintext = "This is a plaintext message."
// Act
keyedEncryptionMethods.each { EncryptionMethod em ->
logger.info("Using algorithm: ${em.getAlgorithm()}")
byte[] iv = cipherProvider.generateIV()
logger.info("IV: ${Hex.encodeHexString(iv)}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, key, iv, true)
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"))
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}")
def msg = shouldFail(IllegalArgumentException) {
cipher = cipherProvider.getCipher(em, key, false)
}
// Assert
assert msg =~ "Cannot decrypt without a valid IV"
}
}
@Test
public void testGetCipherShouldRejectInvalidIVLengths() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
final def INVALID_IVS = (0..15).collect { int length -> new byte[length] }
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
INVALID_IVS.each { byte[] badIV ->
logger.info("IV: ${Hex.encodeHexString(badIV)} ${badIV.length}")
// Encrypt should print a warning about the bad IV but overwrite it
Cipher cipher = cipherProvider.getCipher(encryptionMethod, key, badIV, true)
// Decrypt should fail
def msg = shouldFail(IllegalArgumentException) {
cipher = cipherProvider.getCipher(encryptionMethod, key, badIV, false)
}
logger.expected(msg)
// Assert
assert msg =~ "Cannot decrypt without a valid IV"
}
}
@Test
public void testGetCipherShouldRejectEmptyIV() throws Exception {
// Arrange
KeyedCipherProvider cipherProvider = new AESKeyedCipherProvider()
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
byte[] badIV = [0x00 as byte] * 16 as byte[]
// Act
logger.info("IV: ${Hex.encodeHexString(badIV)} ${badIV.length}")
// Encrypt should print a warning about the bad IV but overwrite it
Cipher cipher = cipherProvider.getCipher(encryptionMethod, key, badIV, true)
logger.info("IV after encrypt: ${Hex.encodeHexString(cipher.getIV())}")
// Decrypt should fail
def msg = shouldFail(IllegalArgumentException) {
cipher = cipherProvider.getCipher(encryptionMethod, key, badIV, false)
}
logger.expected(msg)
// Assert
assert msg =~ "Cannot decrypt without a valid IV"
}
}

View File

@ -0,0 +1,549 @@
/*
* 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.binary.Base64
import org.apache.commons.codec.binary.Hex
import org.apache.nifi.processors.standard.util.crypto.bcrypt.BCrypt
import org.apache.nifi.security.util.EncryptionMethod
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Assume
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
//import org.mindrot.jbcrypt.BCrypt
import org.slf4j.LoggerFactory
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import java.security.Security
import static groovy.test.GroovyAssert.shouldFail
import static org.junit.Assert.assertTrue
@RunWith(JUnit4.class)
public class BcryptCipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(BcryptCipherProviderGroovyTest.class);
private static List<EncryptionMethod> strongKDFEncryptionMethods
private static final int DEFAULT_KEY_LENGTH = 128;
public static final String MICROBENCHMARK = "microbenchmark"
private static ArrayList<Integer> AES_KEY_LENGTHS
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider());
strongKDFEncryptionMethods = EncryptionMethod.values().findAll { it.isCompatibleWithStrongKDFs() }
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
if (PasswordBasedEncryptor.supportsUnlimitedStrength()) {
AES_KEY_LENGTHS = [128, 192, 256]
} else {
AES_KEY_LENGTHS = [128]
}
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void testGetCipherShouldBeInternallyConsistent() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "shortPassword";
final byte[] SALT = cipherProvider.generateSalt()
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "shortPassword";
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("01" * 16 as char[]);
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
PasswordBasedEncryptor.supportsUnlimitedStrength());
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "shortPassword";
final byte[] SALT = cipherProvider.generateSalt()
final int LONG_KEY_LENGTH = 256
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, LONG_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, LONG_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testHashPWShouldMatchTestVectors() {
// Arrange
final String PASSWORD = 'abcdefghijklmnopqrstuvwxyz'
final String SALT = '$2a$10$fVH8e28OQRj9tqiDXs1e1u'
final String EXPECTED_HASH = '$2a$10$fVH8e28OQRj9tqiDXs1e1uxpsjN0c7II7YPKXua2NAKYvM6iQk7dq'
// final int WORK_FACTOR = 10
// Act
String calculatedHash = BCrypt.hashpw(PASSWORD, SALT)
logger.info("Generated ${calculatedHash}")
// Assert
assert calculatedHash == EXPECTED_HASH
}
@Test
public void testGetCipherShouldSupportExternalCompatibility() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PLAINTEXT = "This is a plaintext message.";
final String PASSWORD = "thisIsABadPassword";
// These values can be generated by running `$ ./openssl_bcrypt` in the terminal
final byte[] SALT = Hex.decodeHex("81455b915ce9efd1fc61a08eb0255936" as char[]);
final byte[] IV = Hex.decodeHex("41a51e0150df6a1f72826b36c6371f3f" as char[]);
// $v2$w2$base64_salt_22__base64_hash_31
final String FULL_HASH = "\$2a\$10\$gUVbkVzp79H8YaCOsCVZNuz/d759nrMKzjuviaS5/WdcKHzqngGKi"
logger.info("Full Hash: ${FULL_HASH}")
final String HASH = FULL_HASH[-31..-1]
logger.info(" Hash: ${HASH.padLeft(60, " ")}")
logger.info(" B64 Salt: ${CipherUtility.encodeBase64NoPadding(SALT).padLeft(29, " ")}")
String extractedSalt = FULL_HASH[7..<29]
logger.info("Extracted Salt: ${extractedSalt}")
String extractedSaltHex = Hex.encodeHexString(Base64.decodeBase64(extractedSalt))
logger.info("Extracted Salt (hex): ${extractedSaltHex}")
logger.info(" Expected Salt (hex): ${Hex.encodeHexString(SALT)}")
final String CIPHER_TEXT = "3a226ba2b3c8fe559acb806620001246db289375ba8075a68573478b56a69f15"
byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("External cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
// Sanity check
Cipher rubyCipher = Cipher.getInstance(encryptionMethod.algorithm, "BC")
def rubyKey = new SecretKeySpec(Hex.decodeHex("724cd9e1b0b9e87c7f7e7d7b270bca07" as char[]), "AES")
def ivSpec = new IvParameterSpec(IV)
rubyCipher.init(Cipher.ENCRYPT_MODE, rubyKey, ivSpec)
byte[] rubyCipherBytes = rubyCipher.doFinal(PLAINTEXT.bytes)
logger.info("Expected cipher text: ${Hex.encodeHexString(rubyCipherBytes)}")
rubyCipher.init(Cipher.DECRYPT_MODE, rubyKey, ivSpec)
assert rubyCipher.doFinal(rubyCipherBytes) == PLAINTEXT.bytes
assert rubyCipher.doFinal(cipherBytes) == PLAINTEXT.bytes
logger.sanity("Decrypted external cipher text and generated cipher text successfully")
// Sanity for hash generation
final String FULL_SALT = FULL_HASH[0..<29]
logger.sanity("Salt from external: ${FULL_SALT}")
String generatedHash = BCrypt.hashpw(PASSWORD, FULL_SALT)
logger.sanity("Generated hash: ${generatedHash}")
assert generatedHash == FULL_HASH
// Act
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, FULL_SALT.bytes, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
}
@Test
public void testGetCipherShouldHandleFullSalt() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PLAINTEXT = "This is a plaintext message.";
final String PASSWORD = "thisIsABadPassword";
// These values can be generated by running `$ ./openssl_bcrypt.rb` in the terminal
final byte[] IV = Hex.decodeHex("41a51e0150df6a1f72826b36c6371f3f" as char[]);
// $v2$w2$base64_salt_22__base64_hash_31
final String FULL_HASH = "\$2a\$10\$gUVbkVzp79H8YaCOsCVZNuz/d759nrMKzjuviaS5/WdcKHzqngGKi"
logger.info("Full Hash: ${FULL_HASH}")
final String FULL_SALT = FULL_HASH[0..<29]
logger.info(" Salt: ${FULL_SALT}")
final String HASH = FULL_HASH[-31..-1]
logger.info(" Hash: ${HASH.padLeft(60, " ")}")
String extractedSalt = FULL_HASH[7..<29]
logger.info("Extracted Salt: ${extractedSalt}")
String extractedSaltHex = Hex.encodeHexString(Base64.decodeBase64(extractedSalt))
logger.info("Extracted Salt (hex): ${extractedSaltHex}")
final String CIPHER_TEXT = "3a226ba2b3c8fe559acb806620001246db289375ba8075a68573478b56a69f15"
byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("External cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
// Act
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, FULL_SALT.bytes, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
}
@Test
public void testGetCipherShouldHandleUnformedSalt() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "thisIsABadPassword";
final def INVALID_SALTS = ['$ab$00$acbdefghijklmnopqrstuv', 'bad_salt', '$3a$11$', 'x', '$2a$10$']
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
// Act
INVALID_SALTS.each { String salt ->
logger.info("Checking salt ${salt}")
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt.bytes, DEFAULT_KEY_LENGTH, true);
}
// Assert
assert msg =~ "The salt must be of the format \\\$2a\\\$10\\\$gUVbkVzp79H8YaCOsCVZNu\\. To generate a salt, use BcryptCipherProvider#generateSalt"
}
}
String bytesToBitString(byte[] bytes) {
bytes.collect {
String.format("%8s", Integer.toBinaryString(it & 0xFF)).replace(' ', '0')
}.join("")
}
String spaceString(String input, int blockSize = 4) {
input.collect { it.padLeft(blockSize, " ") }.join("")
}
@Test
public void testGetCipherShouldRejectEmptySalt() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "thisIsABadPassword";
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
// Two different errors -- one explaining the no-salt method is not supported, and the other for an empty byte[] passed
// Act
def msg = shouldFail(UnsupportedOperationException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, DEFAULT_KEY_LENGTH, true);
}
logger.expected(msg)
// Assert
assert msg =~ "The cipher cannot be initialized without a valid salt\\. Use BcryptCipherProvider#generateSalt\\(\\) to generate a valid salt"
// Act
msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, new byte[0], DEFAULT_KEY_LENGTH, true);
}
logger.expected(msg)
// Assert
assert msg =~ "The salt cannot be empty\\. To generate a salt, use BcryptCipherProvider#generateSalt"
}
@Test
public void testGetCipherForDecryptShouldRequireIV() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4)
final String PASSWORD = "shortPassword";
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("00" * 16 as char[]);
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
def msg = shouldFail(IllegalArgumentException) {
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, false);
}
// Assert
assert msg =~ "Cannot decrypt without a valid IV"
}
}
@Test
public void testGetCipherShouldAcceptValidKeyLengths() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4);
final String PASSWORD = "shortPassword";
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("01" * 16 as char[]);
final String PLAINTEXT = "This is a plaintext message.";
// Currently only AES ciphers are compatible with Bcrypt, so redundant to test all algorithms
final def VALID_KEY_LENGTHS = AES_KEY_LENGTHS
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
VALID_KEY_LENGTHS.each { int keyLength ->
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} with key length ${keyLength}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
}
}
@Test
public void testGetCipherShouldNotAcceptInvalidKeyLengths() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(4);
final String PASSWORD = "shortPassword";
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("00" * 16 as char[]);
final String PLAINTEXT = "This is a plaintext message.";
// Currently only AES ciphers are compatible with Bcrypt, so redundant to test all algorithms
final def INVALID_KEY_LENGTHS = [-1, 40, 64, 112, 512]
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
INVALID_KEY_LENGTHS.each { int keyLength ->
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} with key length ${keyLength}")
// Initialize a cipher for encryption
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
}
// Assert
assert msg =~ "${keyLength} is not a valid key length for AES"
}
}
@Test
public void testGenerateSaltShouldUseProvidedWorkFactor() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider(11);
int workFactor = cipherProvider.getWorkFactor()
// Act
final byte[] saltBytes = cipherProvider.generateSalt()
String salt = new String(saltBytes)
logger.info("Salt: ${salt}")
// Assert
assert salt =~ /^\$2[axy]\$\d{2}\$/
assert salt.contains("\$${workFactor}\$")
}
@Ignore("This test can be run on a specific machine to evaluate if the default work factor is sufficient")
@Test
public void testDefaultConstructorShouldProvideStrongWorkFactor() {
// Arrange
RandomIVPBECipherProvider cipherProvider = new BcryptCipherProvider();
// Values taken from http://wildlyinaccurate.com/bcrypt-choosing-a-work-factor/ and http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt
// Calculate the work factor to reach 500 ms
int minimumWorkFactor = calculateMinimumWorkFactor()
logger.info("Determined minimum safe work factor to be ${minimumWorkFactor}")
// Act
int workFactor = cipherProvider.getWorkFactor()
logger.info("Default work factor ${workFactor}")
// Assert
assertTrue("The default work factor for BcryptCipherProvider is too weak. Please update the default value to a stronger level.", workFactor >= minimumWorkFactor)
}
/**
* Returns the work factor required for a derivation to exceed 500 ms on this machine. Code adapted from http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt
*
* @return the minimum bcrypt work factor
*/
private static int calculateMinimumWorkFactor() {
// High start-up cost, so run multiple times for better benchmarking
final int RUNS = 10
// Benchmark using a work factor of 5 (the second-lowest allowed)
int workFactor = 5
String salt = BCrypt.gensalt(workFactor)
// Run once to prime the system
double duration = time {
BCrypt.hashpw(MICROBENCHMARK, salt)
}
logger.info("First run of work factor ${workFactor} took ${duration} ms (ignored)")
def durations = []
RUNS.times { int i ->
duration = time {
BCrypt.hashpw(MICROBENCHMARK, salt)
}
logger.info("Work factor ${workFactor} took ${duration} ms")
durations << duration
}
duration = durations.sum() / durations.size()
logger.info("Work factor ${workFactor} averaged ${duration} ms")
// Increasing the work factor by 1 would double the run time
// Keep increasing N until the estimated duration is over 500 ms
while (duration < 500) {
workFactor += 1
duration *= 2
}
logger.info("Returning work factor ${workFactor} for ${duration} ms")
return workFactor
}
private static double time(Closure c) {
long start = System.nanoTime()
c.call()
long end = System.nanoTime()
return (end - start) / 1_000_000.0
}
}

View File

@ -0,0 +1,97 @@
/*
* 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.KeyDerivationFunction
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.security.Security
@RunWith(JUnit4.class)
class CipherProviderFactoryGroovyTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(CipherProviderFactoryGroovyTest.class)
private static final Map<KeyDerivationFunction, Class> EXPECTED_CIPHER_PROVIDERS = [
(KeyDerivationFunction.BCRYPT) : BcryptCipherProvider.class,
(KeyDerivationFunction.NIFI_LEGACY) : NiFiLegacyCipherProvider.class,
(KeyDerivationFunction.NONE) : AESKeyedCipherProvider.class,
(KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY): OpenSSLPKCS5CipherProvider.class,
(KeyDerivationFunction.PBKDF2) : PBKDF2CipherProvider.class,
(KeyDerivationFunction.SCRYPT) : ScryptCipherProvider.class
]
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void testGetCipherProviderShouldResolveRegisteredKDFs() {
// Arrange
// Act
KeyDerivationFunction.values().each { KeyDerivationFunction kdf ->
logger.info("Expected: ${kdf.name} -> ${EXPECTED_CIPHER_PROVIDERS.get(kdf).simpleName}")
CipherProvider cp = CipherProviderFactory.getCipherProvider(kdf)
logger.info("Resolved: ${kdf.name} -> ${cp.class.simpleName}")
// Assert
assert cp.class == (EXPECTED_CIPHER_PROVIDERS.get(kdf))
}
}
@Ignore("Cannot mock enum using Groovy map coercion")
@Test
public void testGetCipherProviderShouldHandleUnregisteredKDFs() {
// Arrange
// Can't mock this; see http://stackoverflow.com/questions/5323505/mocking-java-enum-to-add-a-value-to-test-fail-case
KeyDerivationFunction invalidKDF = [name: "Unregistered", description: "Not a registered KDF"] as KeyDerivationFunction
logger.info("Expected: ${invalidKDF.name} -> error")
// Act
def msg = shouldFail(IllegalArgumentException) {
CipherProvider cp = CipherProviderFactory.getCipherProvider(invalidKDF)
logger.info("Resolved: ${invalidKDF.name} -> ${cp.class.simpleName}")
}
logger.expected(msg)
// Assert
assert msg =~ "No cipher provider registered for ${invalidKDF.name}"
}
}

View File

@ -0,0 +1,251 @@
/*
* 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 org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.security.Security
@RunWith(JUnit4.class)
class CipherUtilityGroovyTest extends GroovyTestCase {
private static final Logger logger = LoggerFactory.getLogger(CipherUtilityGroovyTest.class)
// TripleDES must precede DES for automatic grouping precedence
private static final List<String> CIPHERS = ["AES", "TRIPLEDES", "DES", "RC2", "RC4", "RC5", "TWOFISH"]
private static final List<String> SYMMETRIC_ALGORITHMS = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") || it.algorithm.startsWith("AES") }*.algorithm
private static final Map<String, List<String>> ALGORITHMS_MAPPED_BY_CIPHER = SYMMETRIC_ALGORITHMS.groupBy { String algorithm -> CIPHERS.find { algorithm.contains(it) } }
// Manually mapped as of 01/19/16 0.5.0
private static final Map<Integer, List<String>> ALGORITHMS_MAPPED_BY_KEY_LENGTH = [
(40) : ["PBEWITHSHAAND40BITRC2-CBC",
"PBEWITHSHAAND40BITRC4"],
(64) : ["PBEWITHMD5ANDDES",
"PBEWITHSHA1ANDDES"],
(112): ["PBEWITHSHAAND2-KEYTRIPLEDES-CBC",
"PBEWITHSHAAND3-KEYTRIPLEDES-CBC"],
(128): ["PBEWITHMD5AND128BITAES-CBC-OPENSSL",
"PBEWITHMD5ANDRC2",
"PBEWITHSHA1ANDRC2",
"PBEWITHSHA256AND128BITAES-CBC-BC",
"PBEWITHSHAAND128BITAES-CBC-BC",
"PBEWITHSHAAND128BITRC2-CBC",
"PBEWITHSHAAND128BITRC4",
"PBEWITHSHAANDTWOFISH-CBC",
"AES/CBC/PKCS7Padding",
"AES/CTR/NoPadding",
"AES/GCM/NoPadding"],
(192): ["PBEWITHMD5AND192BITAES-CBC-OPENSSL",
"PBEWITHSHA256AND192BITAES-CBC-BC",
"PBEWITHSHAAND192BITAES-CBC-BC",
"AES/CBC/PKCS7Padding",
"AES/CTR/NoPadding",
"AES/GCM/NoPadding"],
(256): ["PBEWITHMD5AND256BITAES-CBC-OPENSSL",
"PBEWITHSHA256AND256BITAES-CBC-BC",
"PBEWITHSHAAND256BITAES-CBC-BC",
"AES/CBC/PKCS7Padding",
"AES/CTR/NoPadding",
"AES/GCM/NoPadding"]
]
@BeforeClass
static void setUpOnce() {
Security.addProvider(new BouncyCastleProvider());
// Fix because TRIPLEDES -> DESede
def tripleDESAlgorithms = ALGORITHMS_MAPPED_BY_CIPHER.remove("TRIPLEDES")
ALGORITHMS_MAPPED_BY_CIPHER.put("DESede", tripleDESAlgorithms)
logger.info("Mapped algorithms: ${ALGORITHMS_MAPPED_BY_CIPHER}")
}
@Before
void setUp() throws Exception {
}
@After
void tearDown() throws Exception {
}
@Test
void testShouldParseCipherFromAlgorithm() {
// Arrange
final def EXPECTED_ALGORITHMS = ALGORITHMS_MAPPED_BY_CIPHER
// Act
SYMMETRIC_ALGORITHMS.each { String algorithm ->
String cipher = CipherUtility.parseCipherFromAlgorithm(algorithm)
logger.info("Extracted ${cipher} from ${algorithm}")
// Assert
assert EXPECTED_ALGORITHMS.get(cipher).contains(algorithm)
}
}
@Test
void testShouldParseKeyLengthFromAlgorithm() {
// Arrange
final def EXPECTED_ALGORITHMS = ALGORITHMS_MAPPED_BY_KEY_LENGTH
// Act
SYMMETRIC_ALGORITHMS.each { String algorithm ->
int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(algorithm)
logger.info("Extracted ${keyLength} from ${algorithm}")
// Assert
assert EXPECTED_ALGORITHMS.get(keyLength).contains(algorithm)
}
}
@Test
void testShouldDetermineValidKeyLength() {
// Arrange
// Act
ALGORITHMS_MAPPED_BY_KEY_LENGTH.each { int keyLength, List<String> algorithms ->
algorithms.each { String algorithm ->
logger.info("Checking ${keyLength} for ${algorithm}")
// Assert
assert CipherUtility.isValidKeyLength(keyLength, CipherUtility.parseCipherFromAlgorithm(algorithm))
}
}
}
@Test
void testShouldDetermineInvalidKeyLength() {
// Arrange
// Act
ALGORITHMS_MAPPED_BY_KEY_LENGTH.each { int keyLength, List<String> algorithms ->
algorithms.each { String algorithm ->
def invalidKeyLengths = [-1, 0, 1]
if (algorithm =~ "RC\\d") {
invalidKeyLengths += [39, 2049]
} else {
invalidKeyLengths += keyLength + 1
}
logger.info("Checking ${invalidKeyLengths.join(", ")} for ${algorithm}")
// Assert
invalidKeyLengths.each { int invalidKeyLength ->
assert !CipherUtility.isValidKeyLength(invalidKeyLength, CipherUtility.parseCipherFromAlgorithm(algorithm))
}
}
}
}
@Test
void testShouldDetermineValidKeyLengthForAlgorithm() {
// Arrange
// Act
ALGORITHMS_MAPPED_BY_KEY_LENGTH.each { int keyLength, List<String> algorithms ->
algorithms.each { String algorithm ->
logger.info("Checking ${keyLength} for ${algorithm}")
// Assert
assert CipherUtility.isValidKeyLengthForAlgorithm(keyLength, algorithm)
}
}
}
@Test
void testShouldDetermineInvalidKeyLengthForAlgorithm() {
// Arrange
// Act
ALGORITHMS_MAPPED_BY_KEY_LENGTH.each { int keyLength, List<String> algorithms ->
algorithms.each { String algorithm ->
def invalidKeyLengths = [-1, 0, 1]
if (algorithm =~ "RC\\d") {
invalidKeyLengths += [39, 2049]
} else {
invalidKeyLengths += keyLength + 1
}
logger.info("Checking ${invalidKeyLengths.join(", ")} for ${algorithm}")
// Assert
invalidKeyLengths.each { int invalidKeyLength ->
assert !CipherUtility.isValidKeyLengthForAlgorithm(invalidKeyLength, algorithm)
}
}
}
// Extra hard-coded checks
String algorithm = "PBEWITHSHA256AND256BITAES-CBC-BC"
int invalidKeyLength = 192
logger.info("Checking ${invalidKeyLength} for ${algorithm}")
assert !CipherUtility.isValidKeyLengthForAlgorithm(invalidKeyLength, algorithm)
}
@Test
void testShouldGetValidKeyLengthsForAlgorithm() {
// Arrange
def rcKeyLengths = (40..2048).asList()
def CIPHER_KEY_SIZES = [
AES : [128, 192, 256],
DES : [56, 64],
DESede : [56, 64, 112, 128, 168, 192],
RC2 : rcKeyLengths,
RC4 : rcKeyLengths,
RC5 : rcKeyLengths,
TWOFISH: [128, 192, 256]
]
def SINGLE_KEY_SIZE_ALGORITHMS = EncryptionMethod.values()*.algorithm.findAll { CipherUtility.parseActualKeyLengthFromAlgorithm(it) != -1 }
logger.info("Single key size algorithms: ${SINGLE_KEY_SIZE_ALGORITHMS}")
def MULTIPLE_KEY_SIZE_ALGORITHMS = EncryptionMethod.values()*.algorithm - SINGLE_KEY_SIZE_ALGORITHMS
MULTIPLE_KEY_SIZE_ALGORITHMS.removeAll { it.contains("PGP") }
logger.info("Multiple key size algorithms: ${MULTIPLE_KEY_SIZE_ALGORITHMS}")
// Act
SINGLE_KEY_SIZE_ALGORITHMS.each { String algorithm ->
def EXPECTED_KEY_SIZES = [CipherUtility.parseKeyLengthFromAlgorithm(algorithm)]
def validKeySizes = CipherUtility.getValidKeyLengthsForAlgorithm(algorithm)
logger.info("Checking ${algorithm} ${validKeySizes} against expected ${EXPECTED_KEY_SIZES}")
// Assert
assert validKeySizes == EXPECTED_KEY_SIZES
}
// Act
MULTIPLE_KEY_SIZE_ALGORITHMS.each { String algorithm ->
String cipher = CipherUtility.parseCipherFromAlgorithm(algorithm)
def EXPECTED_KEY_SIZES = CIPHER_KEY_SIZES[cipher]
def validKeySizes = CipherUtility.getValidKeyLengthsForAlgorithm(algorithm)
logger.info("Checking ${algorithm} ${validKeySizes} against expected ${EXPECTED_KEY_SIZES}")
// Assert
assert validKeySizes == EXPECTED_KEY_SIZES
}
}
}

View File

@ -0,0 +1,122 @@
/*
* 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.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.Before
import org.junit.BeforeClass
import org.junit.Test
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
import java.security.Security
public class KeyedEncryptorGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(KeyedEncryptorGroovyTest.class)
private static final String TEST_RESOURCES_PREFIX = "src/test/resources/TestEncryptContent/"
private static final File plainFile = new File("${TEST_RESOURCES_PREFIX}/plain.txt")
private static final File encryptedFile = new File("${TEST_RESOURCES_PREFIX}/unsalted_128_raw.asc")
private static final String KEY_HEX = "0123456789ABCDEFFEDCBA9876543210"
private static final SecretKey KEY = new SecretKeySpec(Hex.decodeHex(KEY_HEX as char[]), "AES")
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@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()
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using ${encryptionMethod.name()}")
// Act
KeyedEncryptor encryptor = new KeyedEncryptor(encryptionMethod, KEY)
StreamCallback encryptionCallback = encryptor.getEncryptionCallback()
StreamCallback decryptionCallback = encryptor.getDecryptionCallback()
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: {}\n\n", recovered)
assert PLAINTEXT.equals(recovered)
}
@Test
public void testShouldDecryptOpenSSLUnsaltedCipherTextWithKnownIV() throws Exception {
// Arrange
final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text
logger.info("Plaintext: {}", PLAINTEXT)
byte[] cipherBytes = new File("${TEST_RESOURCES_PREFIX}/unsalted_128_raw.enc").bytes
final String keyHex = "711E85689CE7AFF6F410AEA43ABC5446"
final String ivHex = "842F685B84879B2E00F977C22B9E9A7D"
InputStream cipherStream = new ByteArrayInputStream(cipherBytes)
OutputStream recoveredStream = new ByteArrayOutputStream()
final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
KeyedEncryptor encryptor = new KeyedEncryptor(encryptionMethod, new SecretKeySpec(Hex.decodeHex(keyHex as char[]), "AES"), Hex.decodeHex(ivHex as char[]))
StreamCallback decryptionCallback = encryptor.getDecryptionCallback()
logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}")
// Act
decryptionCallback.process(cipherStream, recoveredStream)
// Assert
byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray()
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: {}", recovered)
assert PLAINTEXT.equals(recovered)
}
}

View File

@ -0,0 +1,288 @@
/*
* 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.binary.Hex
import org.apache.nifi.security.util.EncryptionMethod
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.*
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.PBEParameterSpec
import java.security.Security
import static org.junit.Assert.fail
@RunWith(JUnit4.class)
public class NiFiLegacyCipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(NiFiLegacyCipherProviderGroovyTest.class);
private static List<EncryptionMethod> pbeEncryptionMethods = new ArrayList<>();
private static List<EncryptionMethod> limitedStrengthPbeEncryptionMethods = new ArrayList<>();
private static final String PROVIDER_NAME = "BC";
private static final int ITERATION_COUNT = 1000;
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider());
pbeEncryptionMethods = EncryptionMethod.values().findAll { it.algorithm.toUpperCase().startsWith("PBE") }
limitedStrengthPbeEncryptionMethods = pbeEncryptionMethods.findAll { !it.isUnlimitedStrength() }
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
private static Cipher getLegacyCipher(String password, byte[] salt, String algorithm) {
try {
final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, PROVIDER_NAME);
SecretKey tempKey = factory.generateSecret(pbeKeySpec);
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, ITERATION_COUNT);
Cipher cipher = Cipher.getInstance(algorithm, PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, tempKey, parameterSpec);
return cipher;
} catch (Exception e) {
logger.error("Error generating legacy cipher", e);
fail(e.getMessage());
}
return null;
}
@Test
public void testGetCipherShouldBeInternallyConsistent() throws Exception {
// Arrange
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm());
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
continue
}
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true);
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
PasswordBasedEncryptor.supportsUnlimitedStrength());
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : pbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm());
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true);
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherShouldSupportLegacyCode() throws Exception {
// Arrange
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm());
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
continue
}
// Initialize a legacy cipher for encryption
Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm());
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherWithoutSaltShouldSupportLegacyCode() throws Exception {
// Arrange
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
final String PASSWORD = "shortPassword";
final byte[] SALT = new byte[0];
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm());
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
continue
}
// Initialize a legacy cipher for encryption
Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm());
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, false);
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherShouldIgnoreKeyLength() throws Exception {
// Arrange
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
final String plaintext = "This is a plaintext message.";
final def KEY_LENGTHS = [-1, 40, 64, 128, 192, 256]
// Initialize a cipher for encryption
EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
final Cipher cipher128 = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, true);
byte[] cipherBytes = cipher128.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
// Act
KEY_LENGTHS.each { int keyLength ->
logger.info("Decrypting with 'requested' key length: ${keyLength}")
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, keyLength, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
// Assert
assert plaintext.equals(recovered);
}
}
/**
* This test determines for each PBE encryption algorithm if it actually requires the JCE unlimited strength jurisdiction policies to be installed.
* Even some algorithms that use 128-bit keys (which should be allowed on all systems) throw exceptions because BouncyCastle derives the key
* from the password using a long digest result at the time of key length checking.
* @throws IOException
*/
@Test
public void testShouldDetermineDependenceOnUnlimitedStrengthCrypto() throws IOException {
def encryptionMethods = EncryptionMethod.values().findAll { it.algorithm.startsWith("PBE") }
boolean unlimitedCryptoSupported = PasswordBasedEncryptor.supportsUnlimitedStrength()
logger.info("This JVM supports unlimited strength crypto: ${unlimitedCryptoSupported}")
def longestSupportedPasswordByEM = [:]
encryptionMethods.each { EncryptionMethod encryptionMethod ->
logger.info("Attempting ${encryptionMethod.name()} (${encryptionMethod.algorithm}) which claims unlimited strength required: ${encryptionMethod.unlimitedStrength}")
(1..20).find { int length ->
String password = "x" * length
try {
NiFiLegacyCipherProvider cipherProvider = new NiFiLegacyCipherProvider();
Cipher cipher = cipherProvider.getCipher(encryptionMethod, password, true)
return false
} catch (Exception e) {
logger.error("Unable to create the cipher with ${encryptionMethod.algorithm} and password ${password} (${password.length()}) due to ${e.getMessage()}")
if (!longestSupportedPasswordByEM.containsKey(encryptionMethod)) {
longestSupportedPasswordByEM.put(encryptionMethod, password.length() - 1)
}
return true
}
}
logger.info("\n")
}
logger.info("Longest supported password by encryption method:")
longestSupportedPasswordByEM.each { EncryptionMethod encryptionMethod, int length ->
logger.info("\t${encryptionMethod.algorithm}\t${length}")
}
}
}

View File

@ -0,0 +1,319 @@
/*
* 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.binary.Hex
import org.apache.nifi.security.util.EncryptionMethod
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.*
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.PBEParameterSpec
import java.security.Security
import static groovy.test.GroovyAssert.shouldFail
import static org.junit.Assert.fail
@RunWith(JUnit4.class)
public class OpenSSLPKCS5CipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(OpenSSLPKCS5CipherProviderGroovyTest.class);
private static List<EncryptionMethod> pbeEncryptionMethods = new ArrayList<>();
private static List<EncryptionMethod> limitedStrengthPbeEncryptionMethods = new ArrayList<>();
private static final String PROVIDER_NAME = "BC";
private static final int ITERATION_COUNT = 0;
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider());
pbeEncryptionMethods = EncryptionMethod.values().findAll { it.algorithm.toUpperCase().startsWith("PBE") }
limitedStrengthPbeEncryptionMethods = pbeEncryptionMethods.findAll { !it.isUnlimitedStrength() }
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
private static Cipher getLegacyCipher(String password, byte[] salt, String algorithm) {
try {
final PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
final SecretKeyFactory factory = SecretKeyFactory.getInstance(algorithm, PROVIDER_NAME);
SecretKey tempKey = factory.generateSecret(pbeKeySpec);
final PBEParameterSpec parameterSpec = new PBEParameterSpec(salt, ITERATION_COUNT);
Cipher cipher = Cipher.getInstance(algorithm, PROVIDER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, tempKey, parameterSpec);
return cipher;
} catch (Exception e) {
logger.error("Error generating legacy cipher", e);
fail(e.getMessage());
}
return null;
}
@Test
public void testGetCipherShouldBeInternallyConsistent() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
final String PASSWORD = "short";
final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm());
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
continue
}
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true);
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
PasswordBasedEncryptor.supportsUnlimitedStrength());
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : pbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm());
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, true);
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherShouldSupportLegacyCode() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm());
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
continue
}
// Initialize a legacy cipher for encryption
Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm());
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, SALT, false);
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherWithoutSaltShouldSupportLegacyCode() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
final String PASSWORD = "short";
final byte[] SALT = new byte[0];
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : limitedStrengthPbeEncryptionMethods) {
logger.info("Using algorithm: {}", em.getAlgorithm());
if (!CipherUtility.passwordLengthIsValidForAlgorithmOnLimitedStrengthCrypto(PASSWORD.length(), em)) {
logger.warn("This test is skipped because the password length exceeds the undocumented limit BouncyCastle imposes on a JVM with limited strength crypto policies")
continue
}
// Initialize a legacy cipher for encryption
Cipher legacyCipher = getLegacyCipher(PASSWORD, SALT, em.getAlgorithm());
byte[] cipherBytes = legacyCipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
Cipher providedCipher = cipherProvider.getCipher(em, PASSWORD, false);
byte[] recoveredBytes = providedCipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherShouldIgnoreKeyLength() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("aabbccddeeff0011".toCharArray());
final String plaintext = "This is a plaintext message.";
final def KEY_LENGTHS = [-1, 40, 64, 128, 192, 256]
// Initialize a cipher for encryption
EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
final Cipher cipher128 = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, true);
byte[] cipherBytes = cipher128.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: {} {}", Hex.encodeHexString(cipherBytes), cipherBytes.length);
// Act
KEY_LENGTHS.each { int keyLength ->
logger.info("Decrypting with 'requested' key length: ${keyLength}")
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, keyLength, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherShouldRequireEncryptionMethod() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
// Act
logger.info("Using algorithm: null");
def msg = shouldFail(IllegalArgumentException) {
Cipher providedCipher = cipherProvider.getCipher(null, PASSWORD, SALT, false);
}
// Assert
assert msg =~ "The encryption method must be specified"
}
@Test
public void testGetCipherShouldRequirePassword() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
final byte[] SALT = Hex.decodeHex("0011223344556677".toCharArray());
EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
// Act
logger.info("Using algorithm: ${encryptionMethod}");
def msg = shouldFail(IllegalArgumentException) {
Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, "", SALT, false);
}
// Assert
assert msg =~ "Encryption with an empty password is not supported"
}
@Test
public void testGetCipherShouldValidateSaltLength() throws Exception {
// Arrange
OpenSSLPKCS5CipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider();
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex("00112233445566".toCharArray());
EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
// Act
logger.info("Using algorithm: ${encryptionMethod}");
def msg = shouldFail(IllegalArgumentException) {
Cipher providedCipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, false);
}
// Assert
assert msg =~ "Salt must be 8 bytes US-ASCII encoded"
}
@Test
public void testGenerateSaltShouldProvideValidSalt() throws Exception {
// Arrange
PBECipherProvider cipherProvider = new OpenSSLPKCS5CipherProvider()
// Act
byte[] salt = cipherProvider.generateSalt()
logger.info("Checking salt ${Hex.encodeHexString(salt)}")
// Assert
assert salt.length == cipherProvider.getDefaultSaltLength()
assert salt != [(0x00 as byte) * cipherProvider.defaultSaltLength]
}
}

View File

@ -0,0 +1,540 @@
/*
* 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.binary.Hex
import org.apache.nifi.security.util.EncryptionMethod
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.*
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.crypto.Cipher
import java.security.Security
import static groovy.test.GroovyAssert.shouldFail
import static org.junit.Assert.assertTrue
@RunWith(JUnit4.class)
public class PBKDF2CipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(PBKDF2CipherProviderGroovyTest.class);
private static List<EncryptionMethod> strongKDFEncryptionMethods
public static final String MICROBENCHMARK = "microbenchmark"
private static final int DEFAULT_KEY_LENGTH = 128;
private static final int TEST_ITERATION_COUNT = 1000
private final String DEFAULT_PRF = "SHA-512"
private final String SALT_HEX = "0123456789ABCDEFFEDCBA9876543210"
private final String IV_HEX = "01" * 16
private static ArrayList<Integer> AES_KEY_LENGTHS
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider());
strongKDFEncryptionMethods = EncryptionMethod.values().findAll { it.isCompatibleWithStrongKDFs() }
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
if (PasswordBasedEncryptor.supportsUnlimitedStrength()) {
AES_KEY_LENGTHS = [128, 192, 256]
} else {
AES_KEY_LENGTHS = [128]
}
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void testGetCipherShouldBeInternallyConsistent() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherShouldRejectInvalidIV() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final def INVALID_IVS = (0..15).collect { int length -> new byte[length] }
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
INVALID_IVS.each { byte[] badIV ->
logger.info("IV: ${Hex.encodeHexString(badIV)} ${badIV.length}")
// Encrypt should print a warning about the bad IV but overwrite it
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, badIV, DEFAULT_KEY_LENGTH, true)
// Decrypt should fail
def msg = shouldFail(IllegalArgumentException) {
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, badIV, DEFAULT_KEY_LENGTH, false)
}
// Assert
assert msg =~ "Cannot decrypt without a valid IV"
}
}
@Test
public void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
PasswordBasedEncryptor.supportsUnlimitedStrength());
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final int LONG_KEY_LENGTH = 256
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, LONG_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, LONG_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testShouldRejectEmptyPRF() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
final String plaintext = "This is a plaintext message.";
final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
String prf = ""
// Act
logger.info("Using PRF ${prf}")
def msg = shouldFail(IllegalArgumentException) {
cipherProvider = new PBKDF2CipherProvider(prf, TEST_ITERATION_COUNT);
}
// Assert
assert msg =~ "Cannot resolve empty PRF"
}
@Test
public void testShouldResolveDefaultPRF() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
final String plaintext = "This is a plaintext message.";
final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
final PBKDF2CipherProvider SHA512_PROVIDER = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
String prf = "sha768"
logger.info("Using ${prf}")
// Act
cipherProvider = new PBKDF2CipherProvider(prf, TEST_ITERATION_COUNT);
logger.info("Resolved PRF to ${cipherProvider.getPRFName()}")
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = SHA512_PROVIDER.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
}
@Test
public void testShouldResolveVariousPRFs() throws Exception {
// Arrange
final List<String> PRFS = ["SHA-1", "MD5", "SHA-256", "SHA-384", "SHA-512"]
RandomIVPBECipherProvider cipherProvider
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
final String plaintext = "This is a plaintext message.";
final EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
PRFS.each { String prf ->
logger.info("Using ${prf}")
cipherProvider = new PBKDF2CipherProvider(prf, TEST_ITERATION_COUNT);
logger.info("Resolved PRF to ${cipherProvider.getPRFName()}")
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherShouldSupportExternalCompatibility() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider("SHA-256", TEST_ITERATION_COUNT);
final String PLAINTEXT = "This is a plaintext message.";
final String PASSWORD = "thisIsABadPassword";
// These values can be generated by running `$ ./openssl_pbkdf2.rb` in the terminal
final byte[] SALT = Hex.decodeHex("ae2481bee3d8b5d5b732bf464ea2ff01" as char[]);
final byte[] IV = Hex.decodeHex("26db997dcd18472efd74dabe5ff36853" as char[]);
final String CIPHER_TEXT = "92edbabae06add6275a1d64815755a9ba52afc96e2c1a316d3abbe1826e96f6c"
byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
// Act
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
}
@Test
public void testGetCipherForDecryptShouldRequireIV() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[]);
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
def msg = shouldFail(IllegalArgumentException) {
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, false);
}
// Assert
assert msg =~ "Cannot decrypt without a valid IV"
}
}
@Test
public void testGetCipherShouldRejectInvalidSalt() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
final String PASSWORD = "thisIsABadPassword";
final def INVALID_SALTS = ['pbkdf2', '$3a$11$', 'x', '$2a$10$', '', null]
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
// Act
INVALID_SALTS.each { String salt ->
logger.info("Checking salt ${salt}")
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt?.bytes, DEFAULT_KEY_LENGTH, true);
}
// Assert
assert msg =~ "The salt must be at least 16 bytes\\. To generate a salt, use PBKDF2CipherProvider#generateSalt"
}
}
@Test
public void testGetCipherShouldAcceptValidKeyLengths() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
final String PLAINTEXT = "This is a plaintext message.";
// Currently only AES ciphers are compatible with PBKDF2, so redundant to test all algorithms
final def VALID_KEY_LENGTHS = AES_KEY_LENGTHS
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
VALID_KEY_LENGTHS.each { int keyLength ->
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} with key length ${keyLength}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
}
}
@Test
public void testGetCipherShouldNotAcceptInvalidKeyLengths() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT);
final String PASSWORD = "shortPassword";
final byte[] SALT = Hex.decodeHex(SALT_HEX as char[])
final byte[] IV = Hex.decodeHex(IV_HEX as char[]);
// Currently only AES ciphers are compatible with PBKDF2, so redundant to test all algorithms
final def VALID_KEY_LENGTHS = [-1, 40, 64, 112, 512]
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
VALID_KEY_LENGTHS.each { int keyLength ->
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} with key length ${keyLength}")
// Initialize a cipher for encryption
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
}
// Assert
assert msg =~ "${keyLength} is not a valid key length for AES"
}
}
@Ignore("This test can be run on a specific machine to evaluate if the default iteration count is sufficient")
@Test
public void testDefaultConstructorShouldProvideStrongIterationCount() {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider();
// Values taken from http://wildlyinaccurate.com/bcrypt-choosing-a-work-factor/ and http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt
// Calculate the iteration count to reach 500 ms
int minimumIterationCount = calculateMinimumIterationCount()
logger.info("Determined minimum safe iteration count to be ${minimumIterationCount}")
// Act
int iterationCount = cipherProvider.getIterationCount()
logger.info("Default iteration count ${iterationCount}")
// Assert
assertTrue("The default iteration count for PBKDF2CipherProvider is too weak. Please update the default value to a stronger level.", iterationCount >= minimumIterationCount)
}
/**
* Returns the iteration count required for a derivation to exceed 500 ms on this machine using the default PRF.
* Code adapted from http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt
*
* @return the minimum iteration count
*/
private static int calculateMinimumIterationCount() {
// High start-up cost, so run multiple times for better benchmarking
final int RUNS = 10
// Benchmark using an iteration count of 10k
int iterationCount = 10_000
final byte[] SALT = [0x00 as byte] * 16
final byte[] IV = [0x01 as byte] * 16
String defaultPrf = new PBKDF2CipherProvider().getPRFName()
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(defaultPrf, iterationCount)
// Run once to prime the system
double duration = time {
Cipher cipher = cipherProvider.getCipher(EncryptionMethod.AES_CBC, MICROBENCHMARK, SALT, IV, DEFAULT_KEY_LENGTH, false)
}
logger.info("First run of iteration count ${iterationCount} took ${duration} ms (ignored)")
def durations = []
RUNS.times { int i ->
duration = time {
// Use encrypt mode with provided salt and IV to minimize overhead during benchmark call
Cipher cipher = cipherProvider.getCipher(EncryptionMethod.AES_CBC, "${MICROBENCHMARK}${i}", SALT, IV, DEFAULT_KEY_LENGTH, false)
}
logger.info("Iteration count ${iterationCount} took ${duration} ms")
durations << duration
}
duration = durations.sum() / durations.size()
logger.info("Iteration count ${iterationCount} averaged ${duration} ms")
// Keep increasing iteration count until the estimated duration is over 500 ms
while (duration < 500) {
iterationCount *= 2
duration *= 2
}
logger.info("Returning iteration count ${iterationCount} for ${duration} ms")
return iterationCount
}
private static double time(Closure c) {
long start = System.nanoTime()
c.call()
long end = System.nanoTime()
return (end - start) / 1_000_000.0
}
@Test
public void testGenerateSaltShouldProvideValidSalt() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new PBKDF2CipherProvider(DEFAULT_PRF, TEST_ITERATION_COUNT)
// Act
byte[] salt = cipherProvider.generateSalt()
logger.info("Checking salt ${Hex.encodeHexString(salt)}")
// Assert
assert salt.length == 16
assert salt != [(0x00 as byte) * 16]
}
}

View File

@ -0,0 +1,164 @@
/*
* 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.binary.Hex
import org.apache.nifi.processor.io.StreamCallback
import org.apache.nifi.security.util.EncryptionMethod
import org.apache.nifi.security.util.KeyDerivationFunction
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Assume
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Test
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.security.Security
public class PasswordBasedEncryptorGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(PasswordBasedEncryptorGroovyTest.class)
private static final String TEST_RESOURCES_PREFIX = "src/test/resources/TestEncryptContent/"
private static final File plainFile = new File("${TEST_RESOURCES_PREFIX}/plain.txt")
private static final File encryptedFile = new File("${TEST_RESOURCES_PREFIX}/salted_128_raw.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())
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@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"))
String shortPassword = "shortPassword"
def encryptionMethodsAndKdfs = [
(KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY): EncryptionMethod.MD5_128AES,
(KeyDerivationFunction.NIFI_LEGACY) : EncryptionMethod.MD5_128AES,
(KeyDerivationFunction.BCRYPT) : EncryptionMethod.AES_CBC,
(KeyDerivationFunction.SCRYPT) : EncryptionMethod.AES_CBC,
(KeyDerivationFunction.PBKDF2) : EncryptionMethod.AES_CBC
]
// Act
encryptionMethodsAndKdfs.each { KeyDerivationFunction kdf, EncryptionMethod encryptionMethod ->
OutputStream cipherStream = new ByteArrayOutputStream()
OutputStream recoveredStream = new ByteArrayOutputStream()
logger.info("Using ${kdf.name} and ${encryptionMethod.name()}")
PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, shortPassword.toCharArray(), kdf)
StreamCallback encryptionCallback = encryptor.getEncryptionCallback()
StreamCallback decryptionCallback = encryptor.getDecryptionCallback()
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: {}\n\n", recovered)
assert PLAINTEXT.equals(recovered)
// This is necessary to run multiple iterations
plainStream.reset()
}
}
@Test
public void testShouldDecryptLegacyOpenSSLSaltedCipherText() throws Exception {
// Arrange
Assume.assumeTrue("Skipping test because unlimited strength crypto policy not installed", PasswordBasedEncryptor.supportsUnlimitedStrength())
final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text
logger.info("Plaintext: {}", PLAINTEXT)
byte[] cipherBytes = new File("${TEST_RESOURCES_PREFIX}/salted_128_raw.enc").bytes
InputStream cipherStream = new ByteArrayInputStream(cipherBytes)
OutputStream recoveredStream = new ByteArrayOutputStream()
final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
final KeyDerivationFunction kdf = KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY
PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf)
StreamCallback decryptionCallback = encryptor.getDecryptionCallback()
logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}")
// Act
decryptionCallback.process(cipherStream, 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 testShouldDecryptLegacyOpenSSLUnsaltedCipherText() throws Exception {
// Arrange
Assume.assumeTrue("Skipping test because unlimited strength crypto policy not installed", PasswordBasedEncryptor.supportsUnlimitedStrength())
final String PLAINTEXT = new File("${TEST_RESOURCES_PREFIX}/plain.txt").text
logger.info("Plaintext: {}", PLAINTEXT)
byte[] cipherBytes = new File("${TEST_RESOURCES_PREFIX}/unsalted_128_raw.enc").bytes
InputStream cipherStream = new ByteArrayInputStream(cipherBytes)
OutputStream recoveredStream = new ByteArrayOutputStream()
final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES
final KeyDerivationFunction kdf = KeyDerivationFunction.OPENSSL_EVP_BYTES_TO_KEY
PasswordBasedEncryptor encryptor = new PasswordBasedEncryptor(encryptionMethod, PASSWORD.toCharArray(), kdf)
StreamCallback decryptionCallback = encryptor.getDecryptionCallback()
logger.info("Cipher bytes: ${Hex.encodeHexString(cipherBytes)}")
// Act
decryptionCallback.process(cipherStream, recoveredStream)
// Assert
byte[] recoveredBytes = ((ByteArrayOutputStream) recoveredStream).toByteArray()
String recovered = new String(recoveredBytes, "UTF-8")
logger.info("Recovered: {}", recovered)
assert PLAINTEXT.equals(recovered)
}
}

View File

@ -0,0 +1,608 @@
/*
* 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.binary.Base64
import org.apache.commons.codec.binary.Hex
import org.apache.nifi.processors.standard.util.crypto.scrypt.Scrypt
import org.apache.nifi.security.util.EncryptionMethod
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Assume
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import java.security.SecureRandom
import java.security.Security
import static groovy.test.GroovyAssert.shouldFail
import static org.junit.Assert.assertTrue
@RunWith(JUnit4.class)
public class ScryptCipherProviderGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(ScryptCipherProviderGroovyTest.class);
private static List<EncryptionMethod> strongKDFEncryptionMethods
private static final int DEFAULT_KEY_LENGTH = 128;
public static final String MICROBENCHMARK = "microbenchmark"
private static ArrayList<Integer> AES_KEY_LENGTHS
RandomIVPBECipherProvider cipherProvider
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider());
strongKDFEncryptionMethods = EncryptionMethod.values().findAll { it.isCompatibleWithStrongKDFs() }
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
if (PasswordBasedEncryptor.supportsUnlimitedStrength()) {
AES_KEY_LENGTHS = [128, 192, 256]
} else {
AES_KEY_LENGTHS = [128]
}
}
@Before
public void setUp() throws Exception {
// Very fast parameters to test for correctness rather than production values
cipherProvider = new ScryptCipherProvider(4, 1, 1)
}
@After
public void tearDown() throws Exception {
}
@Test
public void testGetCipherShouldBeInternallyConsistent() throws Exception {
// Arrange
final String PASSWORD = "shortPassword";
final byte[] SALT = cipherProvider.generateSalt()
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherWithExternalIVShouldBeInternallyConsistent() throws Exception {
// Arrange
final String PASSWORD = "shortPassword";
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("01" * 16 as char[]);
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testGetCipherWithUnlimitedStrengthShouldBeInternallyConsistent() throws Exception {
// Arrange
Assume.assumeTrue("Test is being skipped due to this JVM lacking JCE Unlimited Strength Jurisdiction Policy file.",
PasswordBasedEncryptor.supportsUnlimitedStrength());
final String PASSWORD = "shortPassword";
final byte[] SALT = cipherProvider.generateSalt()
final int LONG_KEY_LENGTH = 256
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, LONG_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, iv, LONG_KEY_LENGTH, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
}
}
@Test
public void testScryptShouldSupportExternalCompatibility() throws Exception {
// Arrange
// Default values are N=2^14, r=8, p=1, but the provided salt will contain the parameters used
cipherProvider = new ScryptCipherProvider()
final String PLAINTEXT = "This is a plaintext message.";
final String PASSWORD = "thisIsABadPassword"
final int DK_LEN = 128
// These values can be generated by running `$ ./openssl_scrypt.rb` in the terminal
final byte[] SALT = Hex.decodeHex("f5b8056ea6e66edb8d013ac432aba24a" as char[])
logger.info("Expected salt: ${Hex.encodeHexString(SALT)}")
final byte[] IV = Hex.decodeHex("76a00f00878b8c3db314ae67804c00a1" as char[])
final String CIPHER_TEXT = "604188bf8e9137bc1b24a0ab01973024bc5935e9ae5fedf617bdca028c63c261"
logger.sanity("Ruby cipher text: ${CIPHER_TEXT}")
byte[] cipherBytes = Hex.decodeHex(CIPHER_TEXT as char[])
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Sanity check
String rubyKeyHex = "a8efbc0a709d3f89b6bb35b05fc8edf5"
logger.sanity("Using key: ${rubyKeyHex}")
logger.sanity("Using IV: ${Hex.encodeHexString(IV)}")
Cipher rubyCipher = Cipher.getInstance(encryptionMethod.algorithm, "BC")
def rubyKey = new SecretKeySpec(Hex.decodeHex(rubyKeyHex as char[]), "AES")
def ivSpec = new IvParameterSpec(IV)
rubyCipher.init(Cipher.ENCRYPT_MODE, rubyKey, ivSpec)
byte[] rubyCipherBytes = rubyCipher.doFinal(PLAINTEXT.bytes)
logger.sanity("Created cipher text: ${Hex.encodeHexString(rubyCipherBytes)}")
rubyCipher.init(Cipher.DECRYPT_MODE, rubyKey, ivSpec)
assert rubyCipher.doFinal(rubyCipherBytes) == PLAINTEXT.bytes
logger.sanity("Decrypted generated cipher text successfully")
assert rubyCipher.doFinal(cipherBytes) == PLAINTEXT.bytes
logger.sanity("Decrypted external cipher text successfully")
// n$r$p$hex_salt_SL$hex_hash_HL
final String FULL_HASH = "400\$8\$24\$f5b8056ea6e66edb8d013ac432aba24a\$a8efbc0a709d3f89b6bb35b05fc8edf5"
logger.info("Full Hash: ${FULL_HASH}")
def (String nStr, String rStr, String pStr, String saltHex, String hashHex) = FULL_HASH.split("\\\$")
def (n, r, p) = [nStr, rStr, pStr].collect { Integer.valueOf(it, 16) }
logger.info("N: Hex ${nStr} -> ${n}")
logger.info("r: Hex ${rStr} -> ${r}")
logger.info("p: Hex ${pStr} -> ${p}")
logger.info("Salt: ${saltHex}")
logger.info("Hash: ${hashHex}")
// Form Java-style salt with cost params from Ruby-style
String javaSalt = Scrypt.formatSalt(Hex.decodeHex(saltHex as char[]), n, r, p)
logger.info("Formed Java-style salt: ${javaSalt}")
// Convert hash from hex to Base64
String base64Hash = CipherUtility.encodeBase64NoPadding(Hex.decodeHex(hashHex as char[]))
logger.info("Converted hash from hex ${hashHex} to Base64 ${base64Hash}")
assert Hex.encodeHexString(Base64.decodeBase64(base64Hash)) == hashHex
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
logger.info("External cipher text: ${CIPHER_TEXT} ${cipherBytes.length}");
// Act
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, javaSalt.bytes, IV, DK_LEN, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
}
@Test
public void testGetCipherShouldHandleSaltWithoutParameters() throws Exception {
// Arrange
// To help Groovy resolve implementation private methods not known at interface level
cipherProvider = cipherProvider as ScryptCipherProvider
final String PASSWORD = "shortPassword";
final byte[] SALT = new byte[cipherProvider.defaultSaltLength]
new SecureRandom().nextBytes(SALT)
final String EXPECTED_FORMATTED_SALT = cipherProvider.formatSaltForScrypt(SALT)
logger.info("Expected salt: ${EXPECTED_FORMATTED_SALT}")
final String plaintext = "This is a plaintext message.";
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
// Act
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, DEFAULT_KEY_LENGTH, true);
byte[] iv = cipher.getIV();
logger.info("IV: ${Hex.encodeHexString(iv)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
// Manually initialize a cipher for decrypt with the expected salt
byte[] parsedSalt = new byte[cipherProvider.defaultSaltLength]
def params = []
cipherProvider.parseSalt(EXPECTED_FORMATTED_SALT, parsedSalt, params)
def (int n, int r, int p) = params
byte[] keyBytes = Scrypt.deriveScryptKey(PASSWORD.bytes, parsedSalt, n, r, p, DEFAULT_KEY_LENGTH)
SecretKey key = new SecretKeySpec(keyBytes, "AES")
Cipher manualCipher = Cipher.getInstance(encryptionMethod.algorithm, encryptionMethod.provider)
manualCipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv))
byte[] recoveredBytes = manualCipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert plaintext.equals(recovered);
}
@Test
public void testGetCipherShouldNotAcceptInvalidSalts() throws Exception {
// Arrange
final String PASSWORD = "thisIsABadPassword";
final def INVALID_SALTS = ['bad_sal', '$3a$11$', 'x', '$2a$10$', '$400$1$1$abcdefghijklmnopqrstuvwxyz']
final LENGTH_MESSAGE = "The raw salt must be between 8 and 32 bytes"
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
// Act
INVALID_SALTS.each { String salt ->
logger.info("Checking salt ${salt}")
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt.bytes, DEFAULT_KEY_LENGTH, true);
}
logger.expected(msg)
// Assert
assert msg =~ LENGTH_MESSAGE
}
}
@Test
public void testGetCipherShouldHandleUnformattedSalts() throws Exception {
// Arrange
final String PASSWORD = "thisIsABadPassword";
final def RECOVERABLE_SALTS = ['$ab$00$acbdefghijklmnopqrstuv', '$4$1$1$0123456789abcdef', '$400$1$1$abcdefghijklmnopqrstuv']
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
// Act
RECOVERABLE_SALTS.each { String salt ->
logger.info("Checking salt ${salt}")
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, salt.bytes, DEFAULT_KEY_LENGTH, true);
// Assert
assert cipher
}
}
@Test
public void testGetCipherShouldRejectEmptySalt() throws Exception {
// Arrange
final String PASSWORD = "thisIsABadPassword";
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()}");
// Two different errors -- one explaining the no-salt method is not supported, and the other for an empty byte[] passed
// Act
def msg = shouldFail(UnsupportedOperationException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, DEFAULT_KEY_LENGTH, true);
}
logger.expected(msg)
// Assert
assert msg =~ "The cipher cannot be initialized without a valid salt\\. Use ScryptCipherProvider#generateSalt\\(\\) to generate a valid salt"
// Act
msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, new byte[0], DEFAULT_KEY_LENGTH, true);
}
logger.expected(msg)
// Assert
assert msg =~ "The salt cannot be empty\\. To generate a salt, use ScryptCipherProvider#generateSalt"
}
@Test
public void testGetCipherForDecryptShouldRequireIV() throws Exception {
// Arrange
final String PASSWORD = "shortPassword";
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("00" * 16 as char[]);
final String plaintext = "This is a plaintext message.";
// Act
for (EncryptionMethod em : strongKDFEncryptionMethods) {
logger.info("Using algorithm: ${em.getAlgorithm()}");
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(em, PASSWORD, SALT, IV, DEFAULT_KEY_LENGTH, true);
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
def msg = shouldFail(IllegalArgumentException) {
cipher = cipherProvider.getCipher(em, PASSWORD, SALT, DEFAULT_KEY_LENGTH, false);
}
logger.expected(msg)
// Assert
assert msg =~ "Cannot decrypt without a valid IV"
}
}
@Test
public void testGetCipherShouldAcceptValidKeyLengths() throws Exception {
// Arrange
final String PASSWORD = "shortPassword";
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("01" * 16 as char[]);
final String PLAINTEXT = "This is a plaintext message.";
// Currently only AES ciphers are compatible with Bcrypt, so redundant to test all algorithms
final def VALID_KEY_LENGTHS = AES_KEY_LENGTHS
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
VALID_KEY_LENGTHS.each { int keyLength ->
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} with key length ${keyLength}")
// Initialize a cipher for encryption
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
logger.info("IV: ${Hex.encodeHexString(IV)}")
byte[] cipherBytes = cipher.doFinal(PLAINTEXT.getBytes("UTF-8"));
logger.info("Cipher text: ${Hex.encodeHexString(cipherBytes)} ${cipherBytes.length}");
cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, false);
byte[] recoveredBytes = cipher.doFinal(cipherBytes);
String recovered = new String(recoveredBytes, "UTF-8");
logger.info("Recovered: ${recovered}")
// Assert
assert PLAINTEXT.equals(recovered);
}
}
@Test
public void testGetCipherShouldNotAcceptInvalidKeyLengths() throws Exception {
// Arrange
final String PASSWORD = "shortPassword";
final byte[] SALT = cipherProvider.generateSalt()
final byte[] IV = Hex.decodeHex("00" * 16 as char[]);
final String PLAINTEXT = "This is a plaintext message.";
// Even though Scrypt can derive keys of arbitrary length, it will fail to validate if the underlying cipher does not support it
final def INVALID_KEY_LENGTHS = [-1, 40, 64, 112, 512]
// Currently only AES ciphers are compatible with Scrypt, so redundant to test all algorithms
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
INVALID_KEY_LENGTHS.each { int keyLength ->
logger.info("Using algorithm: ${encryptionMethod.getAlgorithm()} with key length ${keyLength}")
// Initialize a cipher for encryption
def msg = shouldFail(IllegalArgumentException) {
Cipher cipher = cipherProvider.getCipher(encryptionMethod, PASSWORD, SALT, IV, keyLength, true);
}
logger.expected(msg)
// Assert
assert msg =~ "${keyLength} is not a valid key length for AES"
}
}
@Test
public void testScryptShouldNotAcceptInvalidPassword() {
// Arrange
String badPassword = ""
byte[] salt = [0x01 as byte] * 16
EncryptionMethod encryptionMethod = EncryptionMethod.AES_CBC
// Act
def msg = shouldFail(IllegalArgumentException) {
cipherProvider.getCipher(encryptionMethod, badPassword, salt, DEFAULT_KEY_LENGTH, true)
}
// Assert
assert msg =~ "Encryption with an empty password is not supported"
}
@Test
public void testGenerateSaltShouldUseProvidedParameters() throws Exception {
// Arrange
RandomIVPBECipherProvider cipherProvider = new ScryptCipherProvider(8, 2, 2);
int n = cipherProvider.getN()
int r = cipherProvider.getR()
int p = cipherProvider.getP()
// Act
final String salt = new String(cipherProvider.generateSalt())
logger.info("Salt: ${salt}")
// Assert
assert salt =~ "^(?i)\\\$s0\\\$[a-f0-9]{5,16}\\\$"
String params = Scrypt.encodeParams(n, r, p)
assert salt.contains("\$${params}\$")
}
@Test
public void testShouldParseSalt() throws Exception {
// Arrange
cipherProvider = cipherProvider as ScryptCipherProvider
final byte[] EXPECTED_RAW_SALT = Hex.decodeHex("f5b8056ea6e66edb8d013ac432aba24a" as char[])
final int EXPECTED_N = 1024
final int EXPECTED_R = 8
final int EXPECTED_P = 36
final String FORMATTED_SALT = "\$s0\$a0824\$9bgFbqbmbtuNATrEMquiSg"
logger.info("Using salt: ${FORMATTED_SALT}");
byte[] rawSalt = new byte[16]
def params = []
// Act
cipherProvider.parseSalt(FORMATTED_SALT, rawSalt, params)
// Assert
assert rawSalt == EXPECTED_RAW_SALT
assert params[0] == EXPECTED_N
assert params[1] == EXPECTED_R
assert params[2] == EXPECTED_P
}
@Ignore("This test can be run on a specific machine to evaluate if the default parameters are sufficient")
@Test
public void testDefaultConstructorShouldProvideStrongParameters() {
// Arrange
ScryptCipherProvider testCipherProvider = new ScryptCipherProvider()
/** See this Stack Overflow answer for a good visualization of the interplay between N, r, p {@link http://stackoverflow.com/a/30308723} */
// Act
int n = testCipherProvider.getN()
int r = testCipherProvider.getR()
int p = testCipherProvider.getP()
logger.info("Default parameters N=${n}, r=${r}, p=${p}")
// Calculate the parameters to reach 500 ms
def (int minimumN, int minimumR, int minimumP) = calculateMinimumParameters(r, p)
logger.info("Determined minimum safe parameters to be N=${minimumN}, r=${minimumR}, p=${minimumP}")
// Assert
assertTrue("The default parameters for ScryptCipherProvider are too weak. Please update the default values to a stronger level.", n >= minimumN)
}
/**
* Returns the parameters required for a derivation to exceed 500 ms on this machine. Code adapted from http://security.stackexchange.com/questions/17207/recommended-of-rounds-for-bcrypt
*
* @param r the block size in bytes (defaults to 8)
* @param p the parallelization factor (defaults to 1)
* @param maxHeapSize the maximum heap size to use in bytes (defaults to 1 GB)
*
* @return the minimum scrypt parameters as [N, r, p]
*/
private static List<Integer> calculateMinimumParameters(int r = 8, int p = 1, int maxHeapSize = 1024 * 1024 * 1024) {
// High start-up cost, so run multiple times for better benchmarking
final int RUNS = 10
// Benchmark using N=2^4
int n = 2**4
int dkLen = 128
assert Scrypt.calculateExpectedMemory(n, r, p) <= maxHeapSize
byte[] salt = new byte[Scrypt.defaultSaltLength]
new SecureRandom().nextBytes(salt)
// Run once to prime the system
double duration = time {
Scrypt.scrypt(MICROBENCHMARK, salt, n, r, p, dkLen)
}
logger.info("First run of N=${n}, r=${r}, p=${p} took ${duration} ms (ignored)")
def durations = []
RUNS.times { int i ->
duration = time {
Scrypt.scrypt(MICROBENCHMARK, salt, n, r, p, dkLen)
}
logger.info("N=${n}, r=${r}, p=${p} took ${duration} ms")
durations << duration
}
duration = durations.sum() / durations.size()
logger.info("N=${n}, r=${r}, p=${p} averaged ${duration} ms")
// Doubling N would double the run time
// Keep increasing N until the estimated duration is over 500 ms
while (duration < 500) {
n *= 2
duration *= 2
}
logger.info("Returning N=${n}, r=${r}, p=${p} for ${duration} ms")
return [n, r, p]
}
private static double time(Closure c) {
long start = System.nanoTime()
c.call()
long end = System.nanoTime()
return (end - start) / 1_000_000.0
}
}

View File

@ -0,0 +1,399 @@
/*
* 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.scrypt
import org.apache.commons.codec.binary.Hex
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.junit.After
import org.junit.Assume
import org.junit.Before
import org.junit.BeforeClass
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.security.SecureRandom
import java.security.Security
import static groovy.test.GroovyAssert.shouldFail
@RunWith(JUnit4.class)
public class ScryptGroovyTest {
private static final Logger logger = LoggerFactory.getLogger(ScryptGroovyTest.class)
private static final String PASSWORD = "shortPassword"
private static final String SALT_HEX = "0123456789ABCDEFFEDCBA9876543210"
private static final byte[] SALT_BYTES = Hex.decodeHex(SALT_HEX as char[])
// Small values to test for correctness, not timing
private static final int N = 2**4
private static final int R = 1
private static final int P = 1
private static final int DK_LEN = 128
private static final long TWO_GIGABYTES = 2048L * 1024 * 1024
@BeforeClass
public static void setUpOnce() throws Exception {
Security.addProvider(new BouncyCastleProvider())
logger.metaClass.methodMissing = { String name, args ->
logger.info("[${name?.toUpperCase()}] ${(args as List).join(" ")}")
}
}
@Before
public void setUp() throws Exception {
}
@After
public void tearDown() throws Exception {
}
@Test
public void testDeriveScryptKeyShouldBeInternallyConsistent() throws Exception {
// Arrange
def allKeys = []
final int RUNS = 10
logger.info("Running with '${PASSWORD}', '${SALT_HEX}', $N, $R, $P, $DK_LEN")
// Act
RUNS.times {
byte[] keyBytes = Scrypt.deriveScryptKey(PASSWORD.bytes, SALT_BYTES, N, R, P, DK_LEN)
logger.info("Derived key: ${Hex.encodeHexString(keyBytes)}")
allKeys << keyBytes
}
// Assert
assert allKeys.size() == RUNS
assert allKeys.every { it == allKeys.first() }
}
/**
* This test ensures that the local implementation of Scrypt is compatible with the reference implementation from the Colin Percival paper.
*/
@Test
public void testDeriveScryptKeyShouldMatchTestVectors() {
// Arrange
// These values are taken from Colin Percival's scrypt paper: https://www.tarsnap.com/scrypt/scrypt.pdf
final byte[] HASH_2 = Hex.decodeHex("fdbabe1c9d3472007856e7190d01e9fe" +
"7c6ad7cbc8237830e77376634b373162" +
"2eaf30d92e22a3886ff109279d9830da" +
"c727afb94a83ee6d8360cbdfa2cc0640" as char[])
final byte[] HASH_3 = Hex.decodeHex("7023bdcb3afd7348461c06cd81fd38eb" +
"fda8fbba904f8e3ea9b543f6545da1f2" +
"d5432955613f0fcf62d49705242a9af9" +
"e61e85dc0d651e40dfcf017b45575887" as char[])
final def TEST_VECTORS = [
// Empty password is not supported by JCE
[password: "password",
salt : "NaCl",
n : 1024,
r : 8,
p : 16,
dkLen : 64 * 8,
hash : HASH_2],
[password: "pleaseletmein",
salt : "SodiumChloride",
n : 16384,
r : 8,
p : 1,
dkLen : 64 * 8,
hash : HASH_3],
]
// Act
TEST_VECTORS.each { Map params ->
logger.info("Running with '${params.password}', '${params.salt}', ${params.n}, ${params.r}, ${params.p}, ${params.dkLen}")
long memoryInBytes = Scrypt.calculateExpectedMemory(params.n, params.r, params.p)
logger.info("Expected memory usage: (128 * r * N + 128 * r * p) ${memoryInBytes} bytes")
logger.info(" Expected ${Hex.encodeHexString(params.hash)}")
byte[] calculatedHash = Scrypt.deriveScryptKey(params.password.bytes, params.salt.bytes, params.n, params.r, params.p, params.dkLen)
logger.info("Generated ${Hex.encodeHexString(calculatedHash)}")
// Assert
assert calculatedHash == params.hash
}
}
/**
* This test ensures that the local implementation of Scrypt is compatible with the reference implementation from the Colin Percival paper. The test vector requires ~1GB {@code byte[]}
* and therefore the Java heap must be at least 1GB. Because {@link nifi/pom.xml} has a {@code surefire} rule which appends {@code -Xmx1G}
* to the Java options, this overrides any IDE options. To ensure the heap is properly set, using the {@code groovyUnitTest} profile will re-append {@code -Xmx3072m} to the Java options.
*/
@Test
public void testDeriveScryptKeyShouldMatchExpensiveTestVector() {
// Arrange
long totalMemory = Runtime.getRuntime().totalMemory()
logger.info("Required memory: ${TWO_GIGABYTES} bytes")
logger.info("Max heap memory: ${totalMemory} bytes")
Assume.assumeTrue("Test is being skipped due to JVM heap size. Please run with -Xmx3072m to set sufficient heap size",
totalMemory >= TWO_GIGABYTES)
// These values are taken from Colin Percival's scrypt paper: https://www.tarsnap.com/scrypt/scrypt.pdf
final byte[] HASH = Hex.decodeHex("2101cb9b6a511aaeaddbbe09cf70f881" +
"ec568d574a2ffd4dabe5ee9820adaa47" +
"8e56fd8f4ba5d09ffa1c6d927c40f4c3" +
"37304049e8a952fbcbf45c6fa77a41a4" as char[])
// This test vector requires 2GB heap space and approximately 10 seconds on a consumer machine
String password = "pleaseletmein"
String salt = "SodiumChloride"
int n = 1048576
int r = 8
int p = 1
int dkLen = 64 * 8
// Act
logger.info("Running with '${password}', '${salt}', ${n}, ${r}, ${p}, ${dkLen}")
long memoryInBytes = Scrypt.calculateExpectedMemory(n, r, p)
logger.info("Expected memory usage: (128 * r * N + 128 * r * p) ${memoryInBytes} bytes")
logger.info(" Expected ${Hex.encodeHexString(HASH)}")
byte[] calculatedHash = Scrypt.deriveScryptKey(password.bytes, salt.bytes, n, r, p, dkLen)
logger.info("Generated ${Hex.encodeHexString(calculatedHash)}")
// Assert
assert calculatedHash == HASH
}
@Ignore("This test was just to exercise the heap and debug OOME issues")
@Test
void testShouldCauseOutOfMemoryError() {
SecureRandom secureRandom = new SecureRandom()
// int i = 29
(10..31).each { int i ->
int length = 2**i
byte[] bytes = new byte[length]
secureRandom.nextBytes(bytes)
logger.info("Successfully ran with byte[] of length ${length}")
logger.info("${Hex.encodeHexString(bytes[0..<16] as byte[])}...")
}
}
@Test
public void testDeriveScryptKeyShouldSupportExternalCompatibility() {
// Arrange
// These values can be generated by running `$ ./openssl_scrypt.rb` in the terminal
final String EXPECTED_KEY_HEX = "a8efbc0a709d3f89b6bb35b05fc8edf5"
String password = "thisIsABadPassword"
String saltHex = "f5b8056ea6e66edb8d013ac432aba24a"
int n = 1024
int r = 8
int p = 36
int dkLen = 16 * 8
// Act
logger.info("Running with '${password}', ${saltHex}, ${n}, ${r}, ${p}, ${dkLen}")
long memoryInBytes = Scrypt.calculateExpectedMemory(n, r, p)
logger.info("Expected memory usage: (128 * r * N + 128 * r * p) ${memoryInBytes} bytes")
logger.info(" Expected ${EXPECTED_KEY_HEX}")
byte[] calculatedHash = Scrypt.deriveScryptKey(password.bytes, Hex.decodeHex(saltHex as char[]), n, r, p, dkLen)
logger.info("Generated ${Hex.encodeHexString(calculatedHash)}")
// Assert
assert calculatedHash == Hex.decodeHex(EXPECTED_KEY_HEX as char[])
}
@Test
public void testScryptShouldBeInternallyConsistent() throws Exception {
// Arrange
def allHashes = []
final int RUNS = 10
logger.info("Running with '${PASSWORD}', '${SALT_HEX}', $N, $R, $P")
// Act
RUNS.times {
String hash = Scrypt.scrypt(PASSWORD, SALT_BYTES, N, R, P, DK_LEN)
logger.info("Hash: ${hash}")
allHashes << hash
}
// Assert
assert allHashes.size() == RUNS
assert allHashes.every { it == allHashes.first() }
}
@Test
public void testScryptShouldGenerateValidSaltIfMissing() {
// Arrange
// The generated salt should be byte[16], encoded as 22 Base64 chars
final def EXPECTED_SALT_PATTERN = /\$.+\$[0-9a-zA-Z\/\+]{22}\$.+/
// Act
String calculatedHash = Scrypt.scrypt(PASSWORD, N, R, P, DK_LEN)
logger.info("Generated ${calculatedHash}")
// Assert
assert calculatedHash =~ EXPECTED_SALT_PATTERN
}
@Test
public void testScryptShouldNotAcceptInvalidN() throws Exception {
// Arrange
final int MAX_N = Integer.MAX_VALUE / 128 / R - 1
// N must be a power of 2 > 1 and < Integer.MAX_VALUE / 128 / r
final def INVALID_NS = [-2, 0, 1, 3, 4096 - 1, MAX_N + 1]
// Act
INVALID_NS.each { int invalidN ->
logger.info("Using N: ${invalidN}")
def msg = shouldFail(IllegalArgumentException) {
Scrypt.deriveScryptKey(PASSWORD.bytes, SALT_BYTES, invalidN, R, P, DK_LEN)
}
// Assert
assert msg =~ "N must be a power of 2 greater than 1|Parameter N is too large"
}
}
@Test
public void testScryptShouldAcceptValidR() throws Exception {
// Arrange
// Use a large p value to allow r to exceed MAX_R without normal N exceeding MAX_N
int largeP = 2**10
final int MAX_R = Math.ceil(Integer.MAX_VALUE / 128 / largeP) - 1
// r must be in (0..Integer.MAX_VALUE / 128 / p)
final def INVALID_RS = [0, MAX_R + 1]
// Act
INVALID_RS.each { int invalidR ->
logger.info("Using r: ${invalidR}")
def msg = shouldFail(IllegalArgumentException) {
byte[] hash = Scrypt.deriveScryptKey(PASSWORD.bytes, SALT_BYTES, N, invalidR, largeP, DK_LEN)
logger.info("Generated hash: ${Hex.encodeHexString(hash)}")
}
// Assert
assert msg =~ "Parameter r must be 1 or greater|Parameter r is too large"
}
}
@Test
public void testScryptShouldNotAcceptInvalidP() throws Exception {
// Arrange
final int MAX_P = Math.ceil(Integer.MAX_VALUE / 128) - 1
// p must be in (0..Integer.MAX_VALUE / 128)
final def INVALID_PS = [0, MAX_P + 1]
// Act
INVALID_PS.each { int invalidP ->
logger.info("Using p: ${invalidP}")
def msg = shouldFail(IllegalArgumentException) {
byte[] hash = Scrypt.deriveScryptKey(PASSWORD.bytes, SALT_BYTES, N, R, invalidP, DK_LEN)
logger.info("Generated hash: ${Hex.encodeHexString(hash)}")
}
// Assert
assert msg =~ "Parameter p must be 1 or greater|Parameter p is too large"
}
}
@Test
public void testCheckShouldValidateCorrectPassword() throws Exception {
// Arrange
final String PASSWORD = "thisIsABadPassword"
final String EXPECTED_HASH = Scrypt.scrypt(PASSWORD, N, R, P, DK_LEN)
logger.info("Password: ${PASSWORD} -> Hash: ${EXPECTED_HASH}")
// Act
boolean matches = Scrypt.check(PASSWORD, EXPECTED_HASH)
logger.info("Check matches: ${matches}")
// Assert
assert matches
}
@Test
public void testCheckShouldNotValidateIncorrectPassword() throws Exception {
// Arrange
final String PASSWORD = "thisIsABadPassword"
final String EXPECTED_HASH = Scrypt.scrypt(PASSWORD, N, R, P, DK_LEN)
logger.info("Password: ${PASSWORD} -> Hash: ${EXPECTED_HASH}")
// Act
boolean matches = Scrypt.check(PASSWORD.reverse(), EXPECTED_HASH)
logger.info("Check matches: ${matches}")
// Assert
assert !matches
}
@Test
public void testCheckShouldNotAcceptInvalidPassword() throws Exception {
// Arrange
final String HASH = '$s0$a0801$abcdefghijklmnopqrstuv$abcdefghijklmnopqrstuv'
// Even though the spec allows for empty passwords, the JCE does not, so extend enforcement of that to the user boundary
final def INVALID_PASSWORDS = ['', null]
// Act
INVALID_PASSWORDS.each { String invalidPassword ->
logger.info("Using password: ${invalidPassword}")
def msg = shouldFail(IllegalArgumentException) {
boolean matches = Scrypt.check(invalidPassword, HASH)
}
logger.expected(msg)
// Assert
assert msg =~ "Password cannot be empty"
}
}
@Test
public void testCheckShouldNotAcceptInvalidHash() throws Exception {
// Arrange
final String PASSWORD = "thisIsABadPassword"
// Even though the spec allows for empty salts, the JCE does not, so extend enforcement of that to the user boundary
final def INVALID_HASHES = ['', null, '$s0$a0801$', '$s0$a0801$abcdefghijklmnopqrstuv$']
// Act
INVALID_HASHES.each { String invalidHash ->
logger.info("Using hash: ${invalidHash}")
def msg = shouldFail(IllegalArgumentException) {
boolean matches = Scrypt.check(PASSWORD, invalidHash)
}
logger.expected(msg)
// Assert
assert msg =~ "Hash cannot be empty|Hash is not properly formatted"
}
}
}

View File

@ -18,7 +18,8 @@ package org.apache.nifi.processors.standard;
import org.apache.commons.codec.binary.Hex;
import org.apache.nifi.components.ValidationResult;
import org.apache.nifi.processors.standard.util.PasswordBasedEncryptor;
import org.apache.nifi.processors.standard.util.crypto.CipherUtility;
import org.apache.nifi.processors.standard.util.crypto.PasswordBasedEncryptor;
import org.apache.nifi.security.util.EncryptionMethod;
import org.apache.nifi.security.util.KeyDerivationFunction;
import org.apache.nifi.util.MockFlowFile;
@ -51,13 +52,23 @@ public class TestEncryptContent {
@Test
public void testRoundTrip() throws IOException {
final TestRunner testRunner = TestRunners.newTestRunner(new EncryptContent());
testRunner.setProperty(EncryptContent.PASSWORD, "Hello, World!");
testRunner.setProperty(EncryptContent.PASSWORD, "short");
testRunner.setProperty(EncryptContent.KEY_DERIVATION_FUNCTION, KeyDerivationFunction.NIFI_LEGACY.name());
// Must be allowed or short password will cause validation errors
testRunner.setProperty(EncryptContent.ALLOW_WEAK_CRYPTO, "allowed");
for (final EncryptionMethod method : EncryptionMethod.values()) {
if (method.isUnlimitedStrength()) {
for (final EncryptionMethod encryptionMethod : EncryptionMethod.values()) {
if (encryptionMethod.isUnlimitedStrength()) {
continue; // cannot test unlimited strength in unit tests because it's not enabled by the JVM by default.
}
testRunner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, method.name());
// KeyedCiphers tested in TestEncryptContentGroovy.groovy
if (encryptionMethod.isKeyedCipher()) {
continue;
}
logger.info("Attempting {}", encryptionMethod.name());
testRunner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, encryptionMethod.name());
testRunner.setProperty(EncryptContent.MODE, EncryptContent.ENCRYPT_MODE);
testRunner.enqueue(Paths.get("src/test/resources/hello.txt"));
@ -75,7 +86,7 @@ public class TestEncryptContent {
testRunner.run();
testRunner.assertAllFlowFilesTransferred(EncryptContent.REL_SUCCESS, 1);
logger.info("Successfully decrypted {}", method.name());
logger.info("Successfully decrypted {}", encryptionMethod.name());
flowFile = testRunner.getFlowFilesForRelationship(EncryptContent.REL_SUCCESS).get(0);
flowFile.assertContentEquals(new File("src/test/resources/hello.txt"));
@ -334,27 +345,29 @@ public class TestEncryptContent {
runner.enqueue(new byte[0]);
pc = (MockProcessContext) runner.getProcessContext();
results = pc.validate();
Assert.assertEquals(1, results.size());
Assert.assertEquals(results.toString(), 1, results.size());
for (final ValidationResult vr : results) {
Assert.assertTrue(vr.toString()
.contains(EncryptContent.PASSWORD.getDisplayName() + " is required when using algorithm"));
}
runner.enqueue(new byte[0]);
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, EncryptionMethod.MD5_256AES.name());
final EncryptionMethod encryptionMethod = EncryptionMethod.MD5_128AES;
runner.setProperty(EncryptContent.ENCRYPTION_ALGORITHM, encryptionMethod.name());
runner.setProperty(EncryptContent.PASSWORD, "ThisIsAPasswordThatIsLongerThanSixteenCharacters");
pc = (MockProcessContext) runner.getProcessContext();
results = pc.validate();
if (!PasswordBasedEncryptor.supportsUnlimitedStrength()) {
logger.info(results.toString());
Assert.assertEquals(1, results.size());
for (final ValidationResult vr : results) {
Assert.assertTrue(
"Did not successfully catch validation error of a long password in a non-JCE Unlimited Strength environment",
vr.toString().contains("Password length greater than " + PasswordBasedEncryptor.getMaxAllowedKeyLength(EncryptionMethod.MD5_256AES.getAlgorithm())
+ " bits is not supported by this JVM due to lacking JCE Unlimited Strength Jurisdiction Policy files."));
vr.toString().contains("Password length greater than " + CipherUtility.getMaximumPasswordLengthForAlgorithmOnLimitedStrengthCrypto(encryptionMethod)
+ " characters is not supported by this JVM due to lacking JCE Unlimited Strength Jurisdiction Policy files."));
}
} else {
Assert.assertEquals(0, results.size());
Assert.assertEquals(results.toString(), 0, results.size());
}
runner.removeProperty(EncryptContent.PASSWORD);

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.processors.standard.util;
package org.apache.nifi.processors.standard.util.crypto;
import org.apache.commons.codec.binary.Hex;
import org.apache.nifi.processor.io.StreamCallback;

View File

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.nifi.processors.standard.util;
package org.apache.nifi.processors.standard.util.crypto;
import org.apache.commons.codec.binary.Hex;
import org.apache.nifi.processor.io.StreamCallback;

View File

@ -0,0 +1 @@
Salted__!C6Ày5ÑîïÞ<C3AF><C39E>}3âü$/ÍsâBA¯@À•ƒ<]t<>ÔÁLø

View File

@ -21,7 +21,7 @@
<immediateFlush>false</immediateFlush>
</encoder>
</appender>
<appender name="TARGET_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>target/log.txt</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
@ -36,14 +36,15 @@
<pattern>%date %level [%thread] %logger{40} %msg%n</pattern>
<immediateFlush>true</immediateFlush>
</encoder>
</appender>
</appender>
<!-- valid logging levels: TRACE, DEBUG, INFO, WARN, ERROR -->
<logger name="org.apache.nifi" level="INFO"/>
<logger name="org.apache.nifi.processors.standard.util.crypto" level="DEBUG"/>
<!-- Logger for managing logging statements for nifi clusters. -->
<logger name="org.apache.nifi.cluster" level="INFO"/>
<!--
<!--
Logger for logging HTTP requests received by the web server. Setting
log level to 'debug' activates HTTP request logging.
-->
@ -57,11 +58,11 @@
<logger name="org.apache.nifi.processors.standard" level="DEBUG"/>
<logger name="target.file" level="DEBUG" additivity="true">
<appender-ref ref="TARGET_FILE" />
<appender-ref ref="TARGET_FILE"/>
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>

View File

@ -0,0 +1,46 @@
#!/usr/bin/env ruby
# 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.
require 'openssl'
def bin_to_hex(s)
s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
end
plaintext = "This is a plaintext message."
puts "Plaintext: #{plaintext}"
cipher = OpenSSL::Cipher.new 'AES-128-CBC'
cipher.encrypt
iv = cipher.random_iv
key_len = cipher.key_len
digest = OpenSSL::Digest::SHA256.new
key = digest.digest(plaintext)[0..15]
puts ""
puts " IV: #{bin_to_hex(iv)} #{iv.length}"
puts " Key: #{bin_to_hex(key)} #{key.length}"
cipher.key = key
# Now encrypt the data:
encrypted = cipher.update plaintext
encrypted << cipher.final
puts "Cipher text length: #{encrypted.length}"
puts "Cipher text: #{bin_to_hex(encrypted)}"

View File

@ -0,0 +1,62 @@
#!/usr/bin/env ruby
# 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.
require 'openssl'
require 'base64'
# Run `$ gem install bcrypt` >= 2.1.4
require 'bcrypt'
def bin_to_hex(s)
s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
end
plaintext = "This is a plaintext message."
puts "Plaintext: #{plaintext}"
cipher = OpenSSL::Cipher.new 'AES-128-CBC'
cipher.encrypt
iv = cipher.random_iv
password = 'thisIsABadPassword'
puts "Password: #{password} #{password.length}"
work_factor = 10
puts "Work factor: #{work_factor}"
key_len = cipher.key_len
digest = OpenSSL::Digest::SHA512.new
puts ""
hash = BCrypt::Password.create(password, :cost => work_factor)
puts "Hash: #{hash}"
full_salt = hash.salt
puts "Full Salt: #{full_salt} #{full_salt.length}"
key = (digest.digest hash)[0..key_len - 1]
salt = Base64.decode64(hash.salt[7..-1])
puts "Salt: #{bin_to_hex(salt)} #{salt.length}"
puts " IV: #{bin_to_hex(iv)} #{iv.length}"
puts " Key: #{bin_to_hex(key)} #{key.length}"
cipher.key = key
# Now encrypt the data:
encrypted = cipher.update plaintext
encrypted << cipher.final
puts "Cipher text length: #{encrypted.length}"
puts "Cipher text: #{bin_to_hex(encrypted)}"

View File

@ -0,0 +1,52 @@
#!/usr/bin/env ruby
# 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.
require 'openssl'
def bin_to_hex(s)
s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
end
plaintext = "This is a plaintext message."
puts "Plaintext: #{plaintext}"
cipher = OpenSSL::Cipher.new 'AES-128-CBC'
cipher.encrypt
iv = cipher.random_iv
password = 'thisIsABadPassword'
puts "Password: #{password} #{password.length}"
salt = OpenSSL::Random.random_bytes 16
iterations = 1000
puts "Iterations: #{iterations}"
key_len = cipher.key_len
digest = OpenSSL::Digest::SHA256.new
puts ""
key = OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_len, digest)
puts "Salt: #{bin_to_hex(salt)} #{salt.length}"
puts " IV: #{bin_to_hex(iv)} #{iv.length}"
puts " Key: #{bin_to_hex(key)} #{key.length}"
cipher.key = key
# Now encrypt the data:
encrypted = cipher.update plaintext
encrypted << cipher.final
puts "Cipher text length: #{encrypted.length}"
puts "Cipher text: #{bin_to_hex(encrypted)}"

View File

@ -0,0 +1,58 @@
#!/usr/bin/env ruby
# 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.
require 'openssl'
require 'base64'
# Run `$ gem install scrypt`
require 'scrypt'
def bin_to_hex(s)
s.each_byte.map { |b| b.to_s(16).rjust(2, '0') }.join
end
plaintext = "This is a plaintext message."
puts "Plaintext: #{plaintext}"
cipher = OpenSSL::Cipher.new 'AES-128-CBC'
cipher.encrypt
iv = cipher.random_iv
password = 'thisIsABadPassword'
puts "Password: #{password} #{password.length}"
cost = SCrypt::Engine.calibrate
puts "Cost: #{cost} (N$r$p$)"
key_len = cipher.key_len
puts ""
hash = SCrypt::Password.create(password, :cost => cost, :key_len => key_len, :salt_size => 16)
puts "Hash: #{hash}"
# These values are already hex-encoded strings unlike the bcrypt and PBKDF2 examples, so unpack them to binary
salt = [hash.salt].pack('H*')
key = [hash.digest].pack('H*')
puts "Salt: #{bin_to_hex(salt)} #{salt.length}"
puts " IV: #{bin_to_hex(iv)} #{iv.length}"
puts " Key: #{bin_to_hex(key)} #{key.length}"
cipher.key = key
# Now encrypt the data:
encrypted = cipher.update plaintext
encrypted << cipher.final
puts "Cipher text length: #{encrypted.length}"
puts "Cipher text: #{bin_to_hex(encrypted)}"