├── 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 | 
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 | 
17 | 首次运行需要输入用户密码,并且确认密码。请牢记您的密码,当一个文件被加密后,只有持有该密码的用户才能解密。
18 |
19 | ### 2.2 主界面
20 | 
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 | 
45 |
46 |
47 | 联系作者菜单项中可以找到项目源码和作者联系方式
48 | 
49 |
50 | ### 2.4 加密/解密处理进度
51 | 这个是作者非常满意的一个功能,如下所示:
52 | 
53 | 参数解释:
54 | 1. 展示扫描文件总数和总大小
55 | 2. 展示已处理文件总数和已耗时间
56 | 3. 预估剩余处理时间
57 | 4. 展示当前文件处理进度
58 | 5. 展示当前文件预计剩余处理时间
59 | 6. 提前停止,如果用户待处理的文件比较多,可以点击提前停止按钮,系统等待当前文件处理完成,到达安全点后,操作完成,此时部分文件处理成功,部分文件未处理
60 |
61 | 使用场景: 预估剩余时间以及支持提前停止
62 | ## 三 文件加解密示例
63 | #### 3.1 加密类型选择加密类型一(文件名称加密),加密方式使用文件夹级联加密
64 | **加密前**
65 | 
66 | **加密后**
67 | 
68 | 加密后文件和文件夹的名称变成一个递增序号,每个文件夹下会多出一个.fileMask文件夹,用于保存递增的序号信息以及加密过的文件夹原始名称信息
69 |
70 | **解密后**
71 | 
72 | 文件解密后,文件和文件夹名称恢复原样
73 |
74 | #### 3.2 加密类型选择加密类型二(文件头部加密),加密方式使用文件加密
75 | **加密前**
76 | 
77 | **加密后**
78 | 
79 | 加密后文件内容变成乱码
80 |
81 | **解密后**
82 | 
83 | 文件解密后,文件数据恢复原样,如果解密后依然显示乱码,只需要关闭该文件并重新打开即可
84 |
85 | #### 3.3 加密类型选择加密类型三(文件全文加密),加密方式使用文件加密
86 | **加密前**
87 | 
88 | **加密后**
89 | 
90 | 加密后文件内容变成乱码
91 |
92 | **解密后**
93 | 
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 | 
24 | ### 3.2 mac系统(进度页)
25 |
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 | "
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 | --------------------------------------------------------------------------------