mirror of https://github.com/apache/nifi.git
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:
parent
0690aee452
commit
498b5023ce
16
LICENSE
16
LICENSE
|
@ -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.
|
|
@ -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.
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 |
|
@ -53,7 +53,7 @@
|
|||
<configuration>
|
||||
<excludes>**/authentication/generated/*.java,</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencies>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
* System.out.println("It matches");
|
||||
* else
|
||||
* 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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}"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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}")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
Salted__!C‚6Ày5ÑîïÞ<C3AF><C39E>–}3âü$/ÍsâBA¯@À•ƒ<]t<>ÔÁLø
|
Binary file not shown.
|
@ -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>
|
||||
|
|
|
@ -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)}"
|
|
@ -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)}"
|
|
@ -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)}"
|
|
@ -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)}"
|
Loading…
Reference in New Issue