+ * Thread-Safety: This class is NOT thread-safe. + * It extends {@link LinkedHashMap} without synchronization. If multiple threads access + * an instance concurrently, and at least one thread modifies the map structurally, + * it must be synchronized externally. + *
+ *+ * For thread-safe usage, wrap with {@link java.util.Collections#synchronizedMap(Map)}: + *
+ *+ * Map<K, V> syncMap = Collections.synchronizedMap(new LruHashMap<>(100)); + *+ *
+ * Alternatively, for high-concurrency scenarios, consider implementing a custom + * LRU cache using {@link java.util.concurrent.ConcurrentHashMap} with an eviction strategy. + *
* * @author koichik * @param+ * This class provides efficient encryption/decryption by pooling and reusing cipher instances, + * reducing the overhead of repeated cipher initialization. It supports both string-based keys + * and {@link Key} objects, with configurable algorithms and character encodings. + *
+ *+ * Key Features: + *
+ *+ * Security Considerations: + *
+ *+ * Usage Example: + *
+ *
+ * CachedCipher cipher = new CachedCipher();
+ * cipher.setKey("mySecretKey");
+ *
+ * // Encrypt text
+ * String encrypted = cipher.encryptText("Hello World");
+ *
+ * // Decrypt text
+ * String decrypted = cipher.decryptText(encrypted);
+ *
+ * // For AES encryption
+ * CachedCipher aesCipher = new CachedCipher();
+ * aesCipher.setAlgorithm("AES");
+ * aesCipher.setTransformation("AES");
+ * aesCipher.setKey("0123456789abcdef"); // 16-byte key for AES-128
+ *
+ *
+ * @author higa
*/
public class CachedCipher {
@@ -50,17 +94,22 @@ public CachedCipher() {
private static final String BLOWFISH = "Blowfish";
- private static final String RSA = "RSA";
+ private static final String AES = "AES";
/**
* The algorithm to use for the cipher.
+ * Default is Blowfish for backward compatibility.
*/
protected String algorithm = BLOWFISH;
/**
- * The transformation to use for the cipher.
+ * The transformation to use for the cipher when using Key objects.
+ * Default is Blowfish to match the algorithm default.
+ * + * Note: For better security, consider using "AES/GCM/NoPadding" with proper IV handling. + *
*/ - protected String transformation = RSA; + protected String transformation = BLOWFISH; /** * The key to use for encryption/decryption. @@ -89,7 +138,7 @@ public CachedCipher() { * the data to encrypt * @return the encrypted data */ - public byte[] encrypto(final byte[] data) { + public byte[] encrypt(final byte[] data) { final Cipher cipher = pollEncryptoCipher(); byte[] encrypted; try { @@ -104,6 +153,19 @@ public byte[] encrypto(final byte[] data) { return encrypted; } + /** + * Encrypts the given data. + * + * @param data + * the data to encrypt + * @return the encrypted data + * @deprecated Use {@link #encrypt(byte[])} instead. This method name contains a typo. + */ + @Deprecated + public byte[] encrypto(final byte[] data) { + return encrypt(data); + } + /** * Encrypts the given data with the specified key. * @@ -113,7 +175,7 @@ public byte[] encrypto(final byte[] data) { * the key to use for encryption * @return the encrypted data */ - public byte[] encrypto(final byte[] data, final Key key) { + public byte[] encrypt(final byte[] data, final Key key) { final Cipher cipher = pollEncryptoCipher(key); byte[] encrypted; try { @@ -128,6 +190,21 @@ public byte[] encrypto(final byte[] data, final Key key) { return encrypted; } + /** + * Encrypts the given data with the specified key. + * + * @param data + * the data to encrypt + * @param key + * the key to use for encryption + * @return the encrypted data + * @deprecated Use {@link #encrypt(byte[], Key)} instead. This method name contains a typo. + */ + @Deprecated + public byte[] encrypto(final byte[] data, final Key key) { + return encrypt(data, key); + } + /** * Encrypts the given text. * @@ -135,14 +212,27 @@ public byte[] encrypto(final byte[] data, final Key key) { * the text to encrypt * @return the encrypted text */ - public String encryptoText(final String text) { + public String encryptText(final String text) { try { - return Base64Util.encode(encrypto(text.getBytes(charsetName))); + return Base64Util.encode(encrypt(text.getBytes(charsetName))); } catch (final UnsupportedEncodingException e) { throw new UnsupportedEncodingRuntimeException(e); } } + /** + * Encrypts the given text. + * + * @param text + * the text to encrypt + * @return the encrypted text + * @deprecated Use {@link #encryptText(String)} instead. This method name contains a typo. + */ + @Deprecated + public String encryptoText(final String text) { + return encryptText(text); + } + /** * Decrypts the given data. * @@ -150,7 +240,7 @@ public String encryptoText(final String text) { * the data to decrypt * @return the decrypted data */ - public byte[] decrypto(final byte[] data) { + public byte[] decrypt(final byte[] data) { final Cipher cipher = pollDecryptoCipher(); byte[] decrypted; try { @@ -165,6 +255,19 @@ public byte[] decrypto(final byte[] data) { return decrypted; } + /** + * Decrypts the given data. + * + * @param data + * the data to decrypt + * @return the decrypted data + * @deprecated Use {@link #decrypt(byte[])} instead. This method name contains a typo. + */ + @Deprecated + public byte[] decrypto(final byte[] data) { + return decrypt(data); + } + /** * Decrypts the given data with the specified key. * @@ -174,7 +277,7 @@ public byte[] decrypto(final byte[] data) { * the key to use for decryption * @return the decrypted data */ - public byte[] decrypto(final byte[] data, final Key key) { + public byte[] decrypt(final byte[] data, final Key key) { final Cipher cipher = pollDecryptoCipher(key); byte[] decrypted; try { @@ -189,6 +292,21 @@ public byte[] decrypto(final byte[] data, final Key key) { return decrypted; } + /** + * Decrypts the given data with the specified key. + * + * @param data + * the data to decrypt + * @param key + * the key to use for decryption + * @return the decrypted data + * @deprecated Use {@link #decrypt(byte[], Key)} instead. This method name contains a typo. + */ + @Deprecated + public byte[] decrypto(final byte[] data, final Key key) { + return decrypt(data, key); + } + /** * Decrypts the given text. * @@ -196,14 +314,27 @@ public byte[] decrypto(final byte[] data, final Key key) { * the text to decrypt * @return the decrypted text */ - public String decryptoText(final String text) { + public String decryptText(final String text) { try { - return new String(decrypto(Base64Util.decode(text)), charsetName); + return new String(decrypt(Base64Util.decode(text)), charsetName); } catch (final UnsupportedEncodingException e) { throw new UnsupportedEncodingRuntimeException(e); } } + /** + * Decrypts the given text. + * + * @param text + * the text to decrypt + * @return the decrypted text + * @deprecated Use {@link #decryptText(String)} instead. This method name contains a typo. + */ + @Deprecated + public String decryptoText(final String text) { + return decryptText(text); + } + /** * Polls an encryption cipher from the queue, creating a new one if none are available. * @@ -212,8 +343,8 @@ public String decryptoText(final String text) { protected Cipher pollEncryptoCipher() { Cipher cipher = encryptoQueue.poll(); if (cipher == null) { - final SecretKeySpec sksSpec = new SecretKeySpec(key.getBytes(), algorithm); try { + final SecretKeySpec sksSpec = new SecretKeySpec(key.getBytes(charsetName), algorithm); cipher = Cipher.getInstance(algorithm); cipher.init(Cipher.ENCRYPT_MODE, sksSpec); } catch (final InvalidKeyException e) { @@ -222,6 +353,8 @@ protected Cipher pollEncryptoCipher() { throw new NoSuchAlgorithmRuntimeException(e); } catch (final NoSuchPaddingException e) { throw new NoSuchPaddingRuntimeException(e); + } catch (final UnsupportedEncodingException e) { + throw new UnsupportedEncodingRuntimeException(e); } } return cipher; @@ -269,8 +402,8 @@ protected void offerEncryptoCipher(final Cipher cipher) { protected Cipher pollDecryptoCipher() { Cipher cipher = decryptoQueue.poll(); if (cipher == null) { - final SecretKeySpec sksSpec = new SecretKeySpec(key.getBytes(), algorithm); try { + final SecretKeySpec sksSpec = new SecretKeySpec(key.getBytes(charsetName), algorithm); cipher = Cipher.getInstance(algorithm); cipher.init(Cipher.DECRYPT_MODE, sksSpec); } catch (final InvalidKeyException e) { @@ -279,6 +412,8 @@ protected Cipher pollDecryptoCipher() { throw new NoSuchAlgorithmRuntimeException(e); } catch (final NoSuchPaddingException e) { throw new NoSuchPaddingRuntimeException(e); + } catch (final UnsupportedEncodingException e) { + throw new UnsupportedEncodingRuntimeException(e); } } return cipher; diff --git a/src/main/java/org/codelibs/core/io/FileUtil.java b/src/main/java/org/codelibs/core/io/FileUtil.java index 83a3bac..1eb67e3 100644 --- a/src/main/java/org/codelibs/core/io/FileUtil.java +++ b/src/main/java/org/codelibs/core/io/FileUtil.java @@ -30,6 +30,7 @@ import java.nio.channels.FileChannel; import java.nio.charset.Charset; import java.nio.file.Files; +import java.nio.file.Path; import org.codelibs.core.exception.IORuntimeException; import org.codelibs.core.net.URLUtil; @@ -38,6 +39,11 @@ /** * Utility class for handling {@link File}. + *+ * SECURITY NOTE: When accepting file paths from untrusted sources, + * always validate them using {@link #isPathSafe(Path, Path)} to prevent path traversal attacks. + * Methods that accept path strings do not perform automatic validation to maintain backward compatibility. + *
* * @author higa */ @@ -58,6 +64,55 @@ protected FileUtil() { /** Max Buffer Size */ protected static final int MAX_BUF_SIZE = 10 * 1024 * 1024; // 10m + /** + * Validates that a given path is safe and does not attempt path traversal attacks. + *+ * This method checks if the resolved absolute path starts with the allowed base directory, + * preventing access to files outside the intended directory through path traversal + * techniques like "../../../etc/passwd". + *
+ *+ * Example usage: + *
+ *
+ * Path baseDir = Paths.get("/var/app/data");
+ * Path userPath = Paths.get(userInput);
+ * if (!FileUtil.isPathSafe(userPath, baseDir)) {
+ * throw new SecurityException("Path traversal attempt detected");
+ * }
+ *
+ *
+ * @param pathToCheck the path to validate (must not be {@literal null})
+ * @param baseDirectory the base directory that the path must be within (must not be {@literal null})
+ * @return true if the path is safe (within the base directory), false otherwise
+ */
+ public static boolean isPathSafe(final Path pathToCheck, final Path baseDirectory) {
+ assertArgumentNotNull("pathToCheck", pathToCheck);
+ assertArgumentNotNull("baseDirectory", baseDirectory);
+
+ final Path normalizedPath = pathToCheck.toAbsolutePath().normalize();
+ final Path normalizedBase = baseDirectory.toAbsolutePath().normalize();
+ return normalizedPath.startsWith(normalizedBase);
+ }
+
+ /**
+ * Validates that a given file is safe and does not attempt path traversal attacks.
+ * + * This is a convenience method that converts File objects to Path and calls + * {@link #isPathSafe(Path, Path)}. + *
+ * + * @param fileToCheck the file to validate (must not be {@literal null}) + * @param baseDirectory the base directory that the file must be within (must not be {@literal null}) + * @return true if the file is safe (within the base directory), false otherwise + */ + public static boolean isPathSafe(final File fileToCheck, final File baseDirectory) { + assertArgumentNotNull("fileToCheck", fileToCheck); + assertArgumentNotNull("baseDirectory", baseDirectory); + + return isPathSafe(fileToCheck.toPath(), baseDirectory.toPath()); + } + /** * Returns the canonical path string for this abstract pathname. * @@ -92,18 +147,50 @@ public static URL toURL(final File file) { /** * Reads the contents of a file into a byte array and returns it. + *+ * Note: This method loads the entire file into memory. + * For files larger than {@value #MAX_BUF_SIZE} bytes (10MB), an + * {@link IORuntimeException} will be thrown to prevent OutOfMemoryError. + * For large files, use streaming APIs instead. + *
* * @param file * The file. Must not be {@literal null}. * @return A byte array containing the contents of the file. + * @throws IORuntimeException if the file is larger than {@value #MAX_BUF_SIZE} bytes */ public static byte[] readBytes(final File file) { + return readBytes(file, MAX_BUF_SIZE); + } + + /** + * Reads the contents of a file into a byte array and returns it with a custom size limit. + *+ * Note: This method loads the entire file into memory. + * An {@link IORuntimeException} will be thrown if the file exceeds the specified maxSize + * to prevent OutOfMemoryError. For large files, use streaming APIs instead. + *
+ * + * @param file + * The file. Must not be {@literal null}. + * @param maxSize + * The maximum file size in bytes that can be read. + * @return A byte array containing the contents of the file. + * @throws IORuntimeException if the file is larger than maxSize bytes + */ + public static byte[] readBytes(final File file, final long maxSize) { assertArgumentNotNull("file", file); final FileInputStream is = InputStreamUtil.create(file); try { final FileChannel channel = is.getChannel(); - final ByteBuffer buffer = ByteBuffer.allocate((int) ChannelUtil.size(channel)); + final long fileSize = ChannelUtil.size(channel); + + if (fileSize > maxSize) { + throw new IORuntimeException(new IOException("File too large: " + fileSize + " bytes (max: " + maxSize + " bytes). Use streaming APIs for large files.")); + } + + final ByteBuffer buffer = ByteBuffer.allocate((int) fileSize); ChannelUtil.read(channel, buffer); return buffer.array(); } finally { @@ -230,10 +317,15 @@ protected static String read(final Reader reader, final int initialCapacity) { while ((len = reader.read(buf, size, bufferSize - size)) != -1) { size += len; if (size == bufferSize) { - final char[] newBuf = new char[bufferSize + initialCapacity]; + // Enforce MAX_BUF_SIZE to prevent unbounded memory growth + final int newBufferSize = bufferSize + initialCapacity; + if (newBufferSize > MAX_BUF_SIZE) { + throw new IORuntimeException(new IOException("Content too large: exceeds maximum buffer size of " + MAX_BUF_SIZE + " bytes. Use streaming APIs for large content.")); + } + final char[] newBuf = new char[newBufferSize]; System.arraycopy(buf, 0, newBuf, 0, bufferSize); buf = newBuf; - bufferSize += initialCapacity; + bufferSize = newBufferSize; } } return new String(buf, 0, size); diff --git a/src/main/java/org/codelibs/core/io/SerializeUtil.java b/src/main/java/org/codelibs/core/io/SerializeUtil.java index b144813..0748c57 100644 --- a/src/main/java/org/codelibs/core/io/SerializeUtil.java +++ b/src/main/java/org/codelibs/core/io/SerializeUtil.java @@ -21,14 +21,26 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.ObjectInputFilter; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.util.Set; import org.codelibs.core.exception.ClassNotFoundRuntimeException; import org.codelibs.core.exception.IORuntimeException; /** - * Utility for serializing objects. + * Utility for serializing objects with security protections. + *+ * This utility provides object serialization and deserialization with built-in + * security protections against deserialization attacks. By default, it uses an + * ObjectInputFilter to restrict which classes can be deserialized. + *
+ *+ * The default filter allows common safe classes like primitives, arrays, String, + * Number types, collections, and classes in the org.codelibs package. For custom + * requirements, use the overloaded methods that accept a custom filter. + *
* * @author higa */ @@ -42,6 +54,52 @@ protected SerializeUtil() { private static final int BYTE_ARRAY_SIZE = 8 * 1024; + /** + * Default set of allowed class name patterns for deserialization. + * This helps prevent deserialization attacks by restricting which classes can be instantiated. + */ + private static final Set+ * This method applies a default ObjectInputFilter to prevent deserialization attacks. + * Only classes matching the default allowed patterns can be deserialized. + *
* * @param bytes the byte array (must not be {@literal null}) * @return the deserialized object + * @throws IORuntimeException if an I/O error occurs or if a class is rejected by the filter + * @throws ClassNotFoundRuntimeException if the class of a serialized object cannot be found */ public static Object fromBinaryToObject(final byte[] bytes) { + return fromBinaryToObject(bytes, DEFAULT_FILTER); + } + + /** + * Converts a byte array to an object using a custom security filter. + *+ * This method allows you to specify a custom ObjectInputFilter for fine-grained + * control over which classes can be deserialized. Use this when the default filter + * is too restrictive or permissive for your use case. + *
+ * + * @param bytes the byte array (must not be {@literal null}) + * @param filter the ObjectInputFilter to use, or null to disable filtering + * @return the deserialized object + * @throws IORuntimeException if an I/O error occurs or if a class is rejected by the filter + * @throws ClassNotFoundRuntimeException if the class of a serialized object cannot be found + */ + public static Object fromBinaryToObject(final byte[] bytes, final ObjectInputFilter filter) { assertArgumentNotEmpty("bytes", bytes); try { final ByteArrayInputStream bais = new ByteArrayInputStream(bytes); final ObjectInputStream ois = new ObjectInputStream(bais); + if (filter != null) { + ois.setObjectInputFilter(filter); + } try { return ois.readObject(); } finally { @@ -102,4 +187,61 @@ public static Object fromBinaryToObject(final byte[] bytes) { } } + /** + * Creates a permissive filter that allows all classes to be deserialized. + *+ * WARNING: Use this only when you completely trust the data source and have + * other security measures in place. Unrestricted deserialization can lead to + * remote code execution vulnerabilities. + *
+ * + * @return an ObjectInputFilter that allows all classes + */ + public static ObjectInputFilter createPermissiveFilter() { + return filterInfo -> ObjectInputFilter.Status.ALLOWED; + } + + /** + * Creates a custom filter that allows only the specified class patterns. + *+ * Patterns can be exact class names or use wildcards with '*' at the end. + * For example: "com.example.*" allows all classes in the com.example package. + *
+ * + * @param allowedPatterns the patterns of classes to allow + * @return an ObjectInputFilter configured with the specified patterns + */ + public static ObjectInputFilter createCustomFilter(final Set- * This method uses internal JDK APIs and may not be available in all Java versions. - * If the internal API is not available, it falls back to the standard String constructor. + * Note: This method no longer uses internal JDK APIs for safety and compatibility. + * It now uses the standard String constructor. *
* * @param chars * the char array - * @return a new String - */ + * @return a new String, or null if the input is null + * @deprecated This method originally used internal JDK APIs (sun.misc.SharedSecrets) for + * performance optimization, but those APIs have been removed for safety and + * compatibility with modern Java versions. This method now simply calls + * {@code new String(chars)}, providing no additional value. + * Use the {@link String#String(char[])} constructor directly instead. + * This method will be removed in a future version. + */ + @Deprecated public static String newStringUnsafe(final char[] chars) { if (chars == null) { return null; } - if (newStringUnsafeMethod != null) { - try { - return (String) newStringUnsafeMethod.invoke(javaLangAccess, chars); - } catch (final Throwable t) { - // ignore - } - } return new String(chars); } } diff --git a/src/main/java/org/codelibs/core/misc/Base64Util.java b/src/main/java/org/codelibs/core/misc/Base64Util.java index dcf6805..9e8e150 100644 --- a/src/main/java/org/codelibs/core/misc/Base64Util.java +++ b/src/main/java/org/codelibs/core/misc/Base64Util.java @@ -15,11 +15,18 @@ */ package org.codelibs.core.misc; +import java.util.Base64; + import org.codelibs.core.collection.ArrayUtil; import org.codelibs.core.lang.StringUtil; /** * Utility class for handling Base64 encoding and decoding. + *+ * This class now uses the standard {@link java.util.Base64} implementation + * instead of a custom implementation, providing better security and performance. + * The API remains backward compatible with previous versions. + *
* * @author higa */ @@ -31,144 +38,37 @@ public abstract class Base64Util { protected Base64Util() { } - private static final char[] ENCODE_TABLE = { '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', '+', '/' }; - - private static final char PAD = '='; - - private static final byte[] DECODE_TABLE = new byte[128]; - static { - for (int i = 0; i < DECODE_TABLE.length; i++) { - DECODE_TABLE[i] = Byte.MAX_VALUE; - } - for (int i = 0; i < ENCODE_TABLE.length; i++) { - DECODE_TABLE[ENCODE_TABLE[i]] = (byte) i; - } - } - /** * Encodes data in Base64. + *+ * This method uses {@link java.util.Base64.Encoder} for encoding. + *
* * @param inData * The data to encode - * @return The encoded data + * @return The encoded data, or an empty string if the input is null or empty */ public static String encode(final byte[] inData) { if (ArrayUtil.isEmpty(inData)) { return ""; } - final int mod = inData.length % 3; - final int num = inData.length / 3; - char[] outData = null; - if (mod != 0) { - outData = new char[(num + 1) * 4]; - } else { - outData = new char[num * 4]; - } - for (int i = 0; i < num; i++) { - encode(inData, i * 3, outData, i * 4); - } - if (mod == 1) { - encode2pad(inData, num * 3, outData, num * 4); - } else if (mod == 2) { - encode1pad(inData, num * 3, outData, num * 4); - } - return new String(outData); + return Base64.getEncoder().encodeToString(inData); } /** * Decodes data encoded in Base64. + *+ * This method uses {@link java.util.Base64.Decoder} for decoding. + *
* * @param inData * The data to decode - * @return The decoded data + * @return The decoded data, or null if the input is null or empty */ public static byte[] decode(final String inData) { if (StringUtil.isEmpty(inData)) { return null; } - final int num = inData.length() / 4 - 1; - final int lastBytes = getLastBytes(inData); - final byte[] outData = new byte[num * 3 + lastBytes]; - for (int i = 0; i < num; i++) { - decode(inData, i * 4, outData, i * 3); - } - switch (lastBytes) { - case 1: - decode1byte(inData, num * 4, outData, num * 3); - break; - case 2: - decode2byte(inData, num * 4, outData, num * 3); - break; - default: - decode(inData, num * 4, outData, num * 3); - } - return outData; - } - - private static void encode(final byte[] inData, final int inIndex, final char[] outData, final int outIndex) { - - final int i = ((inData[inIndex] & 0xff) << 16) + ((inData[inIndex + 1] & 0xff) << 8) + (inData[inIndex + 2] & 0xff); - outData[outIndex] = ENCODE_TABLE[i >> 18]; - outData[outIndex + 1] = ENCODE_TABLE[i >> 12 & 0x3f]; - outData[outIndex + 2] = ENCODE_TABLE[i >> 6 & 0x3f]; - outData[outIndex + 3] = ENCODE_TABLE[i & 0x3f]; - } - - private static void encode2pad(final byte[] inData, final int inIndex, final char[] outData, final int outIndex) { - - final int i = inData[inIndex] & 0xff; - outData[outIndex] = ENCODE_TABLE[i >> 2]; - outData[outIndex + 1] = ENCODE_TABLE[i << 4 & 0x3f]; - outData[outIndex + 2] = PAD; - outData[outIndex + 3] = PAD; - } - - private static void encode1pad(final byte[] inData, final int inIndex, final char[] outData, final int outIndex) { - - final int i = ((inData[inIndex] & 0xff) << 8) + (inData[inIndex + 1] & 0xff); - outData[outIndex] = ENCODE_TABLE[i >> 10]; - outData[outIndex + 1] = ENCODE_TABLE[i >> 4 & 0x3f]; - outData[outIndex + 2] = ENCODE_TABLE[i << 2 & 0x3f]; - outData[outIndex + 3] = PAD; - } - - private static void decode(final String inData, final int inIndex, final byte[] outData, final int outIndex) { - - final byte b0 = DECODE_TABLE[inData.charAt(inIndex)]; - final byte b1 = DECODE_TABLE[inData.charAt(inIndex + 1)]; - final byte b2 = DECODE_TABLE[inData.charAt(inIndex + 2)]; - final byte b3 = DECODE_TABLE[inData.charAt(inIndex + 3)]; - outData[outIndex] = (byte) (b0 << 2 & 0xfc | b1 >> 4 & 0x3); - outData[outIndex + 1] = (byte) (b1 << 4 & 0xf0 | b2 >> 2 & 0xf); - outData[outIndex + 2] = (byte) (b2 << 6 & 0xc0 | b3 & 0x3f); - } - - private static void decode1byte(final String inData, final int inIndex, final byte[] outData, final int outIndex) { - - final byte b0 = DECODE_TABLE[inData.charAt(inIndex)]; - final byte b1 = DECODE_TABLE[inData.charAt(inIndex + 1)]; - outData[outIndex] = (byte) (b0 << 2 & 0xfc | b1 >> 4 & 0x3); - } - - private static void decode2byte(final String inData, final int inIndex, final byte[] outData, final int outIndex) { - - final byte b0 = DECODE_TABLE[inData.charAt(inIndex)]; - final byte b1 = DECODE_TABLE[inData.charAt(inIndex + 1)]; - final byte b2 = DECODE_TABLE[inData.charAt(inIndex + 2)]; - outData[outIndex] = (byte) (b0 << 2 & 0xfc | b1 >> 4 & 0x3); - outData[outIndex + 1] = (byte) (b1 << 4 & 0xf0 | b2 >> 2 & 0xf); - } - - private static int getLastBytes(final String inData) { - final int len = inData.length(); - if (inData.charAt(len - 2) == PAD) { - return 1; - } else if (inData.charAt(len - 1) == PAD) { - return 2; - } else { - return 3; - } + return Base64.getDecoder().decode(inData); } } diff --git a/src/test/java/org/codelibs/core/crypto/CachedCipherTest.java b/src/test/java/org/codelibs/core/crypto/CachedCipherTest.java new file mode 100644 index 0000000..e17c189 --- /dev/null +++ b/src/test/java/org/codelibs/core/crypto/CachedCipherTest.java @@ -0,0 +1,161 @@ +/* + * Copyright 2012-2025 CodeLibs Project and the Others. + * + * 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 + * + * 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.codelibs.core.crypto; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertThat; + +import org.junit.Test; + +/** + * Tests for {@link CachedCipher}. + */ +public class CachedCipherTest { + + @Test + public void testEncryptDecryptBytes() { + final CachedCipher cipher = new CachedCipher(); + cipher.setKey("mySecretKey"); + + final byte[] original = "Hello World".getBytes(); + final byte[] encrypted = cipher.encrypt(original); + final byte[] decrypted = cipher.decrypt(encrypted); + + assertThat(encrypted, is(not(original))); + assertArrayEquals(original, decrypted); + } + + @Test + public void testEncryptDecryptText() { + final CachedCipher cipher = new CachedCipher(); + cipher.setKey("mySecretKey"); + + final String original = "Hello World"; + final String encrypted = cipher.encryptText(original); + final String decrypted = cipher.decryptText(encrypted); + + assertThat(encrypted, is(not(original))); + assertThat(decrypted, is(original)); + } + + @Test + @SuppressWarnings("deprecation") + public void testDeprecatedEncryptoDecryptoBytes() { + final CachedCipher cipher = new CachedCipher(); + cipher.setKey("mySecretKey"); + + final byte[] original = "Hello World".getBytes(); + final byte[] encrypted = cipher.encrypto(original); + final byte[] decrypted = cipher.decrypto(encrypted); + + assertThat(encrypted, is(not(original))); + assertArrayEquals(original, decrypted); + } + + @Test + @SuppressWarnings("deprecation") + public void testDeprecatedEncryptoDecryptoText() { + final CachedCipher cipher = new CachedCipher(); + cipher.setKey("mySecretKey"); + + final String original = "Hello World"; + final String encrypted = cipher.encryptoText(original); + final String decrypted = cipher.decryptoText(encrypted); + + assertThat(encrypted, is(not(original))); + assertThat(decrypted, is(original)); + } + + @Test + public void testBackwardCompatibilityEncryptDecrypt() { + final CachedCipher cipher = new CachedCipher(); + cipher.setKey("mySecretKey"); + + final String original = "Hello World"; + + // Encrypt with new method, decrypt with old method + @SuppressWarnings("deprecation") + final String encrypted1 = cipher.encryptText(original); + @SuppressWarnings("deprecation") + final String decrypted1 = cipher.decryptoText(encrypted1); + assertThat(decrypted1, is(original)); + + // Encrypt with old method, decrypt with new method + @SuppressWarnings("deprecation") + final String encrypted2 = cipher.encryptoText(original); + final String decrypted2 = cipher.decryptText(encrypted2); + assertThat(decrypted2, is(original)); + } + + @Test + public void testDifferentKeys() { + final CachedCipher cipher1 = new CachedCipher(); + cipher1.setKey("key1"); + + final CachedCipher cipher2 = new CachedCipher(); + cipher2.setKey("key2"); + + final String original = "Hello World"; + final String encrypted1 = cipher1.encryptText(original); + final String encrypted2 = cipher2.encryptText(original); + + assertThat(encrypted1, is(not(encrypted2))); + assertThat(cipher1.decryptText(encrypted1), is(original)); + assertThat(cipher2.decryptText(encrypted2), is(original)); + } + + @Test + public void testEmptyString() { + final CachedCipher cipher = new CachedCipher(); + cipher.setKey("mySecretKey"); + + final String original = ""; + final String encrypted = cipher.encryptText(original); + final String decrypted = cipher.decryptText(encrypted); + + assertThat(decrypted, is(original)); + } + + @Test + public void testLongText() { + final CachedCipher cipher = new CachedCipher(); + cipher.setKey("mySecretKey"); + + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 1000; i++) { + sb.append("Long text for testing. "); + } + final String original = sb.toString(); + final String encrypted = cipher.encryptText(original); + final String decrypted = cipher.decryptText(encrypted); + + assertThat(decrypted, is(original)); + } + + @Test + public void testUnicodeText() { + final CachedCipher cipher = new CachedCipher(); + cipher.setKey("mySecretKey"); + + final String original = "Hello 世界 مرحبا мир"; + final String encrypted = cipher.encryptText(original); + final String decrypted = cipher.decryptText(encrypted); + + assertThat(decrypted, is(original)); + } +} diff --git a/src/test/java/org/codelibs/core/io/FileUtilTest.java b/src/test/java/org/codelibs/core/io/FileUtilTest.java index 9e7655a..6678091 100644 --- a/src/test/java/org/codelibs/core/io/FileUtilTest.java +++ b/src/test/java/org/codelibs/core/io/FileUtilTest.java @@ -17,13 +17,24 @@ import static org.codelibs.core.io.FileUtil.readBytes; import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import org.codelibs.core.exception.IORuntimeException; import org.codelibs.core.net.URLUtil; +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TemporaryFolder; /** * @author koichik @@ -35,6 +46,9 @@ public class FileUtilTest { File inputFile = URLUtil.toFile(url); + @Rule + public TemporaryFolder tempFolder = new TemporaryFolder(); + /** * @throws Exception */ @@ -52,6 +66,279 @@ public void testReadUTF8() throws Exception { assertThat(FileUtil.readUTF8(getPath("hoge_utf8.txt")), is("あ")); } + /** + * Test isPathSafe with safe path + * + * @throws Exception + */ + @Test + public void testIsPathSafe_SafePath() throws Exception { + final Path baseDir = tempFolder.getRoot().toPath(); + final Path safePath = baseDir.resolve("subdir/file.txt"); + + assertTrue("Safe path should be allowed", FileUtil.isPathSafe(safePath, baseDir)); + } + + /** + * Test isPathSafe with path traversal attempt + * + * @throws Exception + */ + @Test + public void testIsPathSafe_PathTraversalAttempt() throws Exception { + final Path baseDir = tempFolder.getRoot().toPath(); + final Path traversalPath = baseDir.resolve("../../../etc/passwd"); + + assertFalse("Path traversal should be blocked", FileUtil.isPathSafe(traversalPath, baseDir)); + } + + /** + * Test isPathSafe with File objects + * + * @throws Exception + */ + @Test + public void testIsPathSafe_WithFiles() throws Exception { + final File baseDir = tempFolder.getRoot(); + final File safeFile = new File(baseDir, "subdir/file.txt"); + final File unsafeFile = new File(baseDir, "../../../etc/passwd"); + + assertTrue("Safe file should be allowed", FileUtil.isPathSafe(safeFile, baseDir)); + assertFalse("Unsafe file should be blocked", FileUtil.isPathSafe(unsafeFile, baseDir)); + } + + /** + * Test isPathSafe with path inside base directory + * + * @throws Exception + */ + @Test + public void testIsPathSafe_PathInsideBase() throws Exception { + final Path baseDir = tempFolder.getRoot().toPath(); + final Path subDir = Files.createDirectory(baseDir.resolve("subdir")); + final Path file = Files.createFile(subDir.resolve("test.txt")); + + assertTrue("Path inside base should be allowed", FileUtil.isPathSafe(file, baseDir)); + } + + /** + * Test isPathSafe with path outside base directory + * + * @throws Exception + */ + @Test + public void testIsPathSafe_PathOutsideBase() throws Exception { + final Path baseDir = tempFolder.getRoot().toPath(); + final Path outsidePath = Paths.get("/tmp/outside.txt"); + + assertFalse("Path outside base should be blocked", FileUtil.isPathSafe(outsidePath, baseDir)); + } + + /** + * Test isPathSafe with same path as base + * + * @throws Exception + */ + @Test + public void testIsPathSafe_SameAsBase() throws Exception { + final Path baseDir = tempFolder.getRoot().toPath(); + + assertTrue("Base directory itself should be allowed", FileUtil.isPathSafe(baseDir, baseDir)); + } + + /** + * Test readBytes with large file throws exception + * + * @throws Exception + */ + @Test + public void testReadBytes_LargeFile() throws Exception { + final File largeFile = tempFolder.newFile("large.dat"); + + // Create a file larger than MAX_BUF_SIZE (10MB) + // Write 11MB of data + try (FileOutputStream fos = new FileOutputStream(largeFile)) { + final byte[] chunk = new byte[1024 * 1024]; // 1MB + for (int i = 0; i < 11; i++) { // Write 11MB + fos.write(chunk); + } + } + + try { + FileUtil.readBytes(largeFile); + fail("Expected IORuntimeException for large file"); + } catch (final IORuntimeException e) { + assertTrue("Error message should mention file size", e.getMessage().contains("File too large")); + } + } + + /** + * Test readBytes with file within size limit + * + * @throws Exception + */ + @Test + public void testReadBytes_NormalFile() throws Exception { + final File normalFile = tempFolder.newFile("normal.txt"); + final String content = "Test content for normal file"; + + try (FileOutputStream fos = new FileOutputStream(normalFile)) { + fos.write(content.getBytes("UTF-8")); + } + + final byte[] result = FileUtil.readBytes(normalFile); + assertThat(new String(result, "UTF-8"), is(content)); + } + + /** + * Test readBytes with empty file + * + * @throws Exception + */ + @Test + public void testReadBytes_EmptyFile() throws Exception { + final File emptyFile = tempFolder.newFile("empty.txt"); + + final byte[] result = FileUtil.readBytes(emptyFile); + assertThat(result.length, is(0)); + } + + /** + * Test readBytes with null file throws exception + */ + @Test + public void testReadBytes_NullFile() { + try { + FileUtil.readBytes(null); + fail("Expected IllegalArgumentException"); + } catch (final IllegalArgumentException e) { + // Expected + } + } + + /** + * Test isPathSafe with null path throws exception + */ + @Test + public void testIsPathSafe_NullPath() { + final Path baseDir = tempFolder.getRoot().toPath(); + try { + FileUtil.isPathSafe((Path) null, baseDir); + fail("Expected IllegalArgumentException"); + } catch (final IllegalArgumentException e) { + // Expected + } + } + + /** + * Test isPathSafe with null base directory throws exception + */ + @Test + public void testIsPathSafe_NullBase() { + final Path path = Paths.get("/tmp/test.txt"); + try { + FileUtil.isPathSafe(path, null); + fail("Expected IllegalArgumentException"); + } catch (final IllegalArgumentException e) { + // Expected + } + } + + /** + * Test readBytes with custom maxSize - file within limit + * + * @throws Exception + */ + @Test + public void testReadBytes_CustomMaxSize_WithinLimit() throws Exception { + final File file = tempFolder.newFile("small.txt"); + final String content = "Small content"; + final FileOutputStream out = new FileOutputStream(file); + try { + out.write(content.getBytes("UTF-8")); + } finally { + out.close(); + } + + // Set maxSize larger than file size + final byte[] bytes = FileUtil.readBytes(file, 1024); + assertThat(new String(bytes, "UTF-8"), is(content)); + } + + /** + * Test readBytes with custom maxSize - file exceeds limit + * + * @throws Exception + */ + @Test(expected = IORuntimeException.class) + public void testReadBytes_CustomMaxSize_ExceedsLimit() throws Exception { + final File file = tempFolder.newFile("medium.txt"); + final byte[] data = new byte[1024]; // 1KB + final FileOutputStream out = new FileOutputStream(file); + try { + out.write(data); + } finally { + out.close(); + } + + // Set maxSize smaller than file size (should throw exception) + FileUtil.readBytes(file, 512); + } + + /** + * Test readBytes with custom maxSize of zero + * + * @throws Exception + */ + @Test(expected = IORuntimeException.class) + public void testReadBytes_CustomMaxSize_Zero() throws Exception { + final File file = tempFolder.newFile("any.txt"); + final FileOutputStream out = new FileOutputStream(file); + try { + out.write("content".getBytes("UTF-8")); + } finally { + out.close(); + } + + // Set maxSize to zero (should throw exception for any non-empty file) + FileUtil.readBytes(file, 0); + } + + /** + * Test readBytes with custom maxSize for empty file + * + * @throws Exception + */ + @Test + public void testReadBytes_CustomMaxSize_EmptyFile() throws Exception { + final File file = tempFolder.newFile("empty.txt"); + + // Empty file should work with any maxSize including 0 + final byte[] bytes = FileUtil.readBytes(file, 0); + assertThat(bytes.length, is(0)); + } + + /** + * Test readBytes with very large custom maxSize + * + * @throws Exception + */ + @Test + public void testReadBytes_CustomMaxSize_VeryLarge() throws Exception { + final File file = tempFolder.newFile("test.txt"); + final String content = "Test content"; + final FileOutputStream out = new FileOutputStream(file); + try { + out.write(content.getBytes("UTF-8")); + } finally { + out.close(); + } + + // Set maxSize very large + final byte[] bytes = FileUtil.readBytes(file, Long.MAX_VALUE); + assertThat(new String(bytes, "UTF-8"), is(content)); + } + private String getPath(final String fileName) { return getClass().getName().replace('.', '/').replaceFirst(getClass().getSimpleName(), fileName); } diff --git a/src/test/java/org/codelibs/core/io/SerializeUtilTest.java b/src/test/java/org/codelibs/core/io/SerializeUtilTest.java index c9cb251..d9fc5d4 100644 --- a/src/test/java/org/codelibs/core/io/SerializeUtilTest.java +++ b/src/test/java/org/codelibs/core/io/SerializeUtilTest.java @@ -15,6 +15,17 @@ */ package org.codelibs.core.io; +import java.io.ObjectInputFilter; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.codelibs.core.exception.IORuntimeException; + import junit.framework.TestCase; /** @@ -42,4 +53,171 @@ public void testObjectAndBinary() throws Exception { final byte[] binary = SerializeUtil.fromObjectToBinary(o); assertEquals(o, SerializeUtil.fromBinaryToObject(binary)); } + + /** + * Test default filter allows common safe classes + * + * @throws Exception + */ + public void testFromBinaryToObject_DefaultFilter_AllowsSafeClasses() throws Exception { + // Test String + final String str = "test string"; + byte[] binary = SerializeUtil.fromObjectToBinary(str); + assertEquals(str, SerializeUtil.fromBinaryToObject(binary)); + + // Test Integer + final Integer num = 42; + binary = SerializeUtil.fromObjectToBinary(num); + assertEquals(num, SerializeUtil.fromBinaryToObject(binary)); + + // Test String array + final String[] arr = new String[] { "a", "b", "c" }; + binary = SerializeUtil.fromObjectToBinary(arr); + final String[] result = (String[]) SerializeUtil.fromBinaryToObject(binary); + assertEquals(arr.length, result.length); + assertEquals(arr[0], result[0]); + + // Test ArrayList + final List