├── .gitignore ├── LICENSE ├── README.md ├── classfinal-core ├── .gitignore ├── pom.xml └── src │ └── main │ └── java │ └── net │ └── roseboy │ └── classfinal │ ├── AgentTransformer.java │ ├── Const.java │ ├── CoreAgent.java │ ├── InputForm.java │ ├── JarDecryptor.java │ ├── JarEncryptor.java │ └── util │ ├── ClassUtils.java │ ├── CmdLineOption.java │ ├── EncryptUtils.java │ ├── HtmlUtils.java │ ├── IoUtils.java │ ├── JarUtils.java │ ├── Log.java │ ├── StrUtils.java │ └── SysUtils.java ├── classfinal-fatjar ├── .gitignore ├── pom.xml └── src │ └── main │ └── java │ └── net │ └── roseboy │ └── classfinal │ ├── Agent.java │ └── Main.java ├── classfinal-maven-plugin ├── .gitignore ├── pom.xml └── src │ └── main │ └── java │ └── net │ └── roseboy │ └── classfinal │ └── plugin │ └── ClassFinalPlugin.java ├── classfinal-web ├── .gitignore ├── pom.xml └── src │ └── main │ ├── java │ └── net │ │ └── roseboy │ │ └── classfinal │ │ ├── Application.java │ │ └── web │ │ └── MainController.java │ └── resources │ ├── application.yml │ ├── logback-spring.xml │ ├── static │ ├── css │ │ └── css.css │ ├── js │ │ ├── jQuery.time.js │ │ ├── jquery-1.9.1.min.js │ │ └── jquery.easing.min.js │ └── pattern.png │ └── templates │ └── index.ftl └── pom.xml /.gitignore: -------------------------------------------------------------------------------- 1 | .mymetadata 2 | .checkstyle 3 | .classpath 4 | .project 5 | .class 6 | .war 7 | .zip 8 | .rar 9 | .idea 10 | *.iml 11 | *.project 12 | .settings/* 13 | .vscode/* 14 | /target/* 15 | /target/ 16 | /target 17 | /bin 18 | log/* 19 | log 20 | /log 21 | logs/* 22 | logs 23 | /logs 24 | .DS_Store 25 | dependency-reduced-pom.xml 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ClassFinal 2 | 3 | ## 介绍 4 | ClassFinal是一款java class文件安全加密工具,支持直接加密jar包或war包,无需修改任何项目代码,兼容spring-framework;可避免源码泄漏或字节码被反编译。 5 | 6 | ##### Gitee: https://gitee.com/roseboy/classfinal 7 | 8 | ## 项目模块说明 9 | * **classfinal-core:** ClassFinal的核心模块,几乎所有加密的代码都在这里; 10 | * **classfinal-fatjar:** ClassFinal打包成独立运行的jar包; 11 | * **classfinal-maven-plugin:** ClassFinal加密的maven插件; 12 | 13 | ## 功能特性 14 | * 无需修改原项目代码,只要把编译好的jar/war包用本工具加密即可。 15 | * 运行加密项目时,无需求修改tomcat,spring等源代码。 16 | * 支持普通jar包、springboot jar包以及普通java web项目编译的war包。 17 | * 支持spring framework、swagger等需要在启动过程中扫描注解或生成字节码的框架。 18 | * 支持maven插件,添加插件后在打包过程中自动加密。 19 | * 支持加密WEB-INF/lib或BOOT-INF/lib下的依赖jar包。 20 | * 支持绑定机器,项目加密后只能在特定机器运行。 21 | * 支持加密springboot的配置文件。 22 | 23 | ## 环境依赖 24 | JDK 1.8 + 25 | 26 | ## 使用说明 27 | 28 | ### 下载 29 | [点此下载](https://repo1.maven.org/maven2/net/roseboy/classfinal-fatjar/1.2.1/classfinal-fatjar-1.2.1.jar) 30 | 31 | ### 加密 32 | 33 | 执行以下命令 34 | 35 | ```sh 36 | java -jar classfinal-fatjar.jar -file yourpaoject.jar -libjars a.jar,b.jar -packages com.yourpackage,com.yourpackage2 -exclude com.yourpackage.Main -pwd 123456 -Y 37 | ``` 38 | 39 | ```text 40 | 参数说明 41 | -file 加密的jar/war完整路径 42 | -packages 加密的包名(可为空,多个用","分割) 43 | -libjars jar/war包lib下要加密jar文件名(可为空,多个用","分割) 44 | -cfgfiles 需要加密的配置文件,一般是classes目录下的yml或properties文件(可为空,多个用","分割) 45 | -exclude 排除的类名(可为空,多个用","分割) 46 | -classpath 外部依赖的jar目录,例如/tomcat/lib(可为空,多个用","分割) 47 | -pwd 加密密码,如果是#号,则使用无密码模式加密 48 | -code 机器码,在绑定的机器生成,加密后只可在此机器上运行 49 | -Y 无需确认,不加此参数会提示确认以上信息 50 | ``` 51 | 52 | 结果: 生成 yourpaoject-encrypted.jar,这个就是加密后的jar文件;加密后的文件不可直接执行,需要配置javaagent。 53 | 54 | > 注: 55 | > 以上示例是直接用参数执行,也可以直接执行 java -jar classfinal-fatjar.jar按照步骤提示输入信息完成加密。 56 | 57 | ### maven插件方式 58 | 59 | 在要加密的项目pom.xml中加入以下插件配置,目前最新版本是:1.2.1。 60 | ```xml 61 | 62 | 63 | net.roseboy 64 | classfinal-maven-plugin 65 | ${classfinal.version} 66 | 67 | 000000 68 | com.yourpackage,com.yourpackage2 69 | application.yml 70 | org.spring 71 | a.jar,b.jar 72 | 73 | 74 | 75 | package 76 | 77 | classFinal 78 | 79 | 80 | 81 | 82 | ``` 83 | 运行mvn package时会在target下自动加密生成yourpaoject-encrypted.jar。 84 | 85 | maven插件的参数名称与直接运行的参数相同,请参考上节的参数说明。 86 | 87 | ### 无密码模式 88 | 89 | 加密时-pwd参数设为#,启动时可不用输入密码; 90 | 如果是war包,启动时指定参数 -nopwd,跳过输密码过程。 91 | 92 | ### 机器绑定 93 | 94 | 机器绑定只允许加密的项目在特定的机器上运行; 95 | 96 | 在需要绑定的机器上执行以下命令,生成机器码 97 | ```sh 98 | java -jar classfinal-fatjar.jar -C 99 | ``` 100 | 加密时用-code指定机器码。机器绑定可同时支持机器码+密码的方式加密。 101 | 102 | 103 | ### 启动加密后的jar 104 | 105 | 加密后的项目需要设置javaagent来启动,项目在启动过程中解密class,完全内存解密,不留下任何解密后的文件。 106 | 107 | 解密功能已经自动加入到 yourpaoject-encrypted.jar中,所以启动时-javaagent与-jar相同,不需要额外的jar包。 108 | 109 | 启动jar项目执行以下命令: 110 | 111 | ```sh 112 | java -javaagent:yourpaoject-encrypted.jar='-pwd 0000000' -jar yourpaoject-encrypted.jar 113 | 114 | //参数说明 115 | // -pwd 加密项目的密码 116 | // -pwdname 环境变量中密码的名字 117 | ``` 118 | 119 | 或者不加pwd参数直接启动,启动后在控制台里输入密码,推荐使用这种方式: 120 | 121 | ```sh 122 | java -javaagent:yourpaoject-encrypted.jar -jar yourpaoject-encrypted.jar 123 | ``` 124 | ~~使用nohup命令启动时,如果系统支持gui,会弹出输入密码的界面,如果是纯命令行下,不支持gui,则需要在同级目录下的classfinal.txt或yourpaoject-encrypted.classfinal.txt中写入密码,项目读取到密码后会清空此文件。~~ 125 | 126 | 密码读取顺序已经改为:参数获取密码||环境变量获取密码||密码文件获取密码||控制台输入密码||GUI输入密码||退出 127 | 128 | 129 | ### tomcat下运行加密后的war 130 | 131 | 将加密后的war放在tomcat/webapps下, 132 | tomcat/bin/catalina 增加以下配置: 133 | 134 | ```sh 135 | //linux下 catalina.sh 136 | CATALINA_OPTS="$CATALINA_OPTS -javaagent:classfinal-fatjar.jar='-pwd 0000000'"; 137 | export CATALINA_OPTS; 138 | 139 | //win下catalina.bat 140 | set JAVA_OPTS="-javaagent:classfinal-fatjar.jar='-pwd 000000'" 141 | 142 | //参数说明 143 | // -pwd 加密项目的密码 144 | // -nopwd 无密码加密时启动加上此参数,跳过输密码过程 145 | // -pwdname 环境变量中密码的名字 146 | ``` 147 | 148 | ------------------------- 149 | 150 | > 本工具使用AES算法加密class文件,密码是保证不被破解的关键,请保存好密码,请勿泄漏。 151 | 152 | > 密码一旦忘记,项目不可启动且无法恢复,请牢记密码。 153 | 154 | > 本工具加密后,原始的class文件并不会完全被加密,只是方法体被清空,保留方法参数、注解等信息,这是为了兼容spring,swagger等扫描注解的框架; 155 | 方法体被清空后,反编译者只能看到方法名和注解,看不到方法的具体内容;当class被classloader加载时,真正的方法体会被解密注入。 156 | 157 | > 为了保证项目在运行时的安全,启动jvm时请加参数: -XX:+DisableAttachMechanism 。 158 | 159 | 160 | ## 版本说明 161 | * v1.2.1 bug修复 162 | * v1.2.0 packages、libjars、cfgfiles、exclude 参数增加通配符功能 163 | * v1.1.7 支持加密springboot的配置文件;增加环境变量中读取密码 164 | * v1.1.6 增加机器绑定功能 165 | * v1.1.5 增加无密码加密方式,启动无需输密码,但是并不安全 166 | * v1.1.4 纯命令行下运行jar时,从配置文件中读取密码,读取后清空文件 167 | * v1.1.3 加入输入密码的弹框 168 | * v1.1.2 修复windows下加密后不能启动的问题 169 | * v1.1.1 启动jar时在控制台输入密码,无需将密码放在参数中 170 | * v1.1.0 加密jar包时将解密代码加入加密后的jar包,无需使用多余的jar文件 171 | * v1.0.0 第一个正式版发布 172 | 173 | 174 | ## 协议声明 175 | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) 176 | -------------------------------------------------------------------------------- /classfinal-core/.gitignore: -------------------------------------------------------------------------------- 1 | .mymetadata 2 | .checkstyle 3 | .classpath 4 | .project 5 | .class 6 | .war 7 | .zip 8 | .rar 9 | .idea 10 | *.iml 11 | *.project 12 | .settings/* 13 | .vscode/* 14 | /target/* 15 | /target/ 16 | /target 17 | /bin 18 | log/* 19 | log 20 | /log 21 | logs/* 22 | logs 23 | /logs 24 | .DS_Store 25 | dependency-reduced-pom.xml 26 | 27 | -------------------------------------------------------------------------------- /classfinal-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 4.0.0 6 | 7 | 8 | net.roseboy 9 | classfinal 10 | 1.2.1 11 | 12 | 13 | classfinal-core 14 | jar 15 | 16 | classfinal-core 17 | 18 | 19 | UTF-8 20 | 1.8 21 | 1.8 22 | 23 | 24 | 25 | 26 | org.javassist 27 | javassist 28 | 3.25.0-GA 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /classfinal-core/src/main/java/net/roseboy/classfinal/AgentTransformer.java: -------------------------------------------------------------------------------- 1 | package net.roseboy.classfinal; 2 | 3 | import net.roseboy.classfinal.util.JarUtils; 4 | import net.roseboy.classfinal.util.StrUtils; 5 | 6 | import java.lang.instrument.ClassFileTransformer; 7 | import java.security.ProtectionDomain; 8 | 9 | 10 | /** 11 | * AgentTransformer 12 | * jvm加载class时回调 13 | * 14 | * @author roseboy 15 | */ 16 | public class AgentTransformer implements ClassFileTransformer { 17 | //密码 18 | private char[] pwd; 19 | 20 | /** 21 | * 构造方法 22 | * 23 | * @param pwd 密码 24 | */ 25 | public AgentTransformer(char[] pwd) { 26 | this.pwd = pwd; 27 | } 28 | 29 | @Override 30 | public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, 31 | ProtectionDomain domain, byte[] classBuffer) { 32 | if (className == null || domain == null || loader == null) { 33 | return classBuffer; 34 | } 35 | 36 | //获取类所在的项目运行路径 37 | String projectPath = domain.getCodeSource().getLocation().getPath(); 38 | projectPath = JarUtils.getRootPath(projectPath); 39 | if (StrUtils.isEmpty(projectPath)) { 40 | return classBuffer; 41 | } 42 | 43 | className = className.replace("/", ".").replace("\\", "."); 44 | 45 | byte[] bytes = JarDecryptor.getInstance().doDecrypt(projectPath, className, this.pwd); 46 | //CAFEBABE,表示解密成功 47 | if (bytes != null && bytes[0] == -54 && bytes[1] == -2 && bytes[2] == -70 && bytes[3] == -66) { 48 | return bytes; 49 | } 50 | return classBuffer; 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /classfinal-core/src/main/java/net/roseboy/classfinal/Const.java: -------------------------------------------------------------------------------- 1 | package net.roseboy.classfinal; 2 | 3 | /** 4 | * 常量 5 | * 6 | * @author roseboy 7 | */ 8 | public class Const { 9 | public static final String VERSION = "v1.2.1"; 10 | 11 | //加密出来的文件名 12 | public static final String FILE_NAME = ".classes"; 13 | 14 | //lib下的jar解压的目录名后缀 15 | public static final String LIB_JAR_DIR = "__temp__"; 16 | 17 | //默认加密方式 18 | public static final int ENCRYPT_TYPE = 1; 19 | 20 | //密码标记 21 | public static final String CONFIG_PASS = "org.springframework.config.Pass"; 22 | //机器码标记 23 | public static final String CONFIG_CODE = "org.springframework.config.Code"; 24 | //加密密码的hash 25 | public static final String CONFIG_PASSHASH = "org.springframework.config.PassHash"; 26 | 27 | //本项目需要打包的代码 28 | public static final String[] CLASSFINAL_FILES = {"CoreAgent.class", "InputForm.class", "InputForm$1.class", 29 | "JarDecryptor.class", "AgentTransformer.class", "Const.class", "CmdLineOption.class", 30 | "EncryptUtils.class", "IoUtils.class", "JarUtils.class", "Log.class", "StrUtils.class", 31 | "SysUtils.class"}; 32 | 33 | //调试模式 34 | public static boolean DEBUG = false; 35 | 36 | public static void pringInfo() { 37 | String sysName = System.getProperty("os.name"); 38 | if (sysName.contains("Windows")) { 39 | System.out.println(); 40 | System.out.println("========================================================="); 41 | System.out.println("= ="); 42 | System.out.println("= Java Class Encryption Tool " + VERSION + " by Mr.K ="); 43 | System.out.println("= ="); 44 | System.out.println("========================================================="); 45 | System.out.println(); 46 | return; 47 | } 48 | 49 | 50 | String[] color = {"\033[31m", "\033[32m", "\033[33m", "\033[34m", "\033[35m", "\033[36m", 51 | "\033[90m", "\033[92m", "\033[93m", "\033[94m", "\033[95m", "\033[96m"}; 52 | System.out.println(); 53 | 54 | for (int i = 0; i < 57; i++) { 55 | System.out.print(color[i % color.length] + "=\033[0m"); 56 | } 57 | System.out.println(); 58 | System.out.println("\033[34m= \033[92m="); 59 | System.out.println("\033[35m= \033[31mJava \033[92mClass \033[95mEncryption \033[96mTool\033[0m \033[37m" 60 | + VERSION + "\033[0m by \033[91mMr.K\033[0m \033[93m="); 61 | System.out.println("\033[36m= \033[94m="); 62 | for (int i = 56; i >= 0; i--) { 63 | System.out.print(color[i % color.length] + "=\033[0m"); 64 | } 65 | System.out.println(); 66 | System.out.println(); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /classfinal-core/src/main/java/net/roseboy/classfinal/CoreAgent.java: -------------------------------------------------------------------------------- 1 | package net.roseboy.classfinal; 2 | 3 | 4 | import net.roseboy.classfinal.util.*; 5 | 6 | import java.io.Console; 7 | import java.io.File; 8 | import java.lang.instrument.Instrumentation; 9 | 10 | 11 | /** 12 | * 监听类加载 13 | * 14 | * @author roseboy 15 | */ 16 | public class CoreAgent { 17 | /** 18 | * man方法执行前调用 19 | * 20 | * @param args 参数 21 | * @param inst inst 22 | */ 23 | public static void premain(String args, Instrumentation inst) { 24 | Const.pringInfo(); 25 | CmdLineOption options = new CmdLineOption(); 26 | options.addOption("pwd", true, "密码"); 27 | options.addOption("pwdname", true, "环境变量密码参数名"); 28 | options.addOption("nopwd", false, "无密码启动"); 29 | options.addOption("debug", false, "调试模式"); 30 | options.addOption("del", true, "读取密码后删除密码"); 31 | 32 | char[] pwd; 33 | 34 | //读取jar隐藏的密码,无密码启动模式(jar) 35 | pwd = JarDecryptor.readPassFromJar(new File(JarUtils.getRootPath(null))); 36 | 37 | if (args != null) { 38 | options.parse(args.split(" ")); 39 | Const.DEBUG = options.hasOption("debug"); 40 | } 41 | 42 | //参数标识 无密码启动 43 | if (options.hasOption("nopwd")) { 44 | pwd = new char[1]; 45 | pwd[0] = '#'; 46 | } 47 | 48 | //参数获取密码 49 | if (StrUtils.isEmpty(pwd)) { 50 | pwd = options.getOptionValue("pwd", "").toCharArray(); 51 | } 52 | 53 | //参数没密码,读取环境变量中的密码 54 | if (StrUtils.isEmpty(pwd)) { 55 | String pwdname = options.getOptionValue("pwdname"); 56 | if (StrUtils.isNotEmpty(pwdname)) { 57 | String p = System.getenv(pwdname); 58 | pwd = p == null ? null : p.toCharArray(); 59 | } 60 | } 61 | 62 | //参数、环境变量都没密码,读取密码配置文件 63 | if (StrUtils.isEmpty(pwd)) { 64 | Log.debug("无法从GUI中获取密码,读取密码文件"); 65 | pwd = readPasswordFromFile(options); 66 | } 67 | 68 | // 配置文件没密码,从控制台获取输入 69 | if (StrUtils.isEmpty(pwd)) { 70 | Log.debug("无法在参数中获取密码,从控制台获取"); 71 | Console console = System.console(); 72 | if (console != null) { 73 | Log.debug("控制台输入"); 74 | pwd = console.readPassword("Password:"); 75 | } 76 | } 77 | 78 | //不支持控制台输入,弹出gui输入 79 | if (StrUtils.isEmpty(pwd)) { 80 | Log.debug("无法从控制台中获取密码,GUI输入"); 81 | InputForm input = new InputForm(); 82 | boolean gui = input.showForm(); 83 | if (gui) { 84 | Log.debug("GUI输入"); 85 | pwd = input.nextPasswordLine(); 86 | input.closeForm(); 87 | } 88 | } 89 | 90 | //还是没有获取密码,退出 91 | if (StrUtils.isEmpty(pwd)) { 92 | Log.println("\nERROR: Startup failed, could not get the password.\n"); 93 | System.exit(0); 94 | } 95 | 96 | //验证密码,jar包是才验证 97 | byte[] passHash = JarDecryptor.readEncryptedFile(new File(JarUtils.getRootPath(null)), Const.CONFIG_PASSHASH); 98 | if (passHash != null) { 99 | char[] p1 = StrUtils.toChars(passHash); 100 | char[] p2 = EncryptUtils.md5(StrUtils.merger(pwd, EncryptUtils.SALT)); 101 | p2 = EncryptUtils.md5(StrUtils.merger(EncryptUtils.SALT, p2)); 102 | if (!StrUtils.equal(p1, p2)) { 103 | Log.println("\nERROR: Startup failed, invalid password.\n"); 104 | System.exit(0); 105 | } 106 | } 107 | 108 | //GO 109 | if (inst != null) { 110 | AgentTransformer tran = new AgentTransformer(pwd); 111 | inst.addTransformer(tran); 112 | } 113 | } 114 | 115 | /** 116 | * 从文件读取密码 117 | * 118 | * @param options 参数开关 119 | * @return 密码 120 | */ 121 | public static char[] readPasswordFromFile(CmdLineOption options) { 122 | String path = JarUtils.getRootPath(null); 123 | if (!path.endsWith(".jar")) { 124 | return null; 125 | } 126 | String jarName = path.substring(path.lastIndexOf("/") + 1); 127 | path = path.substring(0, path.lastIndexOf("/") + 1); 128 | String configName = jarName.substring(0, jarName.length() - 3) + "classfinal.txt"; 129 | File config = new File(path, configName); 130 | if (!config.exists()) { 131 | config = new File(path, "classfinal.txt"); 132 | } 133 | 134 | String args = null; 135 | if (config.exists()) { 136 | args = IoUtils.readTxtFile(config); 137 | } 138 | 139 | if (StrUtils.isEmpty(args)) { 140 | return null; 141 | } 142 | 143 | //不包含空格文件存的就是密码 144 | if (!args.contains(" ")) { 145 | return args.trim().toCharArray(); 146 | } 147 | 148 | options.parse(args.trim().split(" ")); 149 | char[] pwd = options.getOptionValue("pwd", "").toCharArray(); 150 | Const.DEBUG = options.hasOption("debug"); 151 | 152 | //删除文件中的密码 153 | if (!"false".equalsIgnoreCase(options.getOptionValue("del")) 154 | && !"no".equalsIgnoreCase(options.getOptionValue("del"))) { 155 | args = ""; 156 | IoUtils.writeTxtFile(config, args); 157 | } 158 | return pwd; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /classfinal-core/src/main/java/net/roseboy/classfinal/InputForm.java: -------------------------------------------------------------------------------- 1 | package net.roseboy.classfinal; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.awt.event.KeyAdapter; 6 | import java.awt.event.KeyEvent; 7 | 8 | /** 9 | * 密码输入界面,模拟命令行输入 10 | */ 11 | public class InputForm { 12 | //输入提示 13 | private static final String tips = " 项目正在启动,请输入启动密码\r\n Password: "; 14 | //密码显示字符 15 | private static final char passChar = '*'; 16 | //窗口 17 | private JDialog frame; 18 | //文本域 19 | private JTextArea text; 20 | //已输入字符长度 21 | private int keyIndex = 0; 22 | //已输入的字符,最长100 23 | private char[] password = new char[100]; 24 | //是否有下一行 25 | boolean hasNextLine = false; 26 | 27 | /** 28 | * 获取输入的密码 29 | * 30 | * @return 密码char数组 31 | */ 32 | public char[] nextPasswordLine() { 33 | while (!hasNextLine) { 34 | try { 35 | Thread.sleep(50); 36 | } catch (Exception e) { 37 | 38 | } 39 | } 40 | 41 | int charsSize = 0; 42 | while (password[charsSize] != 0) { 43 | charsSize++; 44 | } 45 | char[] chars = new char[charsSize]; 46 | for (int i = 0; i < chars.length; i++) { 47 | chars[i] = password[i]; 48 | } 49 | 50 | keyIndex = 0; 51 | password = new char[100]; 52 | return chars; 53 | 54 | } 55 | 56 | /** 57 | * 显示窗口 58 | * 59 | * @return 显示是否成功 60 | */ 61 | public boolean showForm() { 62 | try { 63 | frame = new JDialog(); 64 | frame.setTitle("项目启动密码 - ClassFinal"); 65 | frame.setSize(560, 320); 66 | frame.setResizable(false); 67 | frame.setLocationRelativeTo(null); 68 | frame.setAlwaysOnTop(true); 69 | frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 70 | text = new JTextArea(); 71 | text.setFont(new Font(null, 0, 18)); 72 | text.setBackground(new Color(0, 0, 0)); 73 | text.setForeground(new Color(0, 255, 0)); 74 | text.setText(tips); 75 | text.addKeyListener(getKeyAdapter()); 76 | text.enableInputMethods(false); 77 | frame.add(text); 78 | frame.setVisible(true); 79 | 80 | return true; 81 | } catch (Exception e) { 82 | return false; 83 | } 84 | } 85 | 86 | /** 87 | * 关闭窗口 88 | */ 89 | public void closeForm() { 90 | frame.setVisible(false); 91 | frame.dispose(); 92 | } 93 | 94 | /** 95 | * 键盘监听事件 96 | * 97 | * @return KeyAdapter 98 | */ 99 | private KeyAdapter getKeyAdapter() { 100 | return new KeyAdapter() { 101 | String fakePass = ""; 102 | 103 | @Override 104 | public void keyReleased(KeyEvent e) { 105 | if (keyIndex < 100 && e.getKeyChar() > 32 && e.getKeyChar() < 127) { 106 | password[keyIndex] = e.getKeyChar(); 107 | keyIndex++; 108 | fakePass += passChar; 109 | } else if (keyIndex > 0 && e.getKeyCode() == 8) {//退格 110 | keyIndex--; 111 | password[keyIndex] = 0; 112 | fakePass = fakePass.substring(1); 113 | } else if (e.getKeyCode() == 10) {//ENTER 114 | fakePass = ""; 115 | hasNextLine = true; 116 | } 117 | text.setText(tips + fakePass); 118 | } 119 | }; 120 | } 121 | 122 | public static void main(String[] args) { 123 | InputForm input = new InputForm(); 124 | boolean gui = input.showForm(); 125 | if (gui) { 126 | System.out.println(input.nextPasswordLine()); 127 | input.closeForm(); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /classfinal-core/src/main/java/net/roseboy/classfinal/JarDecryptor.java: -------------------------------------------------------------------------------- 1 | package net.roseboy.classfinal; 2 | 3 | 4 | import net.roseboy.classfinal.util.*; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.File; 8 | import java.io.InputStream; 9 | 10 | /** 11 | * java class解密 12 | * 13 | * @author roseboy 14 | */ 15 | public class JarDecryptor { 16 | //单例 17 | private static final JarDecryptor single = new JarDecryptor(); 18 | //机器码 19 | private char[] code; 20 | //加密后文件存放位置 21 | private static final String ENCRYPT_PATH = "META-INF/" + Const.FILE_NAME + "/"; 22 | 23 | /** 24 | * 单例 25 | * 26 | * @return 单例 27 | */ 28 | public static JarDecryptor getInstance() { 29 | return single; 30 | } 31 | 32 | /** 33 | * 构造 34 | */ 35 | public JarDecryptor() { 36 | this.code = SysUtils.makeMarchinCode(); 37 | } 38 | 39 | /** 40 | * 根据名称解密出一个文件 41 | * 42 | * @param projectPath 项目所在的路径 43 | * @param fileName 文件名 44 | * @param password 密码 45 | * @return 解密后的字节 46 | */ 47 | public byte[] doDecrypt(String projectPath, String fileName, char[] password) { 48 | long t1 = System.currentTimeMillis(); 49 | File workDir = new File(projectPath); 50 | byte[] bytes = readEncryptedFile(workDir, fileName); 51 | if (bytes == null) { 52 | return null; 53 | } 54 | 55 | //读取机器码,有机器码,先用机器码解密 56 | byte[] codeBytes = readEncryptedFile(workDir, Const.CONFIG_CODE); 57 | if (codeBytes != null) { 58 | //本机器码和打包的机器码不匹配 59 | if (!StrUtils.equal(EncryptUtils.md5(this.code), StrUtils.toChars(codeBytes))) { 60 | Log.println("该项目不可在此机器上运行!\n"); 61 | System.exit(-1); 62 | } 63 | 64 | //用机器码解密 65 | char[] pass = StrUtils.merger(fileName.toCharArray(), code); 66 | bytes = EncryptUtils.de(bytes, pass, Const.ENCRYPT_TYPE); 67 | } 68 | 69 | //无密码启动,读取隐藏的密码 70 | if (password.length == 1 && password[0] == '#') { 71 | password = readPassFromJar(workDir); 72 | } 73 | 74 | //密码解密 75 | char[] pass = StrUtils.merger(password, fileName.toCharArray()); 76 | bytes = EncryptUtils.de(bytes, pass, Const.ENCRYPT_TYPE); 77 | 78 | long t2 = System.currentTimeMillis(); 79 | Log.debug("解密: " + fileName + " (" + (t2 - t1) + " ms)"); 80 | 81 | return bytes; 82 | 83 | } 84 | 85 | /** 86 | * 在jar文件或目录中读取文件字节 87 | * 88 | * @param workDir jar文件或目录 89 | * @param name 文件名 90 | * @return 文件字节数组 91 | */ 92 | public static byte[] readEncryptedFile(File workDir, String name) { 93 | byte[] bytes = null; 94 | String fileName = ENCRYPT_PATH + name; 95 | //jar文件 96 | if (workDir.isFile()) { 97 | bytes = JarUtils.getFileFromJar(workDir, fileName); 98 | } else {//war解压的目录 99 | File file = new File(workDir, fileName); 100 | if (file.exists()) { 101 | bytes = IoUtils.readFileToByte(file); 102 | } 103 | } 104 | return bytes; 105 | } 106 | 107 | /** 108 | * 读取隐藏在jar的密码 109 | * 110 | * @param workDir jar路径 111 | * @return 密码char 112 | */ 113 | public static char[] readPassFromJar(File workDir) { 114 | byte[] passbyte = readEncryptedFile(workDir, Const.CONFIG_PASS); 115 | if (passbyte != null) { 116 | char[] pass = StrUtils.toChars(passbyte); 117 | return EncryptUtils.md5(pass); 118 | } 119 | return null; 120 | } 121 | 122 | /** 123 | * 解密配置文件,spring读取文件时调用 124 | * 125 | * @param path 配置文件路径 126 | * @param in 输入流 127 | * @return 解密的输入流 128 | */ 129 | public InputStream decryptConfigFile(String path, InputStream in, char[] pass) { 130 | if (path.endsWith(".class")) { 131 | return in; 132 | } 133 | String projectPath = JarUtils.getRootPath(null); 134 | if (StrUtils.isEmpty(projectPath)) { 135 | return in; 136 | } 137 | byte[] bytes = null; 138 | try { 139 | bytes = IoUtils.toBytes(in); 140 | } catch (Exception e) { 141 | 142 | } 143 | if (bytes == null || bytes.length == 0) {//需要解密 144 | bytes = this.doDecrypt(projectPath, path, pass); 145 | } 146 | if (bytes == null) { 147 | return in; 148 | } 149 | in = new ByteArrayInputStream(bytes); 150 | return in; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /classfinal-core/src/main/java/net/roseboy/classfinal/JarEncryptor.java: -------------------------------------------------------------------------------- 1 | package net.roseboy.classfinal; 2 | 3 | import javassist.ClassPool; 4 | import javassist.NotFoundException; 5 | import net.roseboy.classfinal.util.*; 6 | 7 | import java.io.File; 8 | import java.util.*; 9 | 10 | /** 11 | * java class加密 12 | * 13 | * @author roseboy 14 | */ 15 | public class JarEncryptor { 16 | //加密配置文件:加载配置文件是注入解密代码的配置 17 | static Map aopMap = new HashMap<>(); 18 | 19 | static { 20 | //org.springframework.core.io.ClassPathResource#getInputStream注入解密功能 21 | aopMap.put("spring.class", "org.springframework.core.io.ClassPathResource#getInputStream"); 22 | aopMap.put("spring.code", "char[] c=${passchar};" 23 | + "is=net.roseboy.classfinal.JarDecryptor.getInstance().decryptConfigFile(this.path,is,c);"); 24 | aopMap.put("spring.line", "999"); 25 | 26 | //com.jfinal.kit.Prop#getInputStream注入解密功能 27 | aopMap.put("jfinal.class", "com.jfinal.kit.Prop#(java.lang.String,java.lang.String)"); 28 | aopMap.put("jfinal.code", "char[] c=${passchar};inputStream=net.roseboy.classfinal.JarDecryptor.getInstance().decryptConfigFile(fileName,inputStream,c);"); 29 | aopMap.put("jfinal.line", "62"); 30 | } 31 | 32 | //要加密的jar或war 33 | private String jarPath = null; 34 | //要加密的包,多个用逗号隔开 35 | private List packages = null; 36 | //-INF/lib下要加密的jar 37 | private List includeJars = null; 38 | //排除的类名 39 | private List excludeClass = null; 40 | //依赖jar路径 41 | private List classPath = null; 42 | //需要加密的配置文件 43 | private List cfgfiles = null; 44 | //密码 45 | private char[] password = null; 46 | //机器码 47 | private char[] code = null; 48 | 49 | //jar还是war 50 | private String jarOrWar = null; 51 | //工作目录 52 | private File targetDir = null; 53 | //-INF/lib目录 54 | private File targetLibDir = null; 55 | //-INF/classes目录 56 | private File targetClassesDir = null; 57 | //加密的文件数量 58 | private Integer encryptFileCount = null; 59 | //存储解析出来的类名和路径 60 | private Map resolveClassName = new HashMap<>(); 61 | 62 | /** 63 | * 构造方法 64 | * 65 | * @param jarPath 要加密的jar或war 66 | * @param password 密码 67 | */ 68 | public JarEncryptor(String jarPath, char[] password) { 69 | super(); 70 | this.jarPath = jarPath; 71 | this.password = password; 72 | } 73 | 74 | /** 75 | * 加密jar的主要过程 76 | * 77 | * @return 解密后生成的文件的绝对路径 78 | */ 79 | public String doEncryptJar() { 80 | if (!jarPath.endsWith(".jar") && !jarPath.endsWith(".war")) { 81 | throw new RuntimeException("jar/war文件格式有误"); 82 | } 83 | if (!new File(jarPath).exists()) { 84 | throw new RuntimeException("文件不存在:" + jarPath); 85 | } 86 | if (password == null || password.length == 0) { 87 | throw new RuntimeException("密码不能为空"); 88 | } 89 | if (password.length == 1 && password[0] == '#') { 90 | Log.debug("加密模式:无密码"); 91 | } 92 | Log.debug("机器绑定:" + (StrUtils.isEmpty(this.code) ? "否" : "是")); 93 | 94 | this.jarOrWar = jarPath.substring(jarPath.lastIndexOf(".") + 1); 95 | Log.debug("加密类型:" + jarOrWar); 96 | //临时work目录 97 | this.targetDir = new File(jarPath.replace("." + jarOrWar, Const.LIB_JAR_DIR)); 98 | this.targetLibDir = new File(this.targetDir, ("jar".equals(jarOrWar) ? "BOOT-INF" : "WEB-INF") 99 | + File.separator + "lib"); 100 | this.targetClassesDir = new File(this.targetDir, ("jar".equals(jarOrWar) ? "BOOT-INF" : "WEB-INF") 101 | + File.separator + "classes"); 102 | Log.debug("临时目录:" + targetDir); 103 | 104 | //[1]释放所有文件 105 | List allFile = JarUtils.unJar(jarPath, this.targetDir.getAbsolutePath()); 106 | allFile.forEach(s -> Log.debug("释放:" + s)); 107 | //[1.1]内部jar只释放需要加密的jar 108 | List libJarFiles = new ArrayList<>(); 109 | allFile.forEach(path -> { 110 | if (!path.toLowerCase().endsWith(".jar")) { 111 | return; 112 | } 113 | String name = path.substring(path.lastIndexOf(File.separator) + 1); 114 | if (StrUtils.isMatchs(this.includeJars, name, false)) { 115 | String targetPath = path.substring(0, path.length() - 4) + Const.LIB_JAR_DIR; 116 | List files = JarUtils.unJar(path, targetPath); 117 | files.forEach(s -> Log.debug("释放:" + s)); 118 | libJarFiles.add(path); 119 | libJarFiles.addAll(files); 120 | } 121 | }); 122 | allFile.addAll(libJarFiles); 123 | 124 | //压缩静态文件 125 | // allFile.forEach(s -> { 126 | // if (!s.endsWith(".ftl")) { 127 | // return; 128 | // } 129 | // File file = new File(s); 130 | // String code = IoUtils.readTxtFile(file); 131 | // code = HtmlUtils.removeComments(code); 132 | // code = HtmlUtils.removeBlankLine(code); 133 | // IoUtils.writeTxtFile(file, code); 134 | // }); 135 | 136 | //[2]提取所有需要加密的class文件 137 | List classFiles = filterClasses(allFile); 138 | 139 | //[3]将本项目的代码添加至jar中 140 | addClassFinalAgent(); 141 | 142 | //[4]将正常的class加密,压缩另存 143 | List encryptClass = encryptClass(classFiles); 144 | this.encryptFileCount = encryptClass.size(); 145 | 146 | //[5]清空class方法体,并保存文件 147 | clearClassMethod(classFiles); 148 | 149 | //[6]加密配置文件 150 | encryptConfigFile(); 151 | 152 | //[7]打包回去 153 | String result = packageJar(libJarFiles); 154 | 155 | return result; 156 | } 157 | 158 | 159 | /** 160 | * 找出所有需要加密的class文件 161 | * 162 | * @param allFile 所有文件 163 | * @return 待加密的class列表 164 | */ 165 | public List filterClasses(List allFile) { 166 | List classFiles = new ArrayList<>(); 167 | allFile.forEach(file -> { 168 | if (!file.endsWith(".class")) { 169 | return; 170 | } 171 | //解析出类全名 172 | String className = resolveClassName(file, true); 173 | //判断包名相同和是否排除的类 174 | if (StrUtils.isMatchs(this.packages, className, false) 175 | && !StrUtils.isMatchs(this.excludeClass, className, false)) { 176 | classFiles.add(new File(file)); 177 | Log.debug("待加密: " + file); 178 | } 179 | }); 180 | return classFiles; 181 | } 182 | 183 | /** 184 | * 加密class文件,放在META-INF/classes里 185 | * 186 | * @param classFiles jar/war 下需要加密的class文件 187 | * @return 已经加密的类名 188 | */ 189 | private List encryptClass(List classFiles) { 190 | List encryptClasses = new ArrayList<>(); 191 | 192 | //加密后存储的位置 193 | File metaDir = new File(this.targetDir, "META-INF" + File.separator + Const.FILE_NAME); 194 | if (!metaDir.exists()) { 195 | metaDir.mkdirs(); 196 | } 197 | 198 | //无密码模式,自动生成一个密码 199 | if (this.password.length == 1 && this.password[0] == '#') { 200 | char[] randChars = EncryptUtils.randChar(32); 201 | this.password = EncryptUtils.md5(randChars); 202 | File configPass = new File(metaDir, Const.CONFIG_PASS); 203 | IoUtils.writeFile(configPass, StrUtils.toBytes(randChars)); 204 | } 205 | 206 | //有机器码 207 | if (StrUtils.isNotEmpty(this.code)) { 208 | File configCode = new File(metaDir, Const.CONFIG_CODE); 209 | IoUtils.writeFile(configCode, StrUtils.toBytes(EncryptUtils.md5(this.code))); 210 | } 211 | 212 | //加密另存 213 | classFiles.forEach(classFile -> { 214 | String className = classFile.getName(); 215 | if (className.endsWith(".class")) { 216 | className = resolveClassName(classFile.getAbsolutePath(), true); 217 | } 218 | byte[] bytes = IoUtils.readFileToByte(classFile); 219 | char[] pass = StrUtils.merger(this.password, className.toCharArray()); 220 | bytes = EncryptUtils.en(bytes, pass, Const.ENCRYPT_TYPE); 221 | //有机器码,再用机器码加密一遍 222 | if (StrUtils.isNotEmpty(this.code)) { 223 | pass = StrUtils.merger(className.toCharArray(), this.code); 224 | bytes = EncryptUtils.en(bytes, pass, Const.ENCRYPT_TYPE); 225 | } 226 | File targetFile = new File(metaDir, className); 227 | IoUtils.writeFile(targetFile, bytes); 228 | encryptClasses.add(className); 229 | Log.debug("加密:" + className); 230 | }); 231 | 232 | //加密密码hash存储,用来验证密码是否正确 233 | char[] pchar = EncryptUtils.md5(StrUtils.merger(this.password, EncryptUtils.SALT)); 234 | pchar = EncryptUtils.md5(StrUtils.merger(EncryptUtils.SALT, pchar)); 235 | IoUtils.writeFile(new File(metaDir, Const.CONFIG_PASSHASH), StrUtils.toBytes(pchar)); 236 | 237 | return encryptClasses; 238 | } 239 | 240 | /** 241 | * 清空class文件的方法体,并保留参数信息 242 | * 243 | * @param classFiles jar/war 下需要加密的class文件 244 | */ 245 | private void clearClassMethod(List classFiles) { 246 | //初始化javassist 247 | ClassPool pool = ClassPool.getDefault(); 248 | //[1]把所有涉及到的类加入到ClassPool的classpath 249 | //[1.1]lib目录所有的jar加入classpath 250 | ClassUtils.loadClassPath(pool, this.targetLibDir); 251 | Log.debug("ClassPath: " + this.targetLibDir.getAbsolutePath()); 252 | 253 | //[1.2]外部依赖的lib加入classpath 254 | ClassUtils.loadClassPath(pool, this.classPath); 255 | this.classPath.forEach(classPath -> Log.debug("ClassPath: " + classPath)); 256 | 257 | //[1.3]要修改的class所在的目录(-INF/classes 和 libjar)加入classpath 258 | List classPaths = new ArrayList<>(); 259 | classFiles.forEach(classFile -> { 260 | String classPath = resolveClassName(classFile.getAbsolutePath(), false); 261 | if (classPaths.contains(classPath)) { 262 | return; 263 | } 264 | try { 265 | pool.insertClassPath(classPath); 266 | } catch (NotFoundException e) { 267 | //Ignore 268 | } 269 | classPaths.add(classPath); 270 | Log.debug("ClassPath: " + classPath); 271 | 272 | }); 273 | 274 | //[2]修改class方法体,并保存文件 275 | classFiles.forEach(classFile -> { 276 | //解析出类全名 277 | String className = resolveClassName(classFile.getAbsolutePath(), true); 278 | byte[] bts = null; 279 | try { 280 | Log.debug("清除方法体: " + className); 281 | bts = ClassUtils.rewriteAllMethods(pool, className); 282 | } catch (Exception e) { 283 | Log.debug("ERROR:" + e.getMessage()); 284 | } 285 | if (bts != null) { 286 | IoUtils.writeFile(classFile, bts); 287 | } 288 | }); 289 | } 290 | 291 | /** 292 | * 向jar文件中添加classfinal的代码 293 | */ 294 | public void addClassFinalAgent() { 295 | List thisJarPaths = new ArrayList<>(); 296 | thisJarPaths.add(this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath()); 297 | //paths.add(ClassPool.class.getProtectionDomain().getCodeSource().getLocation().getPath()); 298 | 299 | //把本项目的class文件打包进去 300 | thisJarPaths.forEach(thisJar -> { 301 | File thisJarFile = new File(thisJar); 302 | if ("jar".endsWith(this.jarOrWar) && thisJar.endsWith(".jar")) { 303 | List includeFiles = Arrays.asList(Const.CLASSFINAL_FILES); 304 | JarUtils.unJar(thisJar, this.targetDir.getAbsolutePath(), includeFiles); 305 | } else if ("war".endsWith(this.jarOrWar) && thisJar.endsWith(".jar")) { 306 | File targetClassFinalJar = new File(this.targetLibDir, thisJarFile.getName()); 307 | byte[] bytes = IoUtils.readFileToByte(thisJarFile); 308 | IoUtils.writeFile(targetClassFinalJar, bytes); 309 | } 310 | //本项目开发环境中未打包 311 | else if (thisJar.endsWith("/classes/")) { 312 | List files = new ArrayList<>(); 313 | IoUtils.listFile(files, new File(thisJar)); 314 | files.forEach(file -> { 315 | String className = file.getAbsolutePath().substring(thisJarFile.getAbsolutePath().length()); 316 | File targetFile = "jar".equals(this.jarOrWar) ? this.targetDir : this.targetClassesDir; 317 | targetFile = new File(targetFile, className); 318 | if (file.isDirectory()) { 319 | targetFile.mkdirs(); 320 | } else if (StrUtils.containsArray(file.getAbsolutePath(), Const.CLASSFINAL_FILES)) { 321 | byte[] bytes = IoUtils.readFileToByte(file); 322 | IoUtils.writeFile(targetFile, bytes); 323 | } 324 | }); 325 | } 326 | }); 327 | 328 | //把javaagent信息加入到MANIFEST.MF 329 | File manifest = new File(this.targetDir, "META-INF/MANIFEST.MF"); 330 | String preMain = "Premain-Class: " + CoreAgent.class.getName(); 331 | String[] txts = {}; 332 | if (manifest.exists()) { 333 | txts = IoUtils.readTxtFile(manifest).split("\r\n"); 334 | } 335 | 336 | String str = StrUtils.insertStringArray(txts, preMain, "Main-Class:"); 337 | IoUtils.writeTxtFile(manifest, str + "\r\n\r\n"); 338 | } 339 | 340 | /** 341 | * 加密classes下的配置文件 342 | */ 343 | private void encryptConfigFile() { 344 | if (this.cfgfiles == null || this.cfgfiles.size() == 0) { 345 | return; 346 | } 347 | 348 | //支持的框架 349 | //String[] supportFrame = {"spring", "jfinal"}; 350 | String[] supportFrame = {"spring"}; 351 | //需要注入解密功能的class 352 | List aopClass = new ArrayList<>(supportFrame.length); 353 | 354 | // [1].读取配置文件时解密 355 | Arrays.asList(supportFrame).forEach(name -> { 356 | String javaCode = aopMap.get(name + ".code"); 357 | String clazz = aopMap.get(name + ".class"); 358 | Integer line = Integer.parseInt(aopMap.get(name + ".line")); 359 | javaCode = javaCode.replace("${passchar}", StrUtils.toCharArrayCode(this.password)); 360 | byte[] bytes = null; 361 | try { 362 | String thisJar = this.getClass().getProtectionDomain().getCodeSource().getLocation().getPath(); 363 | //获取 框架 读取 配置文件的类,将密码注入该类 364 | bytes = ClassUtils.insertCode(clazz, javaCode, line, this.targetLibDir, new File(thisJar)); 365 | } catch (Exception e) { 366 | e.printStackTrace(); 367 | Log.debug(e.getClass().getName() + ":" + e.getMessage()); 368 | } 369 | if (bytes != null) { 370 | File cls = new File(this.targetDir, clazz.split("#")[0] + ".class"); 371 | IoUtils.writeFile(cls, bytes); 372 | aopClass.add(cls); 373 | } 374 | }); 375 | 376 | //加密读取配置文件的类 377 | this.encryptClass(aopClass); 378 | aopClass.forEach(cls -> cls.delete()); 379 | 380 | 381 | //[2].加密配置文件 382 | List configFiles = new ArrayList<>(); 383 | File[] files = this.targetClassesDir.listFiles(); 384 | if (files == null) { 385 | return; 386 | } 387 | for (File file : files) { 388 | if (file.isFile() && StrUtils.isMatchs(this.cfgfiles, file.getName(), false)) { 389 | configFiles.add(file); 390 | } 391 | } 392 | //加密 393 | this.encryptClass(configFiles); 394 | //清空 395 | configFiles.forEach(file -> IoUtils.writeTxtFile(file, "")); 396 | } 397 | 398 | /** 399 | * 压缩成jar 400 | * 401 | * @return 打包后的jar绝对路径 402 | */ 403 | private String packageJar(List libJarFiles) { 404 | //[1]先打包lib下的jar 405 | libJarFiles.forEach(targetJar -> { 406 | if (!targetJar.endsWith(".jar")) { 407 | return; 408 | } 409 | 410 | String srcJarDir = targetJar.substring(0, targetJar.length() - 4) + Const.LIB_JAR_DIR; 411 | if (!new File(srcJarDir).exists()) { 412 | return; 413 | } 414 | JarUtils.doJar(srcJarDir, targetJar); 415 | IoUtils.delete(new File(srcJarDir)); 416 | Log.debug("打包: " + targetJar); 417 | }); 418 | 419 | //删除META-INF下的maven 420 | IoUtils.delete(new File(this.targetDir, "META-INF/maven")); 421 | 422 | //[2]再打包jar 423 | String targetJar = jarPath.replace("." + jarOrWar, "-encrypted." + jarOrWar); 424 | String result = JarUtils.doJar(this.targetDir.getAbsolutePath(), targetJar); 425 | IoUtils.delete(this.targetDir); 426 | Log.debug("打包: " + targetJar); 427 | return result; 428 | } 429 | 430 | /** 431 | * 根据class的绝对路径解析出class名称或class包所在的路径 432 | * 433 | * @param fileName class绝对路径 434 | * @param classOrPath true|false 435 | * @return class名称|包所在的路径 436 | */ 437 | private String resolveClassName(String fileName, boolean classOrPath) { 438 | String result = resolveClassName.get(fileName + classOrPath); 439 | if (result != null) { 440 | return result; 441 | } 442 | String file = fileName.substring(0, fileName.length() - 6); 443 | String K_CLASSES = File.separator + "classes" + File.separator; 444 | String K_LIB = File.separator + "lib" + File.separator; 445 | 446 | String clsPath; 447 | String clsName; 448 | //lib内的的jar包 449 | if (file.contains(K_LIB)) { 450 | clsName = file.substring(file.indexOf(Const.LIB_JAR_DIR, file.indexOf(K_LIB)) 451 | + Const.LIB_JAR_DIR.length() + 1); 452 | clsPath = file.substring(0, file.length() - clsName.length() - 1); 453 | } 454 | //jar/war包-INF/classes下的class文件 455 | else if (file.contains(K_CLASSES)) { 456 | clsName = file.substring(file.indexOf(K_CLASSES) + K_CLASSES.length()); 457 | clsPath = file.substring(0, file.length() - clsName.length() - 1); 458 | 459 | } 460 | //jar包下的class文件 461 | else { 462 | clsName = file.substring(file.indexOf(Const.LIB_JAR_DIR) + Const.LIB_JAR_DIR.length() + 1); 463 | clsPath = file.substring(0, file.length() - clsName.length() - 1); 464 | } 465 | result = classOrPath ? clsName.replace(File.separator, ".") : clsPath; 466 | resolveClassName.put(fileName + classOrPath, result); 467 | return result; 468 | } 469 | 470 | 471 | public Integer getEncryptFileCount() { 472 | return encryptFileCount; 473 | } 474 | 475 | public void setPackages(List packages) { 476 | this.packages = packages; 477 | } 478 | 479 | public void setIncludeJars(List includeJars) { 480 | this.includeJars = includeJars; 481 | } 482 | 483 | public void setExcludeClass(List excludeClass) { 484 | this.excludeClass = excludeClass; 485 | } 486 | 487 | public void setClassPath(List classPath) { 488 | this.classPath = classPath; 489 | } 490 | 491 | public void setCfgfiles(List cfgfiles) { 492 | this.cfgfiles = cfgfiles; 493 | } 494 | 495 | public void setCode(char[] code) { 496 | this.code = code; 497 | } 498 | 499 | } 500 | -------------------------------------------------------------------------------- /classfinal-core/src/main/java/net/roseboy/classfinal/util/ClassUtils.java: -------------------------------------------------------------------------------- 1 | package net.roseboy.classfinal.util; 2 | 3 | import javassist.*; 4 | import javassist.bytecode.*; 5 | import javassist.compiler.CompileError; 6 | import javassist.compiler.Javac; 7 | 8 | import java.io.File; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | /** 13 | * 字节码操作工具类 14 | * 15 | * @author roseboy 16 | */ 17 | public class ClassUtils { 18 | 19 | /** 20 | * 清空方法 21 | * 22 | * @param pool javassist的ClassPool 23 | * @param classname 要修改的class全名 24 | * @return 返回方法体的字节 25 | */ 26 | public static byte[] rewriteAllMethods(ClassPool pool, String classname) { 27 | String name = null; 28 | try { 29 | CtClass cc = pool.getCtClass(classname); 30 | CtMethod[] methods = cc.getDeclaredMethods(); 31 | 32 | for (CtMethod m : methods) { 33 | name = m.getName(); 34 | //不是构造方法,在当前类,不是父lei 35 | if (!m.getName().contains("<") && m.getLongName().startsWith(cc.getName())) { 36 | //m.setBody(null);//清空方法体 37 | CodeAttribute ca = m.getMethodInfo().getCodeAttribute(); 38 | //接口的ca就是null,方法体本来就是空的就是-79 39 | if (ca != null && ca.getCodeLength() != 1 && ca.getCode()[0] != -79) { 40 | ClassUtils.setBodyKeepParamInfos(m, null, true); 41 | if ("void".equalsIgnoreCase(m.getReturnType().getName()) && m.getLongName().endsWith(".main(java.lang.String[])") && m.getMethodInfo().getAccessFlags() == 9) { 42 | m.insertBefore("System.out.println(\"\\nStartup failed, invalid password.\\n\");"); 43 | } 44 | 45 | } 46 | 47 | } 48 | } 49 | return cc.toBytecode(); 50 | } catch (Exception e) { 51 | throw new RuntimeException("[" + classname + "(" + name + ")]" + e.getMessage()); 52 | } 53 | } 54 | 55 | /** 56 | * 修改方法体,并且保留参数信息 57 | * 58 | * @param m javassist的方法 59 | * @param src java代码 60 | * @param rebuild 是否重新构建 61 | * @throws CannotCompileException 编译异常 62 | */ 63 | public static void setBodyKeepParamInfos(CtMethod m, String src, boolean rebuild) throws CannotCompileException { 64 | CtClass cc = m.getDeclaringClass(); 65 | if (cc.isFrozen()) { 66 | throw new RuntimeException(cc.getName() + " class is frozen"); 67 | } 68 | CodeAttribute ca = m.getMethodInfo().getCodeAttribute(); 69 | if (ca == null) { 70 | throw new CannotCompileException("no method body"); 71 | } else { 72 | CodeIterator iterator = ca.iterator(); 73 | Javac jv = new Javac(cc); 74 | 75 | try { 76 | int nvars = jv.recordParams(m.getParameterTypes(), Modifier.isStatic(m.getModifiers())); 77 | jv.recordParamNames(ca, nvars); 78 | jv.recordLocalVariables(ca, 0); 79 | jv.recordReturnType(Descriptor.getReturnType(m.getMethodInfo().getDescriptor(), cc.getClassPool()), false); 80 | //jv.compileStmnt(src); 81 | //Bytecode b = jv.getBytecode(); 82 | Bytecode b = jv.compileBody(m, src); 83 | int stack = b.getMaxStack(); 84 | int locals = b.getMaxLocals(); 85 | if (stack > ca.getMaxStack()) { 86 | ca.setMaxStack(stack); 87 | } 88 | 89 | if (locals > ca.getMaxLocals()) { 90 | ca.setMaxLocals(locals); 91 | } 92 | int pos = iterator.insertEx(b.get()); 93 | iterator.insert(b.getExceptionTable(), pos); 94 | if (rebuild) { 95 | m.getMethodInfo().rebuildStackMapIf6(cc.getClassPool(), cc.getClassFile2()); 96 | } 97 | } catch (NotFoundException var12) { 98 | throw new CannotCompileException(var12); 99 | } catch (CompileError var13) { 100 | throw new CannotCompileException(var13); 101 | } catch (BadBytecode var14) { 102 | throw new CannotCompileException(var14); 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * 加载jar包路径 109 | * 110 | * @param pool javassist的ClassPool 111 | * @param paths lib路径, 112 | */ 113 | public static void loadClassPath(ClassPool pool, List paths) { 114 | for (String path : paths) { 115 | loadClassPath(pool, new File(path)); 116 | } 117 | } 118 | 119 | /** 120 | * 加载jar包路径 121 | * 122 | * @param pool javassist的ClassPool 123 | * @param dir lib路径或jar文件 124 | */ 125 | public static void loadClassPath(ClassPool pool, File dir) { 126 | if (dir == null || !dir.exists()) { 127 | return; 128 | } 129 | 130 | if (dir.isDirectory()) { 131 | List jars = new ArrayList<>(); 132 | IoUtils.listFile(jars, dir, ".jar"); 133 | for (File jar : jars) { 134 | try { 135 | pool.insertClassPath(jar.getAbsolutePath()); 136 | } catch (NotFoundException e) { 137 | //ignore 138 | } 139 | } 140 | } else if (dir.getName().endsWith(".jar")) { 141 | try { 142 | pool.insertClassPath(dir.getAbsolutePath()); 143 | } catch (NotFoundException e) { 144 | //ignore 145 | } 146 | } 147 | } 148 | 149 | /** 150 | * 给方法插入代码并返回bytecode的字节数组 151 | * 152 | * @param classMethod 类名#方法名 153 | * @param javaCode 代码 154 | * @param line 行数 155 | * @param libDir classpath 156 | * @param thisJar 本项目的jar路径 157 | * @return 修改后的字节数组 158 | * @throws Exception Exception 159 | */ 160 | public static byte[] insertCode(String classMethod, String javaCode, int line, File libDir, File thisJar) throws Exception { 161 | String className = classMethod.split("#")[0]; 162 | String methodName = classMethod.split("#")[1]; 163 | ClassPool pool = ClassPool.getDefault(); 164 | loadClassPath(pool, libDir); 165 | if (thisJar != null && thisJar.exists()) { 166 | loadClassPath(pool, thisJar); 167 | } 168 | byte[] bytes; 169 | CtClass cc = pool.getCtClass(className); 170 | if (methodName.startsWith("<") && methodName.contains(">")) { 171 | methodName = methodName.replace("<", "").replace(">", ""); 172 | CtConstructor[] ms = cc.getConstructors(); 173 | for (CtConstructor mt : ms) { 174 | if (mt.getLongName().endsWith(methodName)) { 175 | mt.insertAt(line, javaCode); 176 | } 177 | } 178 | } else { 179 | CtMethod mt = cc.getDeclaredMethod(methodName); 180 | mt.insertAt(line, javaCode); 181 | } 182 | bytes = cc.toBytecode(); 183 | return bytes; 184 | } 185 | 186 | } 187 | -------------------------------------------------------------------------------- /classfinal-core/src/main/java/net/roseboy/classfinal/util/CmdLineOption.java: -------------------------------------------------------------------------------- 1 | package net.roseboy.classfinal.util; 2 | 3 | import java.util.ArrayList; 4 | import java.util.HashMap; 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | /** 9 | * 命令行参数解析工具 10 | * 11 | * @author roseboy 12 | */ 13 | public class CmdLineOption { 14 | /** 15 | * option 16 | */ 17 | private List options = new ArrayList<>(); 18 | /** 19 | * hasArgs 20 | */ 21 | private List hasArgs = new ArrayList<>(); 22 | /** 23 | * descriptions 24 | */ 25 | private List descriptions = new ArrayList<>(); 26 | /** 27 | * option and values 28 | */ 29 | private Map> optionsMap = new HashMap<>(); 30 | 31 | /** 32 | * Add an option that only contains a short-name. 33 | * 34 | *

35 | * It may be specified as requiring an argument. 36 | *

37 | * 38 | * @param opt Short single-character name of the option. 39 | * @param hasArg flag signally if an argument is required after this option 40 | * @param description Self-documenting description 41 | * @return the resulting Options instance 42 | */ 43 | public CmdLineOption addOption(String opt, boolean hasArg, String description) { 44 | opt = resolveOption(opt); 45 | if (!options.contains(opt)) { 46 | options.add(opt); 47 | hasArgs.add(hasArg); 48 | descriptions.add(description); 49 | } 50 | return this; 51 | } 52 | 53 | /** 54 | * Parse the arguments according to the specified options. 55 | * 56 | * @param arguments the command line arguments 57 | * @return CmdLineOption 58 | */ 59 | public CmdLineOption parse(String[] arguments) { 60 | int optIndex = -1; 61 | for (int i = 0; i < arguments.length; i++) { 62 | String arg = arguments[i]; 63 | 64 | if (arg.startsWith("-") || arg.startsWith("--")) { 65 | arg = resolveOption(arg); 66 | 67 | //check last option hasArg 68 | if (optIndex > -1 && hasArgs.get(optIndex)) { 69 | String lastOption = options.get(optIndex); 70 | if (optionsMap.get(lastOption).size() == 0) { 71 | throw new IllegalArgumentException("Missing argument for option: " + lastOption); 72 | } 73 | } 74 | 75 | optIndex = options.indexOf(arg); 76 | if (optIndex < 0) { 77 | throw new IllegalArgumentException("Unrecognized option: " + arguments[i]); 78 | } 79 | optionsMap.put(arg, new ArrayList<>()); 80 | } else if (optIndex > -1) { 81 | String option = options.get(optIndex); 82 | optionsMap.get(option).add(arg); 83 | } 84 | } 85 | return this; 86 | } 87 | 88 | /** 89 | * Retrieve the first argument, if any, of this option. 90 | * 91 | * @param opt the name of the option 92 | * @return Value of the argument if option is set, and has an argument, 93 | * otherwise null. 94 | */ 95 | public String getOptionValue(String opt) { 96 | return getOptionValue(opt, null); 97 | } 98 | 99 | /** 100 | * Retrieve the first argument, if any, of this option. 101 | * 102 | * @param opt the name of the option 103 | * @param dv default value 104 | * @return Value of the argument if option is set, and has an argument, 105 | * otherwise null. 106 | */ 107 | public String getOptionValue(String opt, String dv) { 108 | String[] values = getOptionValues(opt); 109 | return (values == null) ? dv : values[0]; 110 | } 111 | 112 | /** 113 | * Retrieves the array of values, if any, of an option. 114 | * 115 | * @param opt string name of the option 116 | * @return Values of the argument if option is set, and has an argument, 117 | * otherwise null. 118 | */ 119 | public String[] getOptionValues(String opt) { 120 | List values = optionsMap.get(resolveOption(opt)); 121 | return (values == null || values.isEmpty()) ? null : values.toArray(new String[values.size()]); 122 | } 123 | 124 | /** 125 | * Query to see if an option has been set. 126 | * 127 | * @param opt Short name of the option 128 | * @return true if set, false if not 129 | */ 130 | public boolean hasOption(String opt) { 131 | return optionsMap.keySet().contains(resolveOption(opt)); 132 | } 133 | 134 | /** 135 | * Remove the hyphens from the beginning of str and 136 | * return the new String. 137 | * 138 | * @param str The string from which the hyphens should be removed. 139 | * @return the new String. 140 | */ 141 | private static String resolveOption(String str) { 142 | if (str == null) { 143 | return null; 144 | } 145 | if (str.startsWith("--")) { 146 | return str.substring(2, str.length()); 147 | } else if (str.startsWith("-")) { 148 | return str.substring(1, str.length()); 149 | } 150 | 151 | return str; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /classfinal-core/src/main/java/net/roseboy/classfinal/util/EncryptUtils.java: -------------------------------------------------------------------------------- 1 | package net.roseboy.classfinal.util; 2 | 3 | import javax.crypto.Cipher; 4 | import javax.crypto.spec.SecretKeySpec; 5 | import java.math.BigInteger; 6 | import java.security.*; 7 | import java.security.interfaces.RSAPrivateKey; 8 | import java.security.interfaces.RSAPublicKey; 9 | import java.security.spec.PKCS8EncodedKeySpec; 10 | import java.security.spec.X509EncodedKeySpec; 11 | import java.util.*; 12 | 13 | /** 14 | * 简单加密解密 15 | * 16 | * @author roseboy 17 | */ 18 | public class EncryptUtils { 19 | //盐 20 | public static final char[] SALT = {'w', 'h', 'o', 'i', 's', 'y', 'o', 'u', 'r', 'd', 'a', 'd', 'd', 'y', '#', '$', '@', '#', '@'}; 21 | //rsa 长度 22 | private static int KEY_LENGTH = 1024; 23 | 24 | /** 25 | * 加密 26 | * 27 | * @param msg 内容 28 | * @param key 密钥 29 | * @param type 类型 30 | * @return 密文 31 | */ 32 | public static byte[] en(byte[] msg, char[] key, int type) { 33 | if (type == 1) { 34 | return enAES(msg, md5(StrUtils.merger(key, SALT), true)); 35 | } 36 | return enSimple(msg, key); 37 | } 38 | 39 | /** 40 | * 解密 41 | * 42 | * @param msg 密文 43 | * @param key 密钥 44 | * @param type 类型 45 | * @return 明文 46 | */ 47 | public static byte[] de(byte[] msg, char[] key, int type) { 48 | if (type == 1) { 49 | return deAES(msg, md5(StrUtils.merger(key, SALT), true)); 50 | } 51 | return deSimple(msg, key); 52 | } 53 | 54 | /** 55 | * md5加密 56 | * 57 | * @param str 字符串 58 | * @return md5字串 59 | */ 60 | public static byte[] md5byte(char[] str) { 61 | byte[] b = null; 62 | try { 63 | MessageDigest md = MessageDigest.getInstance("MD5"); 64 | byte[] buffer = StrUtils.toBytes(str); 65 | md.update(buffer); 66 | b = md.digest(); 67 | } catch (NoSuchAlgorithmException e) { 68 | e.printStackTrace(); 69 | } 70 | return b; 71 | } 72 | 73 | /** 74 | * md5 75 | * 76 | * @param str 字串 77 | * @return 32位md5 78 | */ 79 | public static char[] md5(char[] str) { 80 | return md5(str, false); 81 | } 82 | 83 | /** 84 | * md5 85 | * 86 | * @param str 字串 87 | * @param sh0rt 是否16位 88 | * @return 32位/16位md5 89 | */ 90 | public static char[] md5(char[] str, boolean sh0rt) { 91 | byte s[] = md5byte(str); 92 | if (s == null) { 93 | return null; 94 | } 95 | int begin = 0; 96 | int end = s.length; 97 | if (sh0rt) { 98 | begin = 8; 99 | end = 16; 100 | } 101 | char[] result = new char[0]; 102 | for (int i = begin; i < end; i++) { 103 | result = StrUtils.merger(result, Integer.toHexString((0x000000FF & s[i]) | 0xFFFFFF00).substring(6).toCharArray()); 104 | } 105 | return result; 106 | } 107 | 108 | 109 | /** 110 | * 加密 111 | * 112 | * @param msg 加密报文 113 | * @param start 开始位置 114 | * @param end 结束位置 115 | * @param key 密钥 116 | * @return 加密后的字节 117 | */ 118 | public static byte[] enSimple(byte[] msg, int start, int end, char[] key) { 119 | byte[] keys = IoUtils.merger(md5byte(StrUtils.merger(key, SALT)), md5byte(StrUtils.merger(SALT, key))); 120 | for (int i = start; i <= end; i++) { 121 | msg[i] = (byte) (msg[i] ^ keys[i % keys.length]); 122 | } 123 | return msg; 124 | } 125 | 126 | /** 127 | * 解密 128 | * 129 | * @param msg 加密报文 130 | * @param start 开始位置 131 | * @param end 结束位置 132 | * @param key 密钥 133 | * @return 解密后的字节 134 | */ 135 | public static byte[] deSimple(byte[] msg, int start, int end, char[] key) { 136 | byte[] keys = IoUtils.merger(md5byte(StrUtils.merger(key, SALT)), md5byte(StrUtils.merger(SALT, key))); 137 | for (int i = start; i <= end; i++) { 138 | msg[i] = (byte) (msg[i] ^ keys[i % keys.length]); 139 | } 140 | return msg; 141 | } 142 | 143 | /** 144 | * 加密 145 | * 146 | * @param msg 加密报文 147 | * @param key 密钥 148 | * @return 加密后的字节 149 | */ 150 | public static byte[] enSimple(byte[] msg, char[] key) { 151 | return enSimple(msg, 0, msg.length - 1, key); 152 | } 153 | 154 | /** 155 | * 解密 156 | * 157 | * @param msg 加密报文 158 | * @param key 密钥 159 | * @return 解密后的字节 160 | */ 161 | public static byte[] deSimple(byte[] msg, char[] key) { 162 | return deSimple(msg, 0, msg.length - 1, key); 163 | } 164 | 165 | /** 166 | * RSA公钥加密 167 | * 168 | * @param str 加密字符串 169 | * @param publicKey 公钥 170 | * @return 密文 171 | */ 172 | public static String enRSA(String str, String publicKey) { 173 | try { 174 | byte[] in = str.getBytes("UTF-8"); 175 | byte[] out = enRSA(in, publicKey); 176 | String outStr = Base64.getEncoder().encodeToString(out); 177 | return outStr; 178 | } catch (Exception e) { 179 | e.printStackTrace(); 180 | } 181 | return null; 182 | } 183 | 184 | /** 185 | * RSA公钥加密 186 | * 187 | * @param msg 要加密的字节 188 | * @param publicKey 公钥 189 | * @return 解密后的字节 190 | */ 191 | public static byte[] enRSA(byte[] msg, String publicKey) { 192 | try { 193 | //base64编码的公钥 194 | byte[] decoded = Base64.getDecoder().decode(publicKey.getBytes("UTF-8")); 195 | RSAPublicKey pubKey = (RSAPublicKey) KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(decoded)); 196 | //RSA加密 197 | Cipher cipher = Cipher.getInstance("RSA"); 198 | cipher.init(Cipher.ENCRYPT_MODE, pubKey); 199 | return cipherDoFinal(cipher, msg, Cipher.ENCRYPT_MODE); 200 | 201 | } catch (Exception e) { 202 | e.printStackTrace(); 203 | } 204 | return null; 205 | } 206 | 207 | 208 | /** 209 | * RSA私钥解密 210 | * 211 | * @param str 要解密的字符串 212 | * @param privateKey 私钥 213 | * @return 明文 214 | */ 215 | public static String deRSA(String str, String privateKey) { 216 | try { 217 | //64位解码加密后的字符串 218 | byte[] inputByte = Base64.getDecoder().decode(str.getBytes("UTF-8")); 219 | String outStr = new String(deRSA(inputByte, privateKey)); 220 | return outStr; 221 | } catch (Exception e) { 222 | e.printStackTrace(); 223 | } 224 | return null; 225 | } 226 | 227 | /** 228 | * RSA私钥解密 229 | * 230 | * @param msg 要解密的字节 231 | * @param privateKey 私钥 232 | * @return 解密后的字节 233 | */ 234 | public static byte[] deRSA(byte[] msg, String privateKey) { 235 | try { 236 | //base64编码的私钥 237 | byte[] decoded = Base64.getDecoder().decode(privateKey.getBytes("UTF-8")); 238 | RSAPrivateKey priKey = (RSAPrivateKey) KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(decoded)); 239 | //RSA解密 240 | Cipher cipher = Cipher.getInstance("RSA"); 241 | cipher.init(Cipher.DECRYPT_MODE, priKey); 242 | return cipherDoFinal(cipher, msg, Cipher.DECRYPT_MODE); 243 | 244 | } catch (Exception e) { 245 | e.printStackTrace(); 246 | } 247 | return null; 248 | } 249 | 250 | /** 251 | * 调用加密解密 252 | * 253 | * @param cipher Cipher 254 | * @param msg 要加密的字节 255 | * @param mode 解密/解密 256 | * @return 结果 257 | * @throws Exception Exception 258 | */ 259 | private static byte[] cipherDoFinal(Cipher cipher, byte[] msg, int mode) throws Exception { 260 | int in_length = 0; 261 | if (mode == Cipher.ENCRYPT_MODE) { 262 | in_length = KEY_LENGTH / 8 - 11; 263 | } else if (mode == Cipher.DECRYPT_MODE) { 264 | in_length = KEY_LENGTH / 8; 265 | } 266 | 267 | byte[] in = new byte[in_length]; 268 | byte[] out = new byte[0]; 269 | 270 | for (int i = 0; i < msg.length; i++) { 271 | if (msg.length - i < in_length && i % in_length == 0) { 272 | in = new byte[msg.length - i]; 273 | } 274 | in[i % in_length] = msg[i]; 275 | if (i == (msg.length - 1) || (i % in_length + 1 == in_length)) { 276 | out = IoUtils.merger(out, cipher.doFinal(in)); 277 | } 278 | } 279 | return out; 280 | } 281 | 282 | /** 283 | * 生成密钥对 284 | * 285 | * @return 密钥信息 286 | * @throws NoSuchAlgorithmException NoSuchAlgorithmException 287 | */ 288 | public static Map genKeyPair() throws NoSuchAlgorithmException { 289 | KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA"); 290 | keyPairGen.initialize(KEY_LENGTH, new SecureRandom()); 291 | KeyPair keyPair = keyPairGen.generateKeyPair(); 292 | RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); // 得到私钥 293 | RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); // 得到公钥 294 | BigInteger publicExponent = publicKey.getPublicExponent(); 295 | BigInteger modulus = publicKey.getModulus(); 296 | 297 | String publicKeyString = new String(Base64.getEncoder().encode(publicKey.getEncoded())); 298 | String privateKeyString = new String(Base64.getEncoder().encode((privateKey.getEncoded()))); 299 | 300 | Map keyMap = new HashMap<>(); 301 | keyMap.put(0, publicKeyString); //0表示公钥 302 | keyMap.put(1, privateKeyString); //1表示私钥 303 | keyMap.put(2, modulus.toString(16));//modulus 304 | keyMap.put(3, publicExponent.toString(16));//e 305 | return keyMap; 306 | } 307 | 308 | 309 | /** 310 | * AES加密字符串 311 | * 312 | * @param str 要加密的字符串 313 | * @param key 密钥 314 | * @return 加密结果 315 | */ 316 | public static String enAES(String str, char[] key) { 317 | byte[] encrypted = null; 318 | try { 319 | encrypted = enAES(str.getBytes("utf-8"), key); 320 | } catch (Exception e) { 321 | e.printStackTrace(); 322 | } 323 | return encrypted == null ? null : Base64.getEncoder().encodeToString(encrypted); 324 | } 325 | 326 | /** 327 | * AES加密字节 328 | * 329 | * @param msg 字节数组 330 | * @param key 密钥 331 | * @return 加密后的字节 332 | */ 333 | public static byte[] enAES(byte[] msg, char[] key) { 334 | byte[] encrypted = null; 335 | try { 336 | byte[] raw = StrUtils.toBytes(key); 337 | SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES"); 338 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");//"算法/模式/补码方式" 339 | cipher.init(Cipher.ENCRYPT_MODE, skeySpec); 340 | encrypted = cipher.doFinal(msg); 341 | } catch (Exception e) { 342 | e.printStackTrace(); 343 | } 344 | return encrypted; 345 | } 346 | 347 | /** 348 | * AES解密 349 | * 350 | * @param str 密文字串 351 | * @param key 密钥 352 | * @return 明文字串 353 | */ 354 | public static String deAES(String str, char[] key) { 355 | String originalString = null; 356 | byte[] msg = Base64.getDecoder().decode(str); 357 | byte[] original = deAES(msg, key); 358 | try { 359 | originalString = new String(original, "utf-8"); 360 | } catch (Exception e) { 361 | 362 | } 363 | return originalString; 364 | } 365 | 366 | /** 367 | * AES解密 368 | * 369 | * @param msg 要解密的字节 370 | * @param key 密钥 371 | * @return 明文字节 372 | */ 373 | public static byte[] deAES(byte[] msg, char[] key) { 374 | byte[] original = null; 375 | try { 376 | byte[] raw = StrUtils.toBytes(key); 377 | SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES"); 378 | Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); 379 | cipher.init(Cipher.DECRYPT_MODE, skeySpec); 380 | original = cipher.doFinal(msg); 381 | } catch (Exception ex) { 382 | 383 | } 384 | return original; 385 | } 386 | 387 | /** 388 | * 随机字串 389 | * 390 | * @param lenght 长度 391 | * @return 字符数组 392 | */ 393 | public static char[] randChar(int lenght) { 394 | char[] result = new char[lenght]; 395 | Character[] chars = new Character[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 396 | 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 397 | 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 398 | 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 399 | 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 400 | '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '=', '_', '+', '.'}; 401 | 402 | List list = Arrays.asList(chars); 403 | Collections.shuffle(list); 404 | for (int i = 0; i < lenght; i++) { 405 | result[i] = list.get(i); 406 | } 407 | return result; 408 | } 409 | } 410 | -------------------------------------------------------------------------------- /classfinal-core/src/main/java/net/roseboy/classfinal/util/HtmlUtils.java: -------------------------------------------------------------------------------- 1 | package net.roseboy.classfinal.util; 2 | 3 | import java.io.File; 4 | 5 | public class HtmlUtils { 6 | public static void main(String[] args) throws Exception { 7 | String txt = IoUtils.readTxtFile(new File("/Users/roseboy/work-yiyon/易用框架/yiyon-framework/jeee-importer/src/main/resources/templates/importer-upload-form-pro.ftl")); 8 | long t1 = System.currentTimeMillis(); 9 | txt = removeComments(txt); 10 | txt = removeBlank(txt); 11 | //txt = removeBr(txt); 12 | 13 | long t2 = System.currentTimeMillis(); 14 | System.out.println(txt); 15 | //System.out.println(t2 - t1); 16 | } 17 | 18 | /** 19 | * 去除代码中的注释 20 | * 21 | * @param code 代码 22 | * @return 代码 23 | */ 24 | public static String removeBlank(String code) { 25 | StringBuilder result = new StringBuilder(); 26 | 27 | int quot = 0;//单引号 28 | int quots = 0;//双引号 29 | boolean inScript = false;//进入script标签 30 | boolean inStyle = false;//进入style标签 31 | boolean inTextArea = false;//进入textaera标签 32 | char[] chars = code.replace("\r\n", "\n").toCharArray(); 33 | for (int i = 0; i < chars.length; i++) { 34 | if (inStyle) { 35 | if (isElementEnd(i, chars, "")) { 36 | inStyle = false; 37 | } 38 | if (chars[i] != ' ' && chars[i] != '\n') { 39 | result.append(chars[i]); 40 | } 41 | continue; 42 | } else if (inScript) { 43 | if (isElementEnd(i, chars, "")) { 44 | inScript = false; 45 | } 46 | if (chars[i] != '\n') { 47 | result.append(chars[i]); 48 | } 49 | continue; 50 | } else if (inTextArea) { 51 | if (isElementEnd(i, chars, "")) { 52 | inTextArea = false; 53 | } 54 | result.append(chars[i]); 55 | continue; 56 | } 57 | 58 | //判断是不是注释开始 59 | if (chars[i] == '"' && chars[i - 1] != '\\' && quot % 2 == 0) {//不是转义,不在单引号内的双引号 60 | quots++; 61 | result.append(chars[i]); 62 | } else if (chars[i] == '\'' && chars[i - 1] != '\\' && quots % 2 == 0) {//不是转义,不在双引号内的单引号 63 | quot++; 64 | result.append(chars[i]); 65 | } else if (quot % 2 == 0 && quots % 2 == 0) {//不在引号内 66 | // 55 | 56 | 57 |









58 |









59 | 60 | 61 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 4.0.0 6 | 7 | org.sonatype.oss 8 | oss-parent 9 | 7 10 | 11 | 12 | net.roseboy 13 | classfinal 14 | 1.2.1 15 | 16 | classfinal-core 17 | classfinal-fatjar 18 | 19 | classfinal-maven-plugin 20 | 21 | pom 22 | 23 | 24 | ClassFinal 25 | java class文件加密 26 | 27 | 28 | UTF-8 29 | UTF-8 30 | 1.8 31 | ${java.home}/../bin/javadoc 32 | 33 | 34 | 35 | 36 | The Apache Software License, Version 2.0 37 | http://www.apache.org/licenses/LICENSE-2.0.txt 38 | repo 39 | 40 | 41 | 42 | https://gitee.com/roseboy/classfinal.git 43 | https://gitee.com/roseboy/classfinal.git 44 | roseboy.net 45 | 46 | 47 | 48 | roseboy 49 | roseboy@live.com 50 | 51 | 52 | 53 | 54 | 55 | 56 | org.sonatype.plugins 57 | nexus-staging-maven-plugin 58 | 1.6.7 59 | true 60 | 61 | sonatype-nexus-staging 62 | https://oss.sonatype.org/ 63 | true 64 | 65 | 66 | 67 | 68 | --------------------------------------------------------------------------------