
com.cedarsoftware.util.EncryptionUtilities Maven / Gradle / Ivy
package com.cedarsoftware.util;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Arrays;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.NoSuchFileException;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
/**
* Utility class providing cryptographic operations including hashing, encryption, and decryption.
*
* This class offers:
*
*
* - Hash Functions:
*
* - MD5 (fast implementation)
* - SHA-1 (fast implementation)
* - SHA-256
* - SHA-384
* - SHA-512
* - SHA3-256
* - SHA3-512
* - Other variants like SHA-224 or SHA3-384 are available via
* {@link java.security.MessageDigest}
*
*
* - Encryption/Decryption:
*
* - AES-128 encryption
* - GCM mode with authentication
* - Random IV per encryption
*
*
* - Optimized File Operations:
*
* - Efficient buffer management
* - Large file handling
* - Custom filesystem support
*
*
*
*
* Hash Function Usage:
* {@code
* // File hashing
* String md5 = EncryptionUtilities.fastMD5(new File("example.txt"));
* String sha1 = EncryptionUtilities.fastSHA1(new File("example.txt"));
*
* // Byte array hashing
* String hash = EncryptionUtilities.calculateMD5Hash(bytes);
* }
*
* Encryption Usage:
* {@code
* // String encryption/decryption
* String encrypted = EncryptionUtilities.encrypt("password", "sensitive data");
* String decrypted = EncryptionUtilities.decrypt("password", encrypted);
*
* // Byte array encryption/decryption
* String encryptedHex = EncryptionUtilities.encryptBytes("password", originalBytes);
* byte[] decryptedBytes = EncryptionUtilities.decryptBytes("password", encryptedHex);
* }
*
* Security Notes:
*
* - MD5 and SHA-1 are provided for legacy compatibility but are cryptographically broken
* - Use SHA-256 or SHA-512 for secure hashing
* - AES implementation uses GCM mode with authentication
* - IV and salt are randomly generated for each encryption
*
*
* Performance Features:
*
* - Optimized buffer sizes for modern storage systems
* - Heap ByteBuffer usage for efficient memory management
* - Efficient memory management
* - Thread-safe implementation
*
*
* @author John DeRegnaucourt ([email protected])
*
* Copyright (c) Cedar Software LLC
*
* Licensed 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
*
* License
*
* 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.
*/
public class EncryptionUtilities {
private EncryptionUtilities() {
}
/**
* Calculates an MD5 hash of a file using optimized I/O operations.
*
* This implementation uses:
*
* - Heap ByteBuffer for efficient memory use
* - FileChannel for optimal file access
* - Fallback for non-standard filesystems
*
*
* @param file the file to hash
* @return hexadecimal string of the MD5 hash, or null if the file cannot be read
*/
public static String fastMD5(File file) {
try (InputStream in = Files.newInputStream(file.toPath())) {
if (in instanceof FileInputStream) {
return calculateFileHash(((FileInputStream) in).getChannel(), getMD5Digest());
}
// Fallback for non-file input streams (rare, but possible with custom filesystem providers)
return calculateStreamHash(in, getMD5Digest());
} catch (NoSuchFileException e) {
return null;
} catch (IOException e) {
throw new java.io.UncheckedIOException(e);
}
}
/**
* Calculates a hash from an InputStream using the specified MessageDigest.
*
* This implementation uses:
*
* - 64KB buffer optimized for modern storage systems
* - Matches OS and filesystem page sizes
* - Aligns with SSD block sizes
*
*
* @param in InputStream to read from
* @param digest MessageDigest to use for hashing
* @return hexadecimal string of the hash value
*/
private static String calculateStreamHash(InputStream in, MessageDigest digest) {
// 64KB buffer size - optimal for:
// 1. Modern OS page sizes
// 2. SSD block sizes
// 3. Filesystem block sizes
// 4. Memory usage vs. throughput balance
final int BUFFER_SIZE = 64 * 1024;
byte[] buffer = new byte[BUFFER_SIZE];
int read;
try {
while ((read = in.read(buffer)) != -1) {
digest.update(buffer, 0, read);
}
} catch (IOException e) {
ExceptionUtilities.uncheckedThrow(e);
}
return ByteUtilities.encode(digest.digest());
}
/**
* Calculates a SHA-1 hash of a file using optimized I/O operations.
*
* This implementation uses:
*
* - Heap ByteBuffer for efficient memory use
* - FileChannel for optimal file access
* - Fallback for non-standard filesystems
*
*
* @param file the file to hash
* @return hexadecimal string of the SHA-1 hash, or null if the file cannot be read
*/
public static String fastSHA1(File file) {
try (InputStream in = Files.newInputStream(file.toPath())) {
if (in instanceof FileInputStream) {
return calculateFileHash(((FileInputStream) in).getChannel(), getSHA1Digest());
}
// Fallback for non-file input streams (rare, but possible with custom filesystem providers)
return calculateStreamHash(in, getSHA1Digest());
} catch (NoSuchFileException e) {
return null;
} catch (IOException e) {
throw new java.io.UncheckedIOException(e);
}
}
/**
* Calculates a SHA-256 hash of a file using optimized I/O operations.
*
* This implementation uses:
*
* - Heap ByteBuffer for efficient memory use
* - FileChannel for optimal file access
* - Fallback for non-standard filesystems
*
*
* @param file the file to hash
* @return hexadecimal string of the SHA-256 hash, or null if the file cannot be read
*/
public static String fastSHA256(File file) {
try (InputStream in = Files.newInputStream(file.toPath())) {
if (in instanceof FileInputStream) {
return calculateFileHash(((FileInputStream) in).getChannel(), getSHA256Digest());
}
// Fallback for non-file input streams (rare, but possible with custom filesystem providers)
return calculateStreamHash(in, getSHA256Digest());
} catch (NoSuchFileException e) {
return null;
} catch (IOException e) {
throw new java.io.UncheckedIOException(e);
}
}
/**
* Calculates a SHA-384 hash of a file using optimized I/O operations.
*
* @param file the file to hash
* @return hexadecimal string of the SHA-384 hash, or null if the file cannot be read
*/
public static String fastSHA384(File file) {
try (InputStream in = Files.newInputStream(file.toPath())) {
if (in instanceof FileInputStream) {
return calculateFileHash(((FileInputStream) in).getChannel(), getSHA384Digest());
}
return calculateStreamHash(in, getSHA384Digest());
} catch (NoSuchFileException e) {
return null;
} catch (IOException e) {
throw new java.io.UncheckedIOException(e);
}
}
/**
* Calculates a SHA-512 hash of a file using optimized I/O operations.
*
* This implementation uses:
*
* - Heap ByteBuffer for efficient memory use
* - FileChannel for optimal file access
* - Fallback for non-standard filesystems
*
*
* @param file the file to hash
* @return hexadecimal string of the SHA-512 hash, or null if the file cannot be read
*/
public static String fastSHA512(File file) {
try (InputStream in = Files.newInputStream(file.toPath())) {
if (in instanceof FileInputStream) {
return calculateFileHash(((FileInputStream) in).getChannel(), getSHA512Digest());
}
// Fallback for non-file input streams (rare, but possible with custom filesystem providers)
return calculateStreamHash(in, getSHA512Digest());
} catch (IOException e) {
return null;
}
}
/**
* Calculates a SHA3-256 hash of a file using optimized I/O operations.
*
* @param file the file to hash
* @return hexadecimal string of the SHA3-256 hash, or null if the file cannot be read
*/
public static String fastSHA3_256(File file) {
try (InputStream in = Files.newInputStream(file.toPath())) {
if (in instanceof FileInputStream) {
return calculateFileHash(((FileInputStream) in).getChannel(), getSHA3_256Digest());
}
return calculateStreamHash(in, getSHA3_256Digest());
} catch (NoSuchFileException e) {
return null;
} catch (IOException e) {
throw new java.io.UncheckedIOException(e);
}
}
/**
* Calculates a SHA3-512 hash of a file using optimized I/O operations.
*
* @param file the file to hash
* @return hexadecimal string of the SHA3-512 hash, or null if the file cannot be read
*/
public static String fastSHA3_512(File file) {
try (InputStream in = Files.newInputStream(file.toPath())) {
if (in instanceof FileInputStream) {
return calculateFileHash(((FileInputStream) in).getChannel(), getSHA3_512Digest());
}
return calculateStreamHash(in, getSHA3_512Digest());
} catch (NoSuchFileException e) {
return null;
} catch (IOException e) {
throw new java.io.UncheckedIOException(e);
}
}
/**
* Calculates a hash of a file using the provided MessageDigest and FileChannel.
*
* This implementation uses:
*
* - 64KB buffer size optimized for modern storage systems
* - Heap ByteBuffer for efficient memory use
* - Efficient buffer management
*
*
* @param channel FileChannel to read from
* @param digest MessageDigest to use for hashing
* @return hexadecimal string of the hash value
* @throws IOException if an I/O error occurs (thrown as unchecked)
*/
public static String calculateFileHash(FileChannel channel, MessageDigest digest) {
// Modern OS/disk optimal transfer size (64KB)
// Matches common SSD page sizes and OS buffer sizes
final int BUFFER_SIZE = 64 * 1024;
// Heap buffer avoids expensive native allocations
// Reuse buffer to reduce garbage creation
ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE);
// Read until EOF
try {
while (channel.read(buffer) != -1) {
buffer.flip(); // Prepare buffer for reading
digest.update(buffer); // Update digest
buffer.clear(); // Prepare buffer for writing
}
return ByteUtilities.encode(digest.digest());
} catch (IOException e) {
ExceptionUtilities.uncheckedThrow(e);
return null; // unreachable
}
}
/**
* Calculates an MD5 hash of a byte array.
*
* @param bytes the data to hash
* @return hexadecimal string of the MD5 hash, or null if input is null
*/
public static String calculateMD5Hash(byte[] bytes) {
return calculateHash(getMD5Digest(), bytes);
}
/**
* Creates a MessageDigest instance for the specified algorithm.
*
* @param digest the name of the digest algorithm
* @return MessageDigest instance for the specified algorithm
* @throws IllegalArgumentException if the algorithm is not available
*/
public static MessageDigest getDigest(String digest) {
try {
return MessageDigest.getInstance(digest);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(String.format("The requested MessageDigest (%s) does not exist", digest), e);
}
}
/**
* Creates an MD5 MessageDigest instance.
*
* @return MessageDigest configured for MD5
* @throws IllegalArgumentException if MD5 algorithm is not available
*/
public static MessageDigest getMD5Digest() {
return getDigest("MD5");
}
/**
* Calculates a SHA-1 hash of a byte array.
*
* @param bytes the data to hash
* @return hexadecimal string of the SHA-1 hash, or null if input is null
*/
public static String calculateSHA1Hash(byte[] bytes) {
return calculateHash(getSHA1Digest(), bytes);
}
/**
* Creates a SHA-1 MessageDigest instance.
*
* @return MessageDigest configured for SHA-1
* @throws IllegalArgumentException if SHA-1 algorithm is not available
*/
public static MessageDigest getSHA1Digest() {
return getDigest("SHA-1");
}
/**
* Calculates a SHA-256 hash of a byte array.
*
* @param bytes the data to hash
* @return hexadecimal string of the SHA-256 hash, or null if input is null
*/
public static String calculateSHA256Hash(byte[] bytes) {
return calculateHash(getSHA256Digest(), bytes);
}
/**
* Creates a SHA-256 MessageDigest instance.
*
* @return MessageDigest configured for SHA-256
* @throws IllegalArgumentException if SHA-256 algorithm is not available
*/
public static MessageDigest getSHA256Digest() {
return getDigest("SHA-256");
}
/**
* Calculates a SHA-384 hash of a byte array.
*
* @param bytes the data to hash
* @return hexadecimal string of the SHA-384 hash, or null if input is null
*/
public static String calculateSHA384Hash(byte[] bytes) {
return calculateHash(getSHA384Digest(), bytes);
}
/**
* Creates a SHA-384 MessageDigest instance.
*
* @return MessageDigest configured for SHA-384
* @throws IllegalArgumentException if SHA-384 algorithm is not available
*/
public static MessageDigest getSHA384Digest() {
return getDigest("SHA-384");
}
/**
* Calculates a SHA-512 hash of a byte array.
*
* @param bytes the data to hash
* @return hexadecimal string of the SHA-512 hash, or null if input is null
*/
public static String calculateSHA512Hash(byte[] bytes) {
return calculateHash(getSHA512Digest(), bytes);
}
/**
* Creates a SHA-512 MessageDigest instance.
*
* @return MessageDigest configured for SHA-512
* @throws IllegalArgumentException if SHA-512 algorithm is not available
*/
public static MessageDigest getSHA512Digest() {
return getDigest("SHA-512");
}
/**
* Calculates a SHA3-256 hash of a byte array.
*
* @param bytes the data to hash
* @return hexadecimal string of the SHA3-256 hash, or null if input is null
*/
public static String calculateSHA3_256Hash(byte[] bytes) {
return calculateHash(getSHA3_256Digest(), bytes);
}
/**
* Creates a SHA3-256 MessageDigest instance.
*
* @return MessageDigest configured for SHA3-256
* @throws IllegalArgumentException if SHA3-256 algorithm is not available
*/
public static MessageDigest getSHA3_256Digest() {
return getDigest("SHA3-256");
}
/**
* Calculates a SHA3-512 hash of a byte array.
*
* @param bytes the data to hash
* @return hexadecimal string of the SHA3-512 hash, or null if input is null
*/
public static String calculateSHA3_512Hash(byte[] bytes) {
return calculateHash(getSHA3_512Digest(), bytes);
}
/**
* Creates a SHA3-512 MessageDigest instance.
*
* @return MessageDigest configured for SHA3-512
* @throws IllegalArgumentException if SHA3-512 algorithm is not available
*/
public static MessageDigest getSHA3_512Digest() {
return getDigest("SHA3-512");
}
/**
* Derives an AES key from a password and salt using PBKDF2.
*
* @param password the password
* @param salt random salt bytes
* @param bitsNeeded key length in bits
* @return derived key bytes
*/
public static byte[] deriveKey(String password, byte[] salt, int bitsNeeded) {
try {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password.toCharArray(), salt, 65536, bitsNeeded);
return factory.generateSecret(spec).getEncoded();
} catch (Exception e) {
throw new IllegalStateException("Unable to derive key", e);
}
}
/**
* Creates a byte array suitable for use as an AES key from a string password.
*
* The key is derived using MD5 and truncated to the specified bit length.
* This legacy method is retained for backward compatibility.
*
* @param key the password to derive the key from
* @param bitsNeeded the required key length in bits (typically 128, 192, or 256)
* @return byte array containing the derived key
* @deprecated Use {@link #deriveKey(String, byte[], int)} for stronger security
*/
public static byte[] createCipherBytes(String key, int bitsNeeded) {
String word = calculateMD5Hash(key.getBytes(StandardCharsets.UTF_8));
return word.substring(0, bitsNeeded / 8).getBytes(StandardCharsets.UTF_8);
}
/**
* Creates an AES cipher in encryption mode.
*
* @param key the encryption key
* @return Cipher configured for AES encryption
*/
@Deprecated
public static Cipher createAesEncryptionCipher(String key) {
return createAesCipher(key, Cipher.ENCRYPT_MODE);
}
/**
* Creates an AES cipher in decryption mode.
*
* @param key the decryption key
* @return Cipher configured for AES decryption
*/
@Deprecated
public static Cipher createAesDecryptionCipher(String key) {
return createAesCipher(key, Cipher.DECRYPT_MODE);
}
/**
* Creates an AES cipher with the specified mode.
*
* Uses CBC mode with PKCS5 padding and IV derived from the key.
*
* @param key the encryption/decryption key
* @param mode Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE
* @return configured Cipher instance
*/
@Deprecated
public static Cipher createAesCipher(String key, int mode) {
Key sKey = new SecretKeySpec(createCipherBytes(key, 128), "AES");
return createAesCipher(sKey, mode);
}
/**
* Creates an AES cipher with the specified key and mode.
*
* Uses CBC mode with PKCS5 padding and IV derived from the key.
*
* @param key SecretKeySpec for encryption/decryption
* @param mode Cipher.ENCRYPT_MODE or Cipher.DECRYPT_MODE
* @return configured Cipher instance
*/
@Deprecated
public static Cipher createAesCipher(Key key, int mode) {
// Use password key as seed for IV (must be 16 bytes)
MessageDigest d = getMD5Digest();
d.update(key.getEncoded());
byte[] iv = d.digest();
AlgorithmParameterSpec paramSpec = new IvParameterSpec(iv);
Cipher cipher = null; // CBC faster than CFB8/NoPadding (but file length changes)
try {
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
ExceptionUtilities.uncheckedThrow(e);
}
try {
cipher.init(mode, key, paramSpec);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
ExceptionUtilities.uncheckedThrow(e);
}
return cipher;
}
/**
* Encrypts a string using AES-128.
*
* @param key encryption key
* @param content string to encrypt
* @return hexadecimal string of encrypted data
* @throws IllegalStateException if encryption fails
*/
public static String encrypt(String key, String content) {
if (key == null || content == null) {
throw new IllegalArgumentException("key and content cannot be null");
}
try {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);
byte[] iv = new byte[12];
random.nextBytes(iv);
SecretKeySpec sKey = new SecretKeySpec(deriveKey(key, salt, 128), "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, sKey, new GCMParameterSpec(128, iv));
byte[] encrypted = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
byte[] out = new byte[1 + salt.length + iv.length + encrypted.length];
out[0] = 1; // version
System.arraycopy(salt, 0, out, 1, salt.length);
System.arraycopy(iv, 0, out, 1 + salt.length, iv.length);
System.arraycopy(encrypted, 0, out, 1 + salt.length + iv.length, encrypted.length);
return ByteUtilities.encode(out);
} catch (Exception e) {
throw new IllegalStateException("Error occurred encrypting data", e);
}
}
/**
* Encrypts a byte array using AES-128.
*
* @param key encryption key
* @param content bytes to encrypt
* @return hexadecimal string of encrypted data
* @throws IllegalStateException if encryption fails
*/
public static String encryptBytes(String key, byte[] content) {
if (key == null || content == null) {
throw new IllegalArgumentException("key and content cannot be null");
}
try {
SecureRandom random = new SecureRandom();
byte[] salt = new byte[16];
random.nextBytes(salt);
byte[] iv = new byte[12];
random.nextBytes(iv);
SecretKeySpec sKey = new SecretKeySpec(deriveKey(key, salt, 128), "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, sKey, new GCMParameterSpec(128, iv));
byte[] encrypted = cipher.doFinal(content);
byte[] out = new byte[1 + salt.length + iv.length + encrypted.length];
out[0] = 1;
System.arraycopy(salt, 0, out, 1, salt.length);
System.arraycopy(iv, 0, out, 1 + salt.length, iv.length);
System.arraycopy(encrypted, 0, out, 1 + salt.length + iv.length, encrypted.length);
return ByteUtilities.encode(out);
} catch (Exception e) {
throw new IllegalStateException("Error occurred encrypting data", e);
}
}
/**
* Decrypts a hexadecimal string of encrypted data to its original string form.
*
* @param key decryption key
* @param hexStr hexadecimal string of encrypted data
* @return decrypted string
* @throws IllegalStateException if decryption fails
*/
public static String decrypt(String key, String hexStr) {
if (key == null || hexStr == null) {
throw new IllegalArgumentException("key and hexStr cannot be null");
}
byte[] data = ByteUtilities.decode(hexStr);
if (data == null || data.length == 0) {
throw new IllegalArgumentException("Invalid hexadecimal input");
}
try {
if (data[0] == 1 && data.length > 29) {
byte[] salt = Arrays.copyOfRange(data, 1, 17);
byte[] iv = Arrays.copyOfRange(data, 17, 29);
byte[] cipherText = Arrays.copyOfRange(data, 29, data.length);
SecretKeySpec sKey = new SecretKeySpec(deriveKey(key, salt, 128), "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, sKey, new GCMParameterSpec(128, iv));
return new String(cipher.doFinal(cipherText), StandardCharsets.UTF_8);
}
return new String(createAesDecryptionCipher(key).doFinal(data), StandardCharsets.UTF_8);
} catch (Exception e) {
throw new IllegalStateException("Error occurred decrypting data", e);
}
}
/**
* Decrypts a hexadecimal string of encrypted data to its original byte array form.
*
* @param key decryption key
* @param hexStr hexadecimal string of encrypted data
* @return decrypted byte array
* @throws IllegalStateException if decryption fails
*/
public static byte[] decryptBytes(String key, String hexStr) {
if (key == null || hexStr == null) {
throw new IllegalArgumentException("key and hexStr cannot be null");
}
byte[] data = ByteUtilities.decode(hexStr);
if (data == null || data.length == 0) {
throw new IllegalArgumentException("Invalid hexadecimal input");
}
try {
if (data[0] == 1 && data.length > 29) {
byte[] salt = Arrays.copyOfRange(data, 1, 17);
byte[] iv = Arrays.copyOfRange(data, 17, 29);
byte[] cipherText = Arrays.copyOfRange(data, 29, data.length);
SecretKeySpec sKey = new SecretKeySpec(deriveKey(key, salt, 128), "AES");
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, sKey, new GCMParameterSpec(128, iv));
return cipher.doFinal(cipherText);
}
return createAesDecryptionCipher(key).doFinal(data);
} catch (Exception e) {
throw new IllegalStateException("Error occurred decrypting data", e);
}
}
/**
* Calculates a hash of a byte array using the specified MessageDigest.
*
* @param d MessageDigest to use
* @param bytes data to hash
* @return hexadecimal string of the hash value, or null if input is null
*/
public static String calculateHash(MessageDigest d, byte[] bytes) {
if (bytes == null) {
return null;
}
d.update(bytes);
return ByteUtilities.encode(d.digest());
}
}