diff --git a/pom.xml b/pom.xml index 0fc3426..ad3681b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ top.gtb520.java.gnu gnu_java_utils_library - 1.3-SNAPSHOT + 1.4-SNAPSHOT 25 diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/main.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/main.java index 565ffe9..f23959c 100644 --- a/src/main/java/top/gtb520/java/gnu/java/utils/library/main.java +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/main.java @@ -2,6 +2,13 @@ package top.gtb520.java.gnu.java.utils.library; import top.gtb520.java.gnu.java.utils.library.utils.print.ColorNameEnum; +import java.io.File; +import java.nio.file.Path; + +import static top.gtb520.java.gnu.java.utils.library.utils.file.CreateFile.BinFileType; +import static top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption.EncryptionType; + + public interface main { // 压缩包操作相关接口与方法 interface compress { @@ -127,4 +134,31 @@ public interface main { } } } + + // 文件操作接口与方法 + interface file { + interface CreateFile { + // 创建文件,创建空白文件,异步IO + static File NewFile(String FileName, boolean isBin, BinFileType binFileType) { + return top.gtb520.java.gnu.java.utils.library.utils.file.CreateFile.NewFile(FileName, isBin, binFileType); + } + + // 以字节流方式保存文件,异步IO + static boolean StreamSaveFile(File file, Path TargePath, String FileName) { + return top.gtb520.java.gnu.java.utils.library.utils.file.CreateFile.StreamSaveFile(file, TargePath, FileName); + } + } + } + + // 文件加解密操作 + interface EncryptionAndDecryption { + // 文件加密 + static File FileEncrypt(File file, EncryptionType type, Path keyPath) throws Exception { + return top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption.EncryptionFile(file, type, keyPath); + } + // 文件解密 + static File DecryptionFile(File file, EncryptionType type, File keyfile) throws Exception { + return top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption.DecryptionFile(file, type, keyfile); + } + } } diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/CreateFile.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/CreateFile.java new file mode 100644 index 0000000..790c192 --- /dev/null +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/CreateFile.java @@ -0,0 +1,89 @@ +package top.gtb520.java.gnu.java.utils.library.utils.file; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.concurrent.CompletableFuture; + +// 创建文件 +public class CreateFile { + /*** + * 创建文件,创建空白文件 + * isBin为True时创建一个空白的二进制文件,否则创建一个空白的普通文件 + * 异步IO操作 + * + * @param FileName 文件名 + * @param isBin 是否二进制文件 + * @param binFileType 二进制文件类型,如果是普通文件则不需要设置次值,将其设置为null即可, + * 参考isBin参数,isBin参数为False则此参数设置为null + * + * @return 文件 + */ + public static File NewFile(String FileName, boolean isBin, BinFileType binFileType) { + File TargetFile = null; + byte[] BufferByte = new byte[1024]; + + try { + TargetFile = new File(FileName); + if (TargetFile.createNewFile()) { + if (isBin && binFileType != null) { + // 根据二进制文件类型写入文件头标识 + byte[] header = switch (binFileType) { + case ELF -> new byte[]{0x7F, 'E', 'L', 'F'}; + case PE -> new byte[]{'M', 'Z'}; + }; + try (OutputStream os = Files.newOutputStream(TargetFile.toPath(), + StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { + os.write(header); + os.flush(); + } + } + } + } catch (IOException e) { + TargetFile = null; + } + + return TargetFile; + } + + /*** + * 以字节流方式保存文件 + * 异步IO操作 + * + * @param file 文件 + * @param TargePath 目标路径 + * @param FileName 文件名 + * + * @return 是否保存成功,True为保存成功,False为保存失败 + */ + public static boolean StreamSaveFile(File file, Path TargePath, String FileName) { + try { + Path targetFile = TargePath.resolve(FileName); + return CompletableFuture.supplyAsync(() -> { + try (OutputStream os = Files.newOutputStream(targetFile, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + byte[] buffer = new byte[1024]; + int bytesRead; + try (var is = Files.newInputStream(file.toPath())) { + while ((bytesRead = is.read(buffer)) != -1) { + os.write(buffer, 0, bytesRead); + } + } + os.flush(); + return true; + } catch (IOException e) { + return false; + } + }).get(); + } catch (Exception e) { + return false; + } + } + + public enum BinFileType { + ELF, PE; + } +} diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/EncryptionAndDecryption.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/EncryptionAndDecryption.java new file mode 100644 index 0000000..e1f0c3a --- /dev/null +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/utils/file/EncryptionAndDecryption.java @@ -0,0 +1,202 @@ +package top.gtb520.java.gnu.java.utils.library.utils.file; + +import java.io.File; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.concurrent.CompletableFuture; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.KeyGenerator; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; + +public class EncryptionAndDecryption { + /*** + * 文件加密 + * 异步执行加密操作 + * 加密后覆盖原文件,不添加额外后缀 + * 密钥保存到 keyPath 目录下,密钥名为 原文件名.key + * + * @param file 文件 + * @param type 加密类型 + * @param keyPath 密钥文件目录路径,密钥名为 文件名.key + * @return 加密后的文件(原文件),失败时返回 null + */ + public static File EncryptionFile(File file, EncryptionType type, Path keyPath) { + File TargetFile = null; + + if (file == null || !file.exists() || !file.isFile()) { + return null; + } + if (keyPath == null) { + return null; + } + + try { + TargetFile = CompletableFuture.supplyAsync(() -> { + try { + // 读取原文件内容 + byte[] plainBytes = Files.readAllBytes(file.toPath()); + + // 根据加密类型生成对称密钥 + SecretKey secretKey = generateSecretKey(type); + byte[] encryptedBytes = encryptBytes(plainBytes, secretKey, type); + + // 将密钥写入 keyPath 目录,密钥名为 文件名.key + Files.createDirectories(keyPath); + String keyFileName = file.getName() + ".key"; + Path keyFilePath = keyPath.resolve(keyFileName); + try (OutputStream kos = Files.newOutputStream(keyFilePath, + StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) { + kos.write(secretKey.getEncoded()); + kos.flush(); + } + + // 加密后覆盖写回原文件 + try (OutputStream os = Files.newOutputStream(file.toPath(), + StandardOpenOption.TRUNCATE_EXISTING)) { + os.write(encryptedBytes); + os.flush(); + } + return file; + } catch (Exception e) { + return null; + } + }).get(); + } catch (Exception e) { + TargetFile = null; + } + + return TargetFile; + } + + /*** + * 生成对称密钥 + * + * @param type 加密类型 + * @return 对称密钥 + */ + private static SecretKey generateSecretKey(EncryptionType type) throws NoSuchAlgorithmException { + String algorithm = switch (type) { + case AES -> "AES"; + case DES -> "DES"; + case RSA -> "AES"; // RSA 为非对称加密,此处用 AES 做对称加密替代 + }; + KeyGenerator keyGen = KeyGenerator.getInstance(algorithm); + int keySize = switch (type) { + case AES -> 128; + case DES -> 56; + case RSA -> 128; + }; + keyGen.init(keySize); + return keyGen.generateKey(); + } + + /*** + * 对字节数据进行加密 + * + * @param data 原始数据 + * @param key 密钥 + * @param type 加密类型 + * @return 加密后的数据 + */ + private static byte[] encryptBytes(byte[] data, SecretKey key, EncryptionType type) + throws NoSuchAlgorithmException, NoSuchPaddingException, + InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + String algorithm = switch (type) { + case AES -> "AES/ECB/PKCS5Padding"; + case DES -> "DES/ECB/PKCS5Padding"; + case RSA -> "AES/ECB/PKCS5Padding"; + }; + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.ENCRYPT_MODE, key); + return cipher.doFinal(data); + } + + /*** + * 文件解密 + * 异步执行解密操作 + * 解密后覆盖写回原文件,不添加额外后缀 + * + * @param file 加密文件 + * @param type 加密类型,须与加密时一致 + * @param keyfile 密钥文件,从中读取解密密钥 + * @return 解密后的文件(原文件),失败时返回 null + */ + public static File DecryptionFile(File file, EncryptionType type, File keyfile) { + File TargetFile = null; + + if (file == null || !file.exists() || !file.isFile()) { + return null; + } + if (keyfile == null || !keyfile.exists() || !keyfile.isFile()) { + return null; + } + + try { + TargetFile = CompletableFuture.supplyAsync(() -> { + try { + // 从密钥文件读取密钥 + byte[] keyBytes = Files.readAllBytes(keyfile.toPath()); + String algorithm = switch (type) { + case AES -> "AES"; + case DES -> "DES"; + case RSA -> "AES"; + }; + SecretKey secretKey = new javax.crypto.spec.SecretKeySpec(keyBytes, algorithm); + + // 读取加密文件内容 + byte[] encryptedBytes = Files.readAllBytes(file.toPath()); + + // 解密 + byte[] decryptedBytes = decryptBytes(encryptedBytes, secretKey, type); + + // 解密后覆盖写回原文件 + try (OutputStream os = Files.newOutputStream(file.toPath(), + StandardOpenOption.TRUNCATE_EXISTING)) { + os.write(decryptedBytes); + os.flush(); + } + return file; + } catch (Exception e) { + return null; + } + }).get(); + } catch (Exception e) { + TargetFile = null; + } + + return TargetFile; + } + + /*** + * 对字节数据进行解密 + * + * @param data 加密数据 + * @param key 密钥 + * @param type 加密类型 + * @return 解密后的数据 + */ + private static byte[] decryptBytes(byte[] data, SecretKey key, EncryptionType type) + throws NoSuchAlgorithmException, NoSuchPaddingException, + InvalidKeyException, IllegalBlockSizeException, BadPaddingException { + String algorithm = switch (type) { + case AES -> "AES/ECB/PKCS5Padding"; + case DES -> "DES/ECB/PKCS5Padding"; + case RSA -> "AES/ECB/PKCS5Padding"; + }; + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.DECRYPT_MODE, key); + return cipher.doFinal(data); + } + + public enum EncryptionType { + AES, DES, RSA; + } +} diff --git a/src/main/java/top/gtb520/java/gnu/java/utils/library/version.java b/src/main/java/top/gtb520/java/gnu/java/utils/library/version.java index 1bdfef8..2e79b66 100644 --- a/src/main/java/top/gtb520/java/gnu/java/utils/library/version.java +++ b/src/main/java/top/gtb520/java/gnu/java/utils/library/version.java @@ -2,7 +2,7 @@ package top.gtb520.java.gnu.java.utils.library; public class version { // 项目的版本信息,不代表实时运行环境 - public static final String VERSION = "1.3"; + public static final String VERSION = "1.4"; public static final String JAVA_VERSION = "25"; public static final String OpenJDK_VERSION = "25.0.0.0.1"; public static final String OpenJDK_Runtime_Environment = "25.0.0.0.1+36-GA"; diff --git a/src/test/java/EncryptionAndDecryptionTest.java b/src/test/java/EncryptionAndDecryptionTest.java new file mode 100644 index 0000000..bed7479 --- /dev/null +++ b/src/test/java/EncryptionAndDecryptionTest.java @@ -0,0 +1,206 @@ +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; + +import top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption; +import top.gtb520.java.gnu.java.utils.library.utils.file.EncryptionAndDecryption.EncryptionType; + +public class EncryptionAndDecryptionTest { + + static int passCount = 0; + static int failCount = 0; + + public static void main(String[] args) throws Exception { + System.out.println("========== EncryptionAndDecryption 测试开始 =========="); + + // 正常场景:AES 加密/解密 + testEncryptDecryptAES(); + // 正常场景:DES 加密/解密 + testEncryptDecryptDES(); + // 正常场景:RSA 加密/解密 + testEncryptDecryptRSA(); + // 正常场景:验证密钥文件生成 + testKeyFileGenerated(); + // 正常场景:加密后文件内容发生变化 + testFileContentChangedAfterEncrypt(); + + // 异常场景:file 为 null + testNullFile(); + // 异常场景:keyPath 为 null + testNullKeyPath(); + // 异常场景:keyfile 为 null(解密) + testNullKeyFile(); + // 异常场景:文件不存在 + testFileNotExist(); + // 异常场景:密钥文件不存在(解密) + testKeyFileNotExist(); + + System.out.println("========== 测试结果 =========="); + System.out.println("通过: " + passCount); + System.out.println("失败: " + failCount); + System.out.println("总计: " + (passCount + failCount)); + + if (failCount > 0) { + System.out.println("存在失败的测试用例!"); + System.exit(1); + } else { + System.out.println("全部测试通过!"); + } + } + + /** + * 创建临时测试文件并写入测试内容 + */ + static File createTempFile(String prefix) throws Exception { + Path tempDir = Files.createTempDirectory("enc_test_"); + File tempFile = tempDir.resolve(prefix + ".txt").toFile(); + Files.writeString(tempFile.toPath(), "Hello, EncryptionAndDecryption Test! 测试内容123"); + return tempFile; + } + + /** + * 创建临时密钥目录 + */ + static Path createTempKeyPath() throws Exception { + return Files.createTempDirectory("enc_key_"); + } + + static void assertEquals(Object expected, Object actual, String testName) { + if (expected == null && actual == null) { + passCount++; + System.out.println("[PASS] " + testName); + } else if (expected != null && expected.equals(actual)) { + passCount++; + System.out.println("[PASS] " + testName); + } else { + failCount++; + System.out.println("[FAIL] " + testName + " | 期望: " + expected + ", 实际: " + actual); + } + } + + static void assertTrue(boolean condition, String testName) { + if (condition) { + passCount++; + System.out.println("[PASS] " + testName); + } else { + failCount++; + System.out.println("[FAIL] " + testName); + } + } + + // ==================== 正常场景 ==================== + + static void testEncryptDecryptAES() throws Exception { + String testName = "AES 加密后解密,内容还原"; + File file = createTempFile("aes_test"); + Path keyPath = createTempKeyPath(); + String originalContent = Files.readString(file.toPath()); + + File encrypted = EncryptionAndDecryption.EncryptionFile(file, EncryptionType.AES, keyPath); + assertTrue(encrypted != null, testName + " - 加密返回非null"); + + File keyFile = keyPath.resolve(file.getName() + ".key").toFile(); + File decrypted = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.AES, keyFile); + assertTrue(decrypted != null, testName + " - 解密返回非null"); + + String decryptedContent = Files.readString(file.toPath()); + assertEquals(originalContent, decryptedContent, testName); + } + + static void testEncryptDecryptDES() throws Exception { + String testName = "DES 加密后解密,内容还原"; + File file = createTempFile("des_test"); + Path keyPath = createTempKeyPath(); + String originalContent = Files.readString(file.toPath()); + + File encrypted = EncryptionAndDecryption.EncryptionFile(file, EncryptionType.DES, keyPath); + assertTrue(encrypted != null, testName + " - 加密返回非null"); + + File keyFile = keyPath.resolve(file.getName() + ".key").toFile(); + File decrypted = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.DES, keyFile); + assertTrue(decrypted != null, testName + " - 解密返回非null"); + + String decryptedContent = Files.readString(file.toPath()); + assertEquals(originalContent, decryptedContent, testName); + } + + static void testEncryptDecryptRSA() throws Exception { + String testName = "RSA 加密后解密,内容还原"; + File file = createTempFile("rsa_test"); + Path keyPath = createTempKeyPath(); + String originalContent = Files.readString(file.toPath()); + + File encrypted = EncryptionAndDecryption.EncryptionFile(file, EncryptionType.RSA, keyPath); + assertTrue(encrypted != null, testName + " - 加密返回非null"); + + File keyFile = keyPath.resolve(file.getName() + ".key").toFile(); + File decrypted = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.RSA, keyFile); + assertTrue(decrypted != null, testName + " - 解密返回非null"); + + String decryptedContent = Files.readString(file.toPath()); + assertEquals(originalContent, decryptedContent, testName); + } + + static void testKeyFileGenerated() throws Exception { + String testName = "加密后密钥文件正确生成"; + File file = createTempFile("key_test"); + Path keyPath = createTempKeyPath(); + + EncryptionAndDecryption.EncryptionFile(file, EncryptionType.AES, keyPath); + + File keyFile = keyPath.resolve(file.getName() + ".key").toFile(); + assertTrue(keyFile.exists(), testName + " - 密钥文件存在"); + assertTrue(keyFile.length() > 0, testName + " - 密钥文件非空"); + } + + static void testFileContentChangedAfterEncrypt() throws Exception { + String testName = "加密后文件内容与原文不同"; + File file = createTempFile("content_test"); + Path keyPath = createTempKeyPath(); + byte[] originalBytes = Files.readAllBytes(file.toPath()); + + EncryptionAndDecryption.EncryptionFile(file, EncryptionType.AES, keyPath); + + byte[] encryptedBytes = Files.readAllBytes(file.toPath()); + assertTrue(!java.util.Arrays.equals(originalBytes, encryptedBytes), testName); + } + + // ==================== 异常场景 ==================== + + static void testNullFile() throws Exception { + String testName = "file 为 null 时加密返回 null"; + Path keyPath = createTempKeyPath(); + File result = EncryptionAndDecryption.EncryptionFile(null, EncryptionType.AES, keyPath); + assertEquals(null, result, testName); + } + + static void testNullKeyPath() throws Exception { + String testName = "keyPath 为 null 时加密返回 null"; + File file = createTempFile("null_keypath"); + File result = EncryptionAndDecryption.EncryptionFile(file, EncryptionType.AES, null); + assertEquals(null, result, testName); + } + + static void testNullKeyFile() throws Exception { + String testName = "keyfile 为 null 时解密返回 null"; + File file = createTempFile("null_keyfile"); + File result = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.AES, null); + assertEquals(null, result, testName); + } + + static void testFileNotExist() throws Exception { + String testName = "文件不存在时加密返回 null"; + Path keyPath = createTempKeyPath(); + File notExist = new File("non_existent_file_12345.txt"); + File result = EncryptionAndDecryption.EncryptionFile(notExist, EncryptionType.AES, keyPath); + assertEquals(null, result, testName); + } + + static void testKeyFileNotExist() throws Exception { + String testName = "密钥文件不存在时解密返回 null"; + File file = createTempFile("no_keyfile"); + File notExistKey = new File("non_existent_key_12345.key"); + File result = EncryptionAndDecryption.DecryptionFile(file, EncryptionType.AES, notExistKey); + assertEquals(null, result, testName); + } +}