├── src ├── META-INF │ ├── MANIFESTMacCopy.MF │ └── MANIFESTWindowsCopy.MF ├── main │ ├── resources │ │ ├── fileMask.png │ │ └── log4j2.properties │ └── java │ │ └── com │ │ └── qzw │ │ └── filemask │ │ ├── util │ │ ├── TestUtil.java │ │ ├── MD5Utils.java │ │ ├── RandomStrUtils.java │ │ ├── ByteUtil.java │ │ ├── EncryptUtil.java │ │ └── DisplayInHumanUtils.java │ │ ├── enums │ │ ├── PlatformEnum.java │ │ ├── ChooseTypeEnum.java │ │ ├── MaskExceptionEnum.java │ │ └── FileEncoderTypeEnum.java │ │ ├── interfaces │ │ └── FileEncoderType.java │ │ ├── component │ │ ├── GlobalPasswordHolder.java │ │ └── PlatformContext.java │ │ ├── exception │ │ └── MaskException.java │ │ ├── fileencoder │ │ ├── FileContentEncoder.java │ │ ├── FileOrDirNameEncoder.java │ │ ├── FileHeaderEncoder.java │ │ └── AbstractFileEncoder.java │ │ ├── service │ │ ├── status │ │ │ ├── StopCommandStatusService.java │ │ │ ├── ComputingStatusService.java │ │ │ └── OperationLockStatusService.java │ │ ├── PasswordService.java │ │ ├── PlatformService.java │ │ ├── AuthenticationService.java │ │ ├── PrivateDataService.java │ │ ├── LoginService.java │ │ ├── StatisticsService.java │ │ ├── WorkFlowService.java │ │ └── TailModelService.java │ │ ├── gui │ │ ├── PanelFactory.java │ │ ├── MenuActionFactory.java │ │ └── ButtonActionFactory.java │ │ ├── model │ │ └── TailModel.java │ │ ├── constant │ │ └── Constants.java │ │ └── FileMaskMain.java └── test │ └── java │ └── com │ └── qzw │ └── demo │ └── filemask │ ├── FileContentTest.java │ ├── FileHeadTest.java │ └── FileNameTest.java ├── Part 4 代码分支管理.md ├── Part 3 版本升级记录.md ├── Part 1 软件使用教程.md ├── README.md ├── pom.xml └── Part 2 核心加密思想.md /src/META-INF/MANIFESTMacCopy.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Main-Class: com.qzw.filemask.FileMaskMain 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/fileMask.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/quanzongwei/fileMask/HEAD/src/main/resources/fileMask.png -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/util/TestUtil.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.util; 2 | 3 | /** 4 | * 仅用于单元测试 5 | * 6 | * @author quanzongwei 7 | * @date 2020/5/16 8 | */ 9 | public class TestUtil { 10 | 11 | /** 12 | * 非单元测试,请勿设置该值 13 | */ 14 | public static String uuid = ""; 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/enums/PlatformEnum.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.enums; 2 | 3 | /** 4 | * 平台枚举 5 | * 6 | * @author quanzongwei 7 | */ 8 | public enum PlatformEnum { 9 | /** 10 | * mac 11 | */ 12 | MAC, 13 | /** 14 | * windows 15 | */ 16 | WINDOWS, 17 | 18 | 19 | PlatformEnum() { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/META-INF/MANIFESTWindowsCopy.MF: -------------------------------------------------------------------------------- 1 | Manifest-Version: 1.0 2 | Main-Class: com.qzw.filemask.FileMaskMain 3 | Class-Path: lib/jgoodies-common-1.8.1.jar lib/log4j-slf4j-impl-2.9.0.jar 4 | lib/hamcrest-core-1.3.jar lib/looks-2.2.2.jar lib/slf4j-api-1.7.21.jar 5 | lib/log4j-api-2.9.0.jar lib/log4j-core-2.9.0.jar lib/junit-4.12.jar li 6 | b/commons-lang3-3.7.jar lib/disruptor-3.3.5.jar lib/lombok-1.18.10.jar 7 | 8 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/interfaces/FileEncoderType.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.interfaces; 2 | 3 | import com.qzw.filemask.enums.FileEncoderTypeEnum; 4 | 5 | /** 6 | * 解密类型 7 | * @author quanzongwei 8 | * @date 2020/1/18 9 | */ 10 | public interface FileEncoderType { 11 | /** 12 | * 获取加密解密类型 13 | * 14 | * @see FileEncoderTypeEnum 15 | */ 16 | FileEncoderTypeEnum getFileEncoderType(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/enums/ChooseTypeEnum.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.enums; 2 | 3 | /** 4 | * 选择方式 5 | * @author quanzongwei 6 | * @date 2020/1/18 7 | */ 8 | public enum ChooseTypeEnum { 9 | /** 10 | * 单个文件 11 | */ 12 | FILE_ONLY(), 13 | /** 14 | * 文件夹(包括文件夹自身和文件夹下文件) 15 | */ 16 | CURRENT_DIR_ONLY(), 17 | /** 18 | * 级联文件夹(包括文件夹自身和文件夹下级联的所有文件) 19 | */ 20 | CASCADE_DIR(); 21 | 22 | ChooseTypeEnum() { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/component/GlobalPasswordHolder.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.component; 2 | 3 | /** 4 | * 用户密码 5 | * @author quanzongwei 6 | * @date 2020/1/18 7 | */ 8 | public class GlobalPasswordHolder { 9 | 10 | private static String password; 11 | 12 | public static String getPassword() { 13 | return password; 14 | } 15 | 16 | public static void setPassword(String password) { 17 | GlobalPasswordHolder.password = password; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/component/PlatformContext.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.component; 2 | 3 | /** 4 | * 平台上下文 5 | * 6 | * @author quanzognwei 7 | * @date 2024/7/12 21:30 8 | */ 9 | public class PlatformContext { 10 | 11 | /** 12 | * 当前支持 13 | * WINDOWS、MAC 14 | */ 15 | public static String CURRENT_PLATFORM = "MAC"; 16 | 17 | public static String VERSION_12 = "1.2 "; 18 | public static String VERSION_11 = "1.1 "; 19 | public static String VERSION_10 = "1.0 "; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/exception/MaskException.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.exception; 2 | 3 | import com.qzw.filemask.enums.MaskExceptionEnum; 4 | 5 | /** 6 | * fileMask异常 7 | * @author quanzongwei 8 | * @date 2020/1/18 9 | */ 10 | public class MaskException extends RuntimeException { 11 | private int type; 12 | 13 | public MaskException(int type,String message) { 14 | super(message); 15 | this.type = type; 16 | } 17 | 18 | public MaskException(MaskExceptionEnum maskEnum) { 19 | super(maskEnum.getMessage()); 20 | this.type = maskEnum.getType(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Part 4 代码分支管理.md: -------------------------------------------------------------------------------- 1 | ## v1.1 2 | 日期:20240712 3 | 4 | 升级内容: 5 | 1. mac和windows平台代码融合,方便统一迭代 6 | 2. 修复登录密码输错后,重新输入正确密码,解密异常问题,特别感谢"木OK木"同学发现该问题 7 | 3. 增加软件版本信息 8 | 9 | 代码分支: 10 | 1. feature/20240712/v1.1/refactor_login_and_show_version 11 | 2. release/20240712/v1.1 12 | ## v1.0 13 | 日期:2020-01-01 14 | 15 | 主要内容: 16 | 1. 级联、批量文件加解密能力 17 | 2. 支持windows版本、mac版本 18 | 19 | 代码分支: 20 | 1. release/20240712/v1.0 21 | 22 | 23 | ## 分支管理原则 24 | 1. master为最新的代码 25 | 2. feature从master拉取,为开发分支 26 | 3. feature开发完成合并到master后,稳定测试通过并打包发布app,最后从master拉取release分支 27 | 4. release只允许从master拉取,分支不允许修改,只允许重新升级 28 | 5. 一个release版本对应一个app版本 29 | 6. feature命名规则:feature/20240712/v1.0/xxxx 30 | 7. release命名规则:release/20240712/v1.0/xxxx -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/fileencoder/FileContentEncoder.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.fileencoder; 2 | 3 | import com.qzw.filemask.enums.FileEncoderTypeEnum; 4 | import lombok.extern.log4j.Log4j2; 5 | 6 | /** 7 | * 加密类型三: 文件内容加密 8 | * 原理:xor加密理论上无法破解,唯一的缺陷是无法抵御已知明文攻击; 9 | * 改加密类型使用xor+uuid的方式,防止已知明文攻击获取用户秘钥, 10 | * 具有极快的加密速度和绝对的安全性 11 | * 12 | * 13 | * 该方式支持军事机密级别的文件加密,无法通过任何手段进行解密 14 | * 请保存好您的密码 15 | * @author quanzongwei 16 | * @date 2020/1/18 17 | */ 18 | @Log4j2 19 | public class FileContentEncoder extends AbstractFileEncoder { 20 | 21 | @Override 22 | public FileEncoderTypeEnum getFileEncoderType() { 23 | return FileEncoderTypeEnum.FILE_CONTENT_ENCODE; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/fileencoder/FileOrDirNameEncoder.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.fileencoder; 2 | 3 | import com.qzw.filemask.enums.FileEncoderTypeEnum; 4 | import lombok.extern.log4j.Log4j2; 5 | 6 | /** 7 | * 加密类型一: 文件名称加密 8 | * 原理: 文件重命名;同时每个文件夹会在私有数据文件夹.fileMask中的.fmvalue中保存递增的序号,该序号就是加密后的文件名 9 | * 10 | * 文件和文件夹的处理方式不同 11 | * 文件: 名称加密后,追加到文件末尾 12 | * 文件夹: 保存在私有数据文件夹.fileMask中,这样做的原因是文件夹无法追加数据 13 | * 14 | * @author quanzongwei 15 | * @date 2020/1/18 16 | */ 17 | @Log4j2 18 | public class FileOrDirNameEncoder extends AbstractFileEncoder { 19 | @Override 20 | public FileEncoderTypeEnum getFileEncoderType() { 21 | return FileEncoderTypeEnum.FILE_OR_DIR_NAME_ENCODE; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/util/MD5Utils.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.util; 2 | 3 | import java.security.MessageDigest; 4 | import java.security.NoSuchAlgorithmException; 5 | 6 | /** 7 | * MD5工具类(MD5加密后得到16字节的数据) 8 | * @author quanzongwei 9 | * @date 2020/1/9 10 | */ 11 | public class MD5Utils { 12 | /** 13 | * Md5加密后的数据 14 | * @return 16字节的数据 15 | */ 16 | public static byte[] getMd5Bytes(String input) { 17 | try { 18 | MessageDigest md = MessageDigest.getInstance("MD5"); 19 | byte[] messageDigest = md.digest(input.getBytes()); 20 | return messageDigest; 21 | } catch (NoSuchAlgorithmException e) { 22 | throw new RuntimeException(e); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/service/status/StopCommandStatusService.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.service.status; 2 | 3 | /** 4 | * 停止运行命令状态服务 5 | * 场景: 程序接收到用户提前停止运行的命令后,在安全点终止程序运行 6 | * 7 | * @author quanzongwei 8 | * @date 2020/6/1 9 | */ 10 | public class StopCommandStatusService { 11 | /** 12 | * 0:无需停止 13 | * 1:需要在安全点停止 14 | */ 15 | private static Integer stopStatus = 0; 16 | 17 | /** 18 | * 无需停止 19 | */ 20 | public static Integer STOP_STATUS_NOT_REQUIRE_STOP = 0; 21 | 22 | /** 23 | * 需要在安全点停止 24 | */ 25 | public static Integer STOP_STATUS_REQUIRE_STOP = 1; 26 | 27 | public synchronized static Integer getStopStatus() { 28 | return stopStatus; 29 | } 30 | 31 | public synchronized static void setStopStatus(Integer stopStatus) { 32 | StopCommandStatusService.stopStatus = stopStatus; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/fileencoder/FileHeaderEncoder.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.fileencoder; 2 | 3 | import com.qzw.filemask.enums.FileEncoderTypeEnum; 4 | import lombok.extern.log4j.Log4j2; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.RandomAccessFile; 9 | 10 | /** 11 | * 加密类型二: 文件头部加密 12 | * 原理: 保存文件头部4字节数据, 然后替换头部4字节,前4个字节为: FF FE 00 00 13 | * 告诉应用程序,这是个文本文件,而且只用UTF-32, little-endian编码, 14 | * 所有非文本文件以及非UTF-32, little-endian编码的文本文件都无法 15 | * 正常打开,由于值修改了文件头部4个字节,所以加密速度极快. 16 | * 17 | * 该方式非常巧妙,如果您对编码较为熟悉 18 | * 19 | * 如果您需要使用更专业的加密方式(即使专业人士也无法破解), 请使用加密 20 | * 类型三(文件内容加密,即全文加密) 21 | * @author quanzongwei 22 | * @date 2020/1/18 23 | */ 24 | @Log4j2 25 | public class FileHeaderEncoder extends AbstractFileEncoder { 26 | 27 | @Override 28 | public FileEncoderTypeEnum getFileEncoderType() { 29 | return FileEncoderTypeEnum.FILE_HEADER_ENCODE; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/service/status/ComputingStatusService.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.service.status; 2 | 3 | /** 4 | * 计算状态服务 5 | * 场景: 用于提示框文本展示,用于标识程序在哪个阶段运行 6 | * 0. 计算状态: 计算待加密或解密文件总数 7 | * 1. 加密/解密状态: 正式开始加密和解密 8 | * 9 | * @author quanzongwei 10 | * @date 2020/6/1 11 | */ 12 | public class ComputingStatusService { 13 | /** 14 | * 0:正在计算 15 | * 1:正在执行加密/解密 16 | */ 17 | private static Integer computeStatus = 0; 18 | 19 | /** 20 | * 正在计算 21 | */ 22 | public static Integer COMPUTE_STATUS_RUNNING_COMPUTING = 0; 23 | /** 24 | * 正在执行加密/解密 25 | */ 26 | public static Integer COMPUTE_STATUS_RUNNING_ENCRYPT = 1; 27 | 28 | 29 | public static synchronized Integer getComputeStatus() { 30 | return computeStatus; 31 | } 32 | 33 | public static synchronized void setComputeStatus(Integer computeStatus) { 34 | ComputingStatusService.computeStatus = computeStatus; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/service/status/OperationLockStatusService.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.service.status; 2 | 3 | /** 4 | * 操作锁状态服务 5 | * 场景: 所有的加密和解密必须获取到该锁才能执行 6 | * 7 | * @author quanzongwei 8 | * @date 2020/6/1 9 | */ 10 | public class OperationLockStatusService { 11 | /** 12 | * 0:未开始解密或者解密 13 | * 1:开始加密或者解密 14 | */ 15 | private static Integer runStatus = 0; 16 | /** 17 | * 未开始解密或者解密 18 | */ 19 | public static Integer RUN_STATUS_NOT_START = 0; 20 | /** 21 | * 开始加密或者解密 22 | */ 23 | public static Integer RUN_STATUS_STARTED = 1; 24 | 25 | public synchronized static boolean lock() { 26 | if (runStatus.equals(RUN_STATUS_STARTED)) { 27 | return false; 28 | } 29 | runStatus = RUN_STATUS_STARTED; 30 | return true; 31 | } 32 | 33 | public synchronized static void releaseLock() { 34 | runStatus = RUN_STATUS_NOT_START; 35 | 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/enums/MaskExceptionEnum.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.enums; 2 | 3 | /** 4 | * 异常枚举 5 | * @author quanzongwei 6 | * @date 2020/1/18 7 | */ 8 | public enum MaskExceptionEnum { 9 | /** 10 | * 文件不存在 11 | */ 12 | FILE_NOT_EXISTS(10000, "文件不存在"), 13 | PASSWORD_NOT_EXISTS(10001, "全局密码不存在"); 14 | 15 | /** 16 | * 异常类型 17 | */ 18 | private int type; 19 | 20 | /** 21 | * 异常内容 22 | */ 23 | private String message; 24 | 25 | MaskExceptionEnum(int type, String message) { 26 | this.type = type; 27 | this.message = message; 28 | } 29 | 30 | public int getType() { 31 | return type; 32 | } 33 | 34 | public void setType(int type) { 35 | this.type = type; 36 | } 37 | 38 | public String getMessage() { 39 | return message; 40 | } 41 | 42 | public void setMessage(String message) { 43 | this.message = message; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/enums/FileEncoderTypeEnum.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.enums; 2 | 3 | import lombok.Getter; 4 | 5 | /** 6 | * 加密类型 7 | * @author quanzongwei 8 | * @date 2020/1/18 9 | */ 10 | public enum FileEncoderTypeEnum { 11 | /** 12 | * 文件名称加密 13 | */ 14 | FILE_OR_DIR_NAME_ENCODE(1, true,0), 15 | /** 16 | * 文件头部加密 17 | */ 18 | FILE_HEADER_ENCODE(2, false,1), 19 | /** 20 | * 文件内容加密 21 | */ 22 | FILE_CONTENT_ENCODE(3, false,2); 23 | 24 | 25 | /** 26 | * 加密类型 27 | */ 28 | @Getter 29 | private int type; 30 | 31 | /** 32 | * 加密类型 33 | */ 34 | @Getter 35 | private int position; 36 | 37 | /** 38 | * 该加密类型是否支持对文件夹本身加密, 例如对文件夹名称进行加密 39 | */ 40 | @Getter 41 | private boolean supportEncryptDir; 42 | 43 | FileEncoderTypeEnum(int type, boolean supportEncryptDir,Integer position) { 44 | this.type = type; 45 | this.supportEncryptDir = supportEncryptDir; 46 | this.position = position; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/gui/PanelFactory.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.gui; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | 6 | /** 7 | * @author quanzongwei 8 | * @date 2020/2/24 9 | */ 10 | public class PanelFactory { 11 | 12 | 13 | public static JPanel generatePanelItem(JButton btn1, JButton btn2, JButton btn3, String label) { 14 | JPanel panel = new JPanel(new BorderLayout(0, 10)); 15 | panel.setMinimumSize(new Dimension(640, 40)); 16 | panel.setMaximumSize(new Dimension(640, 40)); 17 | JPanel subPanel1 = new JPanel(new FlowLayout(FlowLayout.LEFT)); 18 | JPanel subPanel2 = new JPanel(new FlowLayout(FlowLayout.RIGHT)); 19 | if (btn1 != null) { 20 | subPanel2.add(btn1); 21 | } 22 | if (btn2 != null) { 23 | subPanel2.add(btn2); 24 | } 25 | subPanel2.add(btn3); 26 | panel.add(subPanel1,BorderLayout.NORTH); 27 | panel.add(subPanel2,BorderLayout.CENTER); 28 | 29 | // 5是实线 30 | float[] dash1 = {2.0f}; 31 | BasicStroke s = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 5.0f, dash1, 0.0f); 32 | panel.setBorder(BorderFactory.createTitledBorder(BorderFactory.createStrokeBorder(s), label)); 33 | return panel; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Part 3 版本升级记录.md: -------------------------------------------------------------------------------- 1 | 2 | ## v1.2 3 | 日期:20241227 4 | 5 | 升级内容: 6 | 1. 输入框支持密码隐藏 7 | 2. 文件名称加密,密文优化(纯数字调整为“FM”字符开头的随机字符,主要目的:便于文件移动、防止遗忘) 8 | 9 | 下载地址: 10 | 11 | For windows 系统 12 | 版本:1.2 13 | 更新日期:2024-12-21 14 | 链接:https://pan.baidu.com/s/1_NvicvS_jA7ycjoi35QjTw?pwd=8812 15 | 提取码:8812 16 | 17 | For mac 系统 18 | 更新日期:2024-12-27 19 | 版本:1.2 20 | 链接:https://pan.baidu.com/s/1Deu1luaT-HGgcbqpFZiyXw?pwd=6612 21 | 提取码:6612 22 | 23 | ## v1.1 24 | 日期:20240712 25 | 26 | 升级内容: 27 | 1. mac和windows平台代码融合,方便统一迭代 28 | 1. 修复登录密码输错后,重新输入正确密码,解密异常问题,特别感谢"木OK木"同学发现该问题 29 | 2. 增加软件版本信息 30 | 31 | 下载地址: 32 | 33 | For windows 系统 34 | 版本:1.1 35 | 更新日期:2024-07-13 36 | 链接:https://pan.baidu.com/s/1bxjqE549-Tke-3eSh0rmlw?pwd=8811 37 | 提取码:8811 38 | 39 | For mac 系统 40 | 版本:1.1 41 | 链接:https://pan.baidu.com/s/1MRSOc1dP4_V3B1fpxak3_w?pwd=6611 42 | 提取码:6611 43 | 44 | ## v1.0 45 | 日期:2020-07-01 46 | 47 | 主要内容: 48 | 1. 级联、批量文件加解密能力 49 | 2. 支持windows版本、mac版本 50 | 51 | 下载地址: 52 | 53 | For windows 系统 54 | 版本:1.0 链接:https://pan.baidu.com/s/1IoM6dZGE2Exn0UtmII2Uvw 55 | 提取码:8888 56 | 57 | For mac 系统 58 | 版本:1.0 59 | 链接:https://pan.baidu.com/s/1Sn6Vbzd_1hoIHbXvfXWqbA?pwd=8888 60 | 提取码:8888 61 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/model/TailModel.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.model; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * 文件尾部数据结构(未加密的原始数据) 7 | *

8 | * 数据结构 9 | * 16字节: 用户md5 10 | * 16字节: 加密类型 11 | * 32字节: uuid(使用md5-4执行xor,加密方式1,2,3都需使用) 12 | * 4字节: 文件头部(加密方式2使用,md5-23+uuid执行xor) 13 | * n字节: 文件名称(加密方式1使用,md5-23+uuid执行xor) 14 | * 8字节: 文件原始长度 15 | * 16字节: FileMaskTailFlag加密标识 16 | *

17 | * 注: 如果加密方式为文件头部加密且文件数据长度<4,则使用全文加密 18 | * 19 | * @author quanzongwei 20 | * @date 2020/5/13 21 | */ 22 | @Data 23 | public class TailModel { 24 | public static Integer USER_MD5_LENGTH_16 = 16; 25 | public static Integer ENCODE_TYPE_FLAG_16 = 16; 26 | public static Integer UUID_32 = 32; 27 | public static Integer HEAD_4 = 4; 28 | 29 | public static Integer ORIGIN_SIZE_8 = 8; 30 | public static Integer TAIL_FLAG_16 = 16; 31 | 32 | /** 33 | * 最小尾部数据长度92 34 | */ 35 | public static Integer MIN_LENGTH = USER_MD5_LENGTH_16 + ENCODE_TYPE_FLAG_16 + UUID_32 + HEAD_4 + ORIGIN_SIZE_8 + TAIL_FLAG_16; 36 | 37 | public byte[] belongUserMd516; 38 | public byte[] encodeType16; 39 | public byte[] uuid32; 40 | public byte[] head4; 41 | public byte[] fileNameX; 42 | public byte[] originTextSize8; 43 | public byte[] tailFlag16; 44 | 45 | public static void main(String[] args) { 46 | System.out.println(MIN_LENGTH); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/constant/Constants.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.constant; 2 | 3 | import java.io.File; 4 | 5 | /** 6 | * @author quanzongwei 7 | * @date 2020/1/18 8 | */ 9 | public class Constants { 10 | 11 | /** 12 | * 认证信息文件名称 13 | */ 14 | public static String AUTH_FILE_NAME = File.separatorChar + "auth.fileMask"; 15 | /** 16 | * 认证信息所在文件夹 17 | */ 18 | public static final String FILE_MASK_AUTHENTICATION_NAME = "fileMaskAuthentication"; 19 | 20 | 21 | /** 22 | * 文件夹名称加密后的前缀 23 | */ 24 | public static final String DIR_MASK_PREFIX = "nDDir"; 25 | /** 26 | * 文件名称加密后的前缀 27 | */ 28 | public static final String FILE_MASK_PREFIX = "nDFile"; 29 | /** 30 | * 文件、文件夹名称加密前缀 31 | */ 32 | public static final String FILE_MASK_PREFIX_NAME_FOR_NAME_ENCRYPT = "FM"; 33 | /** 34 | * 加密文件夹后缀名长度 35 | */ 36 | public static final int DIRECTORY_SUFFIX_LENGTH = 5; 37 | 38 | 39 | /** 40 | * 私有数据文件夹 41 | */ 42 | public static String PRIVATE_DATA_DIR = ".fileMask"; 43 | 44 | /** 45 | * 项目名称 46 | */ 47 | public static String PRAVATE_String = "fileMask"; 48 | 49 | /** 50 | * 保存文件夹自增数据的文件名称 51 | */ 52 | public static String FILE_NAME_4_AUTO_INCREMENT_SEQUENCE = ".fmvalue"; 53 | 54 | /** 55 | * doc path 56 | */ 57 | public static String DOC_PATH = System.getProperty("user.dir") + File.separatorChar + "doc" + File.separatorChar + "readme.html"; 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/util/RandomStrUtils.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.util; 2 | 3 | import java.security.SecureRandom; 4 | import java.util.Base64; 5 | import java.util.UUID; 6 | 7 | /** 8 | * @author quanzognwei 9 | * @date 2024/11/29 19:13 10 | */ 11 | public class RandomStrUtils { 12 | private static final SecureRandom RANDOM = new SecureRandom(); 13 | 14 | /** 15 | * 生成指定长度的base64随机字符串 16 | */ 17 | public static String generateBase64RandomString(int length) { 18 | // Calculate the number of bytes needed to get at least `length` Base64 characters 19 | int numBytes = (int) Math.ceil(length * 3 / 4.0); 20 | // Generate random bytes 21 | byte[] randomBytes = new byte[numBytes]; 22 | RANDOM.nextBytes(randomBytes); 23 | // Encode bytes to Base64 24 | String base64Str = Base64.getUrlEncoder().withoutPadding().encodeToString(randomBytes); 25 | // Return the first `length` characters of the Base64 string 26 | return base64Str.substring(0, length); 27 | } 28 | 29 | /** 30 | * 基于uuid生成base64字符串 31 | * 目的:缩短字符长度,32 → 23 32 | */ 33 | public static String generateUUIDBase64String() { 34 | // 假设我们有一个 UUID,并将其转换为字节数组 35 | UUID uuid = UUID.randomUUID(); 36 | byte[] uuidBytes = new byte[16]; 37 | long mostSigBits = uuid.getMostSignificantBits(); 38 | long leastSigBits = uuid.getLeastSignificantBits(); 39 | for (int i = 0; i < 8; i++) { 40 | uuidBytes[i] = (byte) (mostSigBits >>> (8 * (7 - i))); 41 | uuidBytes[8 + i] = (byte) (leastSigBits >>> (8 * (7 - i))); 42 | } 43 | // 使用 Base64 URL 安全的编码器,并且不带填充字符 44 | return Base64.getUrlEncoder().withoutPadding().encodeToString(uuidBytes); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/service/PasswordService.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.service; 2 | 3 | import com.qzw.filemask.component.GlobalPasswordHolder; 4 | import com.qzw.filemask.enums.MaskExceptionEnum; 5 | import com.qzw.filemask.exception.MaskException; 6 | import com.qzw.filemask.util.MD5Utils; 7 | 8 | /** 9 | * 密码处理 10 | * @author quanzongwei 11 | * @date 2020/1/18 12 | */ 13 | public class PasswordService { 14 | /** 15 | * 获取用户密码 16 | */ 17 | private static String getPassword() { 18 | String password = GlobalPasswordHolder.getPassword(); 19 | if (isEmptyPassword(password)) { 20 | throw new MaskException(MaskExceptionEnum.PASSWORD_NOT_EXISTS); 21 | } 22 | return GlobalPasswordHolder.getPassword(); 23 | } 24 | 25 | /** 26 | * 判断密码是否为空 27 | */ 28 | public static boolean isEmptyPassword(String password) { 29 | return password == null || password.equals(""); 30 | } 31 | 32 | 33 | /** 34 | * 登录时校验用户身份 35 | * @return 16 byte value 36 | */ 37 | public static byte[] getMd5ForLogin() { 38 | byte[] md5Bytes0 = MD5Utils.getMd5Bytes(getPassword()); 39 | return md5Bytes0; 40 | } 41 | 42 | /** 43 | * 加密解密文件时候校验用户身份 44 | * @return 16 byte value 45 | */ 46 | public static byte[] getMd51ForFileAuthentication() { 47 | byte[] md5Bytes1 = MD5Utils.getMd5Bytes(getPassword()+1); 48 | return md5Bytes1; 49 | } 50 | 51 | /** 52 | * 用于生成加密文件内容的秘钥 53 | * 54 | * @return 32 byte value 55 | */ 56 | public static byte[] getMd523ForContentEncrypt() { 57 | byte[] bytes32 = new byte[32]; 58 | byte[] md5Bytes1 = MD5Utils.getMd5Bytes(getPassword() + 2); 59 | byte[] md5Bytes2 = MD5Utils.getMd5Bytes(getPassword() + 3); 60 | System.arraycopy(md5Bytes1, 0, bytes32, 0, 16); 61 | System.arraycopy(md5Bytes2, 0, bytes32, 16, 16); 62 | return bytes32; 63 | } 64 | 65 | /** 66 | * 用于生成加密uuid字符串的秘钥 67 | * 68 | * @return 32 byte value 69 | */ 70 | public static byte[] getMd545ForUuidEncrypt() { 71 | byte[] md5Bytes4 = new byte[32]; 72 | byte[] md5Bytes1 = MD5Utils.getMd5Bytes(getPassword() + 4); 73 | byte[] md5Bytes2 = MD5Utils.getMd5Bytes(getPassword() + 5); 74 | System.arraycopy(md5Bytes1, 0, md5Bytes4, 0, 16); 75 | System.arraycopy(md5Bytes2, 0, md5Bytes4, 16, 16); 76 | return md5Bytes4; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/util/ByteUtil.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.util; 2 | 3 | import java.nio.ByteBuffer; 4 | 5 | /** 6 | * 字节处理 7 | * @author quanzongwei 8 | * @date 2020/1/12 9 | */ 10 | public class ByteUtil { 11 | 12 | public static byte[] longToBytes(long x) { 13 | ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); 14 | buffer.putLong(x); 15 | return buffer.array(); 16 | } 17 | 18 | public static long bytesToLong(byte[] bytes) { 19 | ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); 20 | buffer.put(bytes); 21 | buffer.flip();//need flip 22 | return buffer.getLong(); 23 | } 24 | 25 | /** 26 | * 将byte转化为16进制 27 | */ 28 | public static String byteToHex(byte[] bs) { 29 | if (0 == bs.length) { 30 | return ""; 31 | } else { 32 | StringBuffer sb = new StringBuffer(); 33 | for (int i = 0; i < bs.length; i++) { 34 | String s = Integer.toHexString(bs[i] & 0xFF); 35 | if (1 == s.length()) { 36 | sb.append("0"); 37 | } 38 | sb = sb.append(s.toUpperCase()); 39 | } 40 | return sb.toString(); 41 | } 42 | } 43 | 44 | /** 45 | * 将16进制转化为byte 46 | */ 47 | public static byte[] hexToByte(String ciphertext) { 48 | byte[] cipherBytes = ciphertext.getBytes(); 49 | if ((cipherBytes.length % 2) != 0) { 50 | throw new IllegalArgumentException("长度不为偶数"); 51 | } else { 52 | byte[] result = new byte[cipherBytes.length / 2]; 53 | for (int i = 0; i < cipherBytes.length; i += 2) { 54 | String item = new String(cipherBytes, i, 2); 55 | result[i / 2] = (byte) Integer.parseInt(item, 16); 56 | } 57 | return result; 58 | } 59 | } 60 | 61 | public static byte[] shortToByte(short s) { 62 | byte[] b = new byte[2]; 63 | b[1] = (byte) (s >> 8); 64 | b[0] = (byte) (s >> 0); 65 | return b; 66 | } 67 | 68 | public static short byteToShort(byte[] b) { 69 | return (short) (((b[1] << 8) | b[0] & 0xff)); 70 | } 71 | 72 | 73 | public static int getUnsignedByte(byte data) { //将data字节型数据转换为0~255 (0xFF 即BYTE)。 74 | return data & 0x0FF; 75 | } 76 | 77 | public static int getUnsignedByte(short data) { //将data字节型数据转换为0~65535 (0xFFFF 即 WORD)。 78 | return data & 0x0FFFF; 79 | } 80 | 81 | public static long getUnsignedIntt(int data) { //将int数据转换为0~4294967295 (0xFFFFFFFF即DWORD)。 82 | return data & 0x0FFFFFFFF; 83 | } 84 | } -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/util/EncryptUtil.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.util; 2 | 3 | /** 4 | * @author quanzongwei 5 | * @date 2020/5/16 6 | */ 7 | public class EncryptUtil { 8 | /** 9 | * 加密内容 10 | * 使用场景: 11 | * 1.加密头部数据 12 | * 2.加密文件内容 13 | * 3.加密文件名称 14 | * 15 | * @param uuid 32位 16 | * @param md532 32位 由password引申出的值 17 | * @param originText 需要加密的数据 18 | * @return 加密后的和原来数据等字节的数据 19 | */ 20 | public static byte[] encryptContent(byte[] uuid, byte[] md532, byte[] originText) { 21 | return doXorForContent(uuid, md532, originText); 22 | } 23 | 24 | /** 25 | * 解密内容 26 | * 使用场景: 27 | * 1.加密头部数据 28 | * 2.加密文件内容 29 | * 3.加密文件名称 30 | * 31 | * @param uuid 32位 32 | * @param md532 32位 由password引申出的值 33 | * @param encryptedText 需要加密的数据 34 | * @return 解密后的字节数据 35 | */ 36 | public static byte[] decryptContent(byte[] uuid, byte[] md532, byte[] encryptedText) { 37 | return doXorForContent(uuid, md532, encryptedText); 38 | } 39 | 40 | /** 41 | * 对文件内容进行xor操作 42 | */ 43 | private static byte[] doXorForContent(byte[] uuid, byte[] md532, byte[] text) { 44 | byte[] contentEncryptXorKey = new byte[uuid.length]; 45 | for (int i = 0; i < uuid.length; i++) { 46 | contentEncryptXorKey[i] = (byte) (uuid[i] ^ md532[i]); 47 | } 48 | byte[] result = new byte[text.length]; 49 | for (int i = 0; i < result.length; i++) { 50 | result[i] = (byte) (text[i] ^ contentEncryptXorKey[i % (contentEncryptXorKey.length)]); 51 | } 52 | return result; 53 | } 54 | 55 | /** 56 | * 加密uuid数据 57 | * 使用场景: 58 | * 1.加密uuid 59 | * 60 | * @param md545 32位 由password引申出的值 61 | * @param uuid 32位 62 | * @return 加密后的和原来数据等字节的数据 63 | */ 64 | public static byte[] encryptUuid(byte[] md545, byte[] uuid) { 65 | return doXorForUuid(md545, uuid); 66 | } 67 | 68 | /** 69 | * 解密uuid数据 70 | * 使用场景: 71 | * 1.解密uuid 72 | * 73 | * @param md545 32位 由password引申出的值 74 | * @param uuid 32位 75 | * @return 加密后的和原来数据等字节的数据 76 | */ 77 | public static byte[] decryptUuid(byte[] md545, byte[] uuid) { 78 | return doXorForUuid(md545, uuid); 79 | } 80 | 81 | /** 82 | * 对uuid字符串数据进行xor操作 83 | */ 84 | private static byte[] doXorForUuid(byte[] md545, byte[] uuid) { 85 | byte[] result = new byte[uuid.length]; 86 | for (int i = 0; i < result.length; i++) { 87 | result[i] = (byte) (md545[i] ^ uuid[i]); 88 | } 89 | return result; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/util/DisplayInHumanUtils.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.util; 2 | 3 | /** 4 | * 展示优化 5 | * 6 | * @author quanzongwei 7 | * @date 2020/6/1 8 | */ 9 | public class DisplayInHumanUtils { 10 | /** 11 | * 优化时间展示(精确到秒) 12 | */ 13 | public static String getSecondInHuman(long second) { 14 | long sec = second; 15 | long secPart = sec % 60; 16 | long minutes = sec / 60; 17 | long minutesPart = minutes % 60; 18 | long hour = minutes / 60; 19 | long hourPart = hour % 60; 20 | long day = hour / 24; 21 | long dayPart = day; 22 | String s = ""; 23 | if (dayPart > 0L) { 24 | s += dayPart + "天"; 25 | } 26 | if (hourPart > 0L) { 27 | s += hourPart + "小时"; 28 | } 29 | if (minutesPart > 0L) { 30 | s += minutesPart + "分钟"; 31 | } 32 | if (secPart > 0L) { 33 | s += secPart + "秒"; 34 | } 35 | if (s.equals("")) { 36 | s = "0秒"; 37 | } 38 | return s; 39 | } 40 | 41 | /** 42 | * 优化时间展示(精确到毫秒) 43 | */ 44 | public static String getMilliSecondInHuman(long millisecond) { 45 | long millisec = millisecond; 46 | long milliPart = millisec % 1000; 47 | long sec = millisec / 1000; 48 | long secPart = sec % 60; 49 | long minutes = sec / 60; 50 | long minutesPart = minutes % 60; 51 | long hour = minutes / 60; 52 | long hourPart = hour % 60; 53 | long day = hour / 24; 54 | long dayPart = day; 55 | String s = ""; 56 | if (dayPart > 0L) { 57 | s += dayPart + "天"; 58 | } 59 | if (hourPart > 0L) { 60 | s += hourPart + "小时"; 61 | } 62 | if (minutesPart > 0L) { 63 | s += minutesPart + "分钟"; 64 | } 65 | if (secPart > 0L) { 66 | s += secPart + "秒"; 67 | } 68 | if (secPart > 0L) { 69 | s += milliPart + "毫秒"; 70 | } 71 | if (s.equals("")) { 72 | s = "0毫秒"; 73 | } 74 | return s; 75 | } 76 | 77 | /** 78 | * 获取文件大小(精确到字节) 79 | */ 80 | public static String getBytesInHuman(long b) { 81 | long bPart = b % 1024; 82 | long k = b / 1024; 83 | long kPart = k % 1024; 84 | long m = k / 1024; 85 | long mPart = m % 1024; 86 | long g = m / 1024; 87 | long gPart = g; 88 | 89 | String s = ""; 90 | if (gPart > 0L) { 91 | s += "," + gPart + "G"; 92 | } 93 | if (mPart > 0L) { 94 | s += ","+mPart + "M"; 95 | } 96 | if (kPart > 0L) { 97 | s += ","+kPart + "K"; 98 | } 99 | if (bPart > 0L) { 100 | s += ","+bPart + "字节"; 101 | } 102 | if (s.equals("")) { 103 | s = ",0字节"; 104 | } 105 | s = s.substring(1); 106 | return s; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/service/PlatformService.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.service; 2 | 3 | import com.jgoodies.looks.plastic.PlasticLookAndFeel; 4 | import com.jgoodies.looks.plastic.theme.DesertBluer; 5 | import com.qzw.filemask.component.PlatformContext; 6 | import com.qzw.filemask.constant.Constants; 7 | import com.qzw.filemask.enums.PlatformEnum; 8 | import lombok.extern.log4j.Log4j2; 9 | 10 | import javax.swing.*; 11 | import javax.swing.filechooser.FileSystemView; 12 | import java.io.File; 13 | 14 | /** 15 | * 平台定制逻辑收敛 16 | * 17 | * @author quanzognwei 18 | * @date 2024/7/12 21:34 19 | */ 20 | @Log4j2 21 | public class PlatformService { 22 | 23 | /** 24 | * 返回用户认证目录 25 | */ 26 | public static String getAuthDirNameByPlatform(String platform) { 27 | FileSystemView fsv = FileSystemView.getFileSystemView(); 28 | if (PlatformEnum.WINDOWS.name().equals(platform)) { 29 | //File tmpDir = new File(fsv.getDefaultDirectory().getPath() + File.separatorChar + "fileMask"); 30 | return System.getProperty("user.dir") + Constants.FILE_MASK_AUTHENTICATION_NAME; 31 | } else if (PlatformEnum.MAC.name().equals(platform)) { 32 | return fsv.getDefaultDirectory().getPath() + File.separatorChar + Constants.FILE_MASK_AUTHENTICATION_NAME; 33 | } 34 | throw new RuntimeException("平台不支持:" + platform); 35 | } 36 | 37 | /* 38 | 设置样式 39 | UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel"); 40 | UIManager.setLookAndFeel("com.apple.laf.AquaLookAndFeel"); 41 | UIManager.setLookAndFeel("javax.swing.plaf.nimbus.NimbusLookAndFeel"); 42 | PlasticLookAndFeel.setPlasticTheme(new DesertBluer()); 43 | PlasticLookAndFeel.setPlasticTheme(new SkyYellow()); 44 | 设置观感 45 | UIManager.setLookAndFeel("com.jgoodies.looks.windows.WindowsLookAndFeel"); 46 | UIManager.setLookAndFeel(new Plastic3DLookAndFeel()); 47 | UIManager.setLookAndFeel("com.jgoodies.looks.plastic.PlasticLookAndFeel"); 48 | UIManager.setLookAndFeel("com.jgoodies.looks.plastic.Plastic3DLookAndFeel"); 49 | UIManager.setLookAndFeel("com.jgoodies.looks.plastic.PlasticXPLookAndFeel"); 50 | */ 51 | public static void setLookAndFeelByPlatform(String platform) { 52 | try { 53 | if (PlatformEnum.WINDOWS.name().equals(platform)) { 54 | UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel"); 55 | PlasticLookAndFeel.setPlasticTheme(new DesertBluer()); 56 | //设置观感 57 | UIManager.setLookAndFeel("com.jgoodies.looks.windows.WindowsLookAndFeel"); 58 | return; 59 | } else if (PlatformEnum.MAC.name().equals(platform)) { 60 | UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel"); 61 | PlasticLookAndFeel.setPlasticTheme(new DesertBluer()); 62 | return; 63 | } 64 | throw new RuntimeException("平台不支持:" + platform); 65 | } catch (Exception e) { 66 | log.error("UI样式设置出错", e); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/service/AuthenticationService.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.service; 2 | 3 | import com.qzw.filemask.component.PlatformContext; 4 | import com.qzw.filemask.constant.Constants; 5 | import com.qzw.filemask.util.MD5Utils; 6 | import lombok.extern.log4j.Log4j2; 7 | 8 | import java.io.File; 9 | import java.io.RandomAccessFile; 10 | import java.util.Arrays; 11 | 12 | /** 13 | * 登录认证 14 | * 15 | * @author quanzongwei 16 | * @date 2020/3/7 17 | */ 18 | @Log4j2 19 | public class AuthenticationService { 20 | 21 | 22 | /** 23 | * 用户密码是否已经配置 24 | */ 25 | public static boolean isExistUserPassword() { 26 | String dir = getAuthDirName(); 27 | File file = new File(dir); 28 | if (!file.exists()) { 29 | file.mkdir(); 30 | } 31 | String authFilePath = dir + Constants.AUTH_FILE_NAME; 32 | File authFile = new File(authFilePath); 33 | if (!authFile.exists()) { 34 | return false; 35 | } 36 | try (RandomAccessFile raf = new RandomAccessFile(authFile, "rw")) { 37 | if (raf.length() == 0) { 38 | return false; 39 | } 40 | return true; 41 | } catch (Exception ex) { 42 | log.error("认证文件访问失败, path", authFile, ex); 43 | } 44 | return false; 45 | } 46 | 47 | public static boolean isCurrentUser(String password) { 48 | String dir = getAuthDirName(); 49 | File file = new File(dir); 50 | if (!file.exists()) { 51 | file.mkdir(); 52 | } 53 | String authFilePath = dir + Constants.AUTH_FILE_NAME; 54 | File authFile = new File(authFilePath); 55 | if (!authFile.exists()) { 56 | return false; 57 | } 58 | try (RandomAccessFile raf = new RandomAccessFile(authFile, "rw")) { 59 | if (raf.length() == 0) { 60 | return false; 61 | } 62 | byte[] inputMd5Bytes = MD5Utils.getMd5Bytes(password); 63 | // md5 数据的长度为16字节 64 | byte[] userMd5Bytes = new byte[inputMd5Bytes.length]; 65 | raf.seek(0); 66 | raf.read(userMd5Bytes); 67 | if (Arrays.equals(userMd5Bytes, inputMd5Bytes)) { 68 | return true; 69 | } 70 | return false; 71 | } catch (Exception ex) { 72 | log.error("认证文件访问失败, path", authFile, ex); 73 | } 74 | return false; 75 | } 76 | 77 | /** 78 | * 设置用户密码 79 | */ 80 | public static void setUserMd5Byte(String password) { 81 | String dir = getAuthDirName(); 82 | File file = new File(dir); 83 | if (!file.exists()) { 84 | file.mkdir(); 85 | } 86 | String authFilePath = dir + Constants.AUTH_FILE_NAME; 87 | File authFile = new File(authFilePath); 88 | try (RandomAccessFile raf = new RandomAccessFile(authFile, "rw")) { 89 | raf.setLength(0); 90 | raf.write(MD5Utils.getMd5Bytes(password)); 91 | } catch (Exception ex) { 92 | log.error("认证文件访问失败, path", authFile, ex); 93 | } 94 | } 95 | 96 | private static String getAuthDirName() { 97 | return PlatformService.getAuthDirNameByPlatform(PlatformContext.CURRENT_PLATFORM); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Part 1 软件使用教程.md: -------------------------------------------------------------------------------- 1 | ## 一 软件目录结构 2 | 解压fileMask.rar文件,软件目录如下所示: 3 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E6%96%87%E4%BB%B6%E7%9B%AE%E5%BD%95.png) 4 | 5 | * authentication目录存放用户认证信息 6 | * doc目录存放使用帮助文档 7 | * icon目录存放软件图标 8 | * jre1.8存放jre文件(源代码大概50k,带上jre后变成了40M,所以主要是jre比较大) 9 | * lib存放jar文件 10 | * logs存放日志文件 11 | * fileMask.exe是应用程序入口,双击运行即可 12 | 13 | ## 二 软件主界面 14 | ### 2.1 首次运行 15 | 16 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E9%A6%96%E6%AC%A1%E8%BF%90%E8%A1%8C.png) 17 | 首次运行需要输入用户密码,并且确认密码。请牢记您的密码,当一个文件被加密后,只有持有该密码的用户才能解密。 18 | 19 | ### 2.2 主界面 20 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E4%B8%BB%E7%95%8C%E9%9D%A2v2.png) 21 | 22 | 23 | #### 2.2.1 加密类型 24 | * 类型一: 文件名称加密 25 | 支持对文件名称和文件夹名称进行加密,加密速度极快(毫秒级) 26 | * 类型二: 文件头部加密 27 | 将文件头部进行加密,加密后无法被正常打开。 比如文本文件打开后所有数据变为乱码,视频音频以及图片文件都无法正常打开,同时,加密速度极快(毫秒级) 28 | * 类型三: 文件内容加密(即全文加密) 29 | 对文件中,所有的数据进行全文加密,无法通过任何手段进行破解。 安全性最高,但是加密速度较慢(100M耗时1秒,1G耗时10秒,12G耗时4分钟) 30 | 31 | #### 2.2.2 加密方式 32 | 每一种加密类型都对应三种加密方式 33 | * 方式1:文件夹级联加密 34 | 对文件夹下所有的子文件夹,以及子文件夹的子文件夹,进行级联加密。 35 | * 方式2:文件夹加密 36 | 只对选择的文件夹下的文件进行加密, 不会级联加密 37 | * 方式3:文件加密 38 | 只对单个文件加密 39 | #### 文件解密 40 | 解密也支持三种解密方式,文件夹级联解密,文件夹解密和文件解密。系统会自动检测文件被哪种或者哪几种加密方式加密过,然后进行解密。 41 | 42 | ### 2.3 菜单栏 43 | 菜单栏中有对应的使用帮助文档 44 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E8%8F%9C%E5%8D%95%E6%A0%8F%E5%B8%AE%E5%8A%A9%E6%96%87%E6%A1%A3.png) 45 | 46 | 47 | 联系作者菜单项中可以找到项目源码和作者联系方式 48 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E8%81%94%E7%B3%BB%E4%BD%9C%E8%80%85.png) 49 | 50 | ### 2.4 加密/解密处理进度 51 | 这个是作者非常满意的一个功能,如下所示: 52 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E8%BF%9B%E5%BA%A6%E6%98%BE%E7%A4%BA.png) 53 | 参数解释: 54 | 1. 展示扫描文件总数和总大小 55 | 2. 展示已处理文件总数和已耗时间 56 | 3. 预估剩余处理时间 57 | 4. 展示当前文件处理进度 58 | 5. 展示当前文件预计剩余处理时间 59 | 6. 提前停止,如果用户待处理的文件比较多,可以点击提前停止按钮,系统等待当前文件处理完成,到达安全点后,操作完成,此时部分文件处理成功,部分文件未处理 60 | 61 | 使用场景: 预估剩余时间以及支持提前停止 62 | ## 三 文件加解密示例 63 | #### 3.1 加密类型选择加密类型一(文件名称加密),加密方式使用文件夹级联加密 64 | **加密前** 65 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E5%8A%A0%E5%AF%86%E5%89%8D-1.png) 66 | **加密后** 67 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E5%8A%A0%E5%AF%86%E5%90%8E-1.png) 68 | 加密后文件和文件夹的名称变成一个递增序号,每个文件夹下会多出一个.fileMask文件夹,用于保存递增的序号信息以及加密过的文件夹原始名称信息 69 | 70 | **解密后** 71 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E8%A7%A3%E5%AF%86%E5%90%8E-1.png) 72 | 文件解密后,文件和文件夹名称恢复原样 73 | 74 | #### 3.2 加密类型选择加密类型二(文件头部加密),加密方式使用文件加密 75 | **加密前** 76 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E5%8A%A0%E5%AF%86%E5%89%8D-2.png) 77 | **加密后** 78 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E5%8A%A0%E5%AF%86%E5%90%8E-2.png) 79 | 加密后文件内容变成乱码 80 | 81 | **解密后** 82 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E8%A7%A3%E5%AF%86%E5%90%8E-2.png) 83 | 文件解密后,文件数据恢复原样,如果解密后依然显示乱码,只需要关闭该文件并重新打开即可 84 | 85 | #### 3.3 加密类型选择加密类型三(文件全文加密),加密方式使用文件加密 86 | **加密前** 87 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E5%8A%A0%E5%AF%86%E5%89%8D-3.png) 88 | **加密后** 89 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E5%8A%A0%E5%AF%86%E5%90%8E-3.png) 90 | 加密后文件内容变成乱码 91 | 92 | **解密后** 93 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E8%A7%A3%E5%AF%86%E5%90%8E-3.png) 94 | 文件解密后,文件数据恢复原样,如果解密后依然显示乱码,只需要关闭该文件并重新打开即可 95 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/service/PrivateDataService.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.service; 2 | 3 | import com.qzw.filemask.constant.Constants; 4 | import com.qzw.filemask.util.RandomStrUtils; 5 | import lombok.extern.log4j.Log4j2; 6 | 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.io.RandomAccessFile; 10 | 11 | /** 12 | * fileMask私有数据 13 | * 作用: 用于保存加密后的秘钥, 或者被加密信息的原文 14 | * 15 | * @author quanzongwei 16 | * @date 2020/3/3 17 | */ 18 | @Log4j2 19 | public class PrivateDataService { 20 | /** 21 | * 获取目标文件对应的私有数据文件夹 22 | */ 23 | public static File getPrivateDataDir(File targetFileOrDir) { 24 | File file = new File(targetFileOrDir.getParent() + File.separatorChar + Constants.PRIVATE_DATA_DIR); 25 | if (!file.exists()) { 26 | file.mkdir(); 27 | } 28 | String sets = "attrib +H \"" + file.getAbsolutePath() + "\""; 29 | try { 30 | Runtime.getRuntime().exec(sets); 31 | } catch (IOException e) { 32 | log.info("文件隐藏失败:filePath" + file.getAbsolutePath()); 33 | } 34 | return file; 35 | } 36 | 37 | /** 38 | * 获取目标文件对应的私有数据文件 39 | */ 40 | public static File getPrivateDataFile(File targetFileOrDir) { 41 | return new File(targetFileOrDir.getParent() + File.separatorChar + Constants.PRIVATE_DATA_DIR + File.separatorChar + targetFileOrDir.getName()); 42 | } 43 | 44 | /** 45 | * 获取目标文件对应的私有数据文件 46 | * 用于:release v2 版本 47 | * Object String or Integer 48 | */ 49 | public static File getPrivateDataFileReleaseV2(File targetFileOrDir, Object sequence) { 50 | return new File(targetFileOrDir.getParent() + File.separatorChar + Constants.PRIVATE_DATA_DIR + File.separatorChar + sequence); 51 | } 52 | 53 | /** 54 | * 判断文件是否是私有数据文件 55 | */ 56 | public static boolean isFileMaskFile(File file) { 57 | boolean fileMask = file.getPath().contains(Constants.PRIVATE_DATA_DIR) 58 | || file.getPath().contains(Constants.PRAVATE_String) 59 | || file.getPath().contains(Constants.FILE_MASK_AUTHENTICATION_NAME); 60 | if (fileMask) { 61 | return true; 62 | } 63 | return false; 64 | } 65 | 66 | /** 67 | * 同步方法,获取目标文件所在文件夹对应的自增Id 68 | * 使用场景:父类加密名称 FM{sequence}XXXXX 69 | */ 70 | public static synchronized Integer getAutoIncrementSequence4ParentDir(File fileOrDir) { 71 | String fmvalueFile = getPrivateDataDir(fileOrDir).getPath() + File.separatorChar + Constants.FILE_NAME_4_AUTO_INCREMENT_SEQUENCE; 72 | File file = new File(fmvalueFile); 73 | try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { 74 | raf.seek(0); 75 | if (raf.length() == 0) { 76 | raf.writeInt(1); 77 | return 0; 78 | } 79 | int sequence = raf.readInt(); 80 | raf.seek(0); 81 | raf.writeInt(sequence + 1); 82 | return sequence; 83 | } catch (Exception e) { 84 | log.info("获取文件下唯一自增值出错,path:{},exception:{}", fileOrDir.getPath(), e.getMessage()); 85 | } 86 | return null; 87 | } 88 | 89 | /** 90 | * 获取加密文件夹名称前缀 91 | * 92 | * @return 93 | */ 94 | public static synchronized String getEncryptedDirNameFromSequenceAndBase64RandomStr(File fileOrDir) { 95 | Integer sequence = getAutoIncrementSequence4ParentDir(fileOrDir); 96 | if (sequence == null) { 97 | // unreachable 98 | sequence = 0; 99 | } 100 | // 文件夹名称加密(随机数长度和数量的关系 5=10亿,4=1千万) 101 | return Constants.FILE_MASK_PREFIX_NAME_FOR_NAME_ENCRYPT + sequence + RandomStrUtils.generateBase64RandomString(Constants.DIRECTORY_SUFFIX_LENGTH); 102 | } 103 | 104 | /** 105 | * 获取加密文件名称前缀 106 | */ 107 | public static synchronized String getEncryptedFileNameFromUuid() { 108 | return Constants.FILE_MASK_PREFIX_NAME_FOR_NAME_ENCRYPT + RandomStrUtils.generateUUIDBase64String(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 下载地址 2 | 3 | 4 | | 操作系统 | 最新版本 | 下载地址(推荐百度云) | 下载地址(github) | 更新日期 | 5 | |--------|------|-------------|-----------------------------------------------------------------------------------------------------------------------|------------| 6 | | windows | v1.2 | https://pan.baidu.com/s/1_NvicvS_jA7ycjoi35QjTw?pwd=8812 | [fileMask-v1.2.rar](https://github.com/quanzongwei/fileMask/releases/download/fileMask-v1.2-binary/fileMask-v1.2.rar) | 2024-12-21 | 7 | | mac | v1.2 | https://pan.baidu.com/s/1Deu1luaT-HGgcbqpFZiyXw?pwd=6612 | [fileMask-v1.2.dmg](https://github.com/quanzongwei/fileMask/releases/download/fileMask-v1.2-binary/fileMask-v1.2.dmg) | 2024-12-27| 8 | 9 | 注:mac系统软件安装完成后,若提示【“fileMask”已损坏,无法打开。 您应该将它移到废纸篓。】,一般不是软件本身的问题,而是Mac启用了安全机制,默认只信任Mac App Store下载的软件以及拥有开发者ID签名的软件,解决方案可参考以下文章,也可自行通过百度或谷歌搜索解决方案: 10 | 1、[macOS 提示:“应用程序” 已损坏,无法打开的解决方法总结](https://sysin.org/blog/macos-if-crashes-when-opening/) 11 | 2、[mac安装应用提示已损坏,打不开。您应该将它移到废纸娄问题解决](https://zhuanlan.zhihu.com/p/617123498) 12 | ## 一 fileMask软件简介 13 | 该软件主要专注于文件和文件夹的加密和解密 14 | 15 | 开发语言: java 16 | 开发周期: 7个月(2019年11月-2020年6月) 17 | 18 | ## 二 软件特点 19 | 界面简洁,功能强大,算法绝对安全(欢迎进行学术性交流),用户体验良好(支持加密进度展示、随时停止、加解密支持幂等性), 具有思想和灵魂的文件和文件夹加密软件。 软件中一个不经意的小细节,就有可能是一个不错的小创意 20 | 21 | ## 三 软件界面 22 | ### 3.1 windows系统(主页) 23 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E7%AE%80%E4%BB%8B-%E4%B8%BB%E7%95%8C%E9%9D%A2.png) 24 | ### 3.2 mac系统(进度页) 25 | your_image 26 | 27 | 28 | ## 四 加密类型 29 | ### 4.1 三种加密类型 30 | * 类型一: 文件名称加密 31 | 支持对文件名称和文件夹名称进行加密,加密速度极快(毫秒级) 32 | * 类型二: 文件头部加密 33 | 将文件头部进行加密,加密后无法被正常打开。 比如文本文件打开后所有数据变为乱码,视频音频以及图片文件都无法正常打开,同时,加密速度极快(毫秒级) 34 | * 类型三: 文件内容加密(即全文加密) 35 | 对文件中,所有的数据进行全文加密,无法通过任何手段进行破解。 安全性最高,但是加密速度较慢(100M耗时1秒,1G耗时10秒,12G耗时4分钟) 36 | 37 | 38 | ### 4.2 组合加密 39 | * 文件名称加密可以和文件头部加密组合使用 40 | * 文件名称加密可以和文件全文加密组合使用 41 | 42 | ## 五 加密方式 43 | 三种加密类型,都对应三种加密方式 44 | * 方式1:文件夹级联加密 45 | 对文件夹下所有的子文件夹,以及子文件夹的子文件夹,进行级联加密 46 | * 方式2:文件夹加密 47 | 只对选择的文件夹下的文件进行加密, 不会级联加密 48 | * 方式3:文件加密 49 | 只对单个文件加密 50 | 51 | ## 六 解密 52 | 解密也支持三种解密方式,文件夹级联解密,文件夹解密和文件解密。系统会自动检测文件被哪种或者哪几种加密方式加密过,然后进行解密 53 | 54 | ## 七 应用场景 55 | 1. 个人笔记本中的文件,同时使用文件名称加密和文件内容加密, 比如小视频,自拍丑照, 再也不用担心电脑借给同学用了,哈哈 56 | 2. 公司电脑保存私人文件, 一键级联加密整个文件夹,此时它就是你的专用文件夹,即使离职,文件不删除也无所谓,因为没有人能打开,也没有人知道他是啥 57 | 3. 网吧,有些同学经常到网吧上网,而且使用的同一台电脑,有些私人文件想存放在电脑中,又不想被其他人看见。此时该软件就是很好的选择 58 | 4. 家庭电脑,假如您和您的家人使用同一台电脑,有些文件是您的私人文件,但是又想存在电脑上,使用该软件即可让这些文件成为你的专属文件 59 | 5. 公司电脑达到更换新机的标准,需要删除个人敏感数据后归还旧电脑。如果直接删除,可以“通过数据恢复软件将磁盘已删除的数据进行恢复”,有数据泄露风险,此时可以使用 60 | 该软件进行加密。最安全的文件删除方式是先加密、后删除,即使被恢复,也无法被解密 61 | 6. 二手电脑挂闲鱼,可以加密敏感文件后再出售,避免直接删除导致数据被恶意恢复 62 | 63 | ## 八 作者建议 64 | **首先**,一般情况下, 使用加密类型一(文件名称加密),就能实现加密效果,一般人无法识别这个文件是啥 65 | 66 | **接着**,如果其他用户会尝试猜测文件类型,然后使用对应的软件直接打开,那么此时使用类型二(文件头部加密),此时,文件无法被打开。例如,我们对mp4文件使用类型二(文件头部加密),那么即使该文件,被拖入到视频播放软件也是无法打开的 67 | 68 | **然后**,如果您是公司高管,军政要员,需要极高安全性的加密,此时可以使用类型三(全文加密)进行加密,该方式,无法通过任何手段进行解密(包括暴力破解),速度相对于类型一和类型二,较慢,单文件预计耗时(100M耗时1秒,1G耗时10秒,12G耗时4分钟)。但是,相对于市场上其他的全文加密软件,作者使用了很巧妙的思路和方法,最大化的提高了加密的速度 69 | 70 | ## 九 更多阅读 71 | 1. [Part 1 软件使用教程](https://github.com/quanzongwei/fileMask/blob/master/Part%201%20%E8%BD%AF%E4%BB%B6%E4%BD%BF%E7%94%A8%E6%95%99%E7%A8%8B.md) 72 | 2. [Part 2 核心加密思想](https://github.com/quanzongwei/fileMask/blob/master/Part%202%20%E6%A0%B8%E5%BF%83%E5%8A%A0%E5%AF%86%E6%80%9D%E6%83%B3.md) 73 | 3. [Part 3 版本升级记录](https://github.com/quanzongwei/fileMask/blob/master/Part%203%20%E7%89%88%E6%9C%AC%E5%8D%87%E7%BA%A7%E8%AE%B0%E5%BD%95.md) 74 | ## 十 捐赠 75 | 76 | > welcome to donate to fileMask 77 | 78 | 79 | | 微信 | 支付宝 | 80 | |---------------------------------------------------------|---------------------------------------------------------| 81 | | | | 82 | 83 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/gui/MenuActionFactory.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.gui; 2 | 3 | import com.qzw.filemask.FileMaskMain; 4 | import com.qzw.filemask.component.PlatformContext; 5 | import com.qzw.filemask.constant.Constants; 6 | import lombok.extern.log4j.Log4j2; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.io.*; 11 | 12 | /** 13 | * @author quanzongwei 14 | * @date 2020/3/14 15 | */ 16 | @Log4j2 17 | public class MenuActionFactory { 18 | 19 | public static JFrame f = FileMaskMain.f; 20 | 21 | public static void menuItem4Exit(JMenuItem item) { 22 | item.addActionListener(e -> System.exit(0)); 23 | } 24 | 25 | public static void menuItem4Help(JMenuItem item) { 26 | item.addActionListener(e -> { 27 | JFrame jFrame = new JFrame("使用说明文档"); 28 | jFrame.setMaximumSize(new Dimension(300, 300)); 29 | jFrame.setMinimumSize(new Dimension(300, 300)); 30 | 31 | JPanel jpanel = new JPanel(new FlowLayout()); 32 | jpanel.setMaximumSize(new Dimension(300, 300)); 33 | jpanel.setMinimumSize(new Dimension(300, 300)); 34 | 35 | // 36 | JTextPane jTextPane = new JTextPane(); 37 | jTextPane.setAutoscrolls(true); 38 | jpanel.add(jTextPane); 39 | 40 | 41 | Container container = jFrame.getContentPane(); 42 | JScrollPane scrollPane = new JScrollPane(jTextPane); 43 | StringBuilder text = new StringBuilder(""); 44 | File file = new File(Constants.DOC_PATH); 45 | scrollPane.add(jpanel); 46 | try (BufferedReader bf = new BufferedReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))) { 47 | String s = ""; 48 | while ((s = bf.readLine()) != null) { 49 | text.append(s); 50 | text.append("\n"); 51 | 52 | } 53 | } catch (FileNotFoundException ex) { 54 | log.error("doc not found !!", ex); 55 | } catch (IOException ex) { 56 | log.error("file open error !!", ex); 57 | } 58 | 59 | jTextPane.setContentType("text/html"); 60 | jTextPane.setText(text.toString()); 61 | container.add(scrollPane); 62 | 63 | jFrame.setSize(800, 800); 64 | int x = (Toolkit.getDefaultToolkit().getScreenSize().width - jFrame.getSize().width) / 2; 65 | int y = (Toolkit.getDefaultToolkit().getScreenSize().height - jFrame.getSize().height) / 2; 66 | jFrame.setLocation(x, y > f.getLocation().getY() ? (int) f.getLocation().getY() : y); 67 | // 68 | jFrame.setVisible(true); 69 | 70 | 71 | }); 72 | } 73 | 74 | public static void menuItem4Contact(JMenuItem item) { 75 | item.addActionListener(e -> { 76 | JFrame jFrame = new JFrame("有任何问题,欢迎联系作者~~"); 77 | jFrame.setSize(new Dimension(400, 200)); 78 | 79 | Container container = jFrame.getContentPane(); 80 | container.setLayout(new BorderLayout(0, 0)); 81 | container.setSize(new Dimension(550, 400)); 82 | container.setMaximumSize(new Dimension(550, 400)); 83 | container.setMinimumSize(new Dimension(550, 400)); 84 | 85 | JTextPane jTextPane = new JTextPane(); 86 | jTextPane.setContentType("text/html"); 87 | 88 | jTextPane.setText("\n" + 89 | "" + 90 | "

作者邮箱: 552114141@qq.com

\n" + 91 | "

作者微信: quanzongwei

\n" + 92 | "

github repository: https://github.com/quanzongwei/fileMask

\n" + 93 | "
版本号: " 94 | + 95 | PlatformContext.VERSION_12 96 | + 97 | "
\n" + 98 | "

" + 99 | ""); 100 | 101 | container.add(jTextPane); 102 | 103 | int x = (Toolkit.getDefaultToolkit().getScreenSize().width - jFrame.getSize().width) / 2; 104 | int y = (Toolkit.getDefaultToolkit().getScreenSize().height - jFrame.getSize().height) / 2; 105 | jFrame.setLocation(x, y); 106 | // 107 | jFrame.setVisible(true); 108 | }); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/test/java/com/qzw/demo/filemask/FileContentTest.java: -------------------------------------------------------------------------------- 1 | package com.qzw.demo.filemask; 2 | 3 | import com.qzw.filemask.component.GlobalPasswordHolder; 4 | import com.qzw.filemask.fileencoder.FileContentEncoder; 5 | import com.qzw.filemask.model.TailModel; 6 | import com.qzw.filemask.service.TailModelService; 7 | import com.qzw.filemask.util.ByteUtil; 8 | import com.qzw.filemask.service.PasswordService; 9 | import com.qzw.filemask.util.TestUtil; 10 | import org.junit.Assert; 11 | import org.junit.Before; 12 | import org.junit.Test; 13 | 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.io.RandomAccessFile; 17 | import java.util.Base64; 18 | 19 | /** 20 | * @author quanzongwei 21 | * @date 2020/5/16 22 | */ 23 | public class FileContentTest { 24 | 25 | static String filename = "D:\\Data测试\\aaaa.txt"; 26 | static String password = "cccccc"; 27 | static String contentText = "中国中国"; 28 | 29 | @Before 30 | public void setup() { 31 | TestUtil.uuid = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 32 | File parent = new File("D:\\Data测试"); 33 | if (!parent.exists()) { 34 | parent.mkdir(); 35 | } 36 | File file = new File(filename); 37 | if (file.exists()) { 38 | file.delete(); 39 | } 40 | } 41 | 42 | @Test 43 | public void fileContentTest() throws IOException { 44 | GlobalPasswordHolder.setPassword(password); 45 | System.out.println("pass:" + GlobalPasswordHolder.getPassword()); 46 | System.out.println("md51:" + base64(PasswordService.getMd51ForFileAuthentication())); 47 | System.out.println("md523:" + base64(PasswordService.getMd523ForContentEncrypt())); 48 | System.out.println("md545:" + base64(PasswordService.getMd545ForUuidEncrypt())); 49 | System.out.println("uuid:" + TestUtil.uuid); 50 | 51 | FileContentEncoder contentEncoder = new FileContentEncoder(); 52 | File file = new File(filename); 53 | 54 | try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { 55 | System.out.println("文件长度:" + raf.length()); 56 | raf.setLength(0); 57 | raf.write(contentText.getBytes("UTF-8")); 58 | raf.getFD().sync(); 59 | } 60 | 61 | contentEncoder.executeEncrypt(file); 62 | 63 | System.out.println(); 64 | try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { 65 | TailModel model = TailModelService.getExistsTailModelInfo(raf); 66 | Assert.assertEquals(base64(PasswordService.getMd51ForFileAuthentication()), base64(model.getBelongUserMd516())); 67 | Assert.assertEquals(ByteUtil.byteToHex(model.getEncodeType16()), getHexFlagString(false, false, true)); 68 | Assert.assertEquals(new String(model.getUuid32()), TestUtil.uuid); 69 | Assert.assertEquals(base64(model.getHead4()), base64(new byte[]{0, 0, 0, 0})); 70 | Assert.assertTrue(model.getFileNameX().length == 0); 71 | Assert.assertEquals(ByteUtil.bytesToLong(model.getOriginTextSize8()), contentText.getBytes("UTF-8").length); 72 | Assert.assertEquals(new String(model.getTailFlag16()), TailModelService.FILE_MASK_TAIL_FLAG); 73 | raf.getFD().sync(); 74 | } 75 | //解密验证 76 | contentEncoder.executeDecrypt(file); 77 | try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { 78 | boolean exists = TailModelService.existsTailModel(raf); 79 | Assert.assertEquals(exists, false); 80 | Assert.assertEquals(raf.length(), contentText.getBytes("UTF-8").length); 81 | byte[] content = new byte[(int) raf.length()]; 82 | raf.read(content); 83 | Assert.assertEquals(new String(content, "UTF-8"), new StringBuilder(contentText).toString()); 84 | raf.getFD().sync(); 85 | } 86 | 87 | } 88 | 89 | static String base64(byte[] bytes) { 90 | return Base64.getEncoder().encodeToString(bytes); 91 | } 92 | 93 | static String getHexFlagString(boolean name, boolean head, boolean content) { 94 | String s1 = "00"; 95 | if (name) { 96 | s1 = "01"; 97 | } 98 | String s2 = "00"; 99 | if (head) { 100 | s2 = "01"; 101 | } 102 | String s3 = "00"; 103 | if (content) { 104 | s3 = "01"; 105 | } 106 | return new StringBuilder().append(s1).append(s2).append(s3).append("00000000000000000000000000").toString(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/com/qzw/demo/filemask/FileHeadTest.java: -------------------------------------------------------------------------------- 1 | package com.qzw.demo.filemask; 2 | 3 | import com.qzw.filemask.component.GlobalPasswordHolder; 4 | import com.qzw.filemask.fileencoder.FileHeaderEncoder; 5 | import com.qzw.filemask.model.TailModel; 6 | import com.qzw.filemask.service.TailModelService; 7 | import com.qzw.filemask.util.ByteUtil; 8 | import com.qzw.filemask.service.PasswordService; 9 | import com.qzw.filemask.service.PrivateDataService; 10 | import com.qzw.filemask.util.TestUtil; 11 | import org.junit.Assert; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.io.RandomAccessFile; 18 | import java.nio.charset.Charset; 19 | import java.util.Arrays; 20 | import java.util.Base64; 21 | 22 | /** 23 | * @author quanzongwei 24 | * @date 2020/5/16 25 | */ 26 | public class FileHeadTest { 27 | 28 | static String filename = "D:\\Data测试\\aaaa.txt"; 29 | static String password = "cccccc"; 30 | static String headContent = "中国中国中国"; 31 | 32 | @Before 33 | public void setup() throws IOException { 34 | TestUtil.uuid = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 35 | File parent = new File("D:\\Data测试"); 36 | if (!parent.exists()) { 37 | parent.mkdir(); 38 | } 39 | File file = new File(filename); 40 | File privateDataDir = PrivateDataService.getPrivateDataDir(file); 41 | if (privateDataDir.exists()) { 42 | privateDataDir.delete(); 43 | } 44 | if (file.exists()) { 45 | file.delete(); 46 | } 47 | 48 | } 49 | 50 | @Test 51 | public void fileHeadTest() throws IOException { 52 | GlobalPasswordHolder.setPassword(password); 53 | System.out.println("pass:" + GlobalPasswordHolder.getPassword()); 54 | System.out.println("md51:" + base64(PasswordService.getMd51ForFileAuthentication())); 55 | System.out.println("md523:" + base64(PasswordService.getMd523ForContentEncrypt())); 56 | System.out.println("md545:" + base64(PasswordService.getMd545ForUuidEncrypt())); 57 | System.out.println("uuid:" + TestUtil.uuid); 58 | 59 | FileHeaderEncoder headEncoder = new FileHeaderEncoder(); 60 | File file = new File(filename); 61 | 62 | try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { 63 | System.out.println("文件长度:" + raf.length()); 64 | raf.setLength(0); 65 | raf.write(headContent.getBytes("UTF-8")); 66 | } 67 | 68 | headEncoder.executeEncrypt(file); 69 | 70 | System.out.println(); 71 | try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { 72 | TailModel model = TailModelService.getExistsTailModelInfo(raf); 73 | Assert.assertEquals(base64(PasswordService.getMd51ForFileAuthentication()), base64(model.getBelongUserMd516())); 74 | Assert.assertEquals(ByteUtil.byteToHex(model.getEncodeType16()), getHexFlagString(false, true, false)); 75 | Assert.assertEquals(new String(model.getUuid32()), TestUtil.uuid); 76 | Assert.assertTrue(Arrays.equals(model.getHead4(), Arrays.copyOfRange(headContent.getBytes("UTF-8"), 0, 4))); 77 | Assert.assertTrue(model.getFileNameX().length == 0); 78 | Assert.assertEquals(ByteUtil.bytesToLong(model.getOriginTextSize8()), headContent.getBytes("UTF-8").length); 79 | Assert.assertEquals(new String(model.getTailFlag16()), TailModelService.FILE_MASK_TAIL_FLAG); 80 | 81 | } 82 | //解密验证 83 | headEncoder.executeDecrypt(file); 84 | try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { 85 | boolean exists = TailModelService.existsTailModel(raf); 86 | Assert.assertEquals(exists, false); 87 | Assert.assertEquals(raf.length(), headContent.getBytes(Charset.forName("UTF-8")).length); 88 | byte[] content = new byte[(int) raf.length()]; 89 | raf.read(content); 90 | Assert.assertEquals(new String(content, "UTF-8"), headContent); 91 | } 92 | } 93 | 94 | static String base64(byte[] bytes) { 95 | return Base64.getEncoder().encodeToString(bytes); 96 | } 97 | 98 | static String getHexFlagString(boolean name, boolean head, boolean content) { 99 | String s1 = "00"; 100 | if (name) { 101 | s1 = "01"; 102 | } 103 | String s2 = "00"; 104 | if (head) { 105 | s2 = "01"; 106 | } 107 | String s3 = "00"; 108 | if (content) { 109 | s3 = "01"; 110 | } 111 | return new StringBuilder().append(s1).append(s2).append(s3).append("00000000000000000000000000").toString(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/java/com/qzw/demo/filemask/FileNameTest.java: -------------------------------------------------------------------------------- 1 | package com.qzw.demo.filemask; 2 | 3 | import com.qzw.filemask.component.GlobalPasswordHolder; 4 | import com.qzw.filemask.fileencoder.FileOrDirNameEncoder; 5 | import com.qzw.filemask.model.TailModel; 6 | import com.qzw.filemask.service.TailModelService; 7 | import com.qzw.filemask.util.ByteUtil; 8 | import com.qzw.filemask.service.PasswordService; 9 | import com.qzw.filemask.service.PrivateDataService; 10 | import com.qzw.filemask.util.TestUtil; 11 | import org.junit.Assert; 12 | import org.junit.Before; 13 | import org.junit.Test; 14 | 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.io.RandomAccessFile; 18 | import java.util.Base64; 19 | 20 | /** 21 | * @author quanzongwei 22 | * @date 2020/5/16 23 | */ 24 | public class FileNameTest { 25 | 26 | static String filename = "D:\\Data测试\\aaaa.txt"; 27 | static String password = "cccccc"; 28 | static String contentText = "中国中国"; 29 | 30 | @Before 31 | public void setup() throws IOException { 32 | TestUtil.uuid = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; 33 | File parent = new File("D:\\Data测试"); 34 | if (!parent.exists()) { 35 | parent.mkdir(); 36 | } 37 | File file = new File(filename); 38 | File privateDataDir = PrivateDataService.getPrivateDataDir(file); 39 | if (privateDataDir.exists()) { 40 | privateDataDir.delete(); 41 | } 42 | if (file.exists()) { 43 | file.delete(); 44 | } 45 | 46 | } 47 | 48 | @Test 49 | public void fileNameTest() throws IOException { 50 | GlobalPasswordHolder.setPassword(password); 51 | System.out.println("pass:" + GlobalPasswordHolder.getPassword()); 52 | System.out.println("md51:" + base64(PasswordService.getMd51ForFileAuthentication())); 53 | System.out.println("md523:" + base64(PasswordService.getMd523ForContentEncrypt())); 54 | System.out.println("md545:" + base64(PasswordService.getMd545ForUuidEncrypt())); 55 | System.out.println("uuid:" + TestUtil.uuid); 56 | 57 | FileOrDirNameEncoder nameEncoder = new FileOrDirNameEncoder(); 58 | File file = new File(filename); 59 | 60 | try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { 61 | System.out.println("文件长度:" + raf.length()); 62 | raf.setLength(0); 63 | raf.write(contentText.getBytes("UTF-8")); 64 | raf.getFD().sync(); 65 | } 66 | 67 | nameEncoder.executeEncrypt(file); 68 | 69 | System.out.println(); 70 | Integer sequence = PrivateDataService.getAutoIncrementSequence4ParentDir(file); 71 | sequence--; 72 | try (RandomAccessFile raf = new RandomAccessFile(file.getParent() + File.separatorChar + sequence, "rw")) { 73 | TailModel model = TailModelService.getExistsTailModelInfo(raf); 74 | Assert.assertEquals(base64(PasswordService.getMd51ForFileAuthentication()), base64(model.getBelongUserMd516())); 75 | Assert.assertEquals(ByteUtil.byteToHex(model.getEncodeType16()), getHexFlagString(true, false, false)); 76 | Assert.assertEquals(new String(model.getUuid32()), TestUtil.uuid); 77 | Assert.assertEquals(base64(model.getHead4()), base64(new byte[]{0, 0, 0, 0})); 78 | Assert.assertTrue(model.getFileNameX().length == "aaaa.txt".getBytes("UTF-8").length); 79 | Assert.assertEquals(ByteUtil.bytesToLong(model.getOriginTextSize8()), contentText.getBytes("UTF-8").length); 80 | Assert.assertEquals(new String(model.getTailFlag16()), TailModelService.FILE_MASK_TAIL_FLAG); 81 | raf.getFD().sync(); 82 | } 83 | //解密验证 84 | nameEncoder.executeDecrypt(new File(file.getParent() + File.separatorChar + sequence)); 85 | try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { 86 | boolean exists = TailModelService.existsTailModel(raf); 87 | Assert.assertEquals(exists, false); 88 | Assert.assertEquals(raf.length(), contentText.getBytes("UTF-8").length); 89 | byte[] content = new byte[(int) raf.length()]; 90 | raf.read(content); 91 | Assert.assertEquals(new String(content, "UTF-8"), new StringBuilder(contentText).toString()); 92 | raf.getFD().sync(); 93 | } 94 | 95 | } 96 | 97 | static String base64(byte[] bytes) { 98 | return Base64.getEncoder().encodeToString(bytes); 99 | } 100 | 101 | static String getHexFlagString(boolean name, boolean head, boolean content) { 102 | String s1 = "00"; 103 | if (name) { 104 | s1 = "01"; 105 | } 106 | String s2 = "00"; 107 | if (head) { 108 | s2 = "01"; 109 | } 110 | String s3 = "00"; 111 | if (content) { 112 | s3 = "01"; 113 | } 114 | return new StringBuilder().append(s1).append(s2).append(s3).append("00000000000000000000000000").toString(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | com.qzw.software 8 | fileMask 9 | 1.0-SNAPSHOT 10 | 11 | fileMask 12 | 13 | https://github.com/quanzongwei/fileMask 14 | 15 | 16 | UTF-8 17 | 1.7 18 | 1.7 19 | 20 | 21 | 22 | 23 | junit 24 | junit 25 | 4.12 26 | 27 | 28 | org.projectlombok 29 | lombok 30 | 1.18.10 31 | 32 | 33 | 34 | 35 | org.slf4j 36 | slf4j-api 37 | 1.7.21 38 | 39 | 40 | 41 | org.apache.logging.log4j 42 | log4j-api 43 | 2.9.0 44 | 45 | 46 | org.apache.logging.log4j 47 | log4j-core 48 | 2.9.0 49 | 50 | 51 | 52 | org.apache.logging.log4j 53 | log4j-slf4j-impl 54 | 2.9.0 55 | 61 | 62 | 63 | 64 | 65 | com.lmax 66 | disruptor 67 | 3.3.5 68 | 69 | 70 | 71 | org.apache.commons 72 | commons-lang3 73 | 3.7 74 | 75 | 76 | 83 | 84 | 85 | 86 | com.jgoodies 87 | jgoodies-common 88 | 1.8.1 89 | 90 | 91 | 92 | com.jgoodies 93 | looks 94 | 2.2.2 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | maven-clean-plugin 105 | 3.1.0 106 | 107 | 108 | maven-resources-plugin 109 | 3.0.2 110 | 111 | 112 | maven-compiler-plugin 113 | 3.8.0 114 | 115 | 116 | maven-surefire-plugin 117 | 2.22.1 118 | 119 | 120 | maven-jar-plugin 121 | 3.0.2 122 | 123 | 124 | maven-install-plugin 125 | 2.5.2 126 | 127 | 128 | maven-deploy-plugin 129 | 2.8.2 130 | 131 | 132 | maven-site-plugin 133 | 3.7.1 134 | 135 | 136 | maven-project-info-reports-plugin 137 | 3.0.0 138 | 139 | 140 | 141 | 142 | 143 | org.apache.maven.plugins 144 | maven-compiler-plugin 145 | 146 | 8 147 | 8 148 | 149 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /Part 2 核心加密思想.md: -------------------------------------------------------------------------------- 1 | ## 一 用户密码生成多个md5数据 2 | 比如用户密码是123456 3 | 则生成以下md5数据 4 | 1. **md5-0**: 执行函数MD5("123456"), 用于用户登录验证,该数据保存在软件目录下authentication文件夹中 5 | 2. **md5-1**: 执行函数MD5("123456"+"1"), 每个被加密的文件末尾都会记录用户的**md5-1**的值.只有指定用户可以执行解密操作 6 | 3. **md5-23**:生成32为的字节数据,用于生成**内容加密秘钥**,生成代码如下: 7 | ``` 8 | public static byte[] getMd523ForContentEncrypt() { 9 | byte[] bytes32 = new byte[32]; 10 | byte[] md5Bytes1 = MD5Utils.getMd5Bytes(getPassword() + 2); 11 | byte[] md5Bytes2 = MD5Utils.getMd5Bytes(getPassword() + 3); 12 | System.arraycopy(md5Bytes1, 0, bytes32, 0, 16); 13 | System.arraycopy(md5Bytes2, 0, bytes32, 16, 16); 14 | return bytes32; 15 | } 16 | ``` 17 | 18 | 4. **md5-45**:生成32字节数据,作为**uuid加密秘钥**,生成代码如下: 19 | ``` 20 | public static byte[] getMd545ForUuidEncrypt() { 21 | byte[] md5Bytes4 = new byte[32]; 22 | byte[] md5Bytes1 = MD5Utils.getMd5Bytes(getPassword() + 4); 23 | byte[] md5Bytes2 = MD5Utils.getMd5Bytes(getPassword() + 5); 24 | System.arraycopy(md5Bytes1, 0, md5Bytes4, 0, 16); 25 | System.arraycopy(md5Bytes2, 0, md5Bytes4, 16, 16); 26 | return md5Bytes4; 27 | } 28 | ``` 29 | 30 | ## 二 文件尾部数据结构 31 | 加密文件成功后,会在文件尾部加上一段字节数据,数据结构如下所示: 32 | ![image](https://github.com/quanzongwei/markdown-picture/blob/main/%E7%AE%97%E6%B3%95-%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.png) 33 | 34 | **区域A**:16字节,存储**md5-1**数据,解密的时候,校验文件是否被当前用户加密,如果是,则允许解密 35 | **区域B**:16字节,存储标志位,前面三个字节为0x01的时候分别表示使用了文件名称加密,文件头部加密和文件内容加密,剩余的13字节为保留字段 36 | **区域C**:32字节,保存每次加密的时候生成的随机数经过**uuid加密秘钥**加密后的密文,该随机数,用于生成**内容加密秘钥** 37 | **区域D**: 4字节,文件头部加密的时候,保存了使用**内容加密秘钥**加密后的原始头部数据 38 | **区域E**: N字节,保存了使用**内容加密秘钥**加密后的**原始文件名称**数据,文件名称长度不固定,所以可能N字节 39 | **区域F**: 8字节,长整型数据(Long),保存了原始文件长度,需要这个区域的原因是,区域E的长度不确定,导致尾部数据结构长度不确定,所以要记录原始文件长度 40 | **区域G**:16字节,保存字符串**"FileMaskTailFlag"**字符串的字节数据,用于标识该文件是使用FileMask软件加密过的文件 41 | 42 | ## 三 两个重要秘钥 43 | ### 3.1 **uuid加密秘钥** 44 | 其中**uuid加密秘钥**即为: **md5-45**(如上文所示) 45 | 区域C存储的是uuid经过**uuid加密秘钥**加密后的密文, 原文数据不能直接暴露,这样做的主要目的是防止明文攻击 46 | 47 | uuid密文生成过程如下所示: 48 | ``` 49 | uuid明文 xor md5-45 = uuid密文 50 | ``` 51 | ### 3.2 **内容加密秘钥** 52 | **内容加密秘钥**生成方式如下: 53 | ``` 54 | uuid明文 xor md5-23 = 内容加密秘钥 55 | ``` 56 | 57 | **内容加密秘钥**的使用场景 58 | 1. 文件名称加密,加密文件名称,如下所示: 59 | ``` 60 | 文件名称明文 xor 内容加密秘钥 = 文件名称密文 61 | ``` 62 | 2. 文件头部加密,加密文件头部四个字节的数据,如下所示: 63 | ``` 64 | 文件头部明文 xor 内容加密秘钥 = 文件头部密文 65 | ``` 66 | 3. 文件内容加密(全文加密),加密文件中每一个字节都被加密 67 | ``` 68 | 文件全文明文 xor 内容加密秘钥 = 文件全文密文 69 | ``` 70 | 71 | ## 四 加密解密流程 72 | ### 4.1 加密流程 73 | 1. 根据用户密码生成**md5-0**(16字节),**md5-1**(16字节),**md5-23**(32字节),**md5-45**(32字节)数据 74 | 2. 随机生成32字节的uuid字节数据 75 | 3. uuid原文 xor **md5-23**得到**内容加密秘钥** 76 | 4. **md5-1**的字节数据填入区域A 77 | 6. 得到uuid密文,填入区域C,计算方式(uuid明文 xor **md5-45**=uuid密文) 78 | 5. 设置加密标志字节 79 | 1. 如果是文件名称加密,区域B的第一个字节设置为0x01 80 | 2. 如果是文件头部加密,区域B的第二个字节设置为0x01 81 | 3. 如果是文件内容加密,区域B的第三个字节设置为0x01 82 | 6. 执行加密 83 | 1. 如果是文件头部加密,则将头部四个字节使用**内容加密秘钥**加密后填入区域D,计算方式(文件头部明文 xor **内容加密秘钥**=文件头部密文) 84 | 2. 如果是文件名称加密,则将文件名称使用**内容加密秘钥**加密后填入区域E,计算方式(文件内容明文 xor **内容加密秘钥** = 文件密文); 接着文件名称重命名为当前文件夹下的唯一自增序号 85 | 3. 如果是文件内容加密则将文件内容全部使用**内容加密秘钥**加密后,在原文所在位置.替换文件原始内容,计算方式(文件内容明文 xor **内容加密秘钥** = 文件密文) 86 | 7. 计算原始文件长度,填入区域F 87 | 8. 将**"FileMaskTailFlag"**字符串的字节数据写入区域G 88 | 89 | 到此为止, 加密操作完成, 请注意, 区域C使用**md5-45**加密后填入,区域D和区域E使用**内容加密秘钥**加密后填入,尾部数据结构中仅此三处区域是密文,其他区域无需加密 90 | 91 | ### 4.2 解密流程 92 | 1. 根据用户密码生成**md5-0**(16字节),**md5-1**(16字节),**md5-23**(32字节),**md5-45**(32字节)数据 93 | 2. 解析文件尾部,从区域G中读取16字节的字符串数据,如果为**"FileMaskTailFlag"**,则表示该文件被加密,否则无需解密 94 | 3. 解析区域F,获取原文长度,然后得到整个尾部数据结构 95 | 4. 获取区域A,和用户**md5-1**比较,如果相同,表示该文件是当前用户加密的,可以执行解密,否则解密不成功 96 | 5. 获取区域C中uuid的密文,使用**md5-45**进行解密得到uuid原文,计算方式(uuid密文 xor **md5-45** = uuid明文) 97 | 6. 根据uuid明文,得到**内容加密秘钥**,计算方式(uuid明文 xor **md5-23**=**内容加密秘钥**) 98 | 7. 获取加密标志字节 99 | 1. 获取区域B中第一个字节数据,为0x01表示文件已使用文件名称加密 100 | 2. 获取区域B中第二个字节数据,为0x01表示文件已使用文件头部加密 101 | 3. 获取区域B中第三个字节数据,为0x01表示文件已使用文件内容加密 102 | 8. 执行解密 103 | 1. 如果解密的是文件头部,则将区域D中的文件头部密文得到文件头部原文,计算方式(文件头部密文 xor **内容加密秘钥**=文件头部原文),然后将文件头部原始数据写入文件头部 104 | 2. 如果解密的是文件名称, 则将区域E中的文件名称密文得到文件名称原文,计算方式(文件名称密文 xor **内容加密秘钥**=文件名称原文),然后对文件重命名 105 | 3. 如果解密的是文件内容,则将文件的原始内容逐个字节使用**内容加密秘钥**解密,计算方式(文件内容密文 xor **内容加密秘钥**=文件内容原文) 106 | 9. 最后解密成功后,根据区域F中保存的原文长度信息, 将整个尾部数据结构全部删除,数据恢复到原始状态 107 | 108 | 109 | 110 | ## 五 答疑 111 | ### 5.1 为什么说该软件加密速度极快 112 | ### 1. 文件名称加密 113 | 加密过程为: 114 | ``` 115 | 文件名称原文 xor 内容加密秘钥 = 文件名称密文 116 | ``` 117 | 只需要执行一次xor操作,和一次重命名操作即可,耗时是毫秒级 118 | #### 2. 文件头部加密 119 | 文件头部加密,作者使用了非常巧妙的方式,只加密头部四个字节 120 | 加密过程为: 121 | ``` 122 | 文件头部原文 xor 内容加密秘钥 = 文件头部密文 123 | ``` 124 | 这四个字节替换为FF FE 00 00,这四个字节头部表示UTF-32(little-endian编码),将文件标识为UTF-32编码,导致目前所有文本文件(BGK,UTF-8,ISO-8859-1)等等编码的文件,都会以UTF-32编码的方式被打开,于是显示乱码; 对于其他类型的文件(比如mp4,mp3,png,jpg),都会被误认为是UTF-32的文本文件,自然是无法打开的,所以文件头部加密,不保证绝对安全性,但是速度极快,只需要加密文件头部四个字节,应付日常生活中的普通的文件加密场景,已经是绰绰有余了 125 | 126 | 加密耗时是毫秒级,所以说速度极快 127 | #### 3. 文件内容加密 128 | 加密过程 129 | ``` 130 | 文件内容原文 xor 内容加密秘钥 = 文件内容密文 131 | ``` 132 | 相对于前两种加密方式,该方式较为耗时,因为他是对原文所有的字节数据执行加密,但是相对于当今世界上的对称加密算法(如:DES之类算法),他的加密速度是极快的,在保证绝对安全的前提下,只执行一次xor操作就能实现加密.而其他对称加密算法,一般都会经过很多轮的数据变换(映射, xor之类的),而我们只需要一轮,所以它快 133 | ### 5.2 为什么说该软件加密保证绝对的安全性(重点) 134 | 只要是使用**内容加密秘钥**加密过的数据,无法通过任何手段,任何工具进行破解,原理如下: 135 | 1. xor加密,在秘钥足够长的情况下,保证绝对的安全性(该软件使用32字节长度的秘钥) 136 | 2. xor唯一的缺点是无法防止已知明文攻击 137 | 138 | 该软件使用uuid和**md5-23**生成一个文件唯一的专属**内容加密秘钥**,由于每个文件生成的uuid会不同,所以,已知一个文件的明文和密文,得到的秘钥无法应用于其他的文件,因为其他文件的uuid是不一样的; 139 | 同时uuid数据使用**md5-45**进行加密, 导致uuid只有密文暴露出去,在只知道密文的情况下,无法推测出明文和秘钥,所以uuid明文无法被推测出,于是用户的**md5-23**绝对安全.所以被**内容加密秘钥**加密过的数据,具有绝对的安全性(这里包括:被加密的文件名称, 文件头部四个字节的数据,以及全文加密的所有内容) 140 | 141 | 简单理解就是,该软件使用加密的辅助数据uuid,防止了xor算法的已知明文攻击,最终保证了绝对的安全性 142 | 143 | ### 5.3 文件头部加密是不是不能保证绝对安全? 144 | 答案:是的 145 | 146 | 他只加密了前四个字节,只保证前四个字节的绝对安全,其他剩余内容都是明文,但是对于一般的用户,这种加密方式已经足够用了. 147 | 148 | 花絮: 作者有一段时间学习UTF编码,对UTF编码较为熟悉,然后写加密软件的时候突发奇想,仅仅通过替换前四个字节,把文件伪装成UTF-32编码的文件, 就达到很好的加密效果,所以作者为什么说这是一个富有思想的加密软件(它是有灵魂的哈哈) 149 | 150 | ### 5.4 为什么说用户体验良好? 151 | 作者站在用户的角度思考问题,其实作者本身就是最大的用户:比如级联加密,级联解密,帮助文档,操作日志等等功能可以极大的提高用户体验 152 | 153 | ### 5.5 为什么不加上修改密码的功能? 154 | 作者考虑到,密码修改后,使用原密码加密的文件是无法通过新密码解密的,于是为了避免加密后的文件无法解密,暂时不提供修改密码的功能.本质上就是希望用户第一次就把密码设置好 155 | 156 | ### 5.6 文件夹名称加密如何实现? 157 | 聪明的读者可能会有这个疑问, 文件夹和文件不同, 不像文件可以在尾部追加加密后的数据, 作者的处理方式如下: 158 | * 在文件夹相同的目录下生成一个.fileMask的隐藏文件夹 159 | * 该隐藏文件夹中生成一个和文件夹一样名字的文件 160 | * 对文件夹的名称加密时, 会将加密后的数据,以及其他数据结构写入.fileMask文件夹下同名的文件 161 | 162 | ### 5.7 文件名称加密如何保证不重名? 163 | 每个文件夹下会生成一个.fileMask隐藏文件夹, 会生成一个.fmvalue文件, 该文件记录属于文件夹的自增Id, 每次对文件夹中的一个文件进行文件名称加密, 被加密的文件被命名为该自增Id, 同时自增Id的值加1, 164 | 以此来保证同个文件夹下各个被加密的文件名称不重复 165 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/gui/ButtonActionFactory.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.gui; 2 | 3 | import com.qzw.filemask.FileMaskMain; 4 | import com.qzw.filemask.enums.ChooseTypeEnum; 5 | import com.qzw.filemask.fileencoder.AbstractFileEncoder; 6 | import com.qzw.filemask.fileencoder.FileContentEncoder; 7 | import com.qzw.filemask.fileencoder.FileHeaderEncoder; 8 | import com.qzw.filemask.fileencoder.FileOrDirNameEncoder; 9 | import com.qzw.filemask.service.WorkFlowService; 10 | import lombok.extern.log4j.Log4j2; 11 | 12 | import javax.swing.*; 13 | import javax.swing.filechooser.FileSystemView; 14 | import java.io.File; 15 | 16 | /** 17 | * @author quanzongwei 18 | * @date 2020/2/24 19 | */ 20 | @Log4j2 21 | public class ButtonActionFactory { 22 | 23 | private static JFileChooser jFileChooser = new JFileChooser(); 24 | 25 | /** 26 | * 加密类型一(文件名称加密):文件夹级联加密 27 | */ 28 | public static void btn11(JButton btn) { 29 | btn.addActionListener(e -> { 30 | String targetDir = directoryAndFileChoose(JFileChooser.DIRECTORIES_ONLY, "请选择文件夹..."); 31 | doEncrypt(targetDir, new FileOrDirNameEncoder(), ChooseTypeEnum.CASCADE_DIR); 32 | }); 33 | } 34 | 35 | 36 | /** 37 | * 加密类型一(文件名称加密):文件夹加密 38 | */ 39 | public static void btn12(JButton btn) { 40 | btn.addActionListener(e -> { 41 | String targetDir = directoryAndFileChoose(JFileChooser.DIRECTORIES_ONLY, "请选择文件夹..."); 42 | doEncrypt(targetDir, new FileOrDirNameEncoder(), ChooseTypeEnum.CURRENT_DIR_ONLY); 43 | }); 44 | } 45 | 46 | /** 47 | * 加密类型一(文件名称加密):文件加密 48 | */ 49 | public static void btn13(JButton btn) { 50 | btn.addActionListener(e -> { 51 | String targetDir = directoryAndFileChoose(JFileChooser.FILES_ONLY, "请选择文件..."); 52 | doEncrypt(targetDir, new FileOrDirNameEncoder(), ChooseTypeEnum.FILE_ONLY); 53 | }); 54 | } 55 | 56 | /** 57 | * 加密类型二(文件头部加密):文件夹级联加密 58 | */ 59 | public static void btn21(JButton btn) { 60 | btn.addActionListener(e -> { 61 | String targetDir = directoryAndFileChoose(JFileChooser.DIRECTORIES_ONLY, "请选择文件夹..."); 62 | doEncrypt(targetDir, new FileHeaderEncoder(), ChooseTypeEnum.CASCADE_DIR); 63 | }); 64 | } 65 | 66 | /** 67 | * 加密类型二(文件头部加密):文件夹加密 68 | */ 69 | public static void btn22(JButton btn) { 70 | btn.addActionListener(e -> { 71 | String targetDir = directoryAndFileChoose(JFileChooser.DIRECTORIES_ONLY, "请选择文件夹..."); 72 | doEncrypt(targetDir, new FileHeaderEncoder(), ChooseTypeEnum.CURRENT_DIR_ONLY); 73 | }); 74 | } 75 | 76 | /** 77 | * 加密类型二(文件头部加密):文件加密 78 | */ 79 | public static void btn23(JButton btn) { 80 | btn.addActionListener(e -> { 81 | String targetDir = directoryAndFileChoose(JFileChooser.FILES_ONLY, "请选择文件..."); 82 | doEncrypt(targetDir, new FileHeaderEncoder(), ChooseTypeEnum.FILE_ONLY); 83 | }); 84 | } 85 | 86 | /** 87 | * 加密类型三(文件内容加密):文件夹级联加密 88 | */ 89 | public static void btn31(JButton btn) { 90 | btn.addActionListener(e -> { 91 | String targetDir = directoryAndFileChoose(JFileChooser.DIRECTORIES_ONLY, "请选择文件夹..."); 92 | doEncrypt(targetDir, new FileContentEncoder(), ChooseTypeEnum.CASCADE_DIR); 93 | }); 94 | } 95 | 96 | /** 97 | * 加密类型三(文件内容加密):文件夹加密 98 | */ 99 | public static void btn32(JButton btn) { 100 | btn.addActionListener(e -> { 101 | String targetDir = directoryAndFileChoose(JFileChooser.DIRECTORIES_ONLY, "请选择文件夹..."); 102 | doEncrypt(targetDir, new FileContentEncoder(), ChooseTypeEnum.CURRENT_DIR_ONLY); 103 | }); 104 | } 105 | 106 | /** 107 | * 加密类型三(文件内容加密):文件加密 108 | */ 109 | public static void btn33(JButton btn) { 110 | btn.addActionListener(e -> { 111 | String targetDir = directoryAndFileChoose(JFileChooser.FILES_ONLY, "请选择文件..."); 112 | doEncrypt(targetDir, new FileContentEncoder(), ChooseTypeEnum.FILE_ONLY); 113 | }); 114 | } 115 | 116 | /** 117 | * 加密 118 | */ 119 | private static void doEncrypt(String targetFileOrDir, AbstractFileEncoder fileEncoder, ChooseTypeEnum chooseTypeEnum) { 120 | WorkFlowService.doEncryptOrDecrypt(targetFileOrDir, fileEncoder, chooseTypeEnum, true); 121 | } 122 | 123 | /** 124 | * 解密:文件夹级联解密 125 | */ 126 | public static void btn41(JButton button) { 127 | button.addActionListener(e -> { 128 | String targetFileOrDir = directoryAndFileChoose(JFileChooser.DIRECTORIES_ONLY, "请选择文件夹..."); 129 | doDecrypt(targetFileOrDir, ChooseTypeEnum.CASCADE_DIR); 130 | }); 131 | } 132 | 133 | /** 134 | * 解密:文件夹解密 135 | */ 136 | public static void btn42(JButton button) { 137 | button.addActionListener(e -> { 138 | String targetFileOrDir = directoryAndFileChoose(JFileChooser.DIRECTORIES_ONLY, "请选择文件夹..."); 139 | doDecrypt(targetFileOrDir, ChooseTypeEnum.CURRENT_DIR_ONLY); 140 | }); 141 | } 142 | 143 | /** 144 | * 解密:文件解密 145 | */ 146 | public static void btn43(JButton button) { 147 | button.addActionListener(e -> { 148 | String targetFileOrDir = directoryAndFileChoose(JFileChooser.FILES_ONLY, "请选择文件..."); 149 | doDecrypt(targetFileOrDir, ChooseTypeEnum.FILE_ONLY); 150 | }); 151 | } 152 | 153 | private static void doDecrypt(String targetFileOrDir, ChooseTypeEnum chooseTypeEnum) { 154 | WorkFlowService.doEncryptOrDecrypt(targetFileOrDir, null, chooseTypeEnum, false); 155 | } 156 | 157 | /** 158 | * GUI文件选择组件 159 | */ 160 | private static String directoryAndFileChoose(int fileSelectMode, String message) { 161 | //选择文件夹 162 | JFileChooser fileChooser = jFileChooser; 163 | FileSystemView fsv = FileSystemView.getFileSystemView(); 164 | File tmpDir = new File(fsv.getDefaultDirectory().getPath() + File.separatorChar + "fileMask"); 165 | if (!tmpDir.exists()) { 166 | tmpDir.mkdir(); 167 | } 168 | fileChooser.setCurrentDirectory(tmpDir); 169 | fileChooser.setDialogTitle(message); 170 | fileChooser.setApproveButtonText("确定"); 171 | fileChooser.setFileSelectionMode(fileSelectMode); 172 | int result = fileChooser.showOpenDialog(null); 173 | if (JFileChooser.APPROVE_OPTION == result) { 174 | String path = fileChooser.getSelectedFile().getPath(); 175 | fileChooser.setEnabled(false); 176 | return path; 177 | } else if (JFileChooser.CANCEL_OPTION == result) { 178 | return null; 179 | } else { 180 | return null; 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/resources/log4j2.properties: -------------------------------------------------------------------------------- 1 | status = warn 2 | name = SDPSAFServer 3 | 4 | property.patternLayout = %d{yyyy-MM-dd HH:mm:ss,SSS} %5p %X{RequestId} - %m%n 5 | property.commonLogName = logs/common.log 6 | property.requestSlowLogName = logs/request_slow.log 7 | property.requestErrorLogName = logs/request_error.log 8 | property.requestInfoLogName = logs/request_info.log 9 | property.5xxErrorLogName = logs/5xx_error.log 10 | property.commonLogNamePattern = logs/$${date:yyyy-MM}/common-%d{yyyy-MM-dd}-%i.log.gz 11 | property.requestSlowLogNamePattern = logs/$${date:yyyy-MM}/request_slow-%d{yyyy-MM-dd}-%i.log.gz 12 | property.requestInfoLogNamePattern = logs/$${date:yyyy-MM}/request_info-%d{yyyy-MM-dd}-%i.log.gz 13 | property.requestErrorLogNamePattern = logs/$${date:yyyy-MM}/request_error-%d{yyyy-MM-dd}-%i.log.gz 14 | property.5xxErrorLogNamePattern = logs/$${date:yyyy-MM}/5xx_error-%d{yyyy-MM-dd}-%i.log.gz 15 | 16 | filters = threshold 17 | filter.threshold.type = ThresholdFilter 18 | filter.threshold.level = debug 19 | 20 | #\u751F\u4EA7\u73AF\u5883\u8BF7\u5173\u95EDstdout\u8F93\u51FA 21 | appenders = console, common, requestSlow, requestError, 5xxError 22 | #appenders = common, requestSlow, requestError, 5xxError,requestInfo 23 | appender.console.type = Console 24 | appender.console.name = STDOUT 25 | appender.console.target = SYSTEM_OUT 26 | appender.console.layout.type = PatternLayout 27 | appender.console.layout.pattern = %d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n 28 | 29 | appender.common.type = RollingFile 30 | appender.common.name = commonLogAppender 31 | appender.common.fileName = ${commonLogName} 32 | appender.common.filePattern = ${commonLogNamePattern} 33 | appender.common.layout.type = PatternLayout 34 | appender.common.layout.pattern = ${patternLayout} 35 | appender.common.policies.type = Policies 36 | appender.common.policies.time.type = TimeBasedTriggeringPolicy 37 | appender.common.policies.time.interval = 1 38 | appender.common.policies.time.modulate = true 39 | appender.common.policies.size.type = SizeBasedTriggeringPolicy 40 | appender.common.policies.size.size=500MB 41 | appender.common.strategy.type = DefaultRolloverStrategy 42 | appender.common.strategy.max = 20 43 | appender.common.immediateFlush = true 44 | 45 | 46 | appender.requestSlow.type = RollingFile 47 | appender.requestSlow.name = requestSlowLogAppender 48 | appender.requestSlow.fileName = ${requestSlowLogName} 49 | appender.requestSlow.filePattern = ${requestSlowLogNamePattern} 50 | appender.requestSlow.layout.type = PatternLayout 51 | appender.requestSlow.layout.pattern = ${patternLayout} 52 | appender.requestSlow.policies.type = Policies 53 | appender.requestSlow.policies.time.type = TimeBasedTriggeringPolicy 54 | appender.requestSlow.policies.time.interval = 1 55 | appender.requestSlow.policies.time.modulate = true 56 | appender.requestSlow.policies.size.type = SizeBasedTriggeringPolicy 57 | appender.requestSlow.policies.size.size=500MB 58 | appender.requestSlow.strategy.type = DefaultRolloverStrategy 59 | appender.requestSlow.strategy.max = 20 60 | appender.requestSlow.immediateFlush = true 61 | 62 | appender.requestError.type = RollingFile 63 | appender.requestError.name = requestErrorLogAppender 64 | appender.requestError.fileName = ${requestErrorLogName} 65 | appender.requestError.filePattern = ${requestErrorLogNamePattern} 66 | appender.requestError.layout.type = PatternLayout 67 | appender.requestError.layout.pattern = ${patternLayout} 68 | appender.requestError.policies.type = Policies 69 | appender.requestError.policies.time.type = TimeBasedTriggeringPolicy 70 | appender.requestError.policies.time.interval = 1 71 | appender.requestError.policies.time.modulate = true 72 | appender.requestError.policies.size.type = SizeBasedTriggeringPolicy 73 | appender.requestError.policies.size.size=500MB 74 | appender.requestError.strategy.type = DefaultRolloverStrategy 75 | appender.requestError.strategy.max = 20 76 | appender.requestError.immediateFlush = true 77 | 78 | appender.5xxError.type = RollingFile 79 | appender.5xxError.name = 5xxErrorLogAppender 80 | appender.5xxError.fileName = ${5xxErrorLogName} 81 | appender.5xxError.filePattern = ${5xxErrorLogNamePattern} 82 | appender.5xxError.layout.type = PatternLayout 83 | appender.5xxError.layout.pattern = ${patternLayout} 84 | appender.5xxError.policies.type = Policies 85 | appender.5xxError.policies.time.type = TimeBasedTriggeringPolicy 86 | appender.5xxError.policies.time.interval = 1 87 | appender.5xxError.policies.time.modulate = true 88 | appender.5xxError.policies.size.type = SizeBasedTriggeringPolicy 89 | appender.5xxError.policies.size.size=500MB 90 | appender.5xxError.strategy.type = DefaultRolloverStrategy 91 | appender.5xxError.strategy.max = 20 92 | appender.5xxError.immediateFlush = true 93 | 94 | 95 | appender.requestInfo.type = RollingFile 96 | appender.requestInfo.name = requestInfoLogAppender 97 | appender.requestInfo.fileName = ${requestInfoLogName} 98 | appender.requestInfo.filePattern = ${requestInfoLogNamePattern} 99 | appender.requestInfo.layout.type = PatternLayout 100 | appender.requestInfo.layout.pattern = ${patternLayout} 101 | appender.requestInfo.policies.type = Policies 102 | appender.requestInfo.policies.time.type = TimeBasedTriggeringPolicy 103 | appender.requestInfo.policies.time.interval = 1 104 | appender.requestInfo.policies.time.modulate = true 105 | appender.requestInfo.policies.size.type = SizeBasedTriggeringPolicy 106 | appender.requestInfo.policies.size.size=500MB 107 | appender.requestInfo.strategy.type = DefaultRolloverStrategy 108 | appender.requestInfo.strategy.max = 20 109 | appender.requestInfo.immediateFlush = true 110 | 111 | 112 | 113 | loggers = 5xx,slow,slowHttp,slowDB,requestError,requestInfo 114 | logger.5xx.name = com.qzw.gaea.rest.exceptions 115 | logger.5xx.level = ERROR 116 | logger.5xx.additivity = false 117 | logger.5xx.appenderRef.5xxErrorLogAppender.ref = 5xxErrorLogAppender 118 | 119 | logger.slow.name = com.qzw.ProfileAspect 120 | logger.slow.level = INFO 121 | logger.slow.additivity = false 122 | logger.slow.appenderRef.requestSlowLogAppender.ref = requestSlowLogAppender 123 | 124 | logger.slowHttp.name = com.qzw.WafHttpClient 125 | logger.slowHttp.level = INFO 126 | logger.slowHttp.additivity = false 127 | logger.slowHttp.appenderRef.requestSlowLogAppender.ref = requestSlowLogAppender 128 | 129 | logger.slowDB.name = com.qzw.RequestSlowFilter 130 | logger.slowDB.level = INFO 131 | logger.slowDB.additivity = false 132 | logger.slowDB.appenderRef.requestSlowLogAppender.ref = requestSlowLogAppender 133 | 134 | logger.requestError.name = com.qzw.exception 135 | logger.requestError.level = ERROR 136 | logger.requestError.additivity = false 137 | logger.requestError.appenderRef.requestErrorLogAppender.ref = requestErrorLogAppender 138 | 139 | 140 | logger.requestInfo.name = com.qzw.ReqAspect 141 | logger.requestInfo.level = INFO 142 | logger.requestInfo.additivity = false 143 | logger.requestInfo.appenderRef.requestInfoLogAppender.ref = requestInfoLogAppender 144 | 145 | 146 | rootLogger.level = INFO 147 | #\u751F\u4EA7\u73AF\u5883\u8BF7\u5173\u95EDstdout\u8F93\u51FA 148 | rootLogger.appenderRef.stdout.ref = STDOUT 149 | rootLogger.appenderRef.commonLogAppender.ref = commonLogAppender -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/FileMaskMain.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask; 2 | 3 | import com.qzw.filemask.component.PlatformContext; 4 | import com.qzw.filemask.enums.PlatformEnum; 5 | import com.qzw.filemask.gui.ButtonActionFactory; 6 | import com.qzw.filemask.gui.MenuActionFactory; 7 | import com.qzw.filemask.gui.PanelFactory; 8 | import com.qzw.filemask.service.LoginService; 9 | import com.qzw.filemask.service.PlatformService; 10 | import lombok.extern.log4j.Log4j2; 11 | 12 | import javax.swing.*; 13 | import javax.swing.border.BevelBorder; 14 | import java.awt.*; 15 | 16 | /** 17 | * fileMask main class 18 | * 19 | * @author quanzongwei 20 | * @date 2020/1/3 21 | */ 22 | @Log4j2 23 | public class FileMaskMain { 24 | public static JFrame f = null; 25 | public static JTextArea ta = new JTextArea(); 26 | 27 | public static void main(String[] args) { 28 | //设置样式 29 | setLookAndFeel(); 30 | 31 | //初始化 32 | f = initializationJFrame(); 33 | 34 | //菜单栏 35 | JMenuBar jMenuBar = initializationMenu(); 36 | f.setJMenuBar(jMenuBar); 37 | 38 | //按钮展示区域 39 | JPanel jPanel = setNorthPanel(); 40 | f.add(jPanel, BorderLayout.NORTH); 41 | 42 | //文本输出框 43 | JScrollPane scrollPane = setScrollPanel(); 44 | f.add(scrollPane, BorderLayout.CENTER); 45 | 46 | //设置icon 47 | ImageIcon imageIcon = new ImageIcon(FileMaskMain.class.getResource("/fileMask.png")); 48 | f.setIconImage(imageIcon.getImage()); 49 | 50 | f.setVisible(true); 51 | //登录 52 | LoginService.doLogin(f); 53 | } 54 | 55 | /** 56 | * 按钮区域 57 | */ 58 | public static JPanel setNorthPanel() { 59 | // button and panel 60 | JButton btn11 = new JButton("文件夹级联加密"); 61 | JButton btn12 = new JButton("文件夹加密"); 62 | JButton btn13 = new JButton("文件加密"); 63 | JPanel panel1 = PanelFactory.generatePanelItem(btn11, btn12, btn13, "加密类型一:文件名称加密(支持对文件夹的名称加密)"); 64 | 65 | JButton btn21 = new JButton("文件夹级联加密"); 66 | JButton btn22 = new JButton("文件夹加密"); 67 | JButton btn23 = new JButton("文件加密"); 68 | JPanel panel2 = PanelFactory.generatePanelItem(btn21, btn22, btn23, "加密类型二:文件头部加密"); 69 | 70 | JButton btn31 = new JButton("文件夹级联加密"); 71 | JButton btn32 = new JButton("文件夹加密"); 72 | JButton btn33 = new JButton("文件加密"); 73 | JPanel panel3 = PanelFactory.generatePanelItem(btn31, btn32, btn33, "加密类型三:文件内容加密(加密速度较慢, 1G大小的文件耗时约10秒)"); 74 | 75 | 76 | JButton btn41 = new JButton("文件夹级联解密"); 77 | JButton btn42 = new JButton("文件夹解密"); 78 | JButton btn43 = new JButton("文件解密"); 79 | JPanel panel4 = PanelFactory.generatePanelItem(btn41, btn42, btn43, "文件解密(系统自动识别文件的加密类型, 并执行解密操作)"); 80 | 81 | 82 | JPanel panelCombine1 = new JPanel(new BorderLayout(0, 20)); 83 | panelCombine1.setMaximumSize(new Dimension(650, 100)); 84 | panelCombine1.setMinimumSize(new Dimension(650, 100)); 85 | panelCombine1.add(panel1, BorderLayout.NORTH); 86 | panelCombine1.add(panel2, BorderLayout.CENTER); 87 | 88 | JPanel panelCombine2 = new JPanel(new BorderLayout(0, 20)); 89 | panelCombine1.setMaximumSize(new Dimension(650, 100)); 90 | panelCombine1.setMinimumSize(new Dimension(650, 100)); 91 | panelCombine2.add(panel3, BorderLayout.NORTH); 92 | panelCombine2.add(panel4, BorderLayout.CENTER); 93 | 94 | JPanel panelCombine3 = new JPanel(new BorderLayout(0, 20)); 95 | panelCombine3.add(panelCombine1, BorderLayout.NORTH); 96 | panelCombine3.add(panelCombine2, BorderLayout.CENTER); 97 | panelCombine1.setMaximumSize(new Dimension(650, 220)); 98 | panelCombine1.setMinimumSize(new Dimension(650, 220)); 99 | 100 | 101 | //事件绑定: 加密类型一(文件名称加密) 102 | ButtonActionFactory.btn11(btn11); 103 | ButtonActionFactory.btn12(btn12); 104 | ButtonActionFactory.btn13(btn13); 105 | //事件绑定: 加密类型二(文件头部加密) 106 | ButtonActionFactory.btn21(btn21); 107 | ButtonActionFactory.btn22(btn22); 108 | ButtonActionFactory.btn23(btn23); 109 | //事件绑定: 加密类型三(文件内容加密) 110 | ButtonActionFactory.btn31(btn31); 111 | ButtonActionFactory.btn32(btn32); 112 | ButtonActionFactory.btn33(btn33); 113 | //事件绑定: 解密 114 | ButtonActionFactory.btn41(btn41); 115 | ButtonActionFactory.btn42(btn42); 116 | ButtonActionFactory.btn43(btn43); 117 | return panelCombine3; 118 | } 119 | 120 | /** 121 | * 文本框区域 122 | */ 123 | private static JScrollPane setScrollPanel() { 124 | ta.setAutoscrolls(true); 125 | ta.setBorder(BorderFactory.createTitledBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED), "输出日志:")); 126 | ta.setForeground(new Color(56, 131, 56)); 127 | JScrollPane scrollPane = new JScrollPane(ta); 128 | scrollPane.setHorizontalScrollBarPolicy( 129 | JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); 130 | scrollPane.setVerticalScrollBarPolicy( 131 | JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED); 132 | return scrollPane; 133 | } 134 | 135 | /** 136 | * 初始化菜单 137 | */ 138 | private static JMenuBar initializationMenu() { 139 | JMenuBar menuBar = new JMenuBar(); 140 | JMenu menuFile = new JMenu("文件"); 141 | JMenu menuHelp = new JMenu("使用帮助"); 142 | 143 | JMenuItem menuItem4Exit = new JMenuItem("退出"); 144 | JMenuItem menuItem4Help = new JMenuItem("使用帮助"); 145 | JMenuItem menuItem4Contact = new JMenuItem("联系作者"); 146 | 147 | menuFile.add(menuItem4Exit); 148 | //windows定制逻辑 149 | if (PlatformEnum.WINDOWS.name().equals(PlatformContext.CURRENT_PLATFORM)) { 150 | menuHelp.add(menuItem4Help); 151 | } 152 | menuHelp.add(menuItem4Contact); 153 | 154 | menuBar.add(menuFile); 155 | menuBar.add(menuHelp); 156 | 157 | //时间绑定 158 | MenuActionFactory.menuItem4Exit(menuItem4Exit); 159 | MenuActionFactory.menuItem4Help(menuItem4Help); 160 | MenuActionFactory.menuItem4Contact(menuItem4Contact); 161 | return menuBar; 162 | } 163 | 164 | /** 165 | * 初始化JFrame 166 | */ 167 | private static JFrame initializationJFrame() { 168 | JFrame frame = new JFrame("FileMask"); 169 | frame.setLayout(new BorderLayout(0, 15)); 170 | frame.setSize(650, 640); 171 | int x = (Toolkit.getDefaultToolkit().getScreenSize().width - frame.getSize().width) / 2; 172 | int y = (Toolkit.getDefaultToolkit().getScreenSize().height - frame.getSize().height) / 2; 173 | frame.setLocation(x, y); 174 | frame.setResizable(true); 175 | frame.setIconImage(Toolkit.getDefaultToolkit().createImage("fileMask.png")); 176 | frame.setBackground(Color.black); 177 | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 178 | return frame; 179 | } 180 | 181 | /** 182 | * 设置样式 183 | */ 184 | private static void setLookAndFeel() { 185 | PlatformService.setLookAndFeelByPlatform(PlatformContext.CURRENT_PLATFORM); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/service/LoginService.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.service; 2 | 3 | import com.qzw.filemask.component.GlobalPasswordHolder; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | /** 9 | * @author quanzongwei 10 | * @date 2020/3/15 11 | */ 12 | public class LoginService { 13 | 14 | /** 15 | * 登录逻辑 16 | */ 17 | public static void doLogin(JFrame f) { 18 | //第一次启用该软件,初始化密码 19 | if (!AuthenticationService.isExistUserPassword()) { 20 | String password = passwordInitializationDialogV2(f); 21 | AuthenticationService.setUserMd5Byte(password); 22 | GlobalPasswordHolder.setPassword(password); 23 | return; 24 | } 25 | //登录认证 26 | authentication(f); 27 | //如果软件密码初始化完成, 同时当前用户是合法用户, 那么,程序正常运行 28 | } 29 | 30 | /** 31 | * 认证 32 | */ 33 | private static void authentication(JFrame f) { 34 | String password = authenticationDialogV2(f); 35 | if (PasswordService.isEmptyPassword(password)) { 36 | nullCHeck(f, 1); 37 | } else { 38 | //认证不成功,重新认证 39 | if (!AuthenticationService.isCurrentUser(password)) { 40 | JOptionPane.showConfirmDialog(f, "对不起密码错误,请重新输入", "提示", JOptionPane.DEFAULT_OPTION); 41 | authentication(f); 42 | } else { 43 | //认证成功,设置全局密码 44 | GlobalPasswordHolder.setPassword(password); 45 | } 46 | } 47 | } 48 | 49 | /** 50 | * 密码初始化 51 | * 不支持:密码输入框隐藏明文密码 52 | */ 53 | @Deprecated 54 | private static String passwordInitializationDialog(JFrame f) { 55 | String passwordFirst = JOptionPane.showInputDialog(f, "您第一次使用该软件, 请设置密码:"); 56 | if (passwordFirst == null || passwordFirst.equals("")) { 57 | return nullCHeck(f, 0); 58 | } else { 59 | if (!isValidPassword(passwordFirst)) { 60 | JOptionPane.showConfirmDialog(f, "密码不合法,请重新输入!(只允许包含数字和字母,位数是1-20位)", "提示", JOptionPane.DEFAULT_OPTION); 61 | return passwordInitializationDialog(f); 62 | } 63 | String passwordSecond = JOptionPane.showInputDialog(f, "请再次确认密码(忘记密码会导致文件无法解密)!"); 64 | if (passwordSecond == null || passwordSecond.equals("")) { 65 | return nullCHeck(f, 0); 66 | } else { 67 | if (!passwordFirst.equals(passwordSecond)) { 68 | JOptionPane.showConfirmDialog(f, "两次密码不相同, 请重新输入!", "提示", JOptionPane.DEFAULT_OPTION); 69 | return passwordInitializationDialog(f); 70 | } 71 | } 72 | } 73 | return passwordFirst; 74 | } 75 | 76 | /** 77 | * 密码初始化 78 | * 支持:密码输入框隐藏明文密码 79 | * 80 | * @since v1.2 81 | */ 82 | private static String passwordInitializationDialogV2(JFrame f) { 83 | String passwordFirst = getPasswordFromDialog(f, "首次使用请设置密码"); 84 | if (PasswordService.isEmptyPassword(passwordFirst)) { 85 | return nullCHeck(f, 0); 86 | } else { 87 | if (!isValidPassword(passwordFirst)) { 88 | JOptionPane.showConfirmDialog(f, "密码不合法,请重新输入!(只允许包含数字和字母,位数是1-20位)", "提示", JOptionPane.DEFAULT_OPTION); 89 | return passwordInitializationDialogV2(f); 90 | } 91 | String passwordSecond = getPasswordFromDialog(f, "请再次确认密码"); 92 | if (PasswordService.isEmptyPassword(passwordSecond)) { 93 | return nullCHeck(f, 0); 94 | } else { 95 | if (!passwordFirst.equals(passwordSecond)) { 96 | JOptionPane.showConfirmDialog(f, "两次密码不相同,请重新输入!", "提示", JOptionPane.DEFAULT_OPTION); 97 | return passwordInitializationDialogV2(f); 98 | } 99 | } 100 | } 101 | JOptionPane.showConfirmDialog(f, "密码设置成功,请您务必牢记", "提示", JOptionPane.DEFAULT_OPTION); 102 | return passwordFirst; 103 | } 104 | 105 | /** 106 | * 认证对话框 107 | * 不支持:密码输入框隐藏明文密码 108 | */ 109 | @Deprecated 110 | private static String authenticationDialog(JFrame f) { 111 | String password = JOptionPane.showInputDialog(f, "请输入密码:"); 112 | if (PasswordService.isEmptyPassword(password)) { 113 | return nullCHeck(f, 1); 114 | } 115 | if (!isValidPassword(password)) { 116 | JOptionPane.showConfirmDialog(f, "密码不合法,请重新输入!(只允许包含数字和字母,位数是1-20位)", "提示", JOptionPane.DEFAULT_OPTION); 117 | return authenticationDialog(f); 118 | } 119 | return password; 120 | } 121 | 122 | /** 123 | * 认证对话框, 124 | * 支持:密码输入框隐藏明文密码 125 | * 126 | * @since v1.2 127 | */ 128 | private static String authenticationDialogV2(JFrame f) { 129 | String password = getPasswordFromDialog(f, "请输入密码"); 130 | if (PasswordService.isEmptyPassword(password)) { 131 | return nullCHeck(f, 1); 132 | } 133 | if (!isValidPassword(password)) { 134 | JOptionPane.showConfirmDialog(f, "密码不合法,请重新输入!(只允许包含数字和字母,位数是1-20位)", "提示", JOptionPane.DEFAULT_OPTION); 135 | return authenticationDialogV2(f); 136 | } 137 | return password; 138 | } 139 | 140 | /** 141 | * 返回输入的密码 142 | * 143 | * @return nullable 144 | */ 145 | private static String getPasswordFromDialog(JFrame jFrame, String title) { 146 | String password = null; 147 | JPanel passwordPanel = new JPanel(); 148 | // 魔法值 149 | JPasswordField jPasswordField = new JPasswordField(10); 150 | passwordPanel.add(jPasswordField); 151 | 152 | JCheckBox showPasswordCheckBox = new JCheckBox("显示密码"); 153 | showPasswordCheckBox.addActionListener(e -> { 154 | if (showPasswordCheckBox.isSelected()) { 155 | jPasswordField.setEchoChar((char) 0); 156 | } else { 157 | jPasswordField.setEchoChar('\u2022'); 158 | } 159 | }); 160 | passwordPanel.add(showPasswordCheckBox); 161 | passwordPanel.setLayout(new FlowLayout()); 162 | int result = JOptionPane.showConfirmDialog(jFrame, passwordPanel, title, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); 163 | if (result == JOptionPane.OK_OPTION) { 164 | char[] passwordChar = jPasswordField.getPassword(); 165 | password = new String(passwordChar); 166 | } 167 | return password; 168 | } 169 | 170 | /** 171 | * 判断密码是否合法 172 | */ 173 | public static boolean isValidPassword(String password) { 174 | if (password.matches("[A-Za-z0-9]{1,20}")) { 175 | return true; 176 | } 177 | return false; 178 | } 179 | 180 | /** 181 | * 不合法输入检查 182 | * 183 | * @param opType 0:密码初始化 1:认证 184 | * @return 返回用户重新输入的密码 185 | */ 186 | private static String nullCHeck(JFrame f, int opType) { 187 | int value = JOptionPane.showConfirmDialog(f, "您未输入任何有效的数据,软件即将退出!(点击取消可以返回重新输入)", "提示", JOptionPane.OK_CANCEL_OPTION); 188 | if (value == JOptionPane.CANCEL_OPTION) { 189 | //密码初始化 190 | if (opType == 0) { 191 | return passwordInitializationDialogV2(f); 192 | } 193 | //认证 194 | else if (opType == 1) { 195 | return authenticationDialogV2(f); 196 | } 197 | } 198 | try { 199 | Thread.sleep(100); 200 | } catch (InterruptedException e) { 201 | // do nothing 202 | } 203 | System.exit(0); 204 | return null; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/fileencoder/AbstractFileEncoder.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.fileencoder; 2 | 3 | import com.qzw.filemask.enums.ChooseTypeEnum; 4 | import com.qzw.filemask.enums.FileEncoderTypeEnum; 5 | import com.qzw.filemask.enums.MaskExceptionEnum; 6 | import com.qzw.filemask.exception.MaskException; 7 | import com.qzw.filemask.interfaces.FileEncoderType; 8 | import com.qzw.filemask.service.StatisticsService; 9 | import com.qzw.filemask.service.TailModelService; 10 | import com.qzw.filemask.service.status.StopCommandStatusService; 11 | import com.qzw.filemask.service.PrivateDataService; 12 | import lombok.extern.log4j.Log4j2; 13 | 14 | import java.io.File; 15 | 16 | /** 17 | * 加密解密抽象类 18 | * 19 | * @author quanzongwei 20 | * @date 2020/1/18 21 | */ 22 | @Log4j2 23 | public abstract class AbstractFileEncoder implements FileEncoderType { 24 | /** 25 | * 确保加解密过程串行执行 26 | */ 27 | public static Object lock = new Object(); 28 | 29 | /** 30 | * 加密入口 31 | */ 32 | public void encodeFileOrDir(File fileOrDir, ChooseTypeEnum dirChooseEnum) { 33 | synchronized (lock) { 34 | if (!fileOrDir.exists()) { 35 | throw new MaskException(MaskExceptionEnum.FILE_NOT_EXISTS.getType(), "文件或者文件夹不存在,加密失败," + fileOrDir.getPath()); 36 | } 37 | if (PrivateDataService.isFileMaskFile(fileOrDir)) { 38 | log.info("私有数据文件无需处理, {}", fileOrDir.getPath()); 39 | return; 40 | } 41 | //文件选择方式:单文件 42 | if (dirChooseEnum.equals(ChooseTypeEnum.FILE_ONLY)) { 43 | executeEncrypt(fileOrDir); 44 | if (ifReceiveStopCommand()) { 45 | return; 46 | } 47 | } 48 | //文件选择方式:文件夹 49 | else if (dirChooseEnum.equals(ChooseTypeEnum.CURRENT_DIR_ONLY)) { 50 | File[] files = fileOrDir.listFiles(); 51 | if (files != null && files.length > 0) { 52 | for (File file : files) { 53 | if (PrivateDataService.isFileMaskFile(file)) { 54 | log.info("私有数据文件无需处理, {}", file.getPath()); 55 | continue; 56 | } 57 | executeEncrypt(file); 58 | if (ifReceiveStopCommand()) { 59 | return; 60 | } 61 | } 62 | } 63 | executeEncrypt(fileOrDir); 64 | if (ifReceiveStopCommand()) { 65 | return; 66 | } 67 | } 68 | //文件选择方式:级联文件夹 69 | else if (dirChooseEnum.equals(ChooseTypeEnum.CASCADE_DIR)) { 70 | File[] files = fileOrDir.listFiles(); 71 | if (files != null && files.length > 0) { 72 | for (File file : files) { 73 | //cascade directory 74 | if (file.isDirectory()) { 75 | encodeFileOrDir(file, ChooseTypeEnum.CASCADE_DIR); 76 | if (ifReceiveStopCommand()) { 77 | return; 78 | } 79 | continue; 80 | } 81 | executeEncrypt(file); 82 | if (ifReceiveStopCommand()) { 83 | return; 84 | } 85 | } 86 | } 87 | executeEncrypt(fileOrDir); 88 | if (ifReceiveStopCommand()) { 89 | return; 90 | } 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * 解密入口 97 | */ 98 | public void decodeFileOrDir(File fileOrDir, ChooseTypeEnum dirChooseEnum) { 99 | synchronized (lock) { 100 | if (!fileOrDir.exists()) { 101 | throw new MaskException(MaskExceptionEnum.FILE_NOT_EXISTS.getType(), "文件或者文件夹不存在,解密失败, " + fileOrDir.getPath()); 102 | } 103 | if (PrivateDataService.isFileMaskFile(fileOrDir)) { 104 | log.info("私有数据文件无需处理,{}", fileOrDir.getPath()); 105 | return; 106 | } 107 | //文件选择方式:单文件 108 | if (dirChooseEnum.equals(ChooseTypeEnum.FILE_ONLY)) { 109 | executeDecrypt(fileOrDir); 110 | if (ifReceiveStopCommand()) { 111 | return; 112 | } 113 | } 114 | //文件选择方式:文件夹 115 | else if (dirChooseEnum.equals(ChooseTypeEnum.CURRENT_DIR_ONLY)) { 116 | File[] files = fileOrDir.listFiles(); 117 | if (files != null && files.length > 0) { 118 | for (File file : files) { 119 | if (PrivateDataService.isFileMaskFile(file)) { 120 | log.info("私有数据文件无需处理, {}", file.getPath()); 121 | continue; 122 | } 123 | executeDecrypt(file); 124 | if (ifReceiveStopCommand()) { 125 | return; 126 | } 127 | } 128 | } 129 | executeDecrypt(fileOrDir); 130 | if (ifReceiveStopCommand()) { 131 | return; 132 | } 133 | } 134 | //文件选择方式:级联文件夹 135 | else if (dirChooseEnum.equals(ChooseTypeEnum.CASCADE_DIR)) { 136 | File[] files = fileOrDir.listFiles(); 137 | if (files != null && files.length > 0) { 138 | for (File file : files) { 139 | //cascade directory 140 | if (file.isDirectory()) { 141 | decodeFileOrDir(file, ChooseTypeEnum.CASCADE_DIR); 142 | if (ifReceiveStopCommand()) { 143 | return; 144 | } 145 | continue; 146 | } 147 | executeDecrypt(file); 148 | if (ifReceiveStopCommand()) { 149 | return; 150 | } 151 | } 152 | } 153 | executeDecrypt(fileOrDir); 154 | if (ifReceiveStopCommand()) { 155 | return; 156 | } 157 | } 158 | } 159 | } 160 | 161 | 162 | public void executeEncrypt(File fileOrDir) { 163 | if (PrivateDataService.isFileMaskFile(fileOrDir)) { 164 | log.info("私有数据文件无需处理,{}", fileOrDir.getPath()); 165 | return; 166 | } 167 | FileEncoderTypeEnum fileEncoderType = getFileEncoderType(); 168 | if (fileOrDir.isDirectory() && !fileEncoderType.isSupportEncryptDir()) { 169 | //加密方式不支持加密文件夹, 直接跳过, 不需要任何日志 170 | return; 171 | } 172 | Long begin = System.currentTimeMillis(); 173 | //[统计] 设置当前文件加密开始时间 174 | StatisticsService.setCurrentFileOperationBeginTime(System.currentTimeMillis()); 175 | //[统计] 设置当前文件名称 176 | StatisticsService.setCurrentFileName(fileOrDir.getName()); 177 | //[统计] 设置当前文件所在文件夹 178 | StatisticsService.setCurrentFileParentName(fileOrDir.getParent()); 179 | //[统计] 设置当期文件总大小 180 | StatisticsService.setCurrentFileBytes(fileOrDir.isDirectory() ? 0 : fileOrDir.length()); 181 | 182 | //核心逻辑: 执行加密 不抛出异常 183 | TailModelService.encryptByType(fileOrDir, fileEncoderType); 184 | 185 | long end = System.currentTimeMillis(); 186 | //[统计] 已完成文件总数+1 187 | StatisticsService.setDoneFileTotalAmount(StatisticsService.getDoneFileTotalAmount() + 1); 188 | if (StatisticsService.isIfCurrentFileExecuteContentEncrypt()) { 189 | //do nothing 190 | } else { 191 | //[统计] 增加非内容加密文件耗时 192 | StatisticsService.setDoneFileAmount4NotContentEncrypt(StatisticsService.getDoneFileAmount4NotContentEncrypt() + 1); 193 | //[统计] 非内容加密文件总数+1 194 | StatisticsService.setDoneFileAmountSpendTime4NotFileContentEncrypt(StatisticsService.getDoneFileAmountSpendTime4NotFileContentEncrypt() + (end - begin)); 195 | } 196 | StatisticsService.clearCurrentFileInfo(); 197 | 198 | } 199 | 200 | 201 | public void executeDecrypt(File fileOrDir) { 202 | if (PrivateDataService.isFileMaskFile(fileOrDir)) { 203 | log.info("私有数据文件无需处理,{}", fileOrDir.getPath()); 204 | return; 205 | } 206 | 207 | Long begin = System.currentTimeMillis(); 208 | //[统计] 设置当前文件加密开始时间 209 | StatisticsService.setCurrentFileOperationBeginTime(System.currentTimeMillis()); 210 | //[统计] 设置当前文件路径名称 211 | StatisticsService.setCurrentFileName(fileOrDir.getName()); 212 | //[统计] 设置当前文件所在文件夹 213 | StatisticsService.setCurrentFileParentName(fileOrDir.getParent()); 214 | //[统计] 设置当期文件总大小 215 | StatisticsService.setCurrentFileBytes(fileOrDir.isDirectory() ? 0 : fileOrDir.length()); 216 | 217 | //核心逻辑: 执行解密 不抛出异常 218 | TailModelService.decryptAllType(fileOrDir); 219 | 220 | long end = System.currentTimeMillis(); 221 | //[统计] 已完成文件总数+1 222 | StatisticsService.setDoneFileTotalAmount(StatisticsService.getDoneFileTotalAmount() + 1); 223 | if (StatisticsService.isIfCurrentFileExecuteContentEncrypt()) { 224 | //do nothing 225 | } else { 226 | //[统计] 增加非内容加密文件耗时 227 | StatisticsService.setDoneFileAmount4NotContentEncrypt(StatisticsService.getDoneFileAmount4NotContentEncrypt() + 1); 228 | //[统计] 非内容加密文件总数+1 229 | StatisticsService.setDoneFileAmountSpendTime4NotFileContentEncrypt(StatisticsService.getDoneFileAmountSpendTime4NotFileContentEncrypt() + (end - begin)); 230 | } 231 | //[统计] 清空当前文件统计信息 232 | StatisticsService.clearCurrentFileInfo(); 233 | } 234 | 235 | /** 236 | * 是否收到提前停止命令 237 | */ 238 | private boolean ifReceiveStopCommand() { 239 | if (StopCommandStatusService.getStopStatus().equals(StopCommandStatusService.STOP_STATUS_REQUIRE_STOP)) { 240 | return true; 241 | } 242 | return false; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/service/StatisticsService.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.service; 2 | 3 | import com.qzw.filemask.util.DisplayInHumanUtils; 4 | 5 | import java.math.BigDecimal; 6 | 7 | /** 8 | * 数据统计服务 9 | * 场景:用于统计文件处理进度 10 | * 11 | * @author quanzongwei 12 | * @date 2020/5/22 13 | */ 14 | 15 | public class StatisticsService { 16 | /** 17 | * 扫描文件总数: 18 | * 1. 加密:文件名称加密会包含文件夹 19 | * 2. 解密:包括文件和文件夹 20 | */ 21 | public static Long scanFileTotalAmount = 0L; 22 | /** 23 | * 已处理完成文件总数,包含不需要处理的文件 24 | */ 25 | public static Long doneFileTotalAmount = 0L; 26 | /** 27 | * 待处理总字节数 28 | * 具体包括: 29 | * 1. 加密: 未使用文件内容加密的; 当前用户且仅使用文件名称加密 30 | * 2. 解密: 该文件被当前用户使用文件内容加密 31 | */ 32 | public static Long todoFileTotalBytes = 0L; 33 | /** 34 | * 已完成处理字节总数 35 | */ 36 | public static Long doneFileTotalBytes = 0L; 37 | /** 38 | * 已完成 非内容加密的文件总数 39 | */ 40 | public static Long doneFileAmount4NotContentEncrypt = 0L; 41 | /** 42 | * 加密开始时间 43 | */ 44 | public static Long operationBeginTime = 0L; 45 | /** 46 | * 已处理字节耗时 47 | */ 48 | public static Long doneFileTotalBytesSpendTime = 0L; 49 | /** 50 | * 样本文件花费时间(即排除使用全文加密的文件) 51 | */ 52 | public static Long doneFileAmountSpendTime4NotFileContentEncrypt = 0L; 53 | 54 | 55 | /** 56 | * 当前文件名称 57 | */ 58 | public static String currentFileName = ""; 59 | /** 60 | * 当前文件所在目录 61 | */ 62 | public static String currentFileParentName = ""; 63 | /** 64 | * 当前文件是否需要执行全文加密 65 | */ 66 | public static boolean ifCurrentFileExecuteContentEncrypt = false; 67 | /** 68 | * 当前文件总字节数 69 | */ 70 | public static Long currentFileBytes = 0L; 71 | /** 72 | * 处理当前文件开始时间 73 | */ 74 | public static Long currentFileOperationBeginTime = 0L; 75 | /** 76 | * 当前文件已处理完成的字节数(仅针对全文加密有效) 77 | */ 78 | public static Long currentFileCompletedBytes = 0L; 79 | 80 | 81 | public static String getCurrentFileParentName() { 82 | return currentFileParentName; 83 | } 84 | 85 | public static void setCurrentFileParentName(String currentFileParentName) { 86 | StatisticsService.currentFileParentName = currentFileParentName; 87 | } 88 | 89 | public static Long getScanFileTotalAmount() { 90 | return scanFileTotalAmount; 91 | } 92 | 93 | public static void setScanFileTotalAmount(Long scanFileTotalAmount) { 94 | StatisticsService.scanFileTotalAmount = scanFileTotalAmount; 95 | } 96 | 97 | public static Long getDoneFileTotalAmount() { 98 | return doneFileTotalAmount; 99 | } 100 | 101 | public static void setDoneFileTotalAmount(Long doneFileTotalAmount) { 102 | StatisticsService.doneFileTotalAmount = doneFileTotalAmount; 103 | } 104 | 105 | public static Long getTodoFileTotalBytes() { 106 | return todoFileTotalBytes; 107 | } 108 | 109 | public static void setTodoFileTotalBytes(Long todoFileTotalBytes) { 110 | StatisticsService.todoFileTotalBytes = todoFileTotalBytes; 111 | } 112 | 113 | public static Long getDoneFileTotalBytes() { 114 | return doneFileTotalBytes; 115 | } 116 | 117 | public static void setDoneFileTotalBytes(Long doneFileTotalBytes) { 118 | StatisticsService.doneFileTotalBytes = doneFileTotalBytes; 119 | } 120 | 121 | public static Long getDoneFileAmount4NotContentEncrypt() { 122 | return doneFileAmount4NotContentEncrypt; 123 | } 124 | 125 | public static void setDoneFileAmount4NotContentEncrypt(Long doneFileAmount4NotContentEncrypt) { 126 | StatisticsService.doneFileAmount4NotContentEncrypt = doneFileAmount4NotContentEncrypt; 127 | } 128 | 129 | public static Long getOperationBeginTime() { 130 | return operationBeginTime; 131 | } 132 | 133 | public static void setOperationBeginTime(Long operationBeginTime) { 134 | StatisticsService.operationBeginTime = operationBeginTime; 135 | } 136 | 137 | public static Long getDoneFileTotalBytesSpendTime() { 138 | return doneFileTotalBytesSpendTime; 139 | } 140 | 141 | public static void setDoneFileTotalBytesSpendTime(Long doneFileTotalBytesSpendTime) { 142 | StatisticsService.doneFileTotalBytesSpendTime = doneFileTotalBytesSpendTime; 143 | } 144 | 145 | public static Long getDoneFileAmountSpendTime4NotFileContentEncrypt() { 146 | return doneFileAmountSpendTime4NotFileContentEncrypt; 147 | } 148 | 149 | public static void setDoneFileAmountSpendTime4NotFileContentEncrypt(Long doneFileAmountSpendTime4NotFileContentEncrypt) { 150 | StatisticsService.doneFileAmountSpendTime4NotFileContentEncrypt = doneFileAmountSpendTime4NotFileContentEncrypt; 151 | } 152 | 153 | public static String getCurrentFileName() { 154 | return currentFileName; 155 | } 156 | 157 | public static void setCurrentFileName(String currentFileName) { 158 | StatisticsService.currentFileName = currentFileName; 159 | } 160 | 161 | public static Long getCurrentFileBytes() { 162 | return currentFileBytes; 163 | } 164 | 165 | 166 | public static void setCurrentFileBytes(Long currentFileBytes) { 167 | StatisticsService.currentFileBytes = currentFileBytes; 168 | } 169 | 170 | public static Long getCurrentFileOperationBeginTime() { 171 | return currentFileOperationBeginTime; 172 | } 173 | 174 | public static void setCurrentFileOperationBeginTime(Long currentFileOperationBeginTime) { 175 | StatisticsService.currentFileOperationBeginTime = currentFileOperationBeginTime; 176 | } 177 | 178 | public static Long getCurrentFileCompletedBytes() { 179 | return currentFileCompletedBytes; 180 | } 181 | 182 | public static void setCurrentFileCompletedBytes(Long currentFileCompletedBytes) { 183 | StatisticsService.currentFileCompletedBytes = currentFileCompletedBytes; 184 | } 185 | 186 | public static boolean isIfCurrentFileExecuteContentEncrypt() { 187 | return ifCurrentFileExecuteContentEncrypt; 188 | } 189 | 190 | public static void setIfCurrentFileExecuteContentEncrypt(boolean ifCurrentFileExecuteContentEncrypt) { 191 | StatisticsService.ifCurrentFileExecuteContentEncrypt = ifCurrentFileExecuteContentEncrypt; 192 | } 193 | 194 | 195 | /** 196 | * 已经花费时间(精确到秒) 197 | */ 198 | public static String generateTotalSpendTimeInHuman() { 199 | if (getOperationBeginTime() == 0L) { 200 | return 0 + "秒"; 201 | } 202 | long second = (System.currentTimeMillis() - getOperationBeginTime()) / 1000; 203 | return DisplayInHumanUtils.getSecondInHuman(second); 204 | } 205 | 206 | /** 207 | * 已经花费时间(精确到毫秒) 208 | */ 209 | public static String generateTotalSpendTimeMillisecondsInHuman() { 210 | if (getOperationBeginTime() == 0L) { 211 | return 0 + "秒"; 212 | } 213 | long millisecond = (System.currentTimeMillis() - getOperationBeginTime()); 214 | return DisplayInHumanUtils.getMilliSecondInHuman(millisecond); 215 | } 216 | 217 | /** 218 | * 剩余时间(精确到秒) 219 | */ 220 | public static String generateLastTime() { 221 | long total = 0L; 222 | long total4File = 0L; 223 | long total4Bytes = 0L; 224 | 225 | Long time4Bytes = getDoneFileTotalBytesSpendTime(); 226 | Long time4File = getDoneFileAmountSpendTime4NotFileContentEncrypt(); 227 | 228 | Long todoFileTotalBytes = getTodoFileTotalBytes(); 229 | Long doneFileTotalBytes = getDoneFileTotalBytes(); 230 | 231 | Long scanFileTotalAmount = getScanFileTotalAmount(); 232 | Long doneFileAmount4NotContentEncrypt = getDoneFileAmount4NotContentEncrypt(); 233 | Long doneFileAmount = getDoneFileTotalAmount(); 234 | 235 | if (!todoFileTotalBytes.equals(0L) && !doneFileTotalBytes.equals(0L)) { 236 | BigDecimal b4Bytes = BigDecimal.valueOf(todoFileTotalBytes - doneFileTotalBytes).divide(BigDecimal.valueOf(doneFileTotalBytes), 6, BigDecimal.ROUND_HALF_UP).multiply(BigDecimal.valueOf(time4Bytes)); 237 | total4Bytes = b4Bytes.longValue(); 238 | } 239 | 240 | if (!scanFileTotalAmount.equals(0L) && !doneFileAmount4NotContentEncrypt.equals(0L)) { 241 | BigDecimal b4File = BigDecimal.valueOf(scanFileTotalAmount - doneFileAmount).divide(BigDecimal.valueOf(doneFileAmount4NotContentEncrypt), 6, BigDecimal.ROUND_HALF_UP).multiply(BigDecimal.valueOf(time4File)); 242 | total4File = b4File.longValue(); 243 | } 244 | 245 | total = total4Bytes + total4File; 246 | 247 | return DisplayInHumanUtils.getSecondInHuman(total / 1000); 248 | } 249 | 250 | /** 251 | * 当前文件剩余执行时间(精确到秒) 252 | */ 253 | public static String computeCurrentFileLastTimeInHuman() { 254 | long encryptFileBytes = getCurrentFileCompletedBytes(); 255 | long spentTime = getCurrentFileEncryptSpentTime(); 256 | if (encryptFileBytes == 0L || spentTime == 0L || currentFileBytes == 0L) { 257 | //特殊情况 258 | return 0 + "秒"; 259 | } 260 | BigDecimal value = BigDecimal.valueOf(currentFileBytes - encryptFileBytes).divide(BigDecimal.valueOf(encryptFileBytes), 7, BigDecimal.ROUND_HALF_UP).multiply(BigDecimal.valueOf(spentTime)); 261 | return DisplayInHumanUtils.getSecondInHuman(value.intValue()); 262 | } 263 | 264 | /** 265 | * 获取当前文件加密已耗时长 266 | */ 267 | public static long getCurrentFileEncryptSpentTime() { 268 | if (currentFileOperationBeginTime == 0L) { 269 | return 0; 270 | } 271 | return (System.currentTimeMillis() - currentFileOperationBeginTime) / 1000; 272 | } 273 | 274 | 275 | /** 276 | * 获取待处理总字节数 277 | */ 278 | public static String getTodoFileTotalBytesInHuman() { 279 | long b = todoFileTotalBytes; 280 | return DisplayInHumanUtils.getBytesInHuman(b); 281 | } 282 | 283 | 284 | /** 285 | * 获取当前文件总大小 286 | */ 287 | public static String getCurrentFileBytesInhuman() { 288 | return DisplayInHumanUtils.getBytesInHuman(currentFileBytes); 289 | } 290 | 291 | /** 292 | * 获取当前文件已处理字节总大小 293 | */ 294 | public static String getCurrentFileEncryptBytesInHuman() { 295 | return DisplayInHumanUtils.getBytesInHuman(currentFileCompletedBytes); 296 | } 297 | 298 | /** 299 | * 获取当前文件加密已耗时长 300 | */ 301 | public static String getCurrentFileEncryptSpentTimeInHuman() { 302 | if (currentFileOperationBeginTime == 0L) { 303 | return 0 + "秒"; 304 | } 305 | return DisplayInHumanUtils.getSecondInHuman((System.currentTimeMillis() - currentFileOperationBeginTime) / 1000); 306 | } 307 | 308 | public static String getCurrentFileParentPath() { 309 | return currentFileParentName; 310 | } 311 | 312 | /** 313 | * 所有数据初始化 314 | * 场景:每次加密或者解密完成后 315 | */ 316 | public static void clearAll() { 317 | todoFileTotalBytes = 0L; 318 | scanFileTotalAmount = 0L; 319 | doneFileAmount4NotContentEncrypt = 0L; 320 | doneFileTotalAmount = 0L; 321 | 322 | operationBeginTime = 0L; 323 | doneFileTotalBytes = 0L; 324 | 325 | doneFileTotalBytesSpendTime = 0L; 326 | doneFileAmountSpendTime4NotFileContentEncrypt = 0L; 327 | 328 | currentFileName = ""; 329 | currentFileParentName = ""; 330 | ifCurrentFileExecuteContentEncrypt = false; 331 | currentFileBytes = 0L; 332 | currentFileOperationBeginTime = 0L; 333 | currentFileCompletedBytes = 0L; 334 | } 335 | 336 | /** 337 | * 清空当前文件统计信息 338 | */ 339 | public static void clearCurrentFileInfo() { 340 | StatisticsService.setCurrentFileBytes(0L); 341 | StatisticsService.setCurrentFileName(""); 342 | currentFileParentName = ""; 343 | ifCurrentFileExecuteContentEncrypt = false; 344 | currentFileCompletedBytes = 0L; 345 | } 346 | } 347 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/service/WorkFlowService.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.service; 2 | 3 | import com.qzw.filemask.FileMaskMain; 4 | import com.qzw.filemask.enums.ChooseTypeEnum; 5 | import com.qzw.filemask.enums.FileEncoderTypeEnum; 6 | import com.qzw.filemask.fileencoder.AbstractFileEncoder; 7 | import com.qzw.filemask.fileencoder.FileContentEncoder; 8 | import com.qzw.filemask.model.TailModel; 9 | import com.qzw.filemask.service.status.ComputingStatusService; 10 | import com.qzw.filemask.service.status.OperationLockStatusService; 11 | import com.qzw.filemask.service.status.StopCommandStatusService; 12 | import lombok.extern.log4j.Log4j2; 13 | 14 | import javax.swing.*; 15 | import java.awt.*; 16 | import java.io.File; 17 | import java.io.RandomAccessFile; 18 | import java.util.concurrent.ExecutorService; 19 | import java.util.concurrent.Executors; 20 | 21 | /** 22 | * 主界面工作流程服务(页面交互,异步刷新,统计展示等等逻辑) 23 | * 24 | * @author quanzongwei 25 | * @date 2020/6/1 26 | */ 27 | @Log4j2 28 | public class WorkFlowService { 29 | private static JTextArea ta = FileMaskMain.ta; 30 | private static JFrame f = FileMaskMain.f; 31 | 32 | /** 33 | * 执行加密或者解密操作 34 | */ 35 | public static void doEncryptOrDecrypt(String targetFileOrDir, AbstractFileEncoder fileEncoder, ChooseTypeEnum chooseTypeEnum, boolean isEncryptOperation) { 36 | String magicWord = getOperationType(isEncryptOperation); 37 | if (targetFileOrDir == null) { 38 | // 用户点击取消按钮, 取消加密 39 | return; 40 | } 41 | if (!isValidPath(targetFileOrDir)) { 42 | JOptionPane.showConfirmDialog(f, magicWord + "路径太短, 请重新选择则!", "提示", JOptionPane.DEFAULT_OPTION); 43 | return; 44 | } 45 | 46 | JPanel fileInfoPanel = new JPanel(new BorderLayout(0, 10)); 47 | 48 | JLabel fileInfoDialogLabel = new JLabel(""); 49 | fileInfoDialogLabel.setHorizontalAlignment(SwingConstants.CENTER); 50 | fileInfoDialogLabel.setForeground(Color.RED); 51 | fileInfoDialogLabel.setBackground(Color.black); 52 | 53 | JTextArea fileInfoDialogTextArea = new JTextArea(); 54 | fileInfoDialogTextArea.setBackground(Color.WHITE); 55 | 56 | fileInfoPanel.add(fileInfoDialogTextArea, BorderLayout.NORTH); 57 | fileInfoPanel.add(fileInfoDialogLabel, BorderLayout.CENTER); 58 | 59 | JButton stopCommandBtn = new JButton("提前停止"); 60 | stopCommandBtn.addActionListener(e -> { 61 | StopCommandStatusService.setStopStatus(StopCommandStatusService.STOP_STATUS_REQUIRE_STOP); 62 | stopCommandBtn.setVisible(false); 63 | }); 64 | fileInfoPanel.add(stopCommandBtn, BorderLayout.SOUTH); 65 | 66 | //文件信息统计对话框 67 | JDialog fileInfoDialog = new JDialog(f); 68 | fileInfoDialog.setTitle("FileMask运行状态"); 69 | fileInfoDialog.setSize(f.getWidth() / 2, 400); 70 | fileInfoDialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); 71 | fileInfoDialog.setModal(true); 72 | fileInfoDialog.add(fileInfoPanel); 73 | //文件信息统计对话框居中展示 74 | int x = (int) ((f.getSize().width - fileInfoDialog.getSize().width) / 2 + f.getLocation().getX()); 75 | int y = (int) ((f.getSize().height - fileInfoDialog.getSize().height) / 2 + f.getLocation().getY()); 76 | fileInfoDialog.setLocation(x, y); 77 | 78 | //刷新文本区域 79 | refreshTextArea(fileInfoDialogTextArea); 80 | //刷新文本标签 81 | refreshLabel(fileInfoDialogLabel, isEncryptOperation); 82 | 83 | //异步执行加密解密任务 84 | asyncRunActualTaskThread(targetFileOrDir, stopCommandBtn, fileEncoder, chooseTypeEnum, isEncryptOperation); 85 | //异步刷新文件统计信息对话框 86 | asyncRefreshDialog(fileInfoDialog, fileInfoDialogTextArea, isEncryptOperation, fileInfoDialogLabel); 87 | 88 | fileInfoDialog.setVisible(true); 89 | } 90 | 91 | 92 | private static void refreshLabel(JLabel label, boolean isEncryptOperation) { 93 | Integer stopStatus = StopCommandStatusService.getStopStatus(); 94 | Integer computeStatus = ComputingStatusService.getComputeStatus(); 95 | // 没有收到停止命令 96 | if (stopStatus.equals(StopCommandStatusService.STOP_STATUS_NOT_REQUIRE_STOP)) { 97 | if (computeStatus.equals(ComputingStatusService.COMPUTE_STATUS_RUNNING_COMPUTING)) { 98 | label.setText("正在统计文件信息(请勿关闭软件或计算机)"); 99 | } else { 100 | label.setText("正在执行" + getOperationType(isEncryptOperation) + "操作(请勿关闭软件或计算机)"); 101 | } 102 | } else { 103 | if (computeStatus.equals(ComputingStatusService.COMPUTE_STATUS_RUNNING_COMPUTING)) { 104 | label.setText("正在等待当前文件处理完成(请勿关闭软件或计算机)"); 105 | } else { 106 | label.setText("正在等待当前文件处理完成(请勿关闭软件或计算机)"); 107 | } 108 | } 109 | } 110 | 111 | private static void asyncRefreshDialog(JDialog fileInfoDialog, JTextArea runningDialogTextArea, boolean isEncryptOperation, JLabel runningDialogLabel) { 112 | String magicWord = getOperationType(isEncryptOperation); 113 | ExecutorService executorService = Executors.newSingleThreadExecutor(); 114 | executorService.submit(() -> { 115 | while (true) { 116 | refreshLabel(runningDialogLabel, isEncryptOperation); 117 | refreshTextArea(runningDialogTextArea); 118 | // 校验是否释放运行锁 119 | boolean lock = OperationLockStatusService.lock(); 120 | if (lock == true) { 121 | //关闭对话框 122 | fileInfoDialog.dispose(); 123 | //生成新的摘要对话框 124 | JOptionPane.showConfirmDialog(f, "成功" + magicWord + "文件: " + StatisticsService.getDoneFileTotalAmount() + "个\n一共扫描文件: " + StatisticsService.getScanFileTotalAmount() + "个\n本次操作耗时: " + StatisticsService.generateTotalSpendTimeInHuman(), magicWord + "成功!", JOptionPane.DEFAULT_OPTION); 125 | //重置所有统计数据 126 | StatisticsService.clearAll(); 127 | //重置提前停止命令为:无需提前停止 128 | StopCommandStatusService.setStopStatus(StopCommandStatusService.STOP_STATUS_NOT_REQUIRE_STOP); 129 | //重置计算状态为:正在计算统计文件信息 130 | ComputingStatusService.setComputeStatus(ComputingStatusService.COMPUTE_STATUS_RUNNING_COMPUTING); 131 | //释放锁 132 | OperationLockStatusService.releaseLock(); 133 | return; 134 | } 135 | try { 136 | Thread.sleep(1000); 137 | } catch (InterruptedException e) { 138 | 139 | } 140 | } 141 | }); 142 | } 143 | 144 | private static void refreshTextArea(JTextArea dialog) { 145 | dialog.setText(""); 146 | dialog.setEditable(false); 147 | dialog.append("(一)、已扫描文件统计数据\n"); 148 | dialog.append("已扫描文件总个数: " + StatisticsService.scanFileTotalAmount + "\n"); 149 | dialog.append("待处理文件总大小: " + StatisticsService.getTodoFileTotalBytesInHuman() + "\n"); 150 | dialog.append("" + "\n"); 151 | dialog.append("(二)、已扫描文件处理进度\n"); 152 | dialog.append("已处理文件总数: " + StatisticsService.getDoneFileTotalAmount() + "\n"); 153 | dialog.append("已处理时长: " + StatisticsService.generateTotalSpendTimeInHuman() + "\n"); 154 | dialog.append("预计剩余时长: " + StatisticsService.generateLastTime() + "\n"); 155 | dialog.append("" + "\n"); 156 | dialog.append("(三)、当前文件处理进度\n"); 157 | dialog.append("当前文件名称: " + StatisticsService.getCurrentFileName() + "\n"); 158 | dialog.append("当前文件所在文件夹夹: " + StatisticsService.getCurrentFileParentPath() + "\n"); 159 | dialog.append("当前文件大小: " + StatisticsService.getCurrentFileBytesInhuman() + "\n"); 160 | dialog.append("当前文件已处理大小: " + StatisticsService.getCurrentFileEncryptBytesInHuman() + "\n"); 161 | dialog.append("当前文件已处理时长: " + StatisticsService.getCurrentFileEncryptSpentTimeInHuman() + "\n"); 162 | dialog.append("当前文件预计剩余时长: " + StatisticsService.computeCurrentFileLastTimeInHuman() + "\n"); 163 | } 164 | 165 | private static void asyncRunActualTaskThread(String targetFileOrDir, JButton stopCommandBtn, AbstractFileEncoder fileEncoder, ChooseTypeEnum chooseTypeEnum, boolean isEncryptOperation) { 166 | String magicWord = getOperationType(isEncryptOperation); 167 | ExecutorService executorService = Executors.newSingleThreadExecutor(); 168 | executorService.submit(() -> { 169 | // 计算文件信息 170 | try { 171 | //获取锁 172 | OperationLockStatusService.lock(); 173 | //统计文件信息 174 | computeFileInfo(new File(targetFileOrDir), fileEncoder, chooseTypeEnum, isEncryptOperation); 175 | //没有收到停止命令,执行状态从计算中变为加密中 176 | if (!StopCommandStatusService.getStopStatus().equals(StopCommandStatusService.STOP_STATUS_REQUIRE_STOP)) { 177 | ComputingStatusService.setComputeStatus(ComputingStatusService.COMPUTE_STATUS_RUNNING_ENCRYPT); 178 | } 179 | //收到停止命令,返回 180 | else { 181 | return; 182 | } 183 | // [统计] 设置加密开始时间 184 | StatisticsService.setOperationBeginTime(System.currentTimeMillis()); 185 | if (isEncryptOperation) { 186 | fileEncoder.encodeFileOrDir(new File(targetFileOrDir), chooseTypeEnum); 187 | } else { 188 | new AbstractFileEncoder() { 189 | @Override 190 | public FileEncoderTypeEnum getFileEncoderType() { 191 | //解密用不到这个参数 192 | return null; 193 | } 194 | }.decodeFileOrDir(new File(targetFileOrDir), chooseTypeEnum); 195 | } 196 | } catch (Exception ex) { 197 | log.info(magicWord + "操作异常" + ",文件路径:" + targetFileOrDir, ex); 198 | ta.append(magicWord + "操作异常:" + ex.getMessage() + "\r\n"); 199 | JOptionPane.showConfirmDialog(f, magicWord + "操作出错!!!", "提示", JOptionPane.DEFAULT_OPTION); 200 | return; 201 | } finally { 202 | ta.append(magicWord + "操作成功, 耗时:" + StatisticsService.generateTotalSpendTimeMillisecondsInHuman() + ", 路径名: " + targetFileOrDir + "\r\n"); 203 | //释放锁 204 | OperationLockStatusService.releaseLock(); 205 | //[统计] 清除统计数据放在asyncRefreshDialog方法中执行 206 | } 207 | }); 208 | } 209 | 210 | /** 211 | * 统计文件信息 212 | * 1. 文件名称加密,统计文件和文件夹总数 213 | * 2. 文件头部加密,统计文件总数 214 | * 3. 文件内容加密,统计文件总数和待加密的文件总数 215 | */ 216 | private static void computeFileInfo(File file, AbstractFileEncoder fileEncoder, ChooseTypeEnum chooseTypeEnum, boolean isEncryptOperation) { 217 | //解密操作没有该参数 218 | if (fileEncoder == null) { 219 | //无任何意义 220 | fileEncoder = new FileContentEncoder(); 221 | } 222 | FileEncoderTypeEnum fileEncoderType = fileEncoder.getFileEncoderType(); 223 | if (PrivateDataService.isFileMaskFile(file)) { 224 | return; 225 | } 226 | if (chooseTypeEnum.equals(ChooseTypeEnum.FILE_ONLY)) { 227 | if (!isEncryptOperation || fileEncoderType.equals(FileEncoderTypeEnum.FILE_CONTENT_ENCODE)) { 228 | computeBytes4ContentEncrypt(file, fileEncoderType, isEncryptOperation); 229 | } 230 | StatisticsService.scanFileTotalAmount++; 231 | return; 232 | } else if (chooseTypeEnum.equals(ChooseTypeEnum.CURRENT_DIR_ONLY)) { 233 | if (!isEncryptOperation || fileEncoderType.equals(FileEncoderTypeEnum.FILE_OR_DIR_NAME_ENCODE)) { 234 | StatisticsService.scanFileTotalAmount++; 235 | } 236 | File[] files = file.listFiles(); 237 | if (files != null && files.length > 0) { 238 | for (File one : files) { 239 | if (one.isDirectory()) { 240 | if (!isEncryptOperation || fileEncoderType.equals(FileEncoderTypeEnum.FILE_OR_DIR_NAME_ENCODE)) { 241 | StatisticsService.scanFileTotalAmount++; 242 | } 243 | } else { 244 | if (!isEncryptOperation || fileEncoderType.equals(FileEncoderTypeEnum.FILE_CONTENT_ENCODE)) { 245 | computeBytes4ContentEncrypt(one, fileEncoderType, isEncryptOperation); 246 | } 247 | StatisticsService.scanFileTotalAmount++; 248 | } 249 | } 250 | } 251 | } else if (chooseTypeEnum.equals(ChooseTypeEnum.CASCADE_DIR)) { 252 | // 收到停止命令 253 | if (StopCommandStatusService.getStopStatus().equals(StopCommandStatusService.STOP_STATUS_REQUIRE_STOP)) { 254 | return; 255 | } 256 | 257 | if (file.isDirectory()) { 258 | if (!isEncryptOperation || fileEncoderType.equals(FileEncoderTypeEnum.FILE_OR_DIR_NAME_ENCODE)) { 259 | StatisticsService.scanFileTotalAmount++; 260 | } 261 | File[] files = file.listFiles(); 262 | if (files != null && files.length > 0) { 263 | for (File one : files) { 264 | if (!one.isDirectory() && (!isEncryptOperation || fileEncoderType.equals(FileEncoderTypeEnum.FILE_CONTENT_ENCODE))) { 265 | computeBytes4ContentEncrypt(one, fileEncoderType, isEncryptOperation); 266 | } 267 | computeFileInfo(one, fileEncoder, chooseTypeEnum, isEncryptOperation); 268 | } 269 | } 270 | } else { 271 | StatisticsService.scanFileTotalAmount++; 272 | } 273 | } 274 | } 275 | 276 | /** 277 | * 计算全文加密涉及的字节总数 278 | */ 279 | private static boolean computeBytes4ContentEncrypt(File file, FileEncoderTypeEnum fileEncoderType, boolean isEncryptOperation) { 280 | if (!file.isDirectory()) { 281 | try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) { 282 | boolean existsTail = TailModelService.existsTailModel(raf); 283 | //不存在 284 | if (!existsTail) { 285 | if (isEncryptOperation) { 286 | StatisticsService.todoFileTotalBytes = StatisticsService.todoFileTotalBytes + raf.length(); 287 | } 288 | } 289 | //存在 290 | else { 291 | TailModel tailModel = TailModelService.getExistsTailModelInfo(raf); 292 | boolean isCurrentUser = TailModelService.isCurrentUser(tailModel.getBelongUserMd516(), PasswordService.getMd51ForFileAuthentication()); 293 | boolean hasEncryptedByTypeOrConflict = TailModelService.isEncryptedByTypeOrConflict(tailModel, fileEncoderType); 294 | byte[] encodeTypeFlagByte = tailModel.getEncodeType16(); 295 | byte flag = encodeTypeFlagByte[fileEncoderType.getPosition()]; 296 | 297 | //加密操作 298 | if (isEncryptOperation) { 299 | if (!hasEncryptedByTypeOrConflict && isCurrentUser) { 300 | StatisticsService.todoFileTotalBytes = StatisticsService.todoFileTotalBytes + raf.length(); 301 | } 302 | } else { 303 | if (flag == TailModelService.ENCODED_FLAG && isCurrentUser) { 304 | StatisticsService.todoFileTotalBytes = StatisticsService.todoFileTotalBytes + raf.length(); 305 | } 306 | } 307 | } 308 | } catch (Exception ex) { 309 | log.info("计算过程中,文件打开失败", file.getPath()); 310 | return true; 311 | } 312 | } 313 | return false; 314 | } 315 | 316 | /** 317 | * 返回操作类型对应的字符串 318 | */ 319 | private static String getOperationType(boolean isEncryptOperation) { 320 | return isEncryptOperation ? "加密" : "解密"; 321 | } 322 | 323 | /** 324 | * 判断选择路径是否合法 325 | * 为了系统安全, C:\ D:\ E:\ F:\ 这类长度太短的路径不支持加密 326 | */ 327 | private static boolean isValidPath(String targetPath) { 328 | if (targetPath.length() <= 3) { 329 | return false; 330 | } 331 | return true; 332 | } 333 | } 334 | -------------------------------------------------------------------------------- /src/main/java/com/qzw/filemask/service/TailModelService.java: -------------------------------------------------------------------------------- 1 | package com.qzw.filemask.service; 2 | 3 | import com.qzw.filemask.constant.Constants; 4 | import com.qzw.filemask.enums.FileEncoderTypeEnum; 5 | import com.qzw.filemask.model.TailModel; 6 | import com.qzw.filemask.util.*; 7 | import lombok.Data; 8 | import lombok.extern.log4j.Log4j2; 9 | import org.apache.commons.lang3.StringUtils; 10 | import org.apache.commons.lang3.math.NumberUtils; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.io.RandomAccessFile; 15 | import java.util.Arrays; 16 | import java.util.UUID; 17 | 18 | /** 19 | * 文件尾部数据结构service 20 | *

21 | * 16字节: 用户md5 22 | * 16字节: 加密类型 23 | * 32字节: uuid(使用md5-4执行xor,加密方式1,2,3都需使用) 24 | * 4字节: 文件头部(加密方式2使用,md5-23+uuid执行xor) 25 | * n字节: 文件名称(加密方式1使用,md5-23+uuid执行xor) 26 | * 8字节: 文件原始长度 27 | * 16字节: FileMaskTailFlag加密标识 28 | *

29 | * 注: 如果加密方式为文件头部加密且文件数据长度<4,则使用全文加密 30 | * 31 | * @author quanzongwei 32 | * @date 2020/5/13 33 | */ 34 | @Data 35 | @Log4j2 36 | public class TailModelService { 37 | public static String FILE_MASK_TAIL_FLAG = "FileMaskTailFlag"; 38 | /** 39 | * 已经加密标志 40 | */ 41 | public static byte ENCODED_FLAG = (byte) 0x01; 42 | 43 | private static int SIZE_1024 = 1024 * 64; 44 | /** 45 | * 文件头部加密阈值,小于阈值的切换为文件内容加密 46 | */ 47 | private static int FILE_HEAD_ENCODE_THRESHOLD = 4; 48 | 49 | /** 50 | * 入口方法:按照某种加密类型加密单个文件或者文件夹 51 | * 52 | * @param fileOrDir 53 | * @param fileEncoderType 54 | */ 55 | public static void encryptByType(File fileOrDir, FileEncoderTypeEnum fileEncoderType) { 56 | //文件夹加密走单独逻辑 57 | if (fileOrDir.isDirectory()) { 58 | if (fileEncoderType.equals(FileEncoderTypeEnum.FILE_OR_DIR_NAME_ENCODE)) { 59 | try { 60 | encryptOrDecryptDirectoryName(fileOrDir, true); 61 | } catch (IOException e) { 62 | log.info("加密文件夹失败,{}", fileOrDir); 63 | } 64 | return; 65 | } 66 | return; 67 | } 68 | 69 | // 流程:尾部数据结构是否存在? 是否存在是当前用户? 是否是使用同一种方式加密? 是否互斥? 70 | try (RandomAccessFile raf = new RandomAccessFile(fileOrDir, "rw")) { 71 | boolean existsTail = TailModelService.existsTailModel(raf); 72 | //不存在 73 | if (!existsTail) { 74 | TailModelService.doEncryptFileAndResetTailModel(fileOrDir, raf, fileEncoderType, true); 75 | } 76 | //存在 77 | else { 78 | TailModel tailModel = getExistsTailModelInfo(raf); 79 | boolean isCurrentUser = TailModelService.isCurrentUser(tailModel.getBelongUserMd516(), PasswordService.getMd51ForFileAuthentication()); 80 | if (!isCurrentUser) { 81 | log.info("文件认证用户失败,{}", fileOrDir); 82 | return; 83 | } 84 | boolean hasEncryptedByTypeOrConflict = TailModelService.isEncryptedByTypeOrConflict(tailModel, fileEncoderType); 85 | if (hasEncryptedByTypeOrConflict) { 86 | log.info("文件已加密,无需重复加密,{}", fileOrDir); 87 | return; 88 | } 89 | TailModelService.doEncryptFileAndResetTailModel(fileOrDir, raf, fileEncoderType, false); 90 | } 91 | } catch (Exception ex) { 92 | log.info("文件操作失败,加密操作不成功,{}", fileOrDir.getPath()); 93 | return; 94 | } 95 | 96 | //IO操作完成后才可以执行重命名操作 97 | if (fileEncoderType.equals(FileEncoderTypeEnum.FILE_OR_DIR_NAME_ENCODE)) { 98 | String encryptedFileNameFromUuid = PrivateDataService.getEncryptedFileNameFromUuid(); 99 | String targetPath = fileOrDir.getParent() + File.separatorChar + encryptedFileNameFromUuid; 100 | boolean b = fileOrDir.renameTo(new File(targetPath)); 101 | log.info("文件名称是否加密成功:{},{}", b, fileOrDir.getPath()); 102 | } 103 | } 104 | 105 | /** 106 | * 入口方法: 解密文件或者文件夹 107 | * 108 | * @param fileOrDir 109 | */ 110 | public static void decryptAllType(File fileOrDir) { 111 | // 文件夹走单独逻辑 112 | if (fileOrDir.isDirectory()) { 113 | try { 114 | encryptOrDecryptDirectoryName(fileOrDir, false); 115 | } catch (IOException e) { 116 | log.info("解密文件夹失败,{}", fileOrDir); 117 | } 118 | return; 119 | } 120 | 121 | String targetPath = null; 122 | try (RandomAccessFile raf = new RandomAccessFile(fileOrDir, "rw")) { 123 | if (!existsTailModel(raf)) { 124 | log.info("尾部文件不存在,无需解密,{}", fileOrDir); 125 | return; 126 | } 127 | TailModel model = getExistsTailModelInfo(raf); 128 | boolean currentUser = isCurrentUser(model.getBelongUserMd516(), PasswordService.getMd51ForFileAuthentication()); 129 | if (!currentUser) { 130 | log.info("文件用户认证失败,解密操作不成功,{}", fileOrDir.getPath()); 131 | return; 132 | } 133 | byte[] encodeType16 = model.getEncodeType16(); 134 | if (encodeType16[FileEncoderTypeEnum.FILE_OR_DIR_NAME_ENCODE.getPosition()] == ENCODED_FLAG) { 135 | targetPath = fileOrDir.getParent() + File.separator + new String(model.getFileNameX(), "UTF-8"); 136 | } 137 | if (encodeType16[FileEncoderTypeEnum.FILE_HEADER_ENCODE.getPosition()] == ENCODED_FLAG) { 138 | raf.seek(0); 139 | raf.write(model.getHead4()); 140 | 141 | } else if (encodeType16[FileEncoderTypeEnum.FILE_CONTENT_ENCODE.getPosition()] == ENCODED_FLAG) { 142 | //执行解密 143 | doFileContentEncryptionOrDecryption(raf, false, model.getUuid32(), ByteUtil.bytesToLong(model.getOriginTextSize8())); 144 | } 145 | //最后直接删除尾部文件,数据恢复正常 146 | raf.setLength(ByteUtil.bytesToLong(model.getOriginTextSize8())); 147 | 148 | } catch (Exception ex) { 149 | log.info("文件操作失败,解密操作不成功,{},{}", fileOrDir.getPath(), ex.getMessage()); 150 | return; 151 | } 152 | 153 | //IO操作完成后才可以执行重命名操作 154 | if (StringUtils.isNotBlank(targetPath)) { 155 | boolean b = fileOrDir.renameTo(new File(targetPath)); 156 | log.info("文件名称是否解密成功:{},{}", b, fileOrDir.getPath()); 157 | } 158 | } 159 | 160 | /** 161 | * 加密或者解密文件夹 162 | * 163 | * @param fileOrDir 164 | * @param ifEncodeOperation 165 | * @throws IOException 166 | */ 167 | private static void encryptOrDecryptDirectoryName(File fileOrDir, boolean ifEncodeOperation) throws IOException { 168 | // 加密 169 | if (ifEncodeOperation) { 170 | String encryptedDirName = PrivateDataService.getEncryptedDirNameFromSequenceAndBase64RandomStr(fileOrDir); 171 | File existsPrivateDataFile = PrivateDataService.getPrivateDataFileReleaseV2(fileOrDir, fileOrDir.getName()); 172 | if (existsPrivateDataFile.exists()) { 173 | log.info("文件夹已加密成功,无需重复加密,{}", fileOrDir); 174 | return; 175 | } 176 | 177 | File privateDataFile = PrivateDataService.getPrivateDataFileReleaseV2(fileOrDir, encryptedDirName); 178 | if (!privateDataFile.exists()) { 179 | try { 180 | privateDataFile.createNewFile(); 181 | } catch (IOException e) { 182 | log.info("创建私有数据文件失败,{}", fileOrDir); 183 | return; 184 | } 185 | } else { 186 | // fixed since v1.2 187 | log.info("已存在同名私有数据文件,不执行加密{}", fileOrDir); 188 | return; 189 | } 190 | 191 | try (RandomAccessFile raf = new RandomAccessFile(privateDataFile, "rw")) { 192 | TailModelService.doEncryptFileAndResetTailModel(fileOrDir, raf, FileEncoderTypeEnum.FILE_OR_DIR_NAME_ENCODE, true); 193 | } catch (Exception ex) { 194 | log.info("私有数据文件保存加密文件夹名称失败,{}", fileOrDir); 195 | return; 196 | } 197 | boolean success = fileOrDir.renameTo(new File(fileOrDir.getParent() + File.separator + encryptedDirName)); 198 | if (!success) { 199 | log.info("源文件重命名失败,{}", fileOrDir); 200 | //同时删除私有数据文件 201 | privateDataFile.delete(); 202 | } 203 | } 204 | //解密 205 | else { 206 | //目前只有FM开头或数字命名才可能是加密后的文件夹名称(数字是为了兼容历史逻辑) 207 | if (!(NumberUtils.isCreatable(fileOrDir.getName()) 208 | || fileOrDir.getName().startsWith(Constants.FILE_MASK_PREFIX_NAME_FOR_NAME_ENCRYPT))) { 209 | log.info("非加密文件夹,无需解密,{}", fileOrDir); 210 | return; 211 | } 212 | File privateDataFile = PrivateDataService.getPrivateDataFileReleaseV2(fileOrDir, fileOrDir.getName()); 213 | if (!privateDataFile.exists()) { 214 | log.info("文件夹未加密,无需解密,{}", fileOrDir); 215 | return; 216 | } 217 | TailModel model; 218 | try (RandomAccessFile raf = new RandomAccessFile(privateDataFile, "rw")) { 219 | model = getExistsTailModelInfo(raf); 220 | } 221 | boolean isCurrentUser = TailModelService.isCurrentUser(model.getBelongUserMd516(), PasswordService.getMd51ForFileAuthentication()); 222 | if (!isCurrentUser) { 223 | log.info("当前文件夹已被其他用户加密,解密失败,{}", fileOrDir); 224 | return; 225 | } 226 | byte[] fileNameX = model.getFileNameX(); 227 | boolean success = fileOrDir.renameTo(new File(fileOrDir.getParent() + File.separator + new String(fileNameX, "UTF-8"))); 228 | if (success) { 229 | boolean delete = privateDataFile.delete(); 230 | if (!delete) { 231 | log.info("文件夹解密成功,但私有数据文件删除失败,请检查,{}", fileOrDir); 232 | return; 233 | } 234 | log.info("文件夹解密成功,{}", fileOrDir); 235 | } else { 236 | log.info("文件夹解密失败,{}", fileOrDir); 237 | } 238 | } 239 | } 240 | 241 | /** 242 | * 是否存在尾部数据结构 243 | * 通过: FileMaskTailFlag标识 244 | */ 245 | public static boolean existsTailModel(RandomAccessFile raf) throws IOException { 246 | long length = raf.length(); 247 | if (length < TailModel.MIN_LENGTH) { 248 | return false; 249 | } 250 | raf.seek(length - FILE_MASK_TAIL_FLAG.length()); 251 | byte[] info = new byte[FILE_MASK_TAIL_FLAG.length()]; 252 | 253 | raf.read(info); 254 | 255 | String s = new String(info); 256 | if (s.equals(FILE_MASK_TAIL_FLAG)) { 257 | return true; 258 | } 259 | return false; 260 | } 261 | 262 | /** 263 | * 获取原始文本大小 264 | */ 265 | public static long getOriginTextLength(RandomAccessFile raf) throws IOException { 266 | raf.seek(raf.length() - TailModel.ORIGIN_SIZE_8 - TailModel.TAIL_FLAG_16); 267 | byte[] tailSizeByte = new byte[TailModel.ORIGIN_SIZE_8]; 268 | raf.read(tailSizeByte); 269 | return ByteUtil.bytesToLong(tailSizeByte); 270 | } 271 | 272 | /** 273 | * 判断是否已经加密过或者加密类型冲突 274 | */ 275 | public static boolean isEncryptedByTypeOrConflict(TailModel model, FileEncoderTypeEnum fileEncoderType) throws IOException { 276 | byte[] encodeTypeFlagByte = model.getEncodeType16(); 277 | byte flag = encodeTypeFlagByte[fileEncoderType.getPosition()]; 278 | if (flag == ENCODED_FLAG) { 279 | return true; 280 | } 281 | //互斥问题处理 282 | if (fileEncoderType.equals(FileEncoderTypeEnum.FILE_HEADER_ENCODE)) { 283 | byte flag2 = encodeTypeFlagByte[FileEncoderTypeEnum.FILE_CONTENT_ENCODE.getPosition()]; 284 | if (flag2 == ENCODED_FLAG) { 285 | return true; 286 | } 287 | } 288 | if (fileEncoderType.equals(FileEncoderTypeEnum.FILE_CONTENT_ENCODE)) { 289 | byte flag2 = encodeTypeFlagByte[FileEncoderTypeEnum.FILE_HEADER_ENCODE.getPosition()]; 290 | if (flag2 == ENCODED_FLAG) { 291 | return true; 292 | } 293 | } 294 | 295 | return false; 296 | } 297 | 298 | /** 299 | * 重置尾部数据结构 300 | * 使用场景: 301 | * 1. 首次加密文件 302 | * 2. 非首次加密文件 303 | * 3. 首次加密文件夹 304 | * 305 | * @param firstSetTail 是否是首次设置尾部数据 true:尾部数据结构不存在 false:尾部数据结构已存在 306 | */ 307 | private static void doEncryptFileAndResetTailModel(File fileOrDir, RandomAccessFile raf, FileEncoderTypeEnum fileEncoderType, boolean firstSetTail) throws IOException { 308 | TailModel model = new TailModel(); 309 | if (firstSetTail) { 310 | //如果TestUtil中的uuid不空的话,表示代码正在走单元测试,这个值固定写死; 正常代码逻辑中该值必须为空 311 | String uuid = TestUtil.uuid; 312 | if (StringUtils.isBlank(uuid)) { 313 | uuid = UUID.randomUUID().toString().replaceAll("-", ""); 314 | } 315 | byte[] md51 = PasswordService.getMd51ForFileAuthentication(); 316 | model.setBelongUserMd516(md51); 317 | model.setEncodeType16(new byte[TailModel.ENCODE_TYPE_FLAG_16]); 318 | model.setUuid32(uuid.getBytes()); 319 | model.setHead4(new byte[TailModel.HEAD_4]); 320 | model.setOriginTextSize8(ByteUtil.longToBytes(raf.length())); 321 | model.setTailFlag16(TailModelService.FILE_MASK_TAIL_FLAG.getBytes()); 322 | } else { 323 | model = getExistsTailModelInfo(raf); 324 | } 325 | 326 | byte[] type16 = model.getEncodeType16(); 327 | if (type16 == null||type16.length==0) { 328 | type16 = new byte[TailModel.ENCODE_TYPE_FLAG_16]; 329 | } 330 | 331 | //文件名称加密 332 | if (fileEncoderType.equals(FileEncoderTypeEnum.FILE_OR_DIR_NAME_ENCODE)) { 333 | //设置标记位 334 | type16[FileEncoderTypeEnum.FILE_OR_DIR_NAME_ENCODE.getPosition()] = ENCODED_FLAG; 335 | model.setEncodeType16(type16); 336 | //设置加密文件名 337 | model.setFileNameX(fileOrDir.getName().getBytes("UTF-8")); 338 | } 339 | 340 | //文件头部加密 341 | if (fileEncoderType.equals(FileEncoderTypeEnum.FILE_HEADER_ENCODE)) { 342 | if (raf.length() < FILE_HEAD_ENCODE_THRESHOLD) { 343 | log.info("文件数据不足4字节,切换为全文加密,{}", fileOrDir); 344 | fileEncoderType = FileEncoderTypeEnum.FILE_CONTENT_ENCODE; 345 | } else { 346 | //设置标记位 347 | type16[FileEncoderTypeEnum.FILE_HEADER_ENCODE.getPosition()] = ENCODED_FLAG; 348 | model.setEncodeType16(type16); 349 | raf.seek(0); 350 | //设置加密头部数据 351 | byte[] originHead4 = new byte[4]; 352 | raf.read(originHead4); 353 | model.setHead4(originHead4); 354 | //执行加密操作 355 | raf.seek(0); 356 | raf.writeByte(255); 357 | raf.writeByte(254); 358 | raf.writeByte(0); 359 | raf.writeByte(0); 360 | } 361 | } 362 | //文件全文加密 363 | if (fileEncoderType.equals(FileEncoderTypeEnum.FILE_CONTENT_ENCODE)) { 364 | //[统计] 设置文件是否是全文加密 365 | StatisticsService.setIfCurrentFileExecuteContentEncrypt(true); 366 | 367 | //设置标记位 368 | type16[FileEncoderTypeEnum.FILE_CONTENT_ENCODE.getPosition()] = ENCODED_FLAG; 369 | model.setEncodeType16(type16); 370 | //全文加密 371 | doFileContentEncryptionOrDecryption(raf, true, model.getUuid32(), raf.length()); 372 | } 373 | 374 | resetTailModel(raf, model); 375 | } 376 | 377 | /** 378 | * 重置尾部数据结构 379 | * 380 | * 加密操作同一在此处进行 381 | */ 382 | private static void resetTailModel(RandomAccessFile raf, TailModel model) throws IOException { 383 | long length = ByteUtil.bytesToLong(model.getOriginTextSize8()); 384 | //截取 385 | raf.setLength(length); 386 | //移动游标 387 | raf.seek(length); 388 | raf.write(model.getBelongUserMd516()); 389 | raf.write(model.getEncodeType16()); 390 | raf.write(EncryptUtil.encryptUuid(PasswordService.getMd545ForUuidEncrypt(), model.getUuid32())); 391 | raf.write(EncryptUtil.encryptContent(model.getUuid32(), PasswordService.getMd523ForContentEncrypt(), model.getHead4())); 392 | if (model.getFileNameX() != null && model.getFileNameX().length > 0) { 393 | raf.write(EncryptUtil.encryptContent(model.getUuid32(), PasswordService.getMd523ForContentEncrypt(), model.getFileNameX())); 394 | } 395 | // 这个很重要,一定是writeLong才会占用四个字节 396 | raf.writeLong(length); 397 | raf.write(model.getTailFlag16()); 398 | } 399 | 400 | /** 401 | * 使用场景(全文加密): 402 | * 1. 首次生成尾部数据结构加密 403 | * 2. 非首次生成尾部数据结构加密 404 | * 3. 解密 405 | * 406 | * @param isEncodeOperation 是否是加密操作 407 | * @param originTextLength 原始文件长度 408 | * @throws IOException 409 | */ 410 | private static void doFileContentEncryptionOrDecryption(RandomAccessFile raf, boolean isEncodeOperation, byte[] originUuidBytes, long originTextLength) throws IOException { 411 | long length = originTextLength; 412 | long blockNum = length / SIZE_1024; 413 | Long remain = length % SIZE_1024; 414 | for (long i = 0; i < blockNum; i++) { 415 | 416 | long begin = System.currentTimeMillis(); 417 | 418 | byte[] bBlock = new byte[SIZE_1024]; 419 | raf.seek(0 + i * SIZE_1024); 420 | raf.read(bBlock, 0, SIZE_1024); 421 | if (isEncodeOperation) { 422 | bBlock = EncryptUtil.encryptContent(originUuidBytes, PasswordService.getMd523ForContentEncrypt(), bBlock); 423 | } else { 424 | bBlock = EncryptUtil.decryptContent(originUuidBytes, PasswordService.getMd523ForContentEncrypt(), bBlock); 425 | } 426 | raf.seek(0 + i * SIZE_1024); 427 | raf.write(bBlock); 428 | long end = System.currentTimeMillis(); 429 | //[统计] 增加所有文件字节数据已加密总时间 430 | StatisticsService.setDoneFileTotalBytesSpendTime(StatisticsService.getDoneFileTotalBytesSpendTime() + (end - begin)); 431 | //[统计] 增加所有文件已加密大小 432 | StatisticsService.setDoneFileTotalBytes(StatisticsService.getDoneFileTotalBytes() + bBlock.length); 433 | //[统计] 增加当前文件已加密大小 434 | StatisticsService.setCurrentFileCompletedBytes(StatisticsService.getCurrentFileCompletedBytes() + bBlock.length); 435 | } 436 | // 尾部数据处理 437 | if (remain > 0) { 438 | 439 | long begin = System.currentTimeMillis(); 440 | 441 | byte[] bRemain = new byte[remain.intValue()]; 442 | raf.seek(0 + blockNum * SIZE_1024); 443 | raf.read(bRemain, 0, remain.intValue()); 444 | if (isEncodeOperation) { 445 | bRemain = EncryptUtil.encryptContent(originUuidBytes, PasswordService.getMd523ForContentEncrypt(), bRemain); 446 | } else { 447 | bRemain = EncryptUtil.decryptContent(originUuidBytes, PasswordService.getMd523ForContentEncrypt(), bRemain); 448 | } 449 | raf.seek(0 + blockNum * SIZE_1024); 450 | raf.write(bRemain); 451 | long end = System.currentTimeMillis(); 452 | //[统计] 增加所有文件字节数据已加密总时间 453 | StatisticsService.setDoneFileTotalBytesSpendTime(StatisticsService.getDoneFileTotalBytesSpendTime() + (end - begin)); 454 | //[统计] 增加所有文件已加密大小 455 | StatisticsService.setDoneFileTotalBytes(StatisticsService.getDoneFileTotalBytes() + bRemain.length); 456 | //[统计] 增加当前文件已加密大小 457 | StatisticsService.setCurrentFileCompletedBytes(StatisticsService.getCurrentFileCompletedBytes() + bRemain.length); 458 | } 459 | } 460 | 461 | /** 462 | * 获取已经存在的TailModel数据结构,同时解密 463 | * @param raf 464 | * @return 调用这个方法之前, 需要保证尾部数据结构一定存在的 465 | * @throws IOException 466 | */ 467 | public static TailModel getExistsTailModelInfo(RandomAccessFile raf) throws IOException { 468 | TailModel model = new TailModel(); 469 | long originTextSize = getOriginTextLength(raf); 470 | raf.seek(originTextSize); 471 | byte[] tailModelByte = new byte[Math.toIntExact(raf.length() - originTextSize)]; 472 | raf.read(tailModelByte); 473 | 474 | byte[] userMd5 = Arrays.copyOfRange(tailModelByte, 0, TailModel.USER_MD5_LENGTH_16); 475 | byte[] encodeType16 = Arrays.copyOfRange(tailModelByte, 0 + TailModel.USER_MD5_LENGTH_16, 0 + TailModel.USER_MD5_LENGTH_16 + TailModel.ENCODE_TYPE_FLAG_16); 476 | byte[] uuid32 = Arrays.copyOfRange(tailModelByte, 0 + TailModel.USER_MD5_LENGTH_16 + TailModel.ENCODE_TYPE_FLAG_16, 0 + TailModel.USER_MD5_LENGTH_16 + TailModel.ENCODE_TYPE_FLAG_16 + TailModel.UUID_32); 477 | byte[] head4 = Arrays.copyOfRange(tailModelByte, 0 + TailModel.USER_MD5_LENGTH_16 + TailModel.ENCODE_TYPE_FLAG_16 + TailModel.UUID_32, 0 + TailModel.USER_MD5_LENGTH_16 + TailModel.ENCODE_TYPE_FLAG_16 + TailModel.UUID_32 + TailModel.HEAD_4); 478 | // 可以copy 0 字节数据 479 | byte[] fileNameX = Arrays.copyOfRange(tailModelByte, 0 + TailModel.USER_MD5_LENGTH_16 + TailModel.ENCODE_TYPE_FLAG_16 + TailModel.UUID_32 + TailModel.HEAD_4, 0 + TailModel.USER_MD5_LENGTH_16 + TailModel.ENCODE_TYPE_FLAG_16 + TailModel.UUID_32 + TailModel.HEAD_4 + tailModelByte.length - TailModel.MIN_LENGTH); 480 | byte[] originSize8 = Arrays.copyOfRange(tailModelByte, tailModelByte.length - TailModel.TAIL_FLAG_16 - TailModel.ORIGIN_SIZE_8, tailModelByte.length - TailModel.TAIL_FLAG_16); 481 | byte[] tailFlag16 = Arrays.copyOfRange(tailModelByte, tailModelByte.length - TailModel.TAIL_FLAG_16, tailModelByte.length); 482 | 483 | model.setBelongUserMd516(userMd5); 484 | model.setEncodeType16(encodeType16); 485 | byte[] originUuid32 = EncryptUtil.decryptUuid(PasswordService.getMd545ForUuidEncrypt(), uuid32); 486 | model.setUuid32(originUuid32); 487 | model.setHead4(EncryptUtil.decryptContent(originUuid32, PasswordService.getMd523ForContentEncrypt(), head4)); 488 | model.setFileNameX(EncryptUtil.decryptContent(originUuid32, PasswordService.getMd523ForContentEncrypt(), fileNameX)); 489 | model.setOriginTextSize8(originSize8); 490 | model.setTailFlag16(tailFlag16); 491 | 492 | return model; 493 | } 494 | 495 | /** 496 | * 是否是当前用户 497 | */ 498 | public static boolean isCurrentUser(byte[] belongUserMd516, byte[] md51ForFileAuthentication) { 499 | return Arrays.equals(belongUserMd516, md51ForFileAuthentication); 500 | } 501 | } 502 | --------------------------------------------------------------------------------