blob: 6cba254295417f5dd37eb6e8658e712cfc84d85a [file] [log] [blame]
/*
* 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.encrypt;
import java.nio.charset.StandardCharsets;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import javax.crypto.Cipher;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.apache.nifi.security.kms.CryptoUtils;
import org.apache.nifi.security.util.EncryptionMethod;
import org.apache.nifi.security.util.KeyDerivationFunction;
import org.apache.nifi.security.util.crypto.Argon2SecureHasher;
import org.apache.nifi.security.util.crypto.CipherProvider;
import org.apache.nifi.security.util.crypto.CipherProviderFactory;
import org.apache.nifi.security.util.crypto.CipherUtility;
import org.apache.nifi.security.util.crypto.KeyedCipherProvider;
import org.apache.nifi.security.util.crypto.PBECipherProvider;
import org.apache.nifi.security.util.crypto.RandomIVPBECipherProvider;
import org.apache.nifi.util.NiFiProperties;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <p>
* An application specific string encryptor that collects configuration from the
* application properties, system properties, and/or system environment.
* </p>
* <p>
* <p>
* Instance of this class are thread-safe</p>
* <p>
* <p>
* The encryption provider and algorithm is configured using the application
* properties:
* <ul>
* <li>nifi.sensitive.props.provider</li>
* <li>nifi.sensitive.props.algorithm</li>
* </ul>
* </p>
* <p>
* <p>
* The encryptor's password may be set by configuring the below property:
* <ul>
* <li>nifi.sensitive.props.key</li>
* </ul>
* </p>
*/
@SuppressWarnings("deprecation")
public class StringEncryptor {
private static final Logger logger = LoggerFactory.getLogger(StringEncryptor.class);
private static final List<String> SUPPORTED_ALGORITHMS = new ArrayList<>();
private static final List<String> SUPPORTED_PROVIDERS = new ArrayList<>();
private static final String ARGON2_AES_GCM_256_ALGORITHM = "NIFI_ARGON2_AES_GCM_256";
private static final String ARGON2_AES_GCM_128_ALGORITHM = "NIFI_ARGON2_AES_GCM_128";
private static final List<String> CUSTOM_ALGORITHMS = Arrays.asList(ARGON2_AES_GCM_128_ALGORITHM, ARGON2_AES_GCM_256_ALGORITHM);
// Length of Argon2 encoded cost parameters + 22 B64 raw salt
public static final int CUSTOM_ALGORITHM_SALT_LENGTH = 53;
private static final int IV_LENGTH = 16;
private final String algorithm;
private final String provider;
private final PBEKeySpec password;
private SecretKeySpec key;
private static final String HEX_ENCODING = "HEX";
private static final String B64_ENCODING = "BASE64";
private String encoding = HEX_ENCODING;
private CipherProvider cipherProvider;
static {
Security.addProvider(new BouncyCastleProvider());
SUPPORTED_ALGORITHMS.addAll(CUSTOM_ALGORITHMS);
for (EncryptionMethod em : EncryptionMethod.values()) {
SUPPORTED_ALGORITHMS.add(em.getAlgorithm());
}
logger.debug("Supported encryption algorithms: " + StringUtils.join(SUPPORTED_ALGORITHMS, "\n"));
for (Provider provider : Security.getProviders()) {
SUPPORTED_PROVIDERS.add(provider.getName());
}
logger.debug("Supported providers: " + StringUtils.join(SUPPORTED_PROVIDERS, "\n"));
}
public static final String NF_SENSITIVE_PROPS_KEY = "nifi.sensitive.props.key";
public static final String NF_SENSITIVE_PROPS_ALGORITHM = "nifi.sensitive.props.algorithm";
public static final String NF_SENSITIVE_PROPS_PROVIDER = "nifi.sensitive.props.provider";
private static final String DEFAULT_SENSITIVE_PROPS_KEY = "nififtw!";
/**
* This constructor creates an encryptor using <em>Password-Based Encryption</em> (PBE). The <em>key</em> value is the direct value provided in <code>nifi.sensitive.props.key</code> in
* <code>nifi.properties</code>, which is a <em>PASSWORD</em> rather than a <em>KEY</em>, but is named such for backward/legacy logical compatibility throughout the rest of the codebase.
* <p>
* For actual raw key provision, see {@link #StringEncryptor(String, String, byte[])}.
*
* @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#getAlgorithm()})
* @param provider the JCA Security provider ({@link EncryptionMethod#getProvider()})
* @param key the UTF-8 characters from nifi.properties -- nifi.sensitive.props.key
*/
public StringEncryptor(final String algorithm, final String provider, final String key) {
this.algorithm = algorithm;
this.provider = provider;
this.key = null;
this.password = new PBEKeySpec(key == null
? DEFAULT_SENSITIVE_PROPS_KEY.toCharArray()
: key.toCharArray());
initialize();
}
/**
* This constructor creates an encryptor using <em>Keyed Encryption</em>. The <em>key</em> value is the raw byte value of a symmetric encryption key
* (usually expressed for human-readability/transmission in hexadecimal or Base64 encoded format).
*
* @param algorithm the PBE cipher algorithm ({@link EncryptionMethod#getAlgorithm()})
* @param provider the JCA Security provider ({@link EncryptionMethod#getProvider()})
* @param key a raw encryption key in bytes
*/
public StringEncryptor(final String algorithm, final String provider, final byte[] key) {
this.algorithm = algorithm;
this.provider = provider;
this.key = new SecretKeySpec(key, extractKeyTypeFromAlgorithm(algorithm));
this.password = null;
initialize();
}
/**
* A default constructor for mocking during testing.
*/
protected StringEncryptor() {
this.algorithm = null;
this.provider = null;
this.key = null;
this.password = null;
}
/**
* Extracts the cipher "family" (i.e. "AES", "DES", "RC4") from the full algorithm name.
*
* @param algorithm the algorithm ({@link EncryptionMethod#getAlgorithm()})
* @return the cipher family
* @throws EncryptionException if the algorithm is null/empty or not supported
*/
private String extractKeyTypeFromAlgorithm(String algorithm) throws EncryptionException {
if (StringUtils.isBlank(algorithm)) {
throw new EncryptionException("The algorithm cannot be null or empty");
}
String parsedCipher = CipherUtility.parseCipherFromAlgorithm(algorithm);
if (parsedCipher.equals(algorithm)) {
throw new EncryptionException("No supported algorithm detected");
} else {
return parsedCipher;
}
}
/**
* Creates an instance of the NiFi sensitive property encryptor.
*
* @param niFiProperties properties
* @return encryptor
* @throws EncryptionException if any issues arise initializing or
* validating the encryptor
* @see #createEncryptor(String, String, String)
* @deprecated as of NiFi 1.4.0 because the entire {@link NiFiProperties} object is not necessary to generate the encryptor.
*/
@Deprecated
public static StringEncryptor createEncryptor(final NiFiProperties niFiProperties) throws EncryptionException {
// Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
final String sensitivePropAlgorithmVal = niFiProperties.getProperty(NF_SENSITIVE_PROPS_ALGORITHM);
final String sensitivePropProviderVal = niFiProperties.getProperty(NF_SENSITIVE_PROPS_PROVIDER);
String sensitivePropValueNifiPropVar = niFiProperties.getProperty(NF_SENSITIVE_PROPS_KEY);
// TODO: This method should be removed in 2.0.0 and replaced globally with the String, String, String method
if (StringUtils.isBlank(sensitivePropValueNifiPropVar)) {
printBlankKeyWarning();
sensitivePropValueNifiPropVar = DEFAULT_SENSITIVE_PROPS_KEY;
}
return createEncryptor(sensitivePropAlgorithmVal, sensitivePropProviderVal, sensitivePropValueNifiPropVar);
}
/**
* Creates an instance of the NiFi sensitive property encryptor. If the password is blank, the default will be used and an error will be printed to the log.
*
* @param algorithm the encryption (and key derivation) algorithm ({@link EncryptionMethod#getAlgorithm()})
* @param provider the JCA Security provider ({@link EncryptionMethod#getProvider()})
* @param password the UTF-8 characters from nifi.properties -- nifi.sensitive.props.key
* @return the initialized encryptor
*/
public static StringEncryptor createEncryptor(String algorithm, String provider, String password) {
if (StringUtils.isBlank(algorithm)) {
throw new EncryptionException(NF_SENSITIVE_PROPS_ALGORITHM + " must be set");
}
if (StringUtils.isBlank(provider)) {
throw new EncryptionException(NF_SENSITIVE_PROPS_PROVIDER + " must be set");
}
// Can't throw an exception because users who have not populated a key expect fallback to default.
// TODO: This should be removed in 2.0.0 and replaced with strict enforcement of a explicit unique key
if (StringUtils.isBlank(password)) {
printBlankKeyWarning();
password = DEFAULT_SENSITIVE_PROPS_KEY;
}
return new StringEncryptor(algorithm, provider, password);
}
private static void printBlankKeyWarning() {
logger.error(StringUtils.repeat("*", 80));
logger.error(centerString("A blank sensitive properties key was provided"));
logger.error(centerString("Specify a unique key in nifi.properties"));
logger.error(centerString("for nifi.sensitive.props.key"));
logger.error(centerString(""));
logger.error(centerString("The Encrypt Config Tool in NiFi Toolkit can be used to"));
logger.error(centerString("migrate the flow to the new key"));
logger.error(StringUtils.repeat("*", 80));
}
private static String centerString(String msg) {
return "*" + StringUtils.center(msg, 78, " ") + "*";
}
protected void initialize() {
if (isInitialized()) {
logger.debug("Attempted to initialize an already-initialized StringEncryptor");
return;
}
if (paramsAreValid()) {
if (isCustomAlgorithm(algorithm)) {
// Handle the initialization for Argon2 + AES
// Perform the Argon2 key derivation once and store the key
Argon2SecureHasher argon2SecureHasher = new Argon2SecureHasher();
byte[] passwordBytes = new String(password.getPassword()).getBytes(StandardCharsets.UTF_8);
byte[] derivedKey = argon2SecureHasher.hashRaw(passwordBytes);
key = new SecretKeySpec(derivedKey, "AES");
// Use an AES keyed cipher provider to avoid derivation every time
cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE);
} else if (CipherUtility.isPBECipher(algorithm)) {
cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NIFI_LEGACY);
} else {
cipherProvider = CipherProviderFactory.getCipherProvider(KeyDerivationFunction.NONE);
}
} else {
throw new EncryptionException("Cannot initialize the StringEncryptor because some configuration values are invalid");
}
}
/**
* Returns {@code true} if the provided algorithm is considered a "custom" algorithm (a combination of KDF
* and cipher not present in {@link EncryptionMethod} and implemented specially for string encryption). Case-insensitive.
*
* @param algorithm the algorithm to evaluate
* @return true if present in {@link #CUSTOM_ALGORITHMS}
*/
public static boolean isCustomAlgorithm(String algorithm) {
return CUSTOM_ALGORITHMS.contains(algorithm.toUpperCase());
}
private boolean paramsAreValid() {
boolean algorithmAndProviderValid = algorithmIsValid(algorithm) && providerIsValid(provider);
boolean secretIsValid = false;
if (isCustomAlgorithm(algorithm)) {
// If this isn't valid, throw an exception directly to indicate the problem (minimum password length)
secretIsValid = customSecretIsValid(password, key, algorithm);
if (!secretIsValid) {
throw new EncryptionException("The nifi.sensitive.props.key password provided is invalid for algorithm " + algorithm + "; must be >= 12 characters");
}
} else if (CipherUtility.isPBECipher(algorithm)) {
secretIsValid = passwordIsValid(password);
} else if (CipherUtility.isKeyedCipher(algorithm)) {
secretIsValid = keyIsValid(key, algorithm);
}
return algorithmAndProviderValid && secretIsValid;
}
private boolean customSecretIsValid(PBEKeySpec password, SecretKeySpec key, String algorithm) {
// Currently, the only custom algorithms use AES-G/CM with a password via Argon2
String rawPassword = new String(password.getPassword());
return StringUtils.isNotBlank(rawPassword) && rawPassword.trim().length() >= 12;
}
private boolean keyIsValid(SecretKeySpec key, String algorithm) {
return key != null && CipherUtility.getValidKeyLengthsForAlgorithm(algorithm).contains(key.getEncoded().length * 8);
}
private boolean passwordIsValid(PBEKeySpec password) {
try {
return password.getPassword().length > 0;
} catch (IllegalStateException | NullPointerException e) {
return false;
}
}
public void setEncoding(String base) {
if (HEX_ENCODING.equalsIgnoreCase(base)) {
this.encoding = HEX_ENCODING;
} else if (B64_ENCODING.equalsIgnoreCase(base)) {
this.encoding = B64_ENCODING;
} else {
throw new IllegalArgumentException("The encoding base must be 'HEX' or 'BASE64'");
}
}
/**
* Encrypts the given clear text.
*
* @param clearText the message to encrypt
* @return the cipher text
* @throws EncryptionException if the encrypt fails
*/
public String encrypt(String clearText) throws EncryptionException {
try {
if (isInitialized()) {
byte[] rawBytes;
if (CipherUtility.isPBECipher(algorithm)) {
rawBytes = encryptPBE(clearText);
} else {
// Currently all custom algorithms are keyed (Argon2 KDF has already run in initialization)
rawBytes = encryptKeyed(clearText);
}
return encode(rawBytes);
} else {
throw new EncryptionException("The encryptor is not initialized");
}
} catch (final Exception e) {
throw new EncryptionException(e);
}
}
private byte[] encryptPBE(String plaintext) {
PBECipherProvider pbecp = (PBECipherProvider) cipherProvider;
final EncryptionMethod encryptionMethod = getEncryptionMethodForAlgorithm(algorithm);
// Generate salt
byte[] salt;
// NiFi legacy code determined the salt length based on the cipher block size
if (pbecp instanceof org.apache.nifi.security.util.crypto.NiFiLegacyCipherProvider) {
salt = ((org.apache.nifi.security.util.crypto.NiFiLegacyCipherProvider) pbecp).generateSalt(encryptionMethod);
} else {
salt = pbecp.generateSalt();
}
// Determine necessary key length
int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(algorithm);
// Generate cipher
try {
byte[] ivBytes = new byte[0];
Cipher cipher;
// Generate IV if necessary (allows for future use of Argon2, PBKDF2, Bcrypt, or Scrypt)
if (cipherProvider instanceof RandomIVPBECipherProvider) {
// Generating the IV here rather than delegating to the cipher provider suppresses the warning messages
ivBytes = new byte[IV_LENGTH];
new SecureRandom().nextBytes(ivBytes);
cipher = ((RandomIVPBECipherProvider) pbecp).getCipher(encryptionMethod, new String(password.getPassword()), salt, ivBytes, keyLength, true);
} else {
cipher = pbecp.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, true);
}
// Encrypt the plaintext
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// Combine the output
return CryptoUtils.concatByteArrays(salt, ivBytes, cipherBytes);
} catch (Exception e) {
throw new EncryptionException("Could not encrypt sensitive value", e);
}
}
private EncryptionMethod getEncryptionMethodForAlgorithm(String algorithm) {
if (isCustomAlgorithm(algorithm)) {
// We may add more implementations later, but currently all custom algorithms are AES-G/CM
return EncryptionMethod.AES_GCM;
} else {
return EncryptionMethod.forAlgorithm(algorithm);
}
}
private byte[] encryptKeyed(String plaintext) {
KeyedCipherProvider keyedcp = (KeyedCipherProvider) cipherProvider;
// Generate cipher
try {
SecureRandom sr = new SecureRandom();
byte[] iv = new byte[16];
sr.nextBytes(iv);
Cipher cipher = keyedcp.getCipher(getEncryptionMethodForAlgorithm(algorithm), key, iv, true);
// Encrypt the plaintext
byte[] cipherBytes = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// Combine the output
return CryptoUtils.concatByteArrays(iv, cipherBytes);
} catch (Exception e) {
throw new EncryptionException("Could not encrypt sensitive value", e);
}
}
private String encode(byte[] rawBytes) {
if (this.encoding.equalsIgnoreCase(HEX_ENCODING)) {
return Hex.encodeHexString(rawBytes);
} else {
return Base64.toBase64String(rawBytes);
}
}
/**
* Decrypts the given cipher text.
*
* @param cipherText the message to decrypt
* @return the clear text
* @throws EncryptionException if the decrypt fails
*/
public String decrypt(String cipherText) throws EncryptionException {
try {
if (isInitialized()) {
byte[] plainBytes;
byte[] cipherBytes = decode(cipherText);
if (CipherUtility.isPBECipher(algorithm)) {
plainBytes = decryptPBE(cipherBytes);
} else {
// Currently all custom algorithms are keyed (Argon2 KDF has already run in initialization)
plainBytes = decryptKeyed(cipherBytes);
}
return new String(plainBytes, StandardCharsets.UTF_8);
} else {
throw new EncryptionException("The encryptor is not initialized");
}
} catch (final Exception e) {
throw new EncryptionException(e);
}
}
private byte[] decryptPBE(byte[] cipherBytes) {
PBECipherProvider pbecp = (PBECipherProvider) cipherProvider;
final EncryptionMethod encryptionMethod = getEncryptionMethodForAlgorithm(algorithm);
// Extract salt
int saltLength = determineSaltLength(algorithm);
byte[] salt = new byte[saltLength];
System.arraycopy(cipherBytes, 0, salt, 0, saltLength);
// Read IV if necessary (allows for future use of Argon2, PBKDF2, Bcrypt, or Scrypt)
byte[] ivBytes = new byte[0];
int cipherBytesStart = saltLength;
if (pbecp instanceof RandomIVPBECipherProvider) {
ivBytes = new byte[16];
System.arraycopy(cipherBytes, saltLength, ivBytes, 0, ivBytes.length);
cipherBytesStart = saltLength + ivBytes.length;
}
byte[] actualCipherBytes = Arrays.copyOfRange(cipherBytes, cipherBytesStart, cipherBytes.length);
// Determine necessary key length
int keyLength = CipherUtility.parseKeyLengthFromAlgorithm(algorithm);
// Generate cipher
try {
Cipher cipher;
if (pbecp instanceof RandomIVPBECipherProvider) {
cipher = ((RandomIVPBECipherProvider) pbecp).getCipher(encryptionMethod, new String(password.getPassword()), salt, ivBytes, keyLength, false);
} else {
cipher = pbecp.getCipher(encryptionMethod, new String(password.getPassword()), salt, keyLength, false);
}
// Decrypt the plaintext
return cipher.doFinal(actualCipherBytes);
} catch (Exception e) {
throw new EncryptionException("Could not decrypt sensitive value", e);
}
}
private static int determineSaltLength(String algorithm) {
if (isCustomAlgorithm(algorithm)) {
return CUSTOM_ALGORITHM_SALT_LENGTH;
} else {
return CipherUtility.getSaltLengthForAlgorithm(algorithm);
}
}
private byte[] decryptKeyed(byte[] cipherBytes) {
KeyedCipherProvider keyedcp = (KeyedCipherProvider) cipherProvider;
// Generate cipher
try {
int ivLength = 16;
byte[] iv = new byte[ivLength];
System.arraycopy(cipherBytes, 0, iv, 0, ivLength);
byte[] actualCipherBytes = Arrays.copyOfRange(cipherBytes, ivLength, cipherBytes.length);
Cipher cipher = keyedcp.getCipher(getEncryptionMethodForAlgorithm(algorithm), key, iv, false);
// Encrypt the plaintext
return cipher.doFinal(actualCipherBytes);
} catch (Exception e) {
throw new EncryptionException("Could not decrypt sensitive value", e);
}
}
private byte[] decode(String encoded) throws DecoderException {
if (this.encoding.equalsIgnoreCase("HEX")) {
return Hex.decodeHex(encoded.toCharArray());
} else {
return Base64.decode(encoded);
}
}
public boolean isInitialized() {
return this.cipherProvider != null;
}
protected static boolean algorithmIsValid(String algorithm) {
return SUPPORTED_ALGORITHMS.contains(algorithm);
}
protected static boolean providerIsValid(String provider) {
return SUPPORTED_PROVIDERS.contains(provider);
}
/**
* Returns {@code true} if the two {@code StringEncryptor} objects are logically equivalent.
* This requires the same {@code algorithm}, {@code provider}, {@code encoding}, and
* {@code key}/{@code password}.
* <p>
* A {@code ciphertext} generated by one object can be decrypted by a separate object if they are equal as determined by this method.
*
* @param o the other StringEncryptor
* @return true if these instances are equal
*/
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StringEncryptor that = (StringEncryptor) o;
return Objects.equals(algorithm, that.algorithm)
&& Objects.equals(provider, that.provider)
&& Objects.equals(encoding, that.encoding)
&& secretsAreEqual(that.password, that.key);
}
/**
* Returns true if the provided {@code password} and {@code key} match those contained in this {@code StringEncryptor}. This method does <strong>not</strong> compare {@code password == key}.
* <p>
* Internally, uses {@link #isPBEKeySpecEqual(PBEKeySpec, PBEKeySpec)} and {@link SecretKeySpec#equals(Object)}.
*
* @param otherPassword the password {@link PBEKeySpec}
* @param otherKey the key {@link SecretKeySpec}
* @return true if the passwords match and the keys match
*/
private boolean secretsAreEqual(PBEKeySpec otherPassword, SecretKeySpec otherKey) {
// SecretKeySpec implements null-safe equals(), but PBEKeySpec does not
return isPBEKeySpecEqual(this.password, otherPassword) && Objects.equals(this.key, otherKey);
}
/**
* Returns true if the two {@link PBEKeySpec} objects are logically equivalent (same params and password).
*
* @param a a PBEKeySpec to compare
* @param b a PBEKeySpec to compare
* @return true if they can be used for encryption interchangeably
*/
private static boolean isPBEKeySpecEqual(PBEKeySpec a, PBEKeySpec b) {
if (a != null) {
if (b == null) {
return false;
} else {
// Compare all the accessors that will not throw exceptions
boolean nonNullsEqual = a.getIterationCount() == b.getIterationCount()
&& a.getKeyLength() == b.getKeyLength()
&& Arrays.equals(a.getSalt(), b.getSalt());
// Compare the passwords using constant-time equality while catching exceptions
boolean passwordsEqual;
try {
passwordsEqual = CryptoUtils.constantTimeEquals(a.getPassword(), b.getPassword());
} catch (IllegalStateException e) {
logger.warn("Encountered an error trying to compare password equality (one or more passwords have been cleared)");
// Assume any key spec with password cleared is unusable; return false
return false;
}
// Logging for debug assistance
if (logger.isDebugEnabled()) {
logger.debug("The PBEKeySpec objects have equal non-null elements ({}) and equal passwords ({})", new Object[]{String.valueOf(nonNullsEqual), String.valueOf(passwordsEqual)});
}
return nonNullsEqual && passwordsEqual;
}
} else {
// If here, a == null
return b == null;
}
}
/**
* Returns the hashcode of this object. Does not include {@code cipherProvider} in hashcode calculations.
*
* @return the hashcode
*/
@Override
public int hashCode() {
return Objects.hash(algorithm, provider, encoding, password, key);
}
/**
* Returns a String containing the {@code algorithm}, {@code provider}, {@code encoding}, and {@code cipherProvider} class name.
*
* @return a String representation of the object state
*/
@Override
public String toString() {
StringBuilder sb = new StringBuilder("StringEncryptor using ").append(algorithm)
.append(" from ").append(provider)
.append(" with ").append(encoding).append(" encoding and cipher provider ")
.append(cipherProvider.getClass().getName());
return sb.toString();
}
}