├── ApkJiagu.iml ├── LICENSE ├── README.md ├── outlibs ├── AXMLEditor.jar ├── AXMLPrinter2.jar └── apktool_2.5.0.jar └── src └── com └── sakuqi └── shell ├── Main.kt └── utils ├── DexUtils.kt ├── EncryptUtils.kt ├── FileUtils.kt ├── SignUtils.kt ├── XmlParseUtils.kt └── ZipUtils.kt /ApkJiagu.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ApkJiagu 2 | 3 | #### 介绍 4 | 该项目讲诉如何将 apk 反编译、编译、修改 AndroidManifest文件、加密 dex 文件、重新打包、签名、加固 5 | 6 | [Android APK 加固技术探究(一)](https://juejin.cn/post/6985038911680544775/) 7 | 8 | [Android APK 加固技术探究(二)](https://juejin.cn/post/6985040096042942495/) 9 | 10 | [Android APK 加固技术探究(三)](https://juejin.cn/post/7025600894829854733/) 11 | 12 | > 为了保证 Android 应用的源码安全性,我们一般会对上线的应用进行代码混淆,然而仅仅做代码混淆还不够,我们还要对我们的应用加固,防止别人通过反编译获取到我们的源码。目前 apk 加固技术比较成熟完善,市面上比较流行的有“360加固”。本文就 apk 加固技术做一个技术探究,希望读者看过后能明白加固的其中原理,并也能自己实现加固方案。 13 | 14 | 在 [Android apk 加固技术探究(二)](https://juejin.cn/post/6985040096042942495/)中,我们已经通过创建 Steady 模块生成了一个 shell.arr 文件,用来对加密后的 dex 文件进行解密和类加载操作。这篇文章主要讲解如何对原 apk 的 dex 加密和把 shell.arr 打入到原 apk 中并最终生成一个新的 apk 15 | 16 | ### 一、反编译 APK 文件 17 | [Android APK 加固技术探究(一)](https://juejin.cn/post/6985038911680544775/)中讲解了如何反编译 apk 文件,这里使用 apktool 这个工具来反编译 apk。通过执行命令 java -jar outlibs/apktool_2.5.0.jar d '待解压apk路径' -o '解压后存放的路径' 18 | 19 | ```kotlin 20 | /** 21 | * 反编译 APK 文件 22 | */ 23 | fun apkDecode(){ 24 | println("开始反编译") 25 | val process = Runtime.getRuntime() 26 | .exec("java -jar outlibs/apktool_2.5.0.jar d "+ orginApk.absolutePath+" -o "+apkDecode.absolutePath) 27 | process.waitFor() 28 | if(process.exitValue() != 0) { 29 | FileUtils.printStream(process.errorStream) 30 | }else{ 31 | FileUtils.printStream(process.inputStream) 32 | } 33 | process.destroy() 34 | } 35 | ``` 36 | ### 二、修改 AndroidManifest.xml 文件 37 | 步骤一中获得了解压后的文件目录,找到目录中的 AndroidManifest 文件。这里修改 AndroidManifest.xml 文件尝试过有2种方式,一种是通过 “AXMLEditor.jar” 和 “AXMLPrinter2.jar” 工具修改 AndroidManifest.xml 文件,另一种是通过 SAX 的方式解析 xml 文件,然后在相应的节点位置插入需要的数据,最后发现方法一虽然修改了xml 文件但是最终打包的新 APK 中 AndroidManifest.xml 文件没有生效,后来使用方法二生效了。下面把2种方式的代码都贴出来,如果哪个大佬发现了方法一中的问题,还请不吝赐教。 38 | 39 | 方法一:关于 “AXMLEditor.jar” 和 “AXMLPrinter2.jar” 两个工具如何使用可以自行百度,这里不做展开 40 | ```kotlin 41 | /** 42 | * 修改 AndroidManifest 43 | */ 44 | fun changeAndroidManifest(apkUnzipDir:File){ 45 | val aManifest = apkUnzipDir.listFiles { _, name -> 46 | name?.equals("AndroidManifest.xml") == true 47 | } 48 | val file = if (aManifest != null && aManifest.isNotEmpty()) { 49 | aManifest[0] 50 | }else{null} 51 | file?.let { 52 | //将模版插入 AndroidManifest 中 53 | val process2 = Runtime.getRuntime() 54 | .exec("java -jar outlibs/AXMLEditor.jar -tag -i tool/src/main/assets/ApplicationName.xml " + 55 | file.absolutePath+" "+file.absolutePath) 56 | process2.waitFor() 57 | if(process2.exitValue() != 0) { 58 | println("2") 59 | FileUtils.printStream(process2.errorStream) 60 | } 61 | process2.destroy() 62 | 63 | //解析出原来的 Application 类名 64 | var process0 = Runtime.getRuntime() 65 | .exec("java -jar tool/libs/AXMLPrinter2.jar "+file.absolutePath) 66 | process0.waitFor() 67 | val applicationPath = XmlParseUtils.sax2xml(process0.inputStream) 68 | if(process0.exitValue() != 0){ 69 | println("0") 70 | FileUtils.printStream(process0.errorStream) 71 | } 72 | process0.destroy() 73 | 74 | //参考 https://github.com/fourbrother/AXMLEditor 75 | //修改 Application 下 插入标签的值 76 | val process1 = Runtime.getRuntime() 77 | .exec("java -jar tool/libs/AXMLEditor.jar -attr -i meta-data package value "+applicationPath 78 | + " " + file.absolutePath+" "+file.absolutePath) 79 | process1.waitFor() 80 | if(process1.exitValue() != 0){ 81 | println("1") 82 | FileUtils.printStream(process1.errorStream) 83 | } 84 | process1.destroy() 85 | 86 | //参考 https://github.com/fourbrother/AXMLEditor 87 | //修改 Application 下 name 标签 88 | val process3 = Runtime.getRuntime() 89 | .exec("java -jar tool/libs/AXMLEditor.jar -attr -m application package name com.sakuqi.shell.NewApplication" 90 | + " " + file.absolutePath+" "+file.absolutePath) 91 | process3.waitFor() 92 | if(process3.exitValue() != 0){ 93 | println("3") 94 | FileUtils.printStream(process3.errorStream) 95 | } 96 | process3.destroy() 97 | 98 | //解析出原来的 Application 类名 99 | var process4 = Runtime.getRuntime() 100 | .exec("java -jar tool/libs/AXMLPrinter2.jar "+file.absolutePath) 101 | process4.waitFor() 102 | FileUtils.printStream(process4.inputStream) 103 | process4.destroy() 104 | 105 | } 106 | } 107 | ``` 108 | 109 | 方法二:SAXReader 的使用方式自行查看相关 API 文档 110 | ```kotlin 111 | /** 112 | * 修改 xml 文件 113 | */ 114 | fun changeAndroidManifest(){ 115 | println("开始修改 AndroidManifest") 116 | var manifestFile = File("output/apktool/decode/AndroidManifest.xml") 117 | changeXmlBySax(manifestFile,"com.sakuqi.steady.SteadyApplication") 118 | //com.sakuqi.steady.SteadyApplication名称为 Shell.arr 中的Application 类 119 | } 120 | 121 | /** 122 | * 修改xml文件 123 | */ 124 | fun changeXmlBySax(fileXml:File,newApplicationName:String){ 125 | var sax = SAXReader() 126 | var document = sax.read(fileXml) 127 | var root = document.rootElement 128 | var application = root.element("application") 129 | //原有的 application 名称 130 | var applicationName = application.attributeValue("name") 131 | var applicationAttr = application.attribute("name") 132 | //将壳中的 application 替换原来的 application 133 | applicationAttr.text = newApplicationName 134 | 135 | var element = application.addElement("meta-data") 136 | element.addAttribute("android:name","app_name") 137 | element.addAttribute("android:value",applicationName) 138 | saveDocument(document,fileXml) 139 | 140 | } 141 | fun saveDocument(document:Document,file:File){ 142 | var osWrite = OutputStreamWriter(FileOutputStream(file)) 143 | var format = OutputFormat.createPrettyPrint()// 获取输出的指定格式 144 | format.encoding = "UTF-8" 145 | var writer = XMLWriter(osWrite,format) 146 | writer.write(document) 147 | writer.flush() 148 | writer.close() 149 | } 150 | ``` 151 | ### 三、编译修改 AndroidManifest.xml 后的反编译目录 152 | 153 | ``` 154 | /** 155 | * 编译 APK 文件 156 | */ 157 | fun apkBuild(){ 158 | println("开始重新编译") 159 | val process = Runtime.getRuntime() 160 | .exec("java -jar outlibs/apktool_2.5.0.jar b "+ "反编译后的目录"+" -o "+ “编译后的目录”) 161 | process.waitFor() 162 | if(process.exitValue() != 0) { 163 | FileUtils.printStream(process.errorStream) 164 | }else{ 165 | FileUtils.printStream(process.inputStream) 166 | } 167 | process.destroy() 168 | } 169 | ``` 170 | 171 | ### 四、解压 APK 文件并加密所以 Dex 文件 172 | 173 | 解压使用的是 java.util.zip.ZipFile 类,这里封装了工具类最后会放到源码里,这里就不展开了。解压后需要将原 apk 中的签名文件删除,以便后续重新签名。过滤出解压目录下的所有 dex 后缀文件,然后对其进行加密,需要注意的是加密方式需要和 shell.arr 中的解密方式保持一致,这里使用的是 AES 的加密方式,源代码会在后续的开源项目中展示。加密后需要将原来的 dex 文件删除。大致代码如下: 174 | ```kotlin 175 | /** 176 | * 解压 APK 文件并加密所有的dex文件 177 | */ 178 | fun unZipApkAndEncrypt(){ 179 | println("解压 APK") 180 | val apkUnzipDir = File("output/unzip/apk") 181 | if(!apkUnzipDir.exists()){ 182 | apkUnzipDir.mkdirs() 183 | } 184 | FileUtils.delete(apkUnzipDir) 185 | ZipUtils.unZip(apkBuild,apkUnzipDir) 186 | //删除 META-INF/CERT.RSA,META-INF/CERT.SF,META-INF/MANIFEST.MF 187 | val certRSA = File(apkUnzipDir,"META-INF/CERT.RSA") 188 | certRSA.delete() 189 | val certSF = File(apkUnzipDir,"META-INF/CERT.SF") 190 | certSF.delete() 191 | val manifestMF = File(apkUnzipDir,"META-INF/MANIFEST.MF") 192 | manifestMF.delete() 193 | //changeAndroidManifest(apkUnzipDir) 194 | //获取dex 文件 195 | val apkFiles = apkUnzipDir.listFiles(object :FilenameFilter{ 196 | override fun accept(dir: File?, name: String?): Boolean { 197 | return name?.endsWith(".dex") == true 198 | } 199 | }) 200 | for (dexFile in apkFiles){ 201 | val name = dexFile.name 202 | println("dex:$name") 203 | val bytes = DexUtils.getBytes(dexFile) 204 | val encrypt: ByteArray? = EncryptUtils.encrypt(bytes, EncryptUtils.ivBytes) 205 | val fos: FileOutputStream = FileOutputStream( 206 | File( 207 | dexFile.parent, 208 | "secret-" + dexFile.getName() 209 | ) 210 | ) 211 | fos.write(encrypt) 212 | fos.flush() 213 | fos.close() 214 | dexFile.delete() 215 | } 216 | 217 | } 218 | ``` 219 | 220 | ### 五、解压壳 aar 得到 class.jar ,然后把 class.jar 在转换成 class.dex,再将class.dex 移到原 apk 的解压目录,最后压缩成新的 apk 文件 221 | 这里解压依然使用的是 unzip 的工具类,转换 class.dex 使用的是 Android SDK 中自带的命令 dx 222 | 223 | ```kotlin 224 | /** 225 | * 解压壳aar 并转化jar 为dex 226 | */ 227 | fun makeDecodeDex(){ 228 | println("解压壳 AAR") 229 | var shellUnzipDir = File("output/unzip/shell") 230 | if(!shellUnzipDir.exists()){ 231 | shellUnzipDir.mkdirs() 232 | } 233 | FileUtils.delete(shellUnzipDir) 234 | //解压 AAR 235 | ZipUtils.unZip(shellAAR,shellUnzipDir) 236 | //将 jar 转成 dex 237 | println("将 jar 转成 dex") 238 | var shellJar = File(shellUnzipDir,"classes.jar") 239 | var shellDex = File("output/unzip/apk","classes.dex") 240 | DexUtils.dxCommand(shellJar,shellDex) 241 | moveLibSoToApk() 242 | //打包 243 | println("打包 APK") 244 | var unsignedApk = File("output/unsigned_$orginApkName") 245 | ZipUtils.zip(File("output/unzip/apk"),unsignedApk) 246 | } 247 | 248 | /** 249 | * 将壳中的lib文件移到apk 中 250 | */ 251 | fun moveLibSoToApk(){ 252 | var shellUnzipLibDir = File("output/unzip/shell/jni") 253 | var apkUnzipLibDir = File("output/unzip/apk/lib") 254 | if(!apkUnzipLibDir.exists()){ 255 | apkUnzipLibDir.mkdirs() 256 | } 257 | 258 | FileUtils.copy(shellUnzipLibDir,apkUnzipLibDir) 259 | } 260 | ``` 261 | ```kotlin 262 | object DexUtils { 263 | @Throws(IOException::class,InterruptedException::class) 264 | fun dxCommand(jar:File,dex:File){ 265 | var runtime = Runtime.getRuntime() 266 | var process = runtime.exec("dx --dex --output "+dex.absolutePath+" "+jar.absolutePath) 267 | try { 268 | process.waitFor() 269 | }catch (e:InterruptedException){ 270 | e.printStackTrace() 271 | throw e 272 | } 273 | if(process.exitValue() != 0){ 274 | val inputStream = process.errorStream 275 | var buffer = ByteArray(1024) 276 | val bos = ByteArrayOutputStream() 277 | var len = inputStream.read(buffer) 278 | while (len != -1){ 279 | bos.write(buffer,0,len) 280 | len = inputStream.read(buffer) 281 | } 282 | System.out.println(String(bos.toByteArray(), Charset.forName("GBK"))) 283 | throw RuntimeException("dx run failed") 284 | }else{ 285 | System.out.println("执行成功:"+process.exitValue()) 286 | } 287 | process.destroy() 288 | } 289 | 290 | /** 291 | * 读取文件 292 | * @param file 293 | * @return 294 | * @throws Exception 295 | */ 296 | @Throws(Exception::class) 297 | fun getBytes(file: File?): ByteArray { 298 | val r = RandomAccessFile(file, "r") 299 | val buffer = ByteArray(r.length().toInt()) 300 | r.readFully(buffer) 301 | r.close() 302 | return buffer 303 | } 304 | } 305 | ``` 306 | 307 | ### 五、将压缩后的新的 apk 文件进行 zip 对齐操作 308 | ```kotlin 309 | /** 310 | * 对齐 311 | */ 312 | fun zipalign(){ 313 | println("将打包的 apk 对齐") 314 | var unsignedApk = File("output/unsigned_$orginApkName") 315 | val alignedApk = File("output/unsigned-aligned_$orginApkName") 316 | val process = Runtime.getRuntime().exec( 317 | "zipalign -p -f -v 4 " + unsignedApk.absolutePath + " " + alignedApk.absolutePath) 318 | process.waitFor(5,TimeUnit.SECONDS) 319 | try { 320 | if (process.exitValue() != 0) { 321 | println("zipalign 出错") 322 | FileUtils.printStream(process.errorStream) 323 | } else { 324 | FileUtils.printStream(process.inputStream) 325 | } 326 | println("完成 apk 的对齐") 327 | process.destroy() 328 | }catch (e:Exception){ 329 | println("对齐超时...") 330 | } 331 | } 332 | ``` 333 | 334 | ### 六、将对齐后的 apk 文件进行签名 335 | ```kotlin 336 | /** 337 | * 对 APK 签名 338 | */ 339 | fun jksToApk(){ 340 | println("签名 APK") 341 | var signedApk = File("output/signed_$orginApkName") 342 | val alignedApk = File("output/unsigned-aligned_$orginApkName") 343 | SignUtils.signature(alignedApk,signedApk,signFile.absolutePath) 344 | } 345 | ``` 346 | ```kotlin 347 | object SignUtils { 348 | @Throws(InterruptedException::class, IOException::class) 349 | fun signature(unsignedApk: File, signedApk: File, keyStore: String) { 350 | val cmd = arrayOf( 351 | "jarsigner", 352 | "-sigalg", 353 | "SHA1withRSA", 354 | "-digestalg", 355 | "SHA1", 356 | "-keystore", 357 | keyStore, 358 | "-storepass", 359 | "密码", 360 | "-keypass", 361 | "密码", 362 | "-signedjar", 363 | signedApk.absolutePath, 364 | unsignedApk.absolutePath, 365 | "alinas" 366 | ) 367 | val process = Runtime.getRuntime().exec(cmd) 368 | println("start sign") 369 | try { 370 | val waitResult = process.waitFor() 371 | println("waitResult: $waitResult") 372 | } catch (e: InterruptedException) { 373 | e.printStackTrace() 374 | throw e 375 | } 376 | 377 | println("process.exitValue() " + process.exitValue()) 378 | if (process.exitValue() != 0) { 379 | val inputStream = process.errorStream 380 | var len: Int 381 | val buffer = ByteArray(2048) 382 | val bos = ByteArrayOutputStream() 383 | len = inputStream.read(buffer) 384 | while (len != -1) { 385 | bos.write(buffer, 0, len) 386 | len = inputStream.read(buffer) 387 | } 388 | println(String(bos.toByteArray(), Charset.forName("gbk"))) 389 | throw RuntimeException("签名执行失败") 390 | } 391 | println("finish signed") 392 | process.destroy() 393 | } 394 | } 395 | ``` 396 | 397 | 至此 apk 的加固流程全部讲完 398 | 399 | #### 参与贡献 400 | 401 | 1. Fork 本仓库 402 | 2. 新建 Feat_xxx 分支 403 | 3. 提交代码 404 | 4. 新建 Pull Request 405 | 406 | 407 | #### 特技 408 | 409 | 1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md 410 | 2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com) 411 | 3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目 412 | 4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目 413 | 5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help) 414 | 6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) 415 | -------------------------------------------------------------------------------- /outlibs/AXMLEditor.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openJK-dev/AppJiagu-java/4456aaae7cd30392f652e5f085a3852f978a047a/outlibs/AXMLEditor.jar -------------------------------------------------------------------------------- /outlibs/AXMLPrinter2.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openJK-dev/AppJiagu-java/4456aaae7cd30392f652e5f085a3852f978a047a/outlibs/AXMLPrinter2.jar -------------------------------------------------------------------------------- /outlibs/apktool_2.5.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openJK-dev/AppJiagu-java/4456aaae7cd30392f652e5f085a3852f978a047a/outlibs/apktool_2.5.0.jar -------------------------------------------------------------------------------- /src/com/sakuqi/shell/Main.kt: -------------------------------------------------------------------------------- 1 | package com.sakuqi.shell 2 | 3 | import com.sakuqi.shell.utils.* 4 | import java.io.File 5 | import java.io.FileOutputStream 6 | import java.io.FilenameFilter 7 | import java.util.concurrent.TimeUnit 8 | 9 | 10 | // 未签名 APK 11 | lateinit var orginApk:File 12 | lateinit var orginApkName:String 13 | // 壳 AAR 14 | lateinit var shellAAR:File 15 | //签名文件 16 | var signFile = File("keystore/wuse.jks") 17 | //apk 反编译目录 18 | val apkDecode = File("output/apktool/decode") 19 | //apk 重新编译 20 | lateinit var apkBuild :File 21 | 22 | fun findOriginApk(){ 23 | var inputDir = File("input/apk") 24 | var apkFile = inputDir.listFiles { _, name -> 25 | name.endsWith(".apk") 26 | }.first() 27 | if(apkFile.exists()){ 28 | orginApk = apkFile 29 | orginApkName = orginApk.name 30 | apkBuild = File("output/apktool",orginApkName) 31 | }else{ 32 | throw Exception("没有找到加固 apk") 33 | } 34 | } 35 | 36 | fun findOriginShell(){ 37 | var inputDir = File("input/shell") 38 | var shellFile = inputDir.listFiles { _, name -> 39 | name.endsWith(".aar") 40 | }.first() 41 | if(shellFile.exists()){ 42 | shellAAR = shellFile 43 | }else{ 44 | throw Exception("没有找到壳 aar") 45 | } 46 | } 47 | 48 | /** 49 | * 反编译 APK 文件 50 | */ 51 | fun apkDecode(){ 52 | println("开始反编译") 53 | val process = Runtime.getRuntime() 54 | .exec("java -jar outlibs/apktool_2.5.0.jar d "+ orginApk.absolutePath+" -o "+apkDecode.absolutePath) 55 | process.waitFor() 56 | if(process.exitValue() != 0) { 57 | FileUtils.printStream(process.errorStream) 58 | }else{ 59 | FileUtils.printStream(process.inputStream) 60 | } 61 | process.destroy() 62 | } 63 | 64 | /** 65 | * 编译 APK 文件 66 | */ 67 | fun apkBuild(){ 68 | println("开始重新编译") 69 | val process = Runtime.getRuntime() 70 | .exec("java -jar outlibs/apktool_2.5.0.jar b "+ apkDecode.absolutePath+" -o "+ apkBuild.absolutePath) 71 | process.waitFor() 72 | if(process.exitValue() != 0) { 73 | FileUtils.printStream(process.errorStream) 74 | }else{ 75 | FileUtils.printStream(process.inputStream) 76 | } 77 | process.destroy() 78 | } 79 | 80 | /** 81 | * 解压 APK 文件并加密所有的dex文件 82 | */ 83 | fun unZipApkAndEncrypt(){ 84 | println("解压 APK") 85 | val apkUnzipDir = File("output/unzip/apk") 86 | if(!apkUnzipDir.exists()){ 87 | apkUnzipDir.mkdirs() 88 | } 89 | FileUtils.delete(apkUnzipDir) 90 | ZipUtils.unZip(apkBuild,apkUnzipDir) 91 | //删除 META-INF/CERT.RSA,META-INF/CERT.SF,META-INF/MANIFEST.MF 92 | val certRSA = File(apkUnzipDir,"META-INF/CERT.RSA") 93 | certRSA.delete() 94 | val certSF = File(apkUnzipDir,"META-INF/CERT.SF") 95 | certSF.delete() 96 | val manifestMF = File(apkUnzipDir,"META-INF/MANIFEST.MF") 97 | manifestMF.delete() 98 | //changeAndroidManifest(apkUnzipDir) 99 | //获取dex 文件 100 | val apkFiles = apkUnzipDir.listFiles(object :FilenameFilter{ 101 | override fun accept(dir: File?, name: String?): Boolean { 102 | return name?.endsWith(".dex") == true 103 | } 104 | }) 105 | for (dexFile in apkFiles){ 106 | val name = dexFile.name 107 | println("dex:$name") 108 | val bytes = DexUtils.getBytes(dexFile) 109 | val encrypt: ByteArray? = EncryptUtils.encrypt(bytes, EncryptUtils.ivBytes) 110 | val fos: FileOutputStream = FileOutputStream( 111 | File( 112 | dexFile.parent, 113 | "secret-" + dexFile.getName() 114 | ) 115 | ) 116 | fos.write(encrypt) 117 | fos.flush() 118 | fos.close() 119 | dexFile.delete() 120 | } 121 | 122 | } 123 | 124 | /** 125 | * 修改 xml 文件 126 | */ 127 | fun changeAndroidManifest(){ 128 | println("开始修改 AndroidManifest") 129 | var manifestFile = File("output/apktool/decode/AndroidManifest.xml") 130 | XmlParseUtils.changeXmlBySax(manifestFile,"com.sakuqi.steady.SteadyApplication") 131 | } 132 | 133 | /** 134 | * 修改 AndroidManifest 135 | */ 136 | fun changeAndroidManifest(apkUnzipDir:File){ 137 | val aManifest = apkUnzipDir.listFiles { _, name -> 138 | name?.equals("AndroidManifest.xml") == true 139 | } 140 | val file = if (aManifest != null && aManifest.isNotEmpty()) { 141 | aManifest[0] 142 | }else{null} 143 | file?.let { 144 | //将模版插入 AndroidManifest 中 145 | val process2 = Runtime.getRuntime() 146 | .exec("java -jar outlibs/AXMLEditor.jar -tag -i tool/src/main/assets/ApplicationName.xml " + 147 | file.absolutePath+" "+file.absolutePath) 148 | process2.waitFor() 149 | if(process2.exitValue() != 0) { 150 | println("2") 151 | FileUtils.printStream(process2.errorStream) 152 | } 153 | process2.destroy() 154 | 155 | //解析出原来的 Application 类名 156 | var process0 = Runtime.getRuntime() 157 | .exec("java -jar tool/libs/AXMLPrinter2.jar "+file.absolutePath) 158 | process0.waitFor() 159 | val applicationPath = XmlParseUtils.sax2xml(process0.inputStream) 160 | if(process0.exitValue() != 0){ 161 | println("0") 162 | FileUtils.printStream(process0.errorStream) 163 | } 164 | process0.destroy() 165 | 166 | //参考 https://github.com/fourbrother/AXMLEditor 167 | //修改 Application 下 插入标签的值 168 | val process1 = Runtime.getRuntime() 169 | .exec("java -jar tool/libs/AXMLEditor.jar -attr -i meta-data package value "+applicationPath 170 | + " " + file.absolutePath+" "+file.absolutePath) 171 | process1.waitFor() 172 | if(process1.exitValue() != 0){ 173 | println("1") 174 | FileUtils.printStream(process1.errorStream) 175 | } 176 | process1.destroy() 177 | 178 | //参考 https://github.com/fourbrother/AXMLEditor 179 | //修改 Application 下 name 标签 180 | val process3 = Runtime.getRuntime() 181 | .exec("java -jar tool/libs/AXMLEditor.jar -attr -m application package name com.sakuqi.shell.NewApplication" 182 | + " " + file.absolutePath+" "+file.absolutePath) 183 | process3.waitFor() 184 | if(process3.exitValue() != 0){ 185 | println("3") 186 | FileUtils.printStream(process3.errorStream) 187 | } 188 | process3.destroy() 189 | 190 | //解析出原来的 Application 类名 191 | var process4 = Runtime.getRuntime() 192 | .exec("java -jar tool/libs/AXMLPrinter2.jar "+file.absolutePath) 193 | process4.waitFor() 194 | FileUtils.printStream(process4.inputStream) 195 | process4.destroy() 196 | 197 | } 198 | } 199 | 200 | /** 201 | * 解压壳aar 并转化jar 为dex 202 | */ 203 | fun makeDecodeDex(){ 204 | println("解压壳 AAR") 205 | var shellUnzipDir = File("output/unzip/shell") 206 | if(!shellUnzipDir.exists()){ 207 | shellUnzipDir.mkdirs() 208 | } 209 | FileUtils.delete(shellUnzipDir) 210 | //解压 AAR 211 | ZipUtils.unZip(shellAAR,shellUnzipDir) 212 | //将 jar 转成 dex 213 | println("将 jar 转成 dex") 214 | var shellJar = File(shellUnzipDir,"classes.jar") 215 | var shellDex = File("output/unzip/apk","classes.dex") 216 | DexUtils.dxCommand(shellJar,shellDex) 217 | moveLibSoToApk() 218 | //打包 219 | println("打包 APK") 220 | var unsignedApk = File("output/unsigned_$orginApkName") 221 | ZipUtils.zip(File("output/unzip/apk"),unsignedApk) 222 | } 223 | 224 | /** 225 | * 将壳中的lib文件移到apk 中 226 | */ 227 | fun moveLibSoToApk(){ 228 | var shellUnzipLibDir = File("output/unzip/shell/jni") 229 | var apkUnzipLibDir = File("output/unzip/apk/lib") 230 | if(!apkUnzipLibDir.exists()){ 231 | apkUnzipLibDir.mkdirs() 232 | } 233 | 234 | FileUtils.copy(shellUnzipLibDir,apkUnzipLibDir) 235 | } 236 | 237 | /** 238 | * 对齐 239 | */ 240 | fun zipalign(){ 241 | println("将打包的 apk 对齐") 242 | var unsignedApk = File("output/unsigned_$orginApkName") 243 | val alignedApk = File("output/unsigned-aligned_$orginApkName") 244 | val process = Runtime.getRuntime().exec( 245 | "zipalign -p -f -v 4 " + unsignedApk.absolutePath + " " + alignedApk.absolutePath) 246 | process.waitFor(5,TimeUnit.SECONDS) 247 | try { 248 | if (process.exitValue() != 0) { 249 | println("zipalign 出错") 250 | FileUtils.printStream(process.errorStream) 251 | } else { 252 | FileUtils.printStream(process.inputStream) 253 | } 254 | println("完成 apk 的对齐") 255 | process.destroy() 256 | }catch (e:Exception){ 257 | println("对齐超时...") 258 | } 259 | } 260 | 261 | /** 262 | * 对 APK 签名 263 | */ 264 | fun jksToApk(){ 265 | println("签名 APK") 266 | var signedApk = File("output/signed_$orginApkName") 267 | val alignedApk = File("output/unsigned-aligned_$orginApkName") 268 | SignUtils.signature(alignedApk,signedApk,signFile.absolutePath) 269 | } 270 | 271 | 272 | fun main() { 273 | //清空输出目录 274 | var output = File("output/") 275 | if (output.exists()) { 276 | FileUtils.delete(output) 277 | } 278 | println("清空工作空间") 279 | findOriginApk() 280 | findOriginShell() 281 | apkDecode() 282 | changeAndroidManifest() 283 | apkBuild() 284 | //解压需要加固的 apk 285 | unZipApkAndEncrypt() 286 | //解压壳aar 287 | makeDecodeDex() 288 | //对齐打包apk 289 | zipalign() 290 | //删除解压目录 291 | //FileUtils.delete("output/unzip") 292 | jksToApk() 293 | println("Finished!!!") 294 | } -------------------------------------------------------------------------------- /src/com/sakuqi/shell/utils/DexUtils.kt: -------------------------------------------------------------------------------- 1 | package com.sakuqi.shell.utils 2 | 3 | import java.io.ByteArrayOutputStream 4 | import java.io.File 5 | import java.io.IOException 6 | import java.io.RandomAccessFile 7 | import java.nio.charset.Charset 8 | 9 | object DexUtils { 10 | @Throws(IOException::class,InterruptedException::class) 11 | fun dxCommand(jar:File,dex:File){ 12 | var runtime = Runtime.getRuntime() 13 | var process = runtime.exec("dx --dex --output "+dex.absolutePath+" "+jar.absolutePath) 14 | try { 15 | process.waitFor() 16 | }catch (e:InterruptedException){ 17 | e.printStackTrace() 18 | throw e 19 | } 20 | if(process.exitValue() != 0){ 21 | val inputStream = process.errorStream 22 | var buffer = ByteArray(1024) 23 | val bos = ByteArrayOutputStream() 24 | var len = inputStream.read(buffer) 25 | while (len != -1){ 26 | bos.write(buffer,0,len) 27 | len = inputStream.read(buffer) 28 | } 29 | System.out.println(String(bos.toByteArray(), Charset.forName("GBK"))) 30 | throw RuntimeException("dx run failed") 31 | }else{ 32 | System.out.println("执行成功:"+process.exitValue()) 33 | } 34 | process.destroy() 35 | } 36 | 37 | /** 38 | * 读取文件 39 | * @param file 40 | * @return 41 | * @throws Exception 42 | */ 43 | @Throws(Exception::class) 44 | fun getBytes(file: File?): ByteArray { 45 | val r = RandomAccessFile(file, "r") 46 | val buffer = ByteArray(r.length().toInt()) 47 | r.readFully(buffer) 48 | r.close() 49 | return buffer 50 | } 51 | } -------------------------------------------------------------------------------- /src/com/sakuqi/shell/utils/EncryptUtils.kt: -------------------------------------------------------------------------------- 1 | package com.sakuqi.shell.utils 2 | 3 | import java.io.* 4 | import java.security.InvalidKeyException 5 | import java.security.NoSuchAlgorithmException 6 | import javax.crypto.BadPaddingException 7 | import javax.crypto.Cipher 8 | import javax.crypto.IllegalBlockSizeException 9 | import javax.crypto.NoSuchPaddingException 10 | import javax.crypto.spec.SecretKeySpec 11 | 12 | 13 | object EncryptUtils { 14 | /** 15 | * 加密算法 16 | */ 17 | private val ENCRY_ALGORITHM = "AES" 18 | 19 | 20 | /** 21 | * 加密算法/加密模式/填充类型 22 | * 本例采用AES加密,ECB加密模式,PKCS5Padding填充 23 | */ 24 | private val CIPHER_MODE = "AES/ECB/PKCS5Padding" 25 | 26 | val ivBytes = "huangdh'l,.AMWK;".toByteArray() 27 | 28 | /** 29 | * 设置iv偏移量 30 | * 本例采用ECB加密模式,不需要设置iv偏移量 31 | */ 32 | private val IV_: String? = null 33 | 34 | /** 35 | * 设置加密字符集 36 | * 本例采用 UTF-8 字符集 37 | */ 38 | private val CHARACTER = Charsets.UTF_8 39 | 40 | /** 41 | * 设置加密密码处理长度。 42 | * 不足此长度补0; 43 | */ 44 | private val PWD_SIZE = 16 45 | 46 | /** 47 | * 密码处理方法 48 | * 如果加解密出问题, 49 | * 请先查看本方法,排除密码长度不足补"0",导致密码不一致 50 | * @param password 待处理的密码 51 | * @return 52 | * @throws UnsupportedEncodingException 53 | */ 54 | @Throws(UnsupportedEncodingException::class) 55 | private fun pwdHandler(password: String?): ByteArray { 56 | var password = password 57 | var data: ByteArray? = null 58 | if (password == null) { 59 | password = "" 60 | } 61 | val sb = StringBuffer(PWD_SIZE) 62 | sb.append(password) 63 | while (sb.length < PWD_SIZE) { 64 | sb.append("0") 65 | } 66 | if (sb.length > PWD_SIZE) { 67 | sb.setLength(PWD_SIZE) 68 | } 69 | 70 | data = sb.toString().toByteArray(charset("UTF-8")) 71 | 72 | return data 73 | } 74 | 75 | //======================>原始加密<====================== 76 | 77 | /** 78 | * 原始加密 79 | * @param clearTextBytes 明文字节数组,待加密的字节数组 80 | * @param pwdBytes 加密密码字节数组 81 | * @return 返回加密后的密文字节数组,加密错误返回null 82 | */ 83 | fun encrypt(clearTextBytes: ByteArray, pwdBytes: ByteArray): ByteArray? { 84 | try { 85 | // 1 获取加密密钥 86 | val keySpec = SecretKeySpec(pwdBytes, ENCRY_ALGORITHM) 87 | 88 | // 2 获取Cipher实例 89 | val cipher = Cipher.getInstance(CIPHER_MODE) 90 | 91 | // 查看数据块位数 默认为16(byte) * 8 =128 bit 92 | // System.out.println("数据块位数(byte):" + cipher.getBlockSize()); 93 | 94 | // 3 初始化Cipher实例。设置执行模式以及加密密钥 95 | cipher.init(Cipher.ENCRYPT_MODE, keySpec) 96 | 97 | // 4 执行 98 | 99 | // 5 返回密文字符集 100 | return cipher.doFinal(clearTextBytes) 101 | 102 | } catch (e: NoSuchPaddingException) { 103 | e.printStackTrace() 104 | } catch (e: NoSuchAlgorithmException) { 105 | e.printStackTrace() 106 | } catch (e: BadPaddingException) { 107 | e.printStackTrace() 108 | } catch (e: IllegalBlockSizeException) { 109 | e.printStackTrace() 110 | } catch (e: InvalidKeyException) { 111 | e.printStackTrace() 112 | } catch (e: Exception) { 113 | e.printStackTrace() 114 | } 115 | 116 | return null 117 | } 118 | 119 | /** 120 | * 原始解密 121 | * @param cipherTextBytes 密文字节数组,待解密的字节数组 122 | * @param pwdBytes 解密密码字节数组 123 | * @return 返回解密后的明文字节数组,解密错误返回null 124 | */ 125 | fun decrypt(cipherTextBytes: ByteArray, pwdBytes: ByteArray): ByteArray? { 126 | 127 | try { 128 | // 1 获取解密密钥 129 | val keySpec = SecretKeySpec(pwdBytes, ENCRY_ALGORITHM) 130 | 131 | // 2 获取Cipher实例 132 | val cipher = Cipher.getInstance(CIPHER_MODE) 133 | 134 | // 查看数据块位数 默认为16(byte) * 8 =128 bit 135 | // System.out.println("数据块位数(byte):" + cipher.getBlockSize()); 136 | 137 | // 3 初始化Cipher实例。设置执行模式以及加密密钥 138 | cipher.init(Cipher.DECRYPT_MODE, keySpec) 139 | 140 | // 4 执行 141 | 142 | // 5 返回明文字符集 143 | return cipher.doFinal(cipherTextBytes) 144 | 145 | } catch (e: NoSuchAlgorithmException) { 146 | e.printStackTrace() 147 | } catch (e: InvalidKeyException) { 148 | e.printStackTrace() 149 | } catch (e: NoSuchPaddingException) { 150 | e.printStackTrace() 151 | } catch (e: BadPaddingException) { 152 | e.printStackTrace() 153 | } catch (e: IllegalBlockSizeException) { 154 | e.printStackTrace() 155 | } catch (e: Exception) { 156 | e.printStackTrace() 157 | } 158 | 159 | // 解密错误 返回null 160 | return null 161 | } 162 | 163 | 164 | //======================>HEX<====================== 165 | 166 | /** 167 | * HEX加密 168 | * @param clearText 明文,待加密的内容 169 | * @param password 密码,加密的密码 170 | * @return 返回密文,加密后得到的内容。加密错误返回null 171 | */ 172 | fun encryptHex(clearText: String, password: String): String? { 173 | try { 174 | // 1 获取加密密文字节数组 175 | val cipherTextBytes = encrypt(clearText.toByteArray(CHARACTER), pwdHandler(password)) 176 | 177 | // 2 对密文字节数组进行 转换为 HEX输出密文 178 | 179 | // 3 返回 HEX输出密文 180 | return byte2hex(cipherTextBytes!!) 181 | } catch (e: UnsupportedEncodingException) { 182 | e.printStackTrace() 183 | } catch (e: Exception) { 184 | e.printStackTrace() 185 | } 186 | 187 | // 加密错误返回null 188 | return null 189 | } 190 | 191 | /** 192 | * HEX解密 193 | * @param cipherText 密文,带解密的内容 194 | * @param password 密码,解密的密码 195 | * @return 返回明文,解密后得到的内容。解密错误返回null 196 | */ 197 | fun decryptHex(cipherText: String?, password: String): String? { 198 | try { 199 | // 1 将HEX输出密文 转为密文字节数组 200 | val cipherTextBytes = hex2byte(cipherText) 201 | 202 | // 2 将密文字节数组进行解密 得到明文字节数组 203 | val clearTextBytes = decrypt(cipherTextBytes, pwdHandler(password)) 204 | 205 | // 3 根据 CHARACTER 转码,返回明文字符串 206 | return String(clearTextBytes!!, CHARACTER) 207 | } catch (e: UnsupportedEncodingException) { 208 | e.printStackTrace() 209 | } catch (e: Exception) { 210 | e.printStackTrace() 211 | } 212 | 213 | // 解密错误返回null 214 | return null 215 | } 216 | 217 | /*字节数组转成16进制字符串 */ 218 | fun byte2hex(bytes: ByteArray): String { // 一个字节的数, 219 | val sb = StringBuffer(bytes.size * 2) 220 | var tmp = "" 221 | for (n in bytes.indices) { 222 | // 整数转成十六进制表示 223 | tmp = Integer.toHexString(bytes[n].toInt() and 0XFF) 224 | if (tmp.length == 1) { 225 | sb.append("0") 226 | } 227 | sb.append(tmp) 228 | } 229 | return sb.toString().toUpperCase() // 转成大写 230 | } 231 | 232 | /*将hex字符串转换成字节数组 */ 233 | private fun hex2byte(str: String?): ByteArray { 234 | var str = str 235 | if (str == null || str.length < 2) { 236 | return ByteArray(0) 237 | } 238 | str = str.toLowerCase() 239 | val l = str.length / 2 240 | val result = ByteArray(l) 241 | for (i in 0 until l) { 242 | val tmp = str.substring(2 * i, 2 * i + 2) 243 | result[i] = (Integer.parseInt(tmp, 16) and 0xFF).toByte() 244 | } 245 | return result 246 | } 247 | 248 | @JvmStatic 249 | fun main(args: Array) { 250 | val data = encrypt("123456".toByteArray(), EncryptUtils.ivBytes) 251 | data?.forEach { 252 | print("$it ") 253 | } 254 | 255 | } 256 | } -------------------------------------------------------------------------------- /src/com/sakuqi/shell/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.sakuqi.shell.utils 2 | 3 | import com.sun.xml.internal.ws.streaming.XMLStreamReaderUtil.close 4 | import java.io.* 5 | import java.net.URL 6 | import java.nio.charset.Charset 7 | import java.security.NoSuchAlgorithmException 8 | import java.security.DigestInputStream 9 | import java.security.MessageDigest 10 | import java.util.* 11 | import java.util.Collections.addAll 12 | import javax.net.ssl.HttpsURLConnection 13 | import kotlin.reflect.jvm.internal.impl.descriptors.ModuleDescriptor.DefaultImpls.accept 14 | import kotlin.experimental.and 15 | 16 | 17 | object FileUtils{ 18 | private val LINE_SEP = System.getProperty("line.separator") 19 | 20 | 21 | /** 22 | * Return the file by path. 23 | * 24 | * @param filePath The path of file. 25 | * @return the file 26 | */ 27 | fun getFileByPath(filePath: String): File? { 28 | return if (isSpace(filePath)) null else File(filePath) 29 | } 30 | 31 | 32 | /** 33 | * Return whether the file exists. 34 | * 35 | * @param file The file. 36 | * @return `true`: yes

`false`: no 37 | */ 38 | fun isFileExists(file: File?): Boolean { 39 | return file != null && file.exists() 40 | } 41 | 42 | /** 43 | * Rename the file. 44 | * 45 | * @param filePath The path of file. 46 | * @param newName The new name of file. 47 | * @return `true`: success

`false`: fail 48 | */ 49 | fun rename(filePath: String, newName: String): Boolean { 50 | return rename(getFileByPath(filePath), newName) 51 | } 52 | 53 | /** 54 | * Rename the file. 55 | * 56 | * @param file The file. 57 | * @param newName The new name of file. 58 | * @return `true`: success

`false`: fail 59 | */ 60 | fun rename(file: File?, newName: String): Boolean { 61 | // file is null then return false 62 | if (file == null) return false 63 | // file doesn't exist then return false 64 | if (!file.exists()) return false 65 | // the new name is space then return false 66 | if (isSpace(newName)) return false 67 | // the new name equals old name then return true 68 | if (newName == file.name) return true 69 | val newFile = File(file.parent + File.separator + newName) 70 | // the new name of file exists then return false 71 | return !newFile.exists() && file.renameTo(newFile) 72 | } 73 | 74 | /** 75 | * Return whether it is a directory. 76 | * 77 | * @param dirPath The path of directory. 78 | * @return `true`: yes

`false`: no 79 | */ 80 | fun isDir(dirPath: String): Boolean { 81 | return isDir(getFileByPath(dirPath)) 82 | } 83 | 84 | /** 85 | * Return whether it is a directory. 86 | * 87 | * @param file The file. 88 | * @return `true`: yes

`false`: no 89 | */ 90 | fun isDir(file: File?): Boolean { 91 | return file != null && file.exists() && file.isDirectory 92 | } 93 | 94 | /** 95 | * Return whether it is a file. 96 | * 97 | * @param filePath The path of file. 98 | * @return `true`: yes

`false`: no 99 | */ 100 | fun isFile(filePath: String): Boolean { 101 | return isFile(getFileByPath(filePath)) 102 | } 103 | 104 | /** 105 | * Return whether it is a file. 106 | * 107 | * @param file The file. 108 | * @return `true`: yes

`false`: no 109 | */ 110 | fun isFile(file: File?): Boolean { 111 | return file != null && file.exists() && file.isFile 112 | } 113 | 114 | /** 115 | * Create a directory if it doesn't exist, otherwise do nothing. 116 | * 117 | * @param dirPath The path of directory. 118 | * @return `true`: exists or creates successfully

`false`: otherwise 119 | */ 120 | fun createOrExistsDir(dirPath: String): Boolean { 121 | return createOrExistsDir(getFileByPath(dirPath)) 122 | } 123 | 124 | /** 125 | * Create a directory if it doesn't exist, otherwise do nothing. 126 | * 127 | * @param file The file. 128 | * @return `true`: exists or creates successfully

`false`: otherwise 129 | */ 130 | fun createOrExistsDir(file: File?): Boolean { 131 | return file != null && if (file.exists()) file.isDirectory else file.mkdirs() 132 | } 133 | 134 | /** 135 | * Create a file if it doesn't exist, otherwise do nothing. 136 | * 137 | * @param filePath The path of file. 138 | * @return `true`: exists or creates successfully

`false`: otherwise 139 | */ 140 | fun createOrExistsFile(filePath: String): Boolean { 141 | return createOrExistsFile(getFileByPath(filePath)) 142 | } 143 | 144 | /** 145 | * Create a file if it doesn't exist, otherwise do nothing. 146 | * 147 | * @param file The file. 148 | * @return `true`: exists or creates successfully

`false`: otherwise 149 | */ 150 | fun createOrExistsFile(file: File?): Boolean { 151 | if (file == null) return false 152 | if (file.exists()) return file.isFile 153 | if (!createOrExistsDir(file.parentFile)) return false 154 | try { 155 | return file.createNewFile() 156 | } catch (e: IOException) { 157 | e.printStackTrace() 158 | return false 159 | } 160 | 161 | } 162 | 163 | /** 164 | * Create a file if it doesn't exist, otherwise delete old file before creating. 165 | * 166 | * @param filePath The path of file. 167 | * @return `true`: success

`false`: fail 168 | */ 169 | fun createFileByDeleteOldFile(filePath: String): Boolean { 170 | return createFileByDeleteOldFile(getFileByPath(filePath)) 171 | } 172 | 173 | /** 174 | * Create a file if it doesn't exist, otherwise delete old file before creating. 175 | * 176 | * @param file The file. 177 | * @return `true`: success

`false`: fail 178 | */ 179 | fun createFileByDeleteOldFile(file: File?): Boolean { 180 | if (file == null) return false 181 | // file exists and unsuccessfully delete then return false 182 | if (file.exists() && !file.delete()) return false 183 | if (!createOrExistsDir(file.parentFile)) return false 184 | try { 185 | return file.createNewFile() 186 | } catch (e: IOException) { 187 | e.printStackTrace() 188 | return false 189 | } 190 | 191 | } 192 | 193 | /** 194 | * Copy the directory or file. 195 | * 196 | * @param srcPath The path of source. 197 | * @param destPath The path of destination. 198 | * @return `true`: success

`false`: fail 199 | */ 200 | fun copy( 201 | srcPath: String, 202 | destPath: String 203 | ): Boolean { 204 | return copy(getFileByPath(srcPath), getFileByPath(destPath), null) 205 | } 206 | 207 | /** 208 | * Copy the directory or file. 209 | * 210 | * @param srcPath The path of source. 211 | * @param destPath The path of destination. 212 | * @param listener The replace listener. 213 | * @return `true`: success

`false`: fail 214 | */ 215 | fun copy( 216 | srcPath: String, 217 | destPath: String, 218 | listener: OnReplaceListener 219 | ): Boolean { 220 | return copy(getFileByPath(srcPath), getFileByPath(destPath), listener) 221 | } 222 | 223 | /** 224 | * Copy the directory or file. 225 | * 226 | * @param src The source. 227 | * @param dest The destination. 228 | * @return `true`: success

`false`: fail 229 | */ 230 | fun copy( 231 | src: File, 232 | dest: File 233 | ): Boolean { 234 | return copy(src, dest, null) 235 | } 236 | 237 | /** 238 | * Copy the directory or file. 239 | * 240 | * @param src The source. 241 | * @param dest The destination. 242 | * @param listener The replace listener. 243 | * @return `true`: success

`false`: fail 244 | */ 245 | fun copy( 246 | src: File?, 247 | dest: File?, 248 | listener: OnReplaceListener? 249 | ): Boolean { 250 | if (src == null) return false 251 | return if (src.isDirectory) { 252 | copyDir(src, dest, listener) 253 | } else copyFile(src, dest, listener) 254 | } 255 | 256 | /** 257 | * Copy the directory. 258 | * 259 | * @param srcDir The source directory. 260 | * @param destDir The destination directory. 261 | * @param listener The replace listener. 262 | * @return `true`: success

`false`: fail 263 | */ 264 | private fun copyDir( 265 | srcDir: File, 266 | destDir: File?, 267 | listener: OnReplaceListener? 268 | ): Boolean { 269 | return copyOrMoveDir(srcDir, destDir, listener, false) 270 | } 271 | 272 | /** 273 | * Copy the file. 274 | * 275 | * @param srcFile The source file. 276 | * @param destFile The destination file. 277 | * @param listener The replace listener. 278 | * @return `true`: success

`false`: fail 279 | */ 280 | private fun copyFile( 281 | srcFile: File, 282 | destFile: File?, 283 | listener: OnReplaceListener? 284 | ): Boolean { 285 | return copyOrMoveFile(srcFile, destFile, listener, false) 286 | } 287 | 288 | /** 289 | * Move the directory or file. 290 | * 291 | * @param srcPath The path of source. 292 | * @param destPath The path of destination. 293 | * @return `true`: success

`false`: fail 294 | */ 295 | fun move( 296 | srcPath: String, 297 | destPath: String 298 | ): Boolean { 299 | return move(getFileByPath(srcPath), getFileByPath(destPath), null) 300 | } 301 | 302 | /** 303 | * Move the directory or file. 304 | * 305 | * @param srcPath The path of source. 306 | * @param destPath The path of destination. 307 | * @param listener The replace listener. 308 | * @return `true`: success

`false`: fail 309 | */ 310 | fun move( 311 | srcPath: String, 312 | destPath: String, 313 | listener: OnReplaceListener 314 | ): Boolean { 315 | return move(getFileByPath(srcPath), getFileByPath(destPath), listener) 316 | } 317 | 318 | /** 319 | * Move the directory or file. 320 | * 321 | * @param src The source. 322 | * @param dest The destination. 323 | * @return `true`: success

`false`: fail 324 | */ 325 | fun move( 326 | src: File, 327 | dest: File 328 | ): Boolean { 329 | return move(src, dest, null) 330 | } 331 | 332 | /** 333 | * Move the directory or file. 334 | * 335 | * @param src The source. 336 | * @param dest The destination. 337 | * @param listener The replace listener. 338 | * @return `true`: success

`false`: fail 339 | */ 340 | fun move( 341 | src: File?, 342 | dest: File?, 343 | listener: OnReplaceListener? 344 | ): Boolean { 345 | if (src == null) return false 346 | return if (src.isDirectory) { 347 | moveDir(src, dest, listener) 348 | } else moveFile(src, dest, listener) 349 | } 350 | 351 | /** 352 | * Move the directory. 353 | * 354 | * @param srcDir The source directory. 355 | * @param destDir The destination directory. 356 | * @param listener The replace listener. 357 | * @return `true`: success

`false`: fail 358 | */ 359 | fun moveDir( 360 | srcDir: File, 361 | destDir: File?, 362 | listener: OnReplaceListener? 363 | ): Boolean { 364 | return copyOrMoveDir(srcDir, destDir, listener, true) 365 | } 366 | 367 | /** 368 | * Move the file. 369 | * 370 | * @param srcFile The source file. 371 | * @param destFile The destination file. 372 | * @param listener The replace listener. 373 | * @return `true`: success

`false`: fail 374 | */ 375 | fun moveFile( 376 | srcFile: File, 377 | destFile: File?, 378 | listener: OnReplaceListener? 379 | ): Boolean { 380 | return copyOrMoveFile(srcFile, destFile, listener, true) 381 | } 382 | 383 | private fun copyOrMoveDir( 384 | srcDir: File?, 385 | destDir: File?, 386 | listener: OnReplaceListener?, 387 | isMove: Boolean 388 | ): Boolean { 389 | if (srcDir == null || destDir == null) return false 390 | // destDir's path locate in srcDir's path then return false 391 | val srcPath = srcDir.path + File.separator 392 | val destPath = destDir.path + File.separator 393 | if (destPath.contains(srcPath)) return false 394 | if (!srcDir.exists() || !srcDir.isDirectory) return false 395 | if (!createOrExistsDir(destDir)) return false 396 | val files = srcDir.listFiles() 397 | for (file in files!!) { 398 | val oneDestFile = File(destPath + file.name) 399 | if (file.isFile) { 400 | if (!copyOrMoveFile(file, oneDestFile, listener, isMove)) return false 401 | } else if (file.isDirectory) { 402 | if (!copyOrMoveDir(file, oneDestFile, listener, isMove)) return false 403 | } 404 | } 405 | return !isMove || deleteDir(srcDir) 406 | } 407 | 408 | private fun copyOrMoveFile( 409 | srcFile: File?, 410 | destFile: File?, 411 | listener: OnReplaceListener?, 412 | isMove: Boolean 413 | ): Boolean { 414 | if (srcFile == null || destFile == null) return false 415 | // srcFile equals destFile then return false 416 | if (srcFile == destFile) return false 417 | // srcFile doesn't exist or isn't a file then return false 418 | if (!srcFile.exists() || !srcFile.isFile) return false 419 | if (destFile.exists()) { 420 | if (listener == null || listener.onReplace(srcFile, destFile)) {// require delete the old file 421 | if (!destFile.delete()) {// unsuccessfully delete then return false 422 | return false 423 | } 424 | } else { 425 | return true 426 | } 427 | } 428 | if (!createOrExistsDir(destFile.parentFile)) return false 429 | try { 430 | return writeFileFromIS(destFile, FileInputStream(srcFile)) && !(isMove && !deleteFile(srcFile)) 431 | } catch (e: FileNotFoundException) { 432 | e.printStackTrace() 433 | return false 434 | } 435 | 436 | } 437 | 438 | /** 439 | * Delete the directory. 440 | * 441 | * @param filePath The path of file. 442 | * @return `true`: success

`false`: fail 443 | */ 444 | fun delete(filePath: String): Boolean { 445 | return delete(getFileByPath(filePath)) 446 | } 447 | 448 | /** 449 | * Delete the directory. 450 | * 451 | * @param file The file. 452 | * @return `true`: success

`false`: fail 453 | */ 454 | fun delete(file: File?): Boolean { 455 | if (file == null) return false 456 | return if (file.isDirectory) { 457 | deleteDir(file) 458 | } else deleteFile(file) 459 | } 460 | 461 | /** 462 | * Delete the directory. 463 | * 464 | * @param dir The directory. 465 | * @return `true`: success

`false`: fail 466 | */ 467 | private fun deleteDir(dir: File?): Boolean { 468 | if (dir == null) return false 469 | // dir doesn't exist then return true 470 | if (!dir.exists()) return true 471 | // dir isn't a directory then return false 472 | if (!dir.isDirectory) return false 473 | val files = dir.listFiles() 474 | if (files != null && files.size != 0) { 475 | for (file in files) { 476 | if (file.isFile) { 477 | if (!file.delete()) return false 478 | } else if (file.isDirectory) { 479 | if (!deleteDir(file)) return false 480 | } 481 | } 482 | } 483 | return dir.delete() 484 | } 485 | 486 | /** 487 | * Delete the file. 488 | * 489 | * @param file The file. 490 | * @return `true`: success

`false`: fail 491 | */ 492 | private fun deleteFile(file: File?): Boolean { 493 | return file != null && (!file.exists() || file.isFile && file.delete()) 494 | } 495 | 496 | /** 497 | * Delete the all in directory. 498 | * 499 | * @param dirPath The path of directory. 500 | * @return `true`: success

`false`: fail 501 | */ 502 | fun deleteAllInDir(dirPath: String): Boolean { 503 | return deleteAllInDir(getFileByPath(dirPath)) 504 | } 505 | 506 | /** 507 | * Delete the all in directory. 508 | * 509 | * @param dir The directory. 510 | * @return `true`: success

`false`: fail 511 | */ 512 | fun deleteAllInDir(dir: File?): Boolean { 513 | return deleteFilesInDirWithFilter(dir, object : FileFilter { 514 | override fun accept(pathname: File): Boolean { 515 | return true 516 | } 517 | }) 518 | } 519 | 520 | /** 521 | * Delete all files in directory. 522 | * 523 | * @param dirPath The path of directory. 524 | * @return `true`: success

`false`: fail 525 | */ 526 | fun deleteFilesInDir(dirPath: String): Boolean { 527 | return deleteFilesInDir(getFileByPath(dirPath)) 528 | } 529 | 530 | /** 531 | * Delete all files in directory. 532 | * 533 | * @param dir The directory. 534 | * @return `true`: success

`false`: fail 535 | */ 536 | fun deleteFilesInDir(dir: File?): Boolean { 537 | return deleteFilesInDirWithFilter(dir, object : FileFilter { 538 | override fun accept(pathname: File): Boolean { 539 | return pathname.isFile 540 | } 541 | }) 542 | } 543 | 544 | /** 545 | * Delete all files that satisfy the filter in directory. 546 | * 547 | * @param dirPath The path of directory. 548 | * @param filter The filter. 549 | * @return `true`: success

`false`: fail 550 | */ 551 | fun deleteFilesInDirWithFilter( 552 | dirPath: String, 553 | filter: FileFilter 554 | ): Boolean { 555 | return deleteFilesInDirWithFilter(getFileByPath(dirPath), filter) 556 | } 557 | 558 | /** 559 | * Delete all files that satisfy the filter in directory. 560 | * 561 | * @param dir The directory. 562 | * @param filter The filter. 563 | * @return `true`: success

`false`: fail 564 | */ 565 | fun deleteFilesInDirWithFilter(dir: File?, filter: FileFilter?): Boolean { 566 | if (dir == null || filter == null) return false 567 | // dir doesn't exist then return true 568 | if (!dir.exists()) return true 569 | // dir isn't a directory then return false 570 | if (!dir.isDirectory) return false 571 | val files = dir.listFiles() 572 | if (files != null && files.size != 0) { 573 | for (file in files) { 574 | if (filter!!.accept(file)) { 575 | if (file.isFile) { 576 | if (!file.delete()) return false 577 | } else if (file.isDirectory) { 578 | if (!deleteDir(file)) return false 579 | } 580 | } 581 | } 582 | } 583 | return true 584 | } 585 | 586 | /** 587 | * Return the files in directory. 588 | * 589 | * Doesn't traverse subdirectories 590 | * 591 | * @param dirPath The path of directory. 592 | * @return the files in directory 593 | */ 594 | fun listFilesInDir(dirPath: String): List { 595 | return listFilesInDir(dirPath, null) 596 | } 597 | 598 | /** 599 | * Return the files in directory. 600 | * 601 | * Doesn't traverse subdirectories 602 | * 603 | * @param dir The directory. 604 | * @return the files in directory 605 | */ 606 | fun listFilesInDir(dir: File): List { 607 | return listFilesInDir(dir, null) 608 | } 609 | 610 | /** 611 | * Return the files in directory. 612 | * 613 | * Doesn't traverse subdirectories 614 | * 615 | * @param dirPath The path of directory. 616 | * @param comparator The comparator to determine the order of the list. 617 | * @return the files in directory 618 | */ 619 | fun listFilesInDir(dirPath: String, comparator: Comparator?): List { 620 | return listFilesInDir(getFileByPath(dirPath), false) 621 | } 622 | 623 | /** 624 | * Return the files in directory. 625 | * 626 | * Doesn't traverse subdirectories 627 | * 628 | * @param dir The directory. 629 | * @param comparator The comparator to determine the order of the list. 630 | * @return the files in directory 631 | */ 632 | fun listFilesInDir(dir: File, comparator: Comparator?): List { 633 | return listFilesInDir(dir, false, comparator) 634 | } 635 | 636 | /** 637 | * Return the files in directory. 638 | * 639 | * @param dirPath The path of directory. 640 | * @param isRecursive True to traverse subdirectories, false otherwise. 641 | * @return the files in directory 642 | */ 643 | fun listFilesInDir(dirPath: String, isRecursive: Boolean): List { 644 | return listFilesInDir(getFileByPath(dirPath), isRecursive) 645 | } 646 | 647 | /** 648 | * Return the files in directory. 649 | * 650 | * @param dir The directory. 651 | * @param isRecursive True to traverse subdirectories, false otherwise. 652 | * @return the files in directory 653 | */ 654 | fun listFilesInDir(dir: File?, isRecursive: Boolean): List { 655 | return listFilesInDir(dir, isRecursive, null) 656 | } 657 | 658 | /** 659 | * Return the files in directory. 660 | * 661 | * @param dirPath The path of directory. 662 | * @param isRecursive True to traverse subdirectories, false otherwise. 663 | * @param comparator The comparator to determine the order of the list. 664 | * @return the files in directory 665 | */ 666 | fun listFilesInDir( 667 | dirPath: String, 668 | isRecursive: Boolean, 669 | comparator: Comparator 670 | ): List { 671 | return listFilesInDir(getFileByPath(dirPath), isRecursive, comparator) 672 | } 673 | 674 | /** 675 | * Return the files in directory. 676 | * 677 | * @param dir The directory. 678 | * @param isRecursive True to traverse subdirectories, false otherwise. 679 | * @param comparator The comparator to determine the order of the list. 680 | * @return the files in directory 681 | */ 682 | fun listFilesInDir( 683 | dir: File?, 684 | isRecursive: Boolean, 685 | comparator: Comparator? 686 | ): List { 687 | return listFilesInDirWithFilter(dir, object : FileFilter { 688 | override fun accept(pathname: File): Boolean { 689 | return true 690 | } 691 | }, isRecursive, comparator) 692 | } 693 | 694 | /** 695 | * Return the files that satisfy the filter in directory. 696 | * 697 | * Doesn't traverse subdirectories 698 | * 699 | * @param dirPath The path of directory. 700 | * @param filter The filter. 701 | * @return the files that satisfy the filter in directory 702 | */ 703 | fun listFilesInDirWithFilter( 704 | dirPath: String, 705 | filter: FileFilter 706 | ): List { 707 | return listFilesInDirWithFilter(getFileByPath(dirPath), filter) 708 | } 709 | 710 | /** 711 | * Return the files that satisfy the filter in directory. 712 | * 713 | * Doesn't traverse subdirectories 714 | * 715 | * @param dir The directory. 716 | * @param filter The filter. 717 | * @return the files that satisfy the filter in directory 718 | */ 719 | fun listFilesInDirWithFilter( 720 | dir: File?, 721 | filter: FileFilter 722 | ): List { 723 | return listFilesInDirWithFilter(dir, filter, false, null) 724 | } 725 | 726 | /** 727 | * Return the files that satisfy the filter in directory. 728 | * 729 | * Doesn't traverse subdirectories 730 | * 731 | * @param dirPath The path of directory. 732 | * @param filter The filter. 733 | * @param comparator The comparator to determine the order of the list. 734 | * @return the files that satisfy the filter in directory 735 | */ 736 | fun listFilesInDirWithFilter( 737 | dirPath: String, 738 | filter: FileFilter, 739 | comparator: Comparator 740 | ): List { 741 | return listFilesInDirWithFilter(getFileByPath(dirPath), filter, comparator) 742 | } 743 | 744 | /** 745 | * Return the files that satisfy the filter in directory. 746 | * 747 | * Doesn't traverse subdirectories 748 | * 749 | * @param dir The directory. 750 | * @param filter The filter. 751 | * @param comparator The comparator to determine the order of the list. 752 | * @return the files that satisfy the filter in directory 753 | */ 754 | fun listFilesInDirWithFilter( 755 | dir: File?, 756 | filter: FileFilter, 757 | comparator: Comparator 758 | ): List { 759 | return listFilesInDirWithFilter(dir, filter, false, comparator) 760 | } 761 | 762 | /** 763 | * Return the files that satisfy the filter in directory. 764 | * 765 | * @param dirPath The path of directory. 766 | * @param filter The filter. 767 | * @param isRecursive True to traverse subdirectories, false otherwise. 768 | * @return the files that satisfy the filter in directory 769 | */ 770 | fun listFilesInDirWithFilter( 771 | dirPath: String, 772 | filter: FileFilter, 773 | isRecursive: Boolean 774 | ): List { 775 | return listFilesInDirWithFilter(getFileByPath(dirPath), filter, isRecursive) 776 | } 777 | 778 | /** 779 | * Return the files that satisfy the filter in directory. 780 | * 781 | * @param dir The directory. 782 | * @param filter The filter. 783 | * @param isRecursive True to traverse subdirectories, false otherwise. 784 | * @return the files that satisfy the filter in directory 785 | */ 786 | fun listFilesInDirWithFilter( 787 | dir: File?, 788 | filter: FileFilter, 789 | isRecursive: Boolean 790 | ): List { 791 | return listFilesInDirWithFilter(dir, filter, isRecursive, null) 792 | } 793 | 794 | 795 | /** 796 | * Return the files that satisfy the filter in directory. 797 | * 798 | * @param dirPath The path of directory. 799 | * @param filter The filter. 800 | * @param isRecursive True to traverse subdirectories, false otherwise. 801 | * @param comparator The comparator to determine the order of the list. 802 | * @return the files that satisfy the filter in directory 803 | */ 804 | fun listFilesInDirWithFilter( 805 | dirPath: String, 806 | filter: FileFilter, 807 | isRecursive: Boolean, 808 | comparator: Comparator 809 | ): List { 810 | return listFilesInDirWithFilter(getFileByPath(dirPath), filter, isRecursive, comparator) 811 | } 812 | 813 | /** 814 | * Return the files that satisfy the filter in directory. 815 | * 816 | * @param dir The directory. 817 | * @param filter The filter. 818 | * @param isRecursive True to traverse subdirectories, false otherwise. 819 | * @param comparator The comparator to determine the order of the list. 820 | * @return the files that satisfy the filter in directory 821 | */ 822 | fun listFilesInDirWithFilter( 823 | dir: File?, 824 | filter: FileFilter, 825 | isRecursive: Boolean, 826 | comparator: Comparator? 827 | ): List { 828 | val files = listFilesInDirWithFilterInner(dir, filter, isRecursive) 829 | if (comparator != null) { 830 | Collections.sort(files, comparator) 831 | } 832 | return files 833 | } 834 | 835 | private fun listFilesInDirWithFilterInner( 836 | dir: File?, 837 | filter: FileFilter, 838 | isRecursive: Boolean 839 | ): List { 840 | val list = ArrayList() 841 | if (!isDir(dir)) return list 842 | val files = dir!!.listFiles() 843 | if (files != null && files.size != 0) { 844 | for (file in files) { 845 | if (filter.accept(file)) { 846 | list.add(file) 847 | } 848 | if (isRecursive && file.isDirectory) { 849 | list.addAll(listFilesInDirWithFilterInner(file, filter, true)) 850 | } 851 | } 852 | } 853 | return list 854 | } 855 | 856 | /** 857 | * Return the time that the file was last modified. 858 | * 859 | * @param filePath The path of file. 860 | * @return the time that the file was last modified 861 | */ 862 | 863 | fun getFileLastModified(filePath: String): Long { 864 | return getFileLastModified(getFileByPath(filePath)) 865 | } 866 | 867 | /** 868 | * Return the time that the file was last modified. 869 | * 870 | * @param file The file. 871 | * @return the time that the file was last modified 872 | */ 873 | fun getFileLastModified(file: File?): Long { 874 | return file?.lastModified() ?: -1 875 | } 876 | 877 | /** 878 | * Return the charset of file simply. 879 | * 880 | * @param filePath The path of file. 881 | * @return the charset of file simply 882 | */ 883 | fun getFileCharsetSimple(filePath: String): String { 884 | return getFileCharsetSimple(getFileByPath(filePath)) 885 | } 886 | 887 | /** 888 | * Return the charset of file simply. 889 | * 890 | * @param file The file. 891 | * @return the charset of file simply 892 | */ 893 | fun getFileCharsetSimple(file: File?): String { 894 | if (file == null) return "" 895 | if (isUtf8(file)) return "UTF-8" 896 | var p = 0 897 | var `is`: InputStream? = null 898 | try { 899 | `is` = BufferedInputStream(FileInputStream(file)) 900 | p = (`is`!!.read() shl 8) + `is`!!.read() 901 | } catch (e: IOException) { 902 | e.printStackTrace() 903 | } finally { 904 | try { 905 | if (`is` != null) { 906 | `is`!!.close() 907 | } 908 | } catch (e: IOException) { 909 | e.printStackTrace() 910 | } 911 | 912 | } 913 | when (p) { 914 | 0xfffe -> return "Unicode" 915 | 0xfeff -> return "UTF-16BE" 916 | else -> return "GBK" 917 | } 918 | } 919 | 920 | /** 921 | * Return whether the charset of file is utf8. 922 | * 923 | * @param filePath The path of file. 924 | * @return `true`: yes

`false`: no 925 | */ 926 | fun isUtf8(filePath: String): Boolean { 927 | return isUtf8(getFileByPath(filePath)) 928 | } 929 | 930 | /** 931 | * Return whether the charset of file is utf8. 932 | * 933 | * @param file The file. 934 | * @return `true`: yes

`false`: no 935 | */ 936 | fun isUtf8(file: File?): Boolean { 937 | if (file == null) return false 938 | var `is`: InputStream? = null 939 | try { 940 | val bytes = ByteArray(24) 941 | `is` = BufferedInputStream(FileInputStream(file)) 942 | val read = `is`!!.read(bytes) 943 | if (read != -1) { 944 | val readArr = ByteArray(read) 945 | System.arraycopy(bytes, 0, readArr, 0, read) 946 | return isUtf8(readArr) == 100 947 | } else { 948 | return false 949 | } 950 | } catch (e: IOException) { 951 | e.printStackTrace() 952 | } finally { 953 | try { 954 | if (`is` != null) { 955 | `is`!!.close() 956 | } 957 | } catch (e: IOException) { 958 | e.printStackTrace() 959 | } 960 | 961 | } 962 | return false 963 | } 964 | 965 | /** 966 | * UTF-8编码方式 967 | * ---------------------------------------------- 968 | * 0xxxxxxx 969 | * 110xxxxx 10xxxxxx 970 | * 1110xxxx 10xxxxxx 10xxxxxx 971 | * 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 972 | */ 973 | private fun isUtf8(raw: ByteArray): Int { 974 | var i: Int 975 | val len: Int 976 | var utf8 = 0 977 | var ascii = 0 978 | if (raw.size > 3) { 979 | if (raw[0] == 0xEF.toByte() && raw[1] == 0xBB.toByte() && raw[2] == 0xBF.toByte()) { 980 | return 100 981 | } 982 | } 983 | len = raw.size 984 | var child = 0 985 | i = 0 986 | while (i < len) { 987 | // UTF-8 byte shouldn't be FF and FE 988 | if ((raw[i].and(0xFF.toByte()) == 0xFF.toByte()).or( raw[i].and(0xFE.toByte()) == 0xFE.toByte())) { 989 | return 0 990 | } 991 | if (child == 0) { 992 | // ASCII format is 0x0******* 993 | if (raw[i] and 0x7F.toByte() == raw[i] && raw[i].toInt() != 0) { 994 | ascii++ 995 | } else if (raw[i] and 0xC0.toByte() == 0xC0.toByte()) { 996 | // 0x11****** maybe is UTF-8 997 | for (bit in 0..7) { 998 | if ((0x80 shr bit).toByte() and raw[i] == (0x80 shr bit).toByte()) { 999 | child = bit 1000 | } else { 1001 | break 1002 | } 1003 | } 1004 | utf8++ 1005 | } 1006 | i++ 1007 | } else { 1008 | child = if (raw.size - i > child) child else raw.size - i 1009 | var currentNotUtf8 = false 1010 | for (children in 0 until child) { 1011 | // format must is 0x10****** 1012 | if (raw[i + children] and 0x80.toByte() != 0x80.toByte()) { 1013 | if (raw[i + children] and 0x7F.toByte() == raw[i + children] && raw[i].toInt() != 0) { 1014 | // ASCII format is 0x0******* 1015 | ascii++ 1016 | } 1017 | currentNotUtf8 = true 1018 | } 1019 | } 1020 | if (currentNotUtf8) { 1021 | utf8-- 1022 | i++ 1023 | } else { 1024 | utf8 += child 1025 | i += child 1026 | } 1027 | child = 0 1028 | } 1029 | } 1030 | // UTF-8 contains ASCII 1031 | return if (ascii == len) { 1032 | 100 1033 | } else (100 * ((utf8 + ascii).toFloat() / len.toFloat())).toInt() 1034 | } 1035 | 1036 | /** 1037 | * Return the number of lines of file. 1038 | * 1039 | * @param filePath The path of file. 1040 | * @return the number of lines of file 1041 | */ 1042 | fun getFileLines(filePath: String): Int { 1043 | return getFileLines(getFileByPath(filePath)) 1044 | } 1045 | 1046 | /** 1047 | * Return the number of lines of file. 1048 | * 1049 | * @param file The file. 1050 | * @return the number of lines of file 1051 | */ 1052 | fun getFileLines(file: File?): Int { 1053 | var count = 1 1054 | var `is`: InputStream? = null 1055 | try { 1056 | `is` = BufferedInputStream(FileInputStream(file!!)) 1057 | val buffer = ByteArray(1024) 1058 | var readChars: Int 1059 | if (LINE_SEP.endsWith("\n")) { 1060 | readChars = `is`!!.read(buffer, 0, 1024) 1061 | while (readChars != -1) { 1062 | for (i in 0 until readChars) { 1063 | if (buffer[i] == '\n'.toByte()) ++count 1064 | } 1065 | readChars = `is`!!.read(buffer, 0, 1024) 1066 | } 1067 | } else { 1068 | readChars = `is`!!.read(buffer, 0, 1024) 1069 | while (readChars != -1) { 1070 | for (i in 0 until readChars) { 1071 | if (buffer[i] == '\r'.toByte()) ++count 1072 | } 1073 | readChars = `is`!!.read(buffer, 0, 1024) 1074 | } 1075 | } 1076 | } catch (e: IOException) { 1077 | e.printStackTrace() 1078 | } finally { 1079 | try { 1080 | if (`is` != null) { 1081 | `is`!!.close() 1082 | } 1083 | } catch (e: IOException) { 1084 | e.printStackTrace() 1085 | } 1086 | 1087 | } 1088 | return count 1089 | } 1090 | 1091 | /** 1092 | * Return the size. 1093 | * 1094 | * @param filePath The path of file. 1095 | * @return the size 1096 | */ 1097 | fun getSize(filePath: String): String { 1098 | return getSize(getFileByPath(filePath)) 1099 | } 1100 | 1101 | /** 1102 | * Return the size. 1103 | * 1104 | * @param file The directory. 1105 | * @return the size 1106 | */ 1107 | fun getSize(file: File?): String { 1108 | if (file == null) return "" 1109 | return if (file.isDirectory) { 1110 | getDirSize(file) 1111 | } else getFileSize(file) 1112 | } 1113 | 1114 | /** 1115 | * Return the size of directory. 1116 | * 1117 | * @param dir The directory. 1118 | * @return the size of directory 1119 | */ 1120 | private fun getDirSize(dir: File): String { 1121 | val len = getDirLength(dir) 1122 | return if (len == -1L) "" else byte2FitMemorySize(len) 1123 | } 1124 | 1125 | /** 1126 | * Return the size of file. 1127 | * 1128 | * @param file The file. 1129 | * @return the length of file 1130 | */ 1131 | private fun getFileSize(file: File): String { 1132 | val len = getFileLength(file) 1133 | return if (len == -1L) "" else byte2FitMemorySize(len) 1134 | } 1135 | 1136 | /** 1137 | * Return the length. 1138 | * 1139 | * @param filePath The path of file. 1140 | * @return the length 1141 | */ 1142 | fun getLength(filePath: String): Long { 1143 | return getLength(getFileByPath(filePath)) 1144 | } 1145 | 1146 | /** 1147 | * Return the length. 1148 | * 1149 | * @param file The file. 1150 | * @return the length 1151 | */ 1152 | fun getLength(file: File?): Long { 1153 | if (file == null) return 0 1154 | return if (file.isDirectory) { 1155 | getDirLength(file) 1156 | } else getFileLength(file) 1157 | } 1158 | 1159 | /** 1160 | * Return the length of directory. 1161 | * 1162 | * @param dir The directory. 1163 | * @return the length of directory 1164 | */ 1165 | private fun getDirLength(dir: File): Long { 1166 | if (!isDir(dir)) return -1 1167 | var len: Long = 0 1168 | val files = dir.listFiles() 1169 | if (files != null && files.size != 0) { 1170 | for (file in files) { 1171 | if (file.isDirectory) { 1172 | len += getDirLength(file) 1173 | } else { 1174 | len += file.length() 1175 | } 1176 | } 1177 | } 1178 | return len 1179 | } 1180 | 1181 | /** 1182 | * Return the length of file. 1183 | * 1184 | * @param filePath The path of file. 1185 | * @return the length of file 1186 | */ 1187 | fun getFileLength(filePath: String): Long { 1188 | val isURL = filePath.matches("[a-zA-z]+://[^\\s]*".toRegex()) 1189 | if (isURL) { 1190 | try { 1191 | val conn = URL(filePath).openConnection() as HttpsURLConnection 1192 | conn.setRequestProperty("Accept-Encoding", "identity") 1193 | conn.connect() 1194 | return if (conn.getResponseCode() === 200) { 1195 | conn.getContentLength().toLong() 1196 | } else -1 1197 | } catch (e: IOException) { 1198 | e.printStackTrace() 1199 | } 1200 | 1201 | } 1202 | return getFileLength(getFileByPath(filePath)) 1203 | } 1204 | 1205 | /** 1206 | * Return the length of file. 1207 | * 1208 | * @param file The file. 1209 | * @return the length of file 1210 | */ 1211 | private fun getFileLength(file: File?): Long { 1212 | return if (!isFile(file)) -1 else file!!.length() 1213 | } 1214 | 1215 | /** 1216 | * Return the MD5 of file. 1217 | * 1218 | * @param filePath The path of file. 1219 | * @return the md5 of file 1220 | */ 1221 | fun getFileMD5ToString(filePath: String): String { 1222 | val file = if (isSpace(filePath)) null else File(filePath) 1223 | return getFileMD5ToString(file) 1224 | } 1225 | 1226 | /** 1227 | * Return the MD5 of file. 1228 | * 1229 | * @param file The file. 1230 | * @return the md5 of file 1231 | */ 1232 | fun getFileMD5ToString(file: File?): String { 1233 | return bytes2HexString(getFileMD5(file)) 1234 | } 1235 | 1236 | /** 1237 | * Return the MD5 of file. 1238 | * 1239 | * @param filePath The path of file. 1240 | * @return the md5 of file 1241 | */ 1242 | fun getFileMD5(filePath: String): ByteArray? { 1243 | return getFileMD5(getFileByPath(filePath)) 1244 | } 1245 | 1246 | /** 1247 | * Return the MD5 of file. 1248 | * 1249 | * @param file The file. 1250 | * @return the md5 of file 1251 | */ 1252 | fun getFileMD5(file: File?): ByteArray? { 1253 | if (file == null) return null 1254 | var dis: DigestInputStream? = null 1255 | try { 1256 | val fis = FileInputStream(file) 1257 | var md = MessageDigest.getInstance("MD5") 1258 | dis = DigestInputStream(fis, md) 1259 | val buffer = ByteArray(1024 * 256) 1260 | while (true) { 1261 | if (dis.read(buffer) <= 0) break 1262 | } 1263 | md = dis.messageDigest 1264 | return md.digest() 1265 | } catch (e: NoSuchAlgorithmException) { 1266 | e.printStackTrace() 1267 | } catch (e: IOException) { 1268 | e.printStackTrace() 1269 | } finally { 1270 | try { 1271 | dis?.close() 1272 | } catch (e: IOException) { 1273 | e.printStackTrace() 1274 | } 1275 | 1276 | } 1277 | return null 1278 | } 1279 | 1280 | /** 1281 | * Return the file's path of directory. 1282 | * 1283 | * @param file The file. 1284 | * @return the file's path of directory 1285 | */ 1286 | fun getDirName(file: File?): String { 1287 | return if (file == null) "" else getDirName(file.absolutePath) 1288 | } 1289 | 1290 | /** 1291 | * Return the file's path of directory. 1292 | * 1293 | * @param filePath The path of file. 1294 | * @return the file's path of directory 1295 | */ 1296 | fun getDirName(filePath: String): String { 1297 | if (isSpace(filePath)) return "" 1298 | val lastSep = filePath.lastIndexOf(File.separator) 1299 | return if (lastSep == -1) "" else filePath.substring(0, lastSep + 1) 1300 | } 1301 | 1302 | /** 1303 | * Return the name of file. 1304 | * 1305 | * @param file The file. 1306 | * @return the name of file 1307 | */ 1308 | fun getFileName(file: File?): String { 1309 | return if (file == null) "" else getFileName(file.absolutePath) 1310 | } 1311 | 1312 | /** 1313 | * Return the name of file. 1314 | * 1315 | * @param filePath The path of file. 1316 | * @return the name of file 1317 | */ 1318 | fun getFileName(filePath: String): String { 1319 | if (isSpace(filePath)) return "" 1320 | val lastSep = filePath.lastIndexOf(File.separator) 1321 | return if (lastSep == -1) filePath else filePath.substring(lastSep + 1) 1322 | } 1323 | 1324 | /** 1325 | * Return the name of file without extension. 1326 | * 1327 | * @param file The file. 1328 | * @return the name of file without extension 1329 | */ 1330 | fun getFileNameNoExtension(file: File?): String { 1331 | return if (file == null) "" else getFileNameNoExtension(file.path) 1332 | } 1333 | 1334 | /** 1335 | * Return the name of file without extension. 1336 | * 1337 | * @param filePath The path of file. 1338 | * @return the name of file without extension 1339 | */ 1340 | fun getFileNameNoExtension(filePath: String): String { 1341 | if (isSpace(filePath)) return "" 1342 | val lastPoi = filePath.lastIndexOf('.') 1343 | val lastSep = filePath.lastIndexOf(File.separator) 1344 | if (lastSep == -1) { 1345 | return if (lastPoi == -1) filePath else filePath.substring(0, lastPoi) 1346 | } 1347 | return if (lastPoi == -1 || lastSep > lastPoi) { 1348 | filePath.substring(lastSep + 1) 1349 | } else filePath.substring(lastSep + 1, lastPoi) 1350 | } 1351 | 1352 | /** 1353 | * Return the extension of file. 1354 | * 1355 | * @param file The file. 1356 | * @return the extension of file 1357 | */ 1358 | fun getFileExtension(file: File?): String { 1359 | return if (file == null) "" else getFileExtension(file.path) 1360 | } 1361 | 1362 | /** 1363 | * Return the extension of file. 1364 | * 1365 | * @param filePath The path of file. 1366 | * @return the extension of file 1367 | */ 1368 | fun getFileExtension(filePath: String): String { 1369 | if (isSpace(filePath)) return "" 1370 | val lastPoi = filePath.lastIndexOf('.') 1371 | val lastSep = filePath.lastIndexOf(File.separator) 1372 | return if (lastPoi == -1 || lastSep >= lastPoi) "" else filePath.substring(lastPoi + 1) 1373 | } 1374 | 1375 | /////////////////////////////////////////////////////////////////////////// 1376 | // interface 1377 | /////////////////////////////////////////////////////////////////////////// 1378 | 1379 | interface OnReplaceListener { 1380 | fun onReplace(srcFile: File, destFile: File): Boolean 1381 | } 1382 | 1383 | /////////////////////////////////////////////////////////////////////////// 1384 | // other utils methods 1385 | /////////////////////////////////////////////////////////////////////////// 1386 | 1387 | private val HEX_DIGITS = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') 1388 | 1389 | private fun bytes2HexString(bytes: ByteArray?): String { 1390 | if (bytes == null) return "" 1391 | val len = bytes.size 1392 | if (len <= 0) return "" 1393 | val ret = CharArray(len shl 1) 1394 | var i = 0 1395 | var j = 0 1396 | while (i < len) { 1397 | ret[j++] = HEX_DIGITS[bytes[i].toInt() shr 4 and 0x0f] 1398 | ret[j++] = HEX_DIGITS[bytes[i].toInt() and 0x0f] 1399 | i++ 1400 | } 1401 | return String(ret) 1402 | } 1403 | 1404 | private fun byte2FitMemorySize(byteNum: Long): String { 1405 | return if (byteNum < 0) { 1406 | "shouldn't be less than zero!" 1407 | } else if (byteNum < 1024) { 1408 | String.format(Locale.getDefault(), "%.3fB", byteNum.toDouble()) 1409 | } else if (byteNum < 1048576) { 1410 | String.format(Locale.getDefault(), "%.3fKB", byteNum.toDouble() / 1024) 1411 | } else if (byteNum < 1073741824) { 1412 | String.format(Locale.getDefault(), "%.3fMB", byteNum.toDouble() / 1048576) 1413 | } else { 1414 | String.format(Locale.getDefault(), "%.3fGB", byteNum.toDouble() / 1073741824) 1415 | } 1416 | } 1417 | 1418 | private fun isSpace(s: String?): Boolean { 1419 | if (s == null) return true 1420 | var i = 0 1421 | val len = s.length 1422 | while (i < len) { 1423 | if (!Character.isWhitespace(s[i])) { 1424 | return false 1425 | } 1426 | ++i 1427 | } 1428 | return true 1429 | } 1430 | 1431 | private fun writeFileFromIS( 1432 | file: File, 1433 | `is`: InputStream 1434 | ): Boolean { 1435 | var os: OutputStream? = null 1436 | try { 1437 | os = BufferedOutputStream(FileOutputStream(file)) 1438 | val data = ByteArray(8192) 1439 | var len: Int 1440 | len = `is`.read(data, 0, 8192) 1441 | while (len != -1) { 1442 | os!!.write(data, 0, len) 1443 | len = `is`.read(data, 0, 8192) 1444 | } 1445 | return true 1446 | } catch (e: IOException) { 1447 | e.printStackTrace() 1448 | return false 1449 | } finally { 1450 | try { 1451 | `is`.close() 1452 | } catch (e: IOException) { 1453 | e.printStackTrace() 1454 | } 1455 | 1456 | try { 1457 | if (os != null) { 1458 | os!!.close() 1459 | } 1460 | } catch (e: IOException) { 1461 | e.printStackTrace() 1462 | } 1463 | 1464 | } 1465 | } 1466 | 1467 | fun printStream(inputStream: InputStream){ 1468 | var buffer = ByteArray(1024) 1469 | val bos = ByteArrayOutputStream() 1470 | var len = inputStream.read(buffer) 1471 | while (len != -1){ 1472 | bos.write(buffer,0,len) 1473 | len = inputStream.read(buffer) 1474 | } 1475 | System.out.println(String(bos.toByteArray(), Charset.forName("UTF-8"))) 1476 | inputStream.close() 1477 | } 1478 | } -------------------------------------------------------------------------------- /src/com/sakuqi/shell/utils/SignUtils.kt: -------------------------------------------------------------------------------- 1 | package com.sakuqi.shell.utils 2 | 3 | import java.io.ByteArrayOutputStream 4 | import java.io.File 5 | import java.io.IOException 6 | import java.nio.charset.Charset 7 | 8 | 9 | object SignUtils { 10 | @Throws(InterruptedException::class, IOException::class) 11 | fun signature(unsignedApk: File, signedApk: File, keyStore: String) { 12 | val cmd = arrayOf( 13 | "jarsigner", 14 | "-sigalg", 15 | "SHA1withRSA", 16 | "-digestalg", 17 | "SHA1", 18 | "-keystore", 19 | keyStore, 20 | "-storepass", 21 | "a66388487", 22 | "-keypass", 23 | "a66388487", 24 | "-signedjar", 25 | signedApk.absolutePath, 26 | unsignedApk.absolutePath, 27 | "wuse" 28 | ) 29 | val process = Runtime.getRuntime().exec(cmd) 30 | println("start sign") 31 | try { 32 | val waitResult = process.waitFor() 33 | println("waitResult: $waitResult") 34 | } catch (e: InterruptedException) { 35 | e.printStackTrace() 36 | throw e 37 | } 38 | 39 | println("process.exitValue() " + process.exitValue()) 40 | if (process.exitValue() != 0) { 41 | val inputStream = process.errorStream 42 | var len: Int 43 | val buffer = ByteArray(2048) 44 | val bos = ByteArrayOutputStream() 45 | len = inputStream.read(buffer) 46 | while (len != -1) { 47 | bos.write(buffer, 0, len) 48 | len = inputStream.read(buffer) 49 | } 50 | println(String(bos.toByteArray(), Charset.forName("gbk"))) 51 | throw RuntimeException("签名执行失败") 52 | } 53 | println("finish signed") 54 | process.destroy() 55 | } 56 | } -------------------------------------------------------------------------------- /src/com/sakuqi/shell/utils/XmlParseUtils.kt: -------------------------------------------------------------------------------- 1 | package com.sakuqi.shell.utils 2 | 3 | import org.dom4j.Document 4 | import org.dom4j.Element 5 | import org.dom4j.io.OutputFormat 6 | import org.dom4j.io.SAXReader 7 | import org.dom4j.io.XMLWriter 8 | import org.xml.sax.Attributes 9 | import org.xml.sax.helpers.DefaultHandler 10 | import java.io.File 11 | import java.io.FileOutputStream 12 | import java.io.InputStream 13 | import java.io.OutputStreamWriter 14 | import javax.xml.parsers.SAXParserFactory 15 | 16 | object XmlParseUtils { 17 | fun sax2xml(ips: InputStream):String{ 18 | val spf = SAXParserFactory.newInstance() 19 | //初始化Sax解析器 20 | val sp = spf.newSAXParser() 21 | val handler = MyHandler() 22 | sp.parse(ips,handler) 23 | return handler.originName?:"" 24 | } 25 | 26 | /** 27 | * 修改xml文件 28 | */ 29 | fun changeXmlBySax(fileXml:File,newApplicationName:String){ 30 | var sax = SAXReader() 31 | var document = sax.read(fileXml) 32 | var root = document.rootElement 33 | var application = root.element("application") 34 | //原有的 application 名称 35 | var applicationName = application.attributeValue("name") 36 | var applicationAttr = application.attribute("name") 37 | //将壳中的 application 替换原来的 application 38 | applicationAttr.text = newApplicationName 39 | 40 | var element = application.addElement("meta-data") 41 | element.addAttribute("android:name","app_name") 42 | element.addAttribute("android:value",applicationName) 43 | saveDocument(document,fileXml) 44 | 45 | } 46 | fun saveDocument(document:Document,file:File){ 47 | var osWrite = OutputStreamWriter(FileOutputStream(file)) 48 | var format = OutputFormat.createPrettyPrint()// 获取输出的指定格式 49 | format.encoding = "UTF-8" 50 | var writer = XMLWriter(osWrite,format) 51 | writer.write(document) 52 | writer.flush() 53 | writer.close() 54 | } 55 | 56 | 57 | class MyHandler: DefaultHandler() { 58 | var originName:String?=null 59 | var isApplication = false 60 | override fun startDocument() { 61 | super.startDocument() 62 | } 63 | 64 | override fun endDocument() { 65 | super.endDocument() 66 | } 67 | 68 | override fun startElement( 69 | uri: String?, 70 | localName: String?, 71 | qName: String?, 72 | attributes: Attributes? 73 | ) { 74 | super.startElement(uri, localName, qName, attributes) 75 | isApplication = "application" == qName 76 | if(isApplication) { 77 | originName = attributes?.getValue("android:name") 78 | } 79 | } 80 | 81 | 82 | 83 | override fun endElement(uri: String?, localName: String?, qName: String?) { 84 | super.endElement(uri, localName, qName) 85 | } 86 | 87 | override fun characters(ch: CharArray, start: Int, length: Int) { 88 | super.characters(ch, start, length) 89 | } 90 | } 91 | } -------------------------------------------------------------------------------- /src/com/sakuqi/shell/utils/ZipUtils.kt: -------------------------------------------------------------------------------- 1 | package com.sakuqi.shell.utils 2 | 3 | import java.io.BufferedInputStream 4 | import java.io.File 5 | import java.io.FileInputStream 6 | import java.io.FileOutputStream 7 | import java.lang.Exception 8 | import java.util.zip.* 9 | 10 | object ZipUtils{ 11 | fun unZip(zip: File,dir:File){ 12 | try{ 13 | dir.delete() 14 | var zipFile = ZipFile(zip) 15 | var entries = zipFile.entries() 16 | while (entries.hasMoreElements()){ 17 | var zipEntry = entries.nextElement() 18 | var name = zipEntry.name 19 | if(name.equals("META-INF/CERT.RSA") || name.equals("META-INF/CERT.SF") 20 | || name.equals("META-INF/MANIFEST.MF")){ 21 | continue 22 | } 23 | if(!zipEntry.isDirectory){ 24 | var file = File(dir,name) 25 | if(!file.parentFile.exists()){ 26 | file.parentFile.mkdirs() 27 | } 28 | var fos = FileOutputStream(file) 29 | var ips = zipFile.getInputStream(zipEntry) 30 | var buffer = ByteArray(1024) 31 | var len = ips.read(buffer) 32 | while (len != -1){ 33 | fos.write(buffer,0,len) 34 | len = ips.read(buffer) 35 | } 36 | ips.close() 37 | fos.close() 38 | } 39 | } 40 | zipFile.close() 41 | }catch (e:Exception){ 42 | e.printStackTrace() 43 | } 44 | } 45 | 46 | fun zip(dir: File,zip: File){ 47 | zip.delete() 48 | var cos = CheckedOutputStream(FileOutputStream(zip),CRC32()) 49 | var zos = ZipOutputStream(cos) 50 | compress(dir,zos,"") 51 | zos.flush() 52 | zos.close() 53 | } 54 | 55 | 56 | private fun compress(dir: File, zos: ZipOutputStream, s: String) { 57 | if(dir.isDirectory){ 58 | compressDir(dir,zos,s) 59 | }else{ 60 | compressFile(dir,zos,s) 61 | } 62 | } 63 | 64 | private fun compressFile(file: File, zos: ZipOutputStream, dir: String) { 65 | var dirName = dir + file.name 66 | var dirNameNew = dirName.split("/") 67 | var buffer = StringBuffer() 68 | if(dirNameNew.size > 1){ 69 | for (i in 1 until dirNameNew.size){ 70 | buffer.append("/") 71 | buffer.append(dirNameNew[i]) 72 | } 73 | }else{ 74 | buffer.append("/") 75 | } 76 | 77 | var entry = ZipEntry(buffer.substring(1)) 78 | zos.putNextEntry(entry) 79 | var bis = BufferedInputStream(FileInputStream(file)) 80 | var data = ByteArray(1024) 81 | var len = bis.read(data,0,1024) 82 | while (len != -1){ 83 | zos.write(data,0,len) 84 | len = bis.read(data,0,1024) 85 | } 86 | bis.close() 87 | zos.closeEntry() 88 | } 89 | 90 | 91 | private fun compressDir(dir: File, zos: ZipOutputStream, basePath: String) { 92 | var files = dir.listFiles() 93 | for (file in files){ 94 | compress(file,zos,basePath+dir.name+"/") 95 | } 96 | } 97 | } --------------------------------------------------------------------------------