├── .github └── workflows │ └── gradle.yml ├── LICENSE.txt ├── README.md ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── patch ├── build.gradle └── src │ └── main │ └── java │ └── dx │ └── Patch.java ├── proguard.pro ├── settings.gradle └── signer ├── build.gradle ├── lib ├── apksigner-32.0.0.jar ├── axml-8df59da6.jar ├── fastzip-86e2efa1.jar └── patch-apksigner.sh └── src └── main ├── java ├── dx │ ├── channel │ │ ├── ApkSigns.java │ │ ├── ChannelBuilder.java │ │ └── FixSha1WithDsaProvider.java │ └── signer │ │ ├── CommandLine.java │ │ ├── FixApk.java │ │ ├── JTextAreaOutputStream.java │ │ ├── SignWorker.java │ │ ├── UX.form │ │ └── UX.java └── fake │ └── security │ └── Signature.java └── resources └── dx └── signer └── LICENSE.txt /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time 6 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 7 | 8 | name: Java CI with Gradle 9 | 10 | on: 11 | push: 12 | branches: [ "main" ] 13 | pull_request: 14 | branches: [ "main" ] 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | build: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Set up JDK 11 27 | uses: actions/setup-java@v3 28 | with: 29 | java-version: '11' 30 | distribution: 'temurin' 31 | - name: Build with Gradle 32 | uses: gradle/gradle-build-action@0d13054264b0bb894ded474f08ebb30921341cee 33 | with: 34 | arguments: fatjar 35 | - name: Upload a Build Artifact 36 | uses: actions/upload-artifact@v3.1.0 37 | with: 38 | name: dx-signer.jar 39 | path: signer/build/libs/dx-signer.jar 40 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apk签名和多渠道打包工具 2 | 3 | ## 编译方法 4 | 5 | ```bash 6 | ./gradlew fatjar 7 | # 输出文件位于 signer/build/libs/dx-signer.jar 8 | ``` 9 | 10 | ## 图形界面 11 | 12 | 请双击`dx-signer.jar`文件启动,或者使用命令行启动。 13 | 14 | ``` 15 | java -jar dx-signer.jar 16 | ``` 17 | 18 | 您需要Java 8+的运行环境,推荐使用OpenJDK的实现。 19 | 请根据界面提示操作。 20 | 当指定渠道清单时,工具进入多渠道模式, 如果 输出apk/aab 指向一个文件,那么渠道包会保存在同目录下; 如果 输出apk/aab 指向目录,那么渠道包会保存在这个目录下。 21 | 22 | ## 命令行界面 23 | 24 | ```bash 25 | java -jar dx-signer.jar sign [--option value]+ 26 | ``` 27 | 28 | 您需要Java 8+的运行环境,推荐使用OpenJDK的实现。 29 | 其中第一个参数必须是`sign`用于区分命令行还是图形界面。 30 | option可以重复出现,后面的值覆盖前面的。 31 | 32 | 33 | 支持的`option`如下 34 | 35 | | option | type | 必须 | 描述 | 36 | | :----------- | :----- | :---: | ---------------------------------------------------------------- | 37 | | config | Path | | 配置文件,文件格式满足java.util.Properties要求,key值与option一样 | 38 | | in | Path | 是 | 输入文件apk、aab | 39 | | out | Path | 是 | 输出文件或文件夹 | 40 | | ks | Path | 是 | Keystore位置 | 41 | | ks-pass | String | | Keystore密码, 默认android | 42 | | ks-key-alias | String | | alias,默认第一个 | 43 | | key-pass | String | | alias密码,默认与ks-pass相同 | 44 | | channel-list | Path | | 渠道清单,格式见 多渠道 | 45 | | in-filename | String | | 多渠道模式下,指定输入文件名 | 46 | 47 | 当指定渠道清单时,工具进入多渠道模式,out参数需要指向一个存在的目录。 48 | config可以视作option的集合, 其避免命令行过长。 49 | 50 | 例如: 51 | 52 | ```bash 53 | 54 | # 使用etc/cfg.properties指定的参数进行签名 55 | java -jar dx-signer.apk sign --config etc/cfg.properties 56 | 57 | # 使用etc/cfg.properties, 并使用keystore.properties里面的证书信息进行签名 58 | java -jar dx-signer.apk sign --config etc/cfg.properties --config keystore.properties 59 | 60 | # 使用etc/cfg.properties指定的参数, 但是修改掉apk的输出路径 61 | java -jar dx-signer.apk sign --config etc/cfg.properties --out path/to/other/location.apk 62 | 63 | # 不使用config进行签名 64 | java -jar dx-signer.apk sign --in in.apk --out signed.apk --ks keystore.JKS --ks-pass android 65 | 66 | # 多渠道 67 | mkdir -p out-apks 68 | java -jar dx-signer.apk sign --config keystore.properties \ 69 | --in in.apk --channel-list channel.txt \ 70 | --out out-apks/ 71 | 72 | ``` 73 | 74 | 75 | ## 多渠道 76 | 77 | 请准备渠道清单文件`channel.txt`, 格式为每一行一个渠道, 例如: 78 | 79 | ``` 80 | 0001_my 81 | 0003_baidu 82 | 0004_huawei 83 | 0005_oppo 84 | 0006_vivo 85 | 0007_360 86 | 0008_xiaomi 87 | 0009_yingyongbao 88 | 0011_lianxiang 89 | 0012_meizu 90 | 0013_yingyonghui 91 | 0014_ali 92 | # 注释行 93 | 0015_test # 注释内容 94 | ``` 95 | 96 | ### 读取渠道信息:UMENG_CHANNEL 97 | 98 | 输出的Apk中将会包含`UMENG_CHANNEL`的`mata-data` 99 | 100 | ```xml 101 | 102 | 105 | 106 | ``` 107 | 108 | 您可以读取这个字段。 109 | 110 | ```java 111 | public static String getChannel(Context ctx) { 112 | String channel = ""; 113 | try { 114 | ApplicationInfo appInfo = ctx.getPackageManager().getApplicationInfo(ctx.getPackageName(), 115 | PackageManager.GET_META_DATA); 116 | channel = appInfo.metaData.getString("UMENG_CHANNEL"); 117 | } catch (PackageManager.NameNotFoundException ignore) { 118 | } 119 | return channel; 120 | } 121 | ``` 122 | 123 | ### 读取渠道信息:Walle 124 | 125 | 输出的Apk也包含Walle风格的渠道信息 126 | 127 | 您可以在使用[Walle](https://github.com/Meituan-Dianping/walle)的方式进行读取。 128 | 129 | 130 | ```gradle 131 | implementation 'com.meituan.android.walle:library:1.1.7' 132 | ``` 133 | 134 | 135 | ```java 136 | 137 | String channel = WalleChannelReader.getChannel(this.getApplicationContext()); 138 | 139 | ``` 140 | 141 | ## License 142 | 143 | ``` 144 | dx-signer 145 | 146 | Copyright 2022 北京顶象技术有限公司 147 | 148 | Licensed under the Apache License, Version 2.0 (the "License"); 149 | you may not use this file except in compliance with the License. 150 | You may obtain a copy of the License at 151 | 152 | http://www.apache.org/licenses/LICENSE-2.0 153 | 154 | Unless required by applicable law or agreed to in writing, software 155 | distributed under the License is distributed on an "AS IS" BASIS, 156 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 157 | See the License for the specific language governing permissions and 158 | limitations under the License. 159 | ``` 160 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dingxiangtech/dx-signer/07e0e7f33dc77c8a65841b9b89761e9d23da870d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /patch/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'idea' 4 | } 5 | 6 | sourceCompatibility = 1.8 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | compile fileTree(dir: 'lib', include: ['*.jar']) 14 | compile group: 'org.ow2.asm', name: 'asm', version: '9.3' 15 | } 16 | 17 | task fatJar(type: Jar, dependsOn: jar) { 18 | baseName = 'dx-patch' 19 | 20 | def deps = configurations.runtime 21 | 22 | def depClasses = { deps.collect { it.isDirectory() ? it : zipTree(it) } } 23 | 24 | from(depClasses) { 25 | exclude 'META-INF/*.MF' 26 | exclude 'META-INF/*.SF' 27 | exclude 'META-INF/*.RSA' 28 | exclude 'META-INF/*.DSA' 29 | exclude '**/*.html' 30 | } 31 | 32 | manifest { 33 | attributes 'Implementation-Title': 'Gradle Jar File Example', 34 | 'Main-Class': 'dx.Patch' 35 | } 36 | 37 | from(sourceSets.main.output) 38 | } 39 | -------------------------------------------------------------------------------- /patch/src/main/java/dx/Patch.java: -------------------------------------------------------------------------------- 1 | /** 2 | * dx-signer 3 | * 4 | * Copyright 2022 北京顶象技术有限公司 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package dx; 19 | 20 | import org.objectweb.asm.*; 21 | 22 | import java.io.IOException; 23 | import java.nio.file.Files; 24 | import java.nio.file.Paths; 25 | 26 | public class Patch { 27 | public static void main(String... args) throws IOException { 28 | ClassReader cr = new ClassReader(Files.readAllBytes(Paths.get(args[0]))); 29 | ClassWriter cw = new ClassWriter(0); 30 | cr.accept(new ClassVisitor(Opcodes.ASM9, cw) { 31 | @Override 32 | public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { 33 | MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); 34 | if (mv != null) { 35 | mv = new MethodVisitor(Opcodes.ASM9, mv) { 36 | @Override 37 | public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { 38 | if (owner.equals("java/security/Signature") && name.equals("getInstance")) { 39 | owner = "fake/security/Signature"; 40 | } 41 | super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); 42 | } 43 | }; 44 | } 45 | return mv; 46 | } 47 | }, 0); 48 | Files.write(Paths.get(args[1]), cw.toByteArray()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /proguard.pro: -------------------------------------------------------------------------------- 1 | 2 | #-dontskipnonpubliclibraryclasses 3 | -allowaccessmodification 4 | -repackageclasses dx.signer 5 | #-optimizations !method/inlining/* 6 | -dontoptimize 7 | #-renamesourcefileattribute SourceFile 8 | -keepattributes AnnotationDefault,RuntimeVisible*Annotations,Signature,SourceFile,LineNumberTable 9 | 10 | #-dontwarn ** 11 | -ignorewarnings 12 | 13 | # Keep - Applications. Keep all application classes, along with their 'main' methods. 14 | -keep public class dx.signer.UX { 15 | public static void main(java.lang.String[]); 16 | } 17 | 18 | 19 | # Also keep - Enumerations. Keep the special static methods that are required in 20 | # enumeration classes. 21 | -keepclassmembers enum * { 22 | public static **[] values(); 23 | public static ** valueOf(java.lang.String); 24 | } 25 | 26 | # Also keep - Swing UI L&F. Keep all extensions of javax.swing.plaf.ComponentUI, 27 | # along with the special 'createUI' method. 28 | -keep class * extends javax.swing.plaf.ComponentUI { 29 | public static javax.swing.plaf.ComponentUI createUI(javax.swing.JComponent); 30 | } 31 | 32 | -keep class org.slf4j.** { 33 | *; 34 | } 35 | 36 | -keep class javax.annotation.** { 37 | *; 38 | } 39 | 40 | -keep class dx.channel.ApkSigns { 41 | public *; 42 | } 43 | 44 | -keep class com.android.apksig.internal.x509.** { 45 | *; 46 | } 47 | -keep class com.android.apksig.internal.pkcs7.** { 48 | *; 49 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'dx-signer' 2 | 3 | include 'signer' 4 | include 'patch' 5 | -------------------------------------------------------------------------------- /signer/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'java' 3 | id 'idea' 4 | } 5 | 6 | sourceCompatibility = 1.8 7 | 8 | repositories { 9 | mavenCentral() 10 | jcenter() // for payload_writer 11 | } 12 | 13 | dependencies { 14 | compile fileTree(dir: 'lib', include: ['*.jar']) 15 | compile 'org.slf4j:slf4j-api:1.7.30' 16 | compile 'com.meituan.android.walle:payload_writer:1.1.7' 17 | compile 'org.json:json:20231013' 18 | compile 'org.slf4j:slf4j-simple:1.7.30' 19 | compile 'com.intellij:forms_rt:7.0.3' 20 | } 21 | 22 | task fatJar(type: Jar, dependsOn: jar) { 23 | baseName = 'dx-signer' 24 | 25 | def deps = configurations.runtime 26 | 27 | def depClasses = { deps.collect { it.isDirectory() ? it : zipTree(it) } } 28 | 29 | from(depClasses) { 30 | exclude 'META-INF/*.MF' 31 | exclude 'META-INF/*.SF' 32 | exclude 'META-INF/*.RSA' 33 | exclude 'META-INF/*.DSA' 34 | exclude '**/*.html' 35 | exclude 'META-INF/maven/**' 36 | } 37 | 38 | manifest { 39 | attributes 'Implementation-Title': 'dx signer', 40 | 'Main-Class': 'dx.signer.UX' 41 | } 42 | 43 | from(sourceSets.main.output) 44 | } 45 | -------------------------------------------------------------------------------- /signer/lib/apksigner-32.0.0.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dingxiangtech/dx-signer/07e0e7f33dc77c8a65841b9b89761e9d23da870d/signer/lib/apksigner-32.0.0.jar -------------------------------------------------------------------------------- /signer/lib/axml-8df59da6.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dingxiangtech/dx-signer/07e0e7f33dc77c8a65841b9b89761e9d23da870d/signer/lib/axml-8df59da6.jar -------------------------------------------------------------------------------- /signer/lib/fastzip-86e2efa1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dingxiangtech/dx-signer/07e0e7f33dc77c8a65841b9b89761e9d23da870d/signer/lib/fastzip-86e2efa1.jar -------------------------------------------------------------------------------- /signer/lib/patch-apksigner.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | unzip -o apksigner-32.0.0.jar com/android/apksig/internal/apk/v1/V1SchemeSigner.class 6 | java -jar ../../patch/build/libs/dx-patch.jar com/android/apksig/internal/apk/v1/V1SchemeSigner.class com/android/apksig/internal/apk/v1/V1SchemeSigner.class 7 | 8 | zip -u apksigner-32.0.0.jar com/android/apksig/internal/apk/v1/V1SchemeSigner.class 9 | 10 | zip -d apksigner-32.0.0.jar 'org/conscrypt/*' 'com/android/apksigner/*' 11 | 12 | rm -rf com 13 | -------------------------------------------------------------------------------- /signer/src/main/java/dx/channel/ApkSigns.java: -------------------------------------------------------------------------------- 1 | /** 2 | * dx-signer 3 | * 4 | * Copyright 2022 北京顶象技术有限公司 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package dx.channel; 19 | 20 | import com.android.apksig.ApkSigner; 21 | import dx.zip.AxmlFastZipOut; 22 | import dx.zip.FastZipEntry; 23 | import dx.zip.FastZipIn; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import java.io.ByteArrayInputStream; 28 | import java.io.IOException; 29 | import java.nio.ByteBuffer; 30 | import java.nio.file.Files; 31 | import java.nio.file.Path; 32 | import java.security.KeyStore; 33 | import java.security.KeyStoreException; 34 | import java.security.MessageDigest; 35 | import java.security.cert.Certificate; 36 | import java.security.cert.X509Certificate; 37 | import java.util.*; 38 | import java.util.jar.JarFile; 39 | import java.util.regex.Pattern; 40 | import java.util.zip.DataFormatException; 41 | 42 | public class ApkSigns { 43 | private static final Logger log = LoggerFactory.getLogger(ApkSigns.class); 44 | private static final Pattern stripPattern = Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA|EC)$"); 45 | 46 | public static void zipAlign(Path inputApk, Path outApk, boolean deleteSignature) throws IOException { 47 | try (FastZipIn in = new FastZipIn(inputApk.toFile())) { 48 | Path parent = outApk.getParent(); 49 | if (parent != null) { 50 | Files.createDirectories(parent); 51 | } 52 | ByteBuffer manifest = null; 53 | for (FastZipEntry entry : in.entries()) { 54 | if (entry.utf8Name().equals("AndroidManifest.xml")) { 55 | manifest = in.getUncompressed(entry); 56 | break; 57 | } 58 | } 59 | 60 | try (AxmlFastZipOut out = new AxmlFastZipOut(outApk.toFile())) { 61 | if (manifest != null) { 62 | out.initByAndroidManifestContent(manifest); 63 | } 64 | List entries = in.entries(); 65 | List zipEntryList = cleanup(entries, deleteSignature); 66 | out.copy(in, zipEntryList); 67 | } 68 | } catch (DataFormatException e) { 69 | throw new IOException(e); 70 | } 71 | } 72 | 73 | public static List cleanup(List entries, boolean deleteSignature) { 74 | List zipEntryList = new ArrayList<>(); 75 | for (FastZipEntry e : entries) { 76 | String name = e.utf8Name(); 77 | if (name.endsWith("/")) { 78 | // skip dir 79 | continue; 80 | } 81 | if (deleteSignature) { 82 | if (name.equals(JarFile.MANIFEST_NAME) || stripPattern.matcher(name).matches()) { 83 | continue; 84 | } 85 | } 86 | zipEntryList.add(e); 87 | } 88 | return zipEntryList; 89 | } 90 | 91 | public static KeyStore.PrivateKeyEntry loadKey(byte[] ksContent, String ksPass, String keyAlias, String keyPass) throws IOException { 92 | return loadKey0(ksContent, ksPass, keyAlias, keyPass); 93 | } 94 | 95 | public static KeyStore.PrivateKeyEntry loadKey(Path ks, String ksPass, String keyAlias, String keyPass) throws IOException { 96 | return loadKey0(ks, ksPass, keyAlias, keyPass); 97 | } 98 | 99 | private static KeyStore.PrivateKeyEntry loadKey0(Object ks, String ksPass, String keyAlias, String keyPass) throws IOException { 100 | KeyStore.PrivateKeyEntry privateKeyEntry = null; 101 | 102 | Set passwordList = new TreeSet<>(); 103 | if (ksPass != null) { 104 | passwordList.add(ksPass); 105 | } 106 | if (keyPass != null) { 107 | passwordList.add(keyPass); 108 | } 109 | passwordList.add("android"); 110 | KeyStore keyStore = loadKeyStore0(ks, passwordList); 111 | 112 | List aliasesList = new ArrayList<>(); 113 | try { 114 | Enumeration aliases = keyStore.aliases(); 115 | while (aliases.hasMoreElements()) { 116 | String alias = aliases.nextElement(); 117 | if (keyStore.isKeyEntry(alias)) { 118 | aliasesList.add(alias); 119 | } 120 | } 121 | if (keyAlias != null) { 122 | aliasesList.remove(keyAlias); 123 | aliasesList.add(0, keyAlias); 124 | } 125 | } catch ( 126 | KeyStoreException e) { 127 | throw new RuntimeException(e); 128 | } 129 | 130 | 131 | for (String alias : aliasesList) { 132 | for (String pass : passwordList) { 133 | if (pass == null){ 134 | continue; 135 | } 136 | try { 137 | KeyStore.ProtectionParameter param = new KeyStore.PasswordProtection(pass.toCharArray()); 138 | privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, param); 139 | break; 140 | } catch (Exception ignore) { 141 | } 142 | } 143 | if (privateKeyEntry != null) { 144 | break; 145 | } 146 | } 147 | if (privateKeyEntry == null) { 148 | throw new IOException("fail load key from keystore"); 149 | } 150 | X509Certificate certificate = (X509Certificate) privateKeyEntry.getCertificate(); 151 | 152 | log.info("loaded certificate {}", certificate.getSubjectDN()); 153 | 154 | try { 155 | byte[] encoded = certificate.getEncoded(); 156 | MessageDigest md5 = MessageDigest.getInstance("MD5"); 157 | String md5hex = toHexString(md5.digest(encoded)); 158 | 159 | log.info("cert md5: {}", md5hex); 160 | 161 | MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); 162 | String sha256hex = toHexString(sha256.digest(encoded)); 163 | 164 | log.info("cert sha256: {}", sha256hex); 165 | } catch (Exception ignore) { 166 | 167 | } 168 | 169 | return privateKeyEntry; 170 | } 171 | 172 | private static String toHexString(byte[] digest) { 173 | StringBuilder sb = new StringBuilder(); 174 | for (byte b : digest) { 175 | sb.append(String.format("%02x", b & 0xFF)); 176 | } 177 | return sb.toString(); 178 | } 179 | 180 | private static KeyStore loadKeyStore0(Object ks, Set passwordList) throws IOException { 181 | if (ks instanceof byte[]) { 182 | return loadKeyStore((byte[]) ks, passwordList); 183 | } else { 184 | return loadKeyStore(Files.readAllBytes((Path) ks), passwordList); 185 | } 186 | } 187 | 188 | public static KeyStore loadKeyStore(Path ks, Set passwordList) throws IOException { 189 | return loadKeyStore(Files.readAllBytes(ks), passwordList); 190 | } 191 | public static KeyStore loadKeyStore(byte[] ksContent, Set passwordList0) throws IOException { 192 | List storeTypes = Arrays.asList("PKCS12", "JKS", KeyStore.getDefaultType()); 193 | 194 | Set passwordList = new HashSet<>(); 195 | if (passwordList0 != null) { 196 | passwordList.addAll(passwordList0); 197 | } 198 | passwordList.add("android"); 199 | 200 | for (String type : storeTypes) { 201 | for (String password : passwordList) { 202 | if (password == null) { 203 | continue; 204 | } 205 | try { 206 | KeyStore keyStore = KeyStore.getInstance(type); 207 | keyStore.load(new ByteArrayInputStream(ksContent), password.toCharArray()); 208 | log.info("loaded keystore with type {}", type); 209 | return keyStore; 210 | } catch (Exception ignore) { 211 | } 212 | } 213 | } 214 | 215 | log.warn("fail load keystore with type {}, bad type or bad password", storeTypes); 216 | throw new IOException("fail to open keystore"); 217 | } 218 | 219 | 220 | public static void sign(Path in, Path out, 221 | Path ks, String ksPass, String keyAlias, String keyPass, 222 | boolean isAAB 223 | ) throws IOException { 224 | Path apkUnsigned = Files.createTempFile(out.getParent(), "unsigned", ".apk"); 225 | try { 226 | zipAlign(in, apkUnsigned, true); 227 | KeyStore.PrivateKeyEntry privateKeyEntry1 = loadKey(ks, ksPass, keyAlias, keyPass); 228 | sign(apkUnsigned, out, privateKeyEntry1, isAAB); 229 | } finally { 230 | Files.deleteIfExists(apkUnsigned); 231 | } 232 | } 233 | 234 | public static void sign(Path in, Path out, KeyStore.PrivateKeyEntry key, boolean isAAB) throws IOException { 235 | List x509Certificates = new ArrayList<>(); 236 | for (Certificate c : key.getCertificateChain()) { 237 | x509Certificates.add((X509Certificate) c); 238 | } 239 | ApkSigner.SignerConfig signerConfig = 240 | new ApkSigner.SignerConfig.Builder( 241 | "cert", key.getPrivateKey(), x509Certificates) 242 | .build(); 243 | ApkSigner.Builder apkSignerBuilder = 244 | new ApkSigner.Builder(Collections.singletonList(signerConfig)) 245 | .setOtherSignersSignaturesPreserved(false) 246 | .setV3SigningEnabled(false) 247 | .setInputApk(in.toFile()) 248 | .setOutputApk(out.toFile()); 249 | 250 | apkSignerBuilder.setV1SigningEnabled(true); 251 | apkSignerBuilder.setV2SigningEnabled(true); 252 | int minSdkVersion = isAAB ? 26 : 0; 253 | if (minSdkVersion > 0) { 254 | apkSignerBuilder.setMinSdkVersion(minSdkVersion); 255 | } 256 | ApkSigner signer = apkSignerBuilder.build(); 257 | try { 258 | signer.sign(); 259 | } catch (RuntimeException | IOException e) { 260 | throw e; 261 | } catch (Exception e) { 262 | throw new RuntimeException(e); 263 | } 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /signer/src/main/java/dx/channel/ChannelBuilder.java: -------------------------------------------------------------------------------- 1 | /** 2 | * dx-signer 3 | * 4 | * Copyright 2022 北京顶象技术有限公司 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package dx.channel; 19 | 20 | import com.meituan.android.walle.ChannelWriter; 21 | import com.meituan.android.walle.SignatureNotFoundException; 22 | import dx.zip.AxmlFastZipOut; 23 | import dx.zip.FastZipEntry; 24 | import dx.zip.FastZipIn; 25 | import dx.zip.Source; 26 | import pxb.android.Res_value; 27 | import pxb.android.axml.Axml; 28 | import pxb.android.axml.NodeVisitor; 29 | import pxb.android.axml.R; 30 | 31 | import java.io.IOException; 32 | import java.nio.ByteBuffer; 33 | import java.nio.charset.StandardCharsets; 34 | import java.nio.file.Files; 35 | import java.nio.file.Path; 36 | import java.security.KeyStore; 37 | import java.util.Comparator; 38 | import java.util.List; 39 | import java.util.Optional; 40 | import java.util.stream.Collectors; 41 | import java.util.zip.DataFormatException; 42 | 43 | public class ChannelBuilder implements AutoCloseable { 44 | private static final String Android_NS = "http://schemas.android.com/apk/res/android"; 45 | private final KeyStore.PrivateKeyEntry key; 46 | private final List entries; 47 | private final Axml axml; 48 | private final FastZipIn in; 49 | 50 | public ChannelBuilder(Path template, KeyStore.PrivateKeyEntry key) throws IOException { 51 | this.key = key; 52 | this.in = new FastZipIn(template.toFile()); 53 | entries = ApkSigns.cleanup(in.entries(), true); 54 | Optional a = entries.stream().filter(e -> e.utf8Name().equals("AndroidManifest.xml")).findFirst(); 55 | if (!a.isPresent()) { 56 | throw new RuntimeException("no AndroidManifest.xml in apk"); 57 | } 58 | ByteBuffer bb; 59 | try { 60 | bb = in.getUncompressed(a.get()); 61 | } catch (DataFormatException e) { 62 | throw new IOException(e); 63 | } 64 | axml = Axml.parse(bb); 65 | entries.remove(a.get()); 66 | entries.sort(Comparator.comparing(FastZipEntry::utf8Name)); 67 | } 68 | 69 | public static void updateUM(Axml axml, String keyValue) throws IOException { 70 | updateMeta(axml, "UMENG_CHANNEL", keyValue); 71 | updateMeta(axml, "CHANNEL", keyValue); 72 | } 73 | 74 | private static void updateMeta(Axml axml, String keyName, String keyValue) { 75 | Axml.Node manifest = axml.findFirst("manifest"); 76 | Axml.Node application = manifest.findFirst("application"); 77 | 78 | Axml.Node k = application.children 79 | .stream() 80 | .filter(n -> { 81 | if (n.name.equals("meta-data")) { 82 | Axml.Node.Attr attr = n.findFirstAttr(R.attr.name); 83 | if (attr.value.type == Res_value.TYPE_STRING) { 84 | return attr.value.raw.equals(keyName); 85 | } 86 | } 87 | return false; 88 | } 89 | ).findFirst().orElseGet(() -> { 90 | NodeVisitor n = application.child(null, "meta-data"); 91 | n.attr(Android_NS, "name", 92 | R.attr.name, keyName, Res_value.newStringValue(keyName)); 93 | return (Axml.Node) n; 94 | }); 95 | 96 | k.replace(Android_NS, "value", 97 | R.attr.value, keyValue, Res_value.newStringValue(keyValue)); 98 | } 99 | 100 | public static List readChannelList(Path channelList) throws IOException { 101 | return Files.readAllLines(channelList, StandardCharsets.UTF_8) 102 | .stream() 103 | .map(String::trim) 104 | .filter(s -> !s.startsWith("#")) 105 | .map(s -> s.split("#")[0]) 106 | .map(String::trim) 107 | .filter(s -> !s.isEmpty()) 108 | .collect(Collectors.toList()); 109 | } 110 | 111 | public void build(String channel, Path out) throws IOException { 112 | out = out.toAbsolutePath(); 113 | Path parent = out.getParent(); 114 | Files.createDirectories(parent); 115 | Path tmp = Files.createTempFile(parent, "tmp", ".apk"); 116 | Files.deleteIfExists(out); 117 | try { 118 | try (AxmlFastZipOut zout = new AxmlFastZipOut(tmp.toFile());) { 119 | zout.initByAndroidManifestContent(axml); 120 | zout.copyPart(in, entries); 121 | updateUM(axml, channel); 122 | ByteBuffer bb = ByteBuffer.wrap(axml.toByteArray()); 123 | Source am = Source.newRawEntry("AndroidManifest.xml", bb); 124 | zout.copyPart(am, am.entries()); 125 | zout.copyEnd(); 126 | } 127 | 128 | ApkSigns.sign(tmp, out, this.key, false); 129 | try { 130 | ChannelWriter.put(out.toFile(), channel); 131 | } catch (SignatureNotFoundException e) { 132 | throw new IOException(e); 133 | } 134 | } finally { 135 | Files.deleteIfExists(tmp); 136 | } 137 | } 138 | 139 | @Override 140 | public void close() throws IOException { 141 | in.close(); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /signer/src/main/java/dx/channel/FixSha1WithDsaProvider.java: -------------------------------------------------------------------------------- 1 | /** 2 | * dx-signer 3 | * 4 | * Copyright 2022 北京顶象技术有限公司 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package dx.channel; 19 | 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | 23 | import java.nio.ByteBuffer; 24 | import java.security.*; 25 | 26 | /** 27 | * JDK-8184341 : Release Note: New defaults for DSA keys in jarsigner and keytool 28 | * https://bugs.java.com/bugdatabase/view_bug.do?bug_id=8184341 29 | *

30 | * 高版本java(8u151+)中DSA限制了最小的长度,而Android低版本兼容要求使用sha1算法, 31 | * apksigner为了兼容,选择使用Sha1WithDSA签名算法, 进而产生崩溃 32 | * 33 | *

 34 |  * Caused by: java.security.InvalidKeyException: The security strength of SHA-1 digest algorithm is not sufficient for this key size
 35 |  * at java.base/sun.security.provider.DSA.checkKey(DSA.java:124)
 36 |  * at java.base/sun.security.provider.DSA.engineInitSign(DSA.java:156)
 37 |  * ...
 38 |  * at java.base/java.security.Signature.initSign(Signature.java:636)
 39 |  * at com.android.apksig.internal.apk.v1.V1SchemeSigner.generateSignatureBlock(V1SchemeSigner.java:511)
 40 |  * 
41 | */ 42 | public class FixSha1WithDsaProvider extends Provider { 43 | private static final Logger log = LoggerFactory.getLogger(FixSha1WithDsaProvider.class); 44 | public FixSha1WithDsaProvider() { 45 | super("fix-sha1-with-dsa-provider", 1.0, "null"); 46 | // 使用BC的实现 47 | // put("Signature.SHA1WITHDSA", org.bouncycastle.jcajce.provider.asymmetric.dsa.DSASigner.stdDSA.class.getName()); 48 | 49 | // 使用RawDSA实现 50 | put("Signature.SHA1WITHDSA", MySignatureSpi.class.getName()); 51 | } 52 | 53 | public static class MySignatureSpi extends SignatureSpi { 54 | 55 | private static boolean logged = false; 56 | public MySignatureSpi() throws NoSuchAlgorithmException { 57 | this.sha1 = MessageDigest.getInstance("SHA-1"); 58 | this.rawDSA = Signature.getInstance("RawDSA"); 59 | if (!logged) { 60 | logged = true; 61 | log.info("Sha1WithDSA patch enabled"); 62 | } 63 | } 64 | 65 | private final MessageDigest sha1; 66 | private final Signature rawDSA; 67 | 68 | @Override 69 | protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException { 70 | rawDSA.initVerify(publicKey); 71 | sha1.reset(); 72 | } 73 | 74 | @Override 75 | protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException { 76 | rawDSA.initSign(privateKey); 77 | sha1.reset(); 78 | } 79 | 80 | protected void engineUpdate(byte b) { 81 | sha1.update(b); 82 | } 83 | 84 | protected void engineUpdate(byte[] data, int off, int len) { 85 | sha1.update(data, off, len); 86 | } 87 | 88 | protected void engineUpdate(ByteBuffer b) { 89 | sha1.update(b); 90 | } 91 | 92 | @Override 93 | protected byte[] engineSign() throws SignatureException { 94 | byte[] data = sha1.digest(); 95 | rawDSA.update(data); 96 | return rawDSA.sign(); 97 | } 98 | 99 | @Override 100 | protected boolean engineVerify(byte[] sigBytes) throws SignatureException { 101 | byte[] data = sha1.digest(); 102 | rawDSA.update(data); 103 | return rawDSA.verify(sigBytes); 104 | } 105 | 106 | @Override 107 | protected void engineSetParameter(String param, Object value) throws InvalidParameterException { 108 | throw new InvalidParameterException("No parameter accepted"); 109 | } 110 | 111 | @Override 112 | protected Object engineGetParameter(String param) throws InvalidParameterException { 113 | return null; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /signer/src/main/java/dx/signer/CommandLine.java: -------------------------------------------------------------------------------- 1 | /** 2 | * dx-signer 3 | * 4 | * Copyright 2022 北京顶象技术有限公司 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package dx.signer; 19 | 20 | import org.slf4j.Logger; 21 | import org.slf4j.LoggerFactory; 22 | import org.slf4j.impl.SimpleLogger; 23 | 24 | import java.io.BufferedReader; 25 | import java.io.IOException; 26 | import java.nio.charset.StandardCharsets; 27 | import java.nio.file.Files; 28 | import java.nio.file.Path; 29 | import java.nio.file.Paths; 30 | import java.util.Properties; 31 | 32 | public class CommandLine { 33 | public static void main(String... args) throws IOException { 34 | System.setProperty(SimpleLogger.SHOW_LOG_NAME_KEY, "false"); 35 | System.setProperty(SimpleLogger.SHOW_THREAD_NAME_KEY, "false"); 36 | 37 | Logger log = LoggerFactory.getLogger(CommandLine.class); 38 | 39 | 40 | Properties p = new Properties(); 41 | try { 42 | if (args.length < 1 || !args[0].equals("sign")) { 43 | throw new RuntimeException("参数解析失败"); 44 | } 45 | for (int i = 1; i < args.length; i += 2) { 46 | String key = args[i]; 47 | String value = args[i + 1]; 48 | if ("--config".equals(key)) { 49 | Properties p2; 50 | try { 51 | p2 = load(Paths.get(value)); 52 | } catch (IOException e) { 53 | throw new RuntimeException("文件" + value + "解析失败", e); 54 | } 55 | p.putAll(p2); 56 | } else { 57 | if (!key.startsWith("--")) { 58 | throw new RuntimeException("参数解析失败"); 59 | } 60 | if (key.equals("--in")) { 61 | p.setProperty("in-filename", ""); 62 | } 63 | p.setProperty(key.substring(2), value); 64 | } 65 | } 66 | for (String k : new String[]{"in", "out", "ks"}) { 67 | String v = p.getProperty(k, ""); 68 | if (v == null || v.length() == 0) { 69 | throw new RuntimeException("请指定参数" + k); 70 | } 71 | } 72 | } catch (Exception e) { 73 | e.printStackTrace(); 74 | System.err.println("USAGE: java -jar dx-signer.apk sign [--option value]*"); 75 | System.err.println(" option:"); 76 | System.err.println(" --config 配置文件"); 77 | System.err.println(" --in 输入文件apk、aab"); 78 | System.err.println(" --out 输出文件、文件夹"); 79 | System.err.println(" --ks Keystore位置"); 80 | System.err.println(" --ks-pass Keystore密码"); 81 | System.err.println(" --ks-key-alias"); 82 | System.err.println(" --key-pass"); 83 | System.err.println(" --channel-list 渠道清单"); 84 | System.exit(3); 85 | } 86 | Path input = Paths.get(p.getProperty("in")); 87 | Path ks = Paths.get(p.getProperty("ks")); 88 | String ksPass = p.getProperty("ks-pass", ""); 89 | 90 | String ksKeyAlias = p.getProperty("ks-key-alias", ""); 91 | String keyPass = p.getProperty("key-pass", ""); 92 | if (p.getProperty("channel-list", "").length() > 0) { 93 | Path out = detectOutDir(p.getProperty("out")); 94 | 95 | int result = SignWorker.signChannelApk(input, p.getProperty("in-filename", ""), 96 | out, 97 | Paths.get(p.getProperty("channel-list")), 98 | ks, ksPass, ksKeyAlias, keyPass); 99 | 100 | if (result != 0) { 101 | log.error("多渠道失败"); 102 | System.exit(2); 103 | } 104 | } else { 105 | Path out = Paths.get(p.getProperty("out")); 106 | int result = SignWorker.signApk(input, out, ks, 107 | ksPass, ksKeyAlias, keyPass); 108 | 109 | if (result != 0) { 110 | log.error("签名失败"); 111 | System.exit(2); 112 | } 113 | } 114 | } 115 | 116 | static Properties load(Path configFile) throws IOException { 117 | Properties p = new Properties(); 118 | try (BufferedReader r = Files.newBufferedReader(configFile, StandardCharsets.UTF_8)) { 119 | p.load(r); 120 | } 121 | return p; 122 | } 123 | 124 | static Path detectOutDir(String out) { 125 | Path path = Paths.get(out); 126 | return (out.endsWith("/") || Files.isDirectory(path)) ? path : path.toAbsolutePath().getParent(); 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /signer/src/main/java/dx/signer/FixApk.java: -------------------------------------------------------------------------------- 1 | /** 2 | * dx-signer 3 | * 4 | * Copyright 2022 北京顶象技术有限公司 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package dx.signer; 19 | 20 | import dx.channel.ApkSigns; 21 | 22 | import java.io.File; 23 | import java.io.IOException; 24 | 25 | public class FixApk { 26 | public static void main(String... args) throws IOException { 27 | ApkSigns.zipAlign(new File(args[0]).toPath(), new File(args[1]).toPath(), false); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /signer/src/main/java/dx/signer/JTextAreaOutputStream.java: -------------------------------------------------------------------------------- 1 | /** 2 | * dx-signer 3 | * 4 | * Copyright 2022 北京顶象技术有限公司 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package dx.signer; 19 | 20 | import javax.swing.*; 21 | import java.io.IOException; 22 | import java.io.OutputStream; 23 | import java.io.PrintStream; 24 | import java.lang.ref.WeakReference; 25 | 26 | public class JTextAreaOutputStream extends OutputStream { 27 | static private WeakReference mDestination = new WeakReference<>(null); 28 | PrintStream orgOut; 29 | 30 | static JTextAreaOutputStream hjOut = new JTextAreaOutputStream(); 31 | static JTextAreaOutputStream hjErr = new JTextAreaOutputStream(); 32 | 33 | private JTextAreaOutputStream() { 34 | } 35 | 36 | public synchronized static void hijack(JTextArea destination) { 37 | if (hjOut.orgOut == null) { 38 | hjOut.orgOut = System.out; 39 | } 40 | if (hjErr.orgOut == null) { 41 | hjErr.orgOut = System.err; 42 | } 43 | mDestination = new WeakReference<>(destination); 44 | 45 | System.setOut(new PrintStream(hjOut, true)); 46 | System.setErr(new PrintStream(hjErr, true)); 47 | } 48 | 49 | @Override 50 | public void write(byte[] buffer, int offset, int length) throws IOException { 51 | final String text = new String(buffer, offset, length); 52 | orgOut.write(buffer, offset, length); 53 | JTextArea t = mDestination.get(); 54 | if (t != null) { 55 | SwingUtilities.invokeLater(() -> t.append(text)); 56 | } 57 | } 58 | 59 | @Override 60 | public void write(int b) throws IOException { 61 | write(new byte[]{(byte) b}, 0, 1); 62 | } 63 | } 64 | 65 | -------------------------------------------------------------------------------- /signer/src/main/java/dx/signer/SignWorker.java: -------------------------------------------------------------------------------- 1 | /** 2 | * dx-signer 3 | * 4 | * Copyright 2022 北京顶象技术有限公司 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package dx.signer; 19 | 20 | 21 | import dx.channel.ApkSigns; 22 | import dx.channel.ChannelBuilder; 23 | import org.slf4j.Logger; 24 | import org.slf4j.LoggerFactory; 25 | 26 | import java.io.IOException; 27 | import java.nio.file.Files; 28 | import java.nio.file.Path; 29 | import java.security.KeyStore; 30 | import java.util.List; 31 | import java.util.zip.DataFormatException; 32 | 33 | public class SignWorker { 34 | private static final Logger log = LoggerFactory.getLogger(SignWorker.class); 35 | 36 | private static void sign(Path inApk, Path ksPath, String ksPass, String keyAlias, String keyPass, Path outApk) throws Throwable { 37 | KeyStore.PrivateKeyEntry key = ApkSigns.loadKey(ksPath, ksPass, keyAlias, keyPass); 38 | ApkSigns.sign(inApk, outApk, key, inApk.getFileName().toString().endsWith("aab")); 39 | } 40 | 41 | public static int signApk(Path apkUnsigned, Path apkOut, Path ksPath, String ksPass, String keyAlias, String keyPass) { 42 | Path tmp = null; 43 | String suffix = apkUnsigned.getFileName().toString().endsWith("aab") ? "aab" : "apk"; 44 | try { 45 | Path p = apkOut.toAbsolutePath().getParent(); 46 | if (!Files.exists(p)) { 47 | Files.createDirectories(p); 48 | } 49 | tmp = Files.createTempFile(p, "tmpsigner", "." + suffix); 50 | } catch (IOException e) { 51 | e.printStackTrace(System.err); 52 | return 2; 53 | } 54 | 55 | try { 56 | log.info("{}", "> 签名中, 请稍等 ..."); 57 | 58 | log.info("{}", ">> 清理原有签名, 对齐 ..."); 59 | ApkSigns.zipAlign(apkUnsigned, tmp, false); 60 | log.info("{}", "<< 完成"); 61 | 62 | log.info("{}", ">> 签名 ..."); 63 | sign(tmp, ksPath, ksPass, keyAlias, keyPass, apkOut); 64 | log.info("{}", "<< 完成"); 65 | 66 | log.info("{}", "< 签名结束, 结果: 完成"); 67 | log.info(" 输出APK: {}", apkOut); 68 | } catch (Throwable e) { 69 | log.info("签名结束, 结果: 失败", e); 70 | return -1; 71 | } finally { 72 | if (tmp != null) { 73 | try { 74 | Files.deleteIfExists(tmp); 75 | } catch (IOException ignore) { 76 | } 77 | } 78 | } 79 | return 0; 80 | } 81 | 82 | public static int signChannelApk(Path input, String inputFileName, Path outDir, 83 | Path channelListFile, 84 | Path ksPath, 85 | String ksPass, 86 | String keyAlias, 87 | String keyPass) throws IOException { 88 | 89 | if (inputFileName == null || inputFileName.trim().length() == 0) { 90 | inputFileName = input.getFileName().toString(); 91 | } 92 | if (inputFileName.endsWith(".aab")) { 93 | throw new RuntimeException("only .apk supported"); 94 | } 95 | 96 | List channelList = ChannelBuilder.readChannelList(channelListFile); 97 | log.info("读取到{}个渠道", channelList.size()); 98 | 99 | KeyStore.PrivateKeyEntry key = ApkSigns.loadKey(ksPath, ksPass, keyAlias, keyPass); 100 | try (ChannelBuilder cb = new ChannelBuilder(input, key)) { 101 | log.info("已加载模板: {}", input); 102 | int dot = inputFileName.lastIndexOf('.'); 103 | String apkName = dot > 0 ? inputFileName.substring(0, dot) : inputFileName; 104 | if (apkName.startsWith("dx_unsigned_")) { 105 | apkName = apkName.substring("dx_unsigned_".length()); 106 | } 107 | for (String channel : channelList) { 108 | String safeName = String.format("SIGNED_%s-%s.apk", apkName, channel) 109 | .replace('/', '_') 110 | .replace('\\', '_') 111 | .replace(' ', '_'); 112 | Path outPath = outDir.resolve(safeName); 113 | log.info("正在输出渠道: {}", channel); 114 | try { 115 | cb.build(channel, outPath); 116 | log.info("已经生成: {}", outPath); 117 | }catch (Throwable e) { 118 | log.error("多渠道失败", e); 119 | return 1; 120 | } 121 | } 122 | log.info("多渠道完成: {}", outDir); 123 | } 124 | 125 | return 0; 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /signer/src/main/java/dx/signer/UX.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 |
280 | -------------------------------------------------------------------------------- /signer/src/main/java/dx/signer/UX.java: -------------------------------------------------------------------------------- 1 | /** 2 | * dx-signer 3 | * 4 | * Copyright 2022 北京顶象技术有限公司 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package dx.signer; 19 | 20 | import com.intellij.uiDesigner.core.GridConstraints; 21 | import com.intellij.uiDesigner.core.GridLayoutManager; 22 | import dx.channel.ApkSigns; 23 | import org.slf4j.impl.SimpleLogger; 24 | 25 | import javax.swing.*; 26 | import javax.swing.event.PopupMenuEvent; 27 | import javax.swing.event.PopupMenuListener; 28 | import javax.swing.filechooser.FileFilter; 29 | import javax.swing.text.DefaultCaret; 30 | import java.awt.*; 31 | import java.io.*; 32 | import java.nio.charset.StandardCharsets; 33 | import java.nio.file.Files; 34 | import java.nio.file.Path; 35 | import java.nio.file.Paths; 36 | import java.security.KeyStore; 37 | import java.util.*; 38 | import java.util.concurrent.ExecutorService; 39 | import java.util.concurrent.Executors; 40 | 41 | public class UX { 42 | ExecutorService es = Executors.newSingleThreadExecutor(); 43 | private JButton inBtn; 44 | private JTextField inPathTF; 45 | private JTabbedPane tabbedPane1; 46 | private JTextField ksPathTF; 47 | private JButton ksBtn; 48 | private JTextField outPathTF; 49 | private JButton signBtn; 50 | private JTextArea loggingTA; 51 | private JCheckBox 保存密码CheckBox; 52 | private JComboBox keyAliasCB; 53 | private JPasswordField keyPassPF; 54 | private JPasswordField ksPassPF; 55 | public JPanel top; 56 | private JProgressBar progressBar1; 57 | private JTextField channelPathTF; 58 | private JButton channelBtn; 59 | private JCheckBox v1SigningEnabledCheckBox; 60 | private JCheckBox v2SigningEnabledCheckBox; 61 | 62 | private boolean readOnly = false; 63 | private String inputFileName = ""; 64 | 65 | public static void main(String[] args) throws IOException { 66 | 67 | System.setProperty(SimpleLogger.SHOW_LOG_NAME_KEY, "false"); 68 | System.setProperty(SimpleLogger.SHOW_THREAD_NAME_KEY, "false"); 69 | 70 | if (args.length >= 1 && args[0].equals("sign")) { 71 | CommandLine.main(args); 72 | return; 73 | } 74 | 75 | JFrame frame = new JFrame("Apk签名&多渠道工具"); 76 | frame.setContentPane(new UX().top); 77 | frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 78 | frame.pack(); 79 | 80 | // make the frame half the height and width 81 | Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); 82 | int width1 = screenSize.width / 2; 83 | int height1 = screenSize.height / 2; 84 | 85 | 86 | width1 = 900; 87 | if (height1 < 600) { 88 | height1 = 600; 89 | } 90 | 91 | frame.setSize(width1, height1); 92 | 93 | // here's the part where i center the jframe on screen 94 | frame.setLocationRelativeTo(null); 95 | 96 | frame.setVisible(true); 97 | } 98 | 99 | 100 | public UX() { 101 | JFileChooser fileChooser = new JFileChooser(); 102 | fileChooser.setCurrentDirectory(new File(".")); 103 | fileChooser.setFileSelectionMode(JFileChooser.FILES_ONLY); 104 | fileChooser.setMultiSelectionEnabled(false); 105 | 106 | inBtn.addActionListener(e -> { 107 | fileChooser.setFileFilter(new FileFilter() { 108 | @Override 109 | public boolean accept(File f) { 110 | String s = f.getName().toLowerCase(); 111 | return f.isDirectory() || s.endsWith(".apk") || s.endsWith(".aab"); 112 | } 113 | 114 | @Override 115 | public String getDescription() { 116 | return "*.apk,*.aab"; 117 | } 118 | }); 119 | int result = fileChooser.showOpenDialog(inBtn); 120 | if (result == JFileChooser.APPROVE_OPTION) { 121 | File file = fileChooser.getSelectedFile(); 122 | setInput(file); 123 | 124 | } 125 | }); 126 | ksBtn.addActionListener(e -> { 127 | fileChooser.setFileFilter(new FileFilter() { 128 | @Override 129 | public boolean accept(File f) { 130 | String s = f.getName().toLowerCase(); 131 | return f.isDirectory() || s.endsWith(".ks") || s.endsWith(".keystore") || s.endsWith(".p12") || s.endsWith(".pfx") || s.endsWith(".jks"); 132 | } 133 | 134 | @Override 135 | public String getDescription() { 136 | return "*.ks, *.keystore, *.p12, *.pfx, *.jks"; 137 | } 138 | }); 139 | int result = fileChooser.showOpenDialog(ksBtn); 140 | if (result == JFileChooser.APPROVE_OPTION) { 141 | File file = fileChooser.getSelectedFile(); 142 | ksPathTF.setText(file.getAbsolutePath()); 143 | } 144 | }); 145 | 146 | channelBtn.addActionListener(e -> { 147 | fileChooser.setFileFilter(new FileFilter() { 148 | @Override 149 | public boolean accept(File f) { 150 | String s = f.getName().toLowerCase(); 151 | return f.isDirectory() || (f.isFile() && s.endsWith(".txt")); 152 | } 153 | 154 | @Override 155 | public String getDescription() { 156 | return "*.txt"; 157 | } 158 | }); 159 | int result = fileChooser.showOpenDialog(channelBtn); 160 | if (result == JFileChooser.APPROVE_OPTION) { 161 | File file = fileChooser.getSelectedFile(); 162 | channelPathTF.setText(file.getAbsolutePath()); 163 | } 164 | }); 165 | 166 | signBtn.addActionListener(e -> { 167 | String channelPath = channelPathTF.getText(); 168 | 169 | String out = outPathTF.getText(); 170 | 171 | if (channelPath != null && channelPath.length() > 0) { 172 | Path apkDir = CommandLine.detectOutDir(out); 173 | if (Files.exists(apkDir)) { 174 | if (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(UX.this.top, "多渠道输出APK目录已经存在,是否覆盖:\n" + apkDir, "输出APK已经存在,是否覆盖", JOptionPane.OK_CANCEL_OPTION)) { 175 | return; 176 | } 177 | } 178 | } else { 179 | if (JOptionPane.YES_OPTION != JOptionPane.showConfirmDialog(UX.this.top, "输出APK已经存在,是否覆盖:\n" + out, "输出APK已经存在,是否覆盖", JOptionPane.OK_CANCEL_OPTION)) { 180 | return; 181 | } 182 | } 183 | 184 | signBtn.setEnabled(false); 185 | 186 | String in = inPathTF.getText(); 187 | 188 | String ksPass; 189 | String keyPass; 190 | try { 191 | ksPass = new String(ksPassPF.getPassword()); 192 | } catch (NullPointerException ignore) { 193 | ksPass = ""; 194 | } 195 | try { 196 | keyPass = new String(UX.this.keyPassPF.getPassword()); 197 | } catch (NullPointerException ignore) { 198 | keyPass = null; 199 | } 200 | String keyAlias = (String) UX.this.keyAliasCB.getSelectedItem(); 201 | Properties mConfig = new Properties(); 202 | String ksPath0 = ksPathTF.getText(); 203 | mConfig.put("ks", ksPath0); 204 | mConfig.put("in", in); 205 | mConfig.put("ks-key-alias", keyAlias); 206 | mConfig.put("in-filename", this.inputFileName); 207 | mConfig.put("out", this.outPathTF.getText()); 208 | mConfig.put("channel-list", this.channelPathTF.getText()); 209 | 210 | if (保存密码CheckBox.isSelected()) { 211 | mConfig.put("ks-pass", ksPass); 212 | mConfig.put("key-pass", keyPass); 213 | } 214 | 215 | if (!readOnly) { 216 | try { 217 | Path configFile = getConfigPath(); 218 | try (BufferedWriter r = Files.newBufferedWriter(configFile, StandardCharsets.UTF_8)) { 219 | mConfig.store(r, "#"); 220 | } 221 | } catch (IOException ignore) { 222 | } 223 | } 224 | 225 | loggingTA.setText(""); 226 | 227 | String finalKsPass = ksPass; 228 | String finalKeyPass = keyPass; 229 | String pbOrg = progressBar1.getString(); 230 | progressBar1.setString("签名中..."); 231 | progressBar1.setStringPainted(true); 232 | progressBar1.setIndeterminate(true); 233 | 234 | Path ksPath = Paths.get(ksPath0); 235 | Path input = Paths.get(in); 236 | 237 | es.submit(() -> { 238 | try { 239 | int result; 240 | 241 | if (channelPath != null && channelPath.length() > 0) { 242 | Path apkDir = CommandLine.detectOutDir(out); 243 | result = SignWorker.signChannelApk(input, inputFileName, 244 | apkDir, 245 | Paths.get(channelPath), 246 | ksPath, finalKsPass, keyAlias, finalKeyPass); 247 | progressBar1.setIndeterminate(false); 248 | progressBar1.setString(pbOrg); 249 | if (result == 0) { 250 | JOptionPane.showMessageDialog(UX.this.top, "多渠道成功, 输出APK文件夹\n" + apkDir); 251 | } else { 252 | JOptionPane.showMessageDialog(UX.this.top, "多渠道失败"); 253 | } 254 | } else { 255 | result = SignWorker.signApk(input, Paths.get(out), ksPath, 256 | finalKsPass, keyAlias, finalKeyPass); 257 | progressBar1.setIndeterminate(false); 258 | progressBar1.setString(pbOrg); 259 | if (result == 0) { 260 | JOptionPane.showMessageDialog(UX.this.top, "签名成功, 输出APK\n" + out); 261 | } else { 262 | JOptionPane.showMessageDialog(UX.this.top, "签名失败"); 263 | } 264 | } 265 | 266 | } catch (Exception ex) { 267 | ex.printStackTrace(); 268 | } 269 | 270 | signBtn.setEnabled(true); 271 | }); 272 | 273 | }); 274 | 275 | keyAliasCB.removeAllItems(); 276 | keyAliasCB.addItem("{{auto}}"); 277 | keyAliasCB.setSelectedItem("{{auto}}"); 278 | keyAliasCB.addPopupMenuListener(new PopupMenuListener() { 279 | @Override 280 | public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 281 | KeyStore keyStore = null; 282 | try { 283 | byte[] d = Files.readAllBytes(Paths.get(ksPathTF.getText())); 284 | Set keyList = new HashSet<>(); 285 | keyList.add(new String(ksPassPF.getPassword())); 286 | keyStore = ApkSigns.loadKeyStore(d, keyList); 287 | } catch (Exception ignore) { 288 | keyStore = null; 289 | } 290 | 291 | if (keyStore != null) { 292 | keyAliasCB.removeAllItems(); 293 | keyAliasCB.addItem("{{auto}}"); 294 | try { 295 | Enumeration aliases = keyStore.aliases(); 296 | while (aliases.hasMoreElements()) { 297 | String alias = aliases.nextElement(); 298 | keyAliasCB.addItem(alias); 299 | } 300 | } catch (Exception ignore) { 301 | } 302 | keyAliasCB.setSelectedItem("{{auto}}"); 303 | } 304 | } 305 | 306 | @Override 307 | public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 308 | 309 | } 310 | 311 | @Override 312 | public void popupMenuCanceled(PopupMenuEvent e) { 313 | 314 | } 315 | }); 316 | 317 | 318 | DefaultCaret caret = (DefaultCaret) loggingTA.getCaret(); 319 | caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE); 320 | 321 | JTextAreaOutputStream.hijack(loggingTA); 322 | 323 | try { 324 | Path configFile = getConfigPath(); 325 | Properties initConfig = CommandLine.load(configFile); 326 | 327 | readOnly = "true".equals(initConfig.getProperty("config-read-only", "")); 328 | ksPathTF.setText(initConfig.getProperty("ks", "")); 329 | 330 | String inPath = initConfig.getProperty("in", ""); 331 | if (inPath.length() > 0) { 332 | setInput(new File(inPath), initConfig.getProperty("in-filename", "")); 333 | } 334 | String outPath = initConfig.getProperty("out", ""); 335 | if (outPath.length() > 0) { 336 | outPathTF.setText(outPath); 337 | } 338 | ksPassPF.setText(initConfig.getProperty("ks-pass", "")); 339 | keyPassPF.setText(initConfig.getProperty("key-pass", "")); 340 | channelPathTF.setText(initConfig.getProperty("channel-list", "")); 341 | 342 | String s = initConfig.getProperty("ks-key-alias", "{{auto}}"); 343 | if (!s.equals("{{auto}}") && s.length() != 0) { 344 | keyAliasCB.addItem(s); 345 | keyAliasCB.setSelectedItem(s); 346 | } 347 | 348 | } catch (IOException ignore) { 349 | } 350 | 351 | if (readOnly) { 352 | 保存密码CheckBox.setEnabled(false); 353 | 保存密码CheckBox.setSelected(false); 354 | channelBtn.setEnabled(false); 355 | channelPathTF.setEnabled(false); 356 | 357 | inBtn.setEnabled(false); 358 | inPathTF.setEnabled(false); 359 | 360 | if (ksPathTF.getText().length() > 0) { 361 | ksBtn.setEnabled(false); 362 | ksPathTF.setEnabled(false); 363 | keyAliasCB.setEnabled(false); 364 | ksPassPF.setEnabled(false); 365 | keyPassPF.setEnabled(false); 366 | } 367 | outPathTF.setEnabled(false); 368 | } 369 | } 370 | 371 | private void setInput(File file) { 372 | setInput(file, null); 373 | } 374 | 375 | private void setInput(File file, String name) { 376 | if (name == null || name.length() == 0) { 377 | name = file.getName(); 378 | } 379 | this.inputFileName = name; 380 | inPathTF.setText(file.getAbsolutePath()); 381 | 382 | String fileName = inputFileName; 383 | if (fileName.startsWith("dx_unsigned")) { 384 | fileName = "SIGNED" + fileName.substring("dx_unsigned".length()); 385 | } else { 386 | fileName = "SIGNED-" + fileName; 387 | } 388 | File out = new File(file.getParent(), fileName); 389 | outPathTF.setText(out.toString()); 390 | } 391 | 392 | private static Path getConfigPath() { 393 | Path HOME = Paths.get("."); 394 | Path configDir = HOME.resolve("etc"); 395 | if (!Files.exists(configDir)) { 396 | try { 397 | Files.createDirectories(configDir); 398 | } catch (IOException ignore) { 399 | 400 | } 401 | } 402 | 403 | return configDir.resolve("cfg.properties"); 404 | } 405 | 406 | { 407 | // GUI initializer generated by IntelliJ IDEA GUI Designer 408 | // >>> IMPORTANT!! <<< 409 | // DO NOT EDIT OR ADD ANY CODE HERE! 410 | $$$setupUI$$$(); 411 | } 412 | 413 | /** 414 | * Method generated by IntelliJ IDEA GUI Designer 415 | * >>> IMPORTANT!! <<< 416 | * DO NOT edit this method OR call it in your code! 417 | * 418 | * @noinspection ALL 419 | */ 420 | private void $$$setupUI$$$() { 421 | top = new JPanel(); 422 | top.setLayout(new GridLayoutManager(3, 1, new Insets(5, 5, 5, 5), -1, -1)); 423 | tabbedPane1 = new JTabbedPane(); 424 | top.add(tabbedPane1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, new Dimension(200, 200), null, 0, false)); 425 | final JPanel panel1 = new JPanel(); 426 | panel1.setLayout(new GridLayoutManager(6, 3, new Insets(0, 0, 0, 0), -1, -1)); 427 | tabbedPane1.addTab("Apk签名 & 多渠道", panel1); 428 | inPathTF = new JTextField(); 429 | inPathTF.setEditable(false); 430 | inPathTF.setText(""); 431 | panel1.add(inPathTF, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); 432 | ksPathTF = new JTextField(); 433 | panel1.add(ksPathTF, new GridConstraints(1, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); 434 | final JLabel label1 = new JLabel(); 435 | label1.setText("输入apk/aab"); 436 | panel1.add(label1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_EAST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 437 | inBtn = new JButton(); 438 | inBtn.setText("1.选择输入APK"); 439 | panel1.add(inBtn, new GridConstraints(0, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 440 | final JLabel label2 = new JLabel(); 441 | label2.setText("KeyStore"); 442 | panel1.add(label2, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_EAST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 443 | ksBtn = new JButton(); 444 | ksBtn.setText("2.选择KeyStore"); 445 | panel1.add(ksBtn, new GridConstraints(1, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 446 | final JLabel label3 = new JLabel(); 447 | label3.setText("KeyStore密码"); 448 | panel1.add(label3, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_EAST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 449 | final JLabel label4 = new JLabel(); 450 | label4.setText("3.输入KeyStore密码"); 451 | panel1.add(label4, new GridConstraints(2, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 452 | outPathTF = new JTextField(); 453 | panel1.add(outPathTF, new GridConstraints(4, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); 454 | final JLabel label5 = new JLabel(); 455 | label5.setText("输出apk/aab"); 456 | panel1.add(label5, new GridConstraints(4, 0, 1, 1, GridConstraints.ANCHOR_EAST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 457 | 保存密码CheckBox = new JCheckBox(); 458 | 保存密码CheckBox.setSelected(true); 459 | 保存密码CheckBox.setText("保存密码"); 460 | panel1.add(保存密码CheckBox, new GridConstraints(3, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 461 | ksPassPF = new JPasswordField(); 462 | panel1.add(ksPassPF, new GridConstraints(2, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); 463 | channelPathTF = new JTextField(); 464 | panel1.add(channelPathTF, new GridConstraints(5, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); 465 | final JLabel label6 = new JLabel(); 466 | label6.setText("渠道清单[可选]"); 467 | panel1.add(label6, new GridConstraints(5, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 468 | channelBtn = new JButton(); 469 | channelBtn.setText("选择渠道清单"); 470 | panel1.add(channelBtn, new GridConstraints(5, 2, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 471 | final JPanel panel2 = new JPanel(); 472 | panel2.setLayout(new GridLayoutManager(4, 4, new Insets(0, 0, 0, 0), -1, -1)); 473 | tabbedPane1.addTab("高级", panel2); 474 | keyAliasCB = new JComboBox(); 475 | panel2.add(keyAliasCB, new GridConstraints(1, 1, 1, 3, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 476 | final JLabel label7 = new JLabel(); 477 | label7.setText("KeyAlias"); 478 | panel2.add(label7, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 479 | keyPassPF = new JPasswordField(); 480 | panel2.add(keyPassPF, new GridConstraints(2, 1, 1, 3, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, new Dimension(150, -1), null, 0, false)); 481 | final JLabel label8 = new JLabel(); 482 | label8.setText("证书密码"); 483 | panel2.add(label8, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 484 | final JLabel label9 = new JLabel(); 485 | label9.setText("如果您的Keystore包含多个证书,或者您的证书密码与Keystore密码不同, 请设置下列参数"); 486 | panel2.add(label9, new GridConstraints(0, 1, 1, 2, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_FIXED, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 487 | v2SigningEnabledCheckBox = new JCheckBox(); 488 | v2SigningEnabledCheckBox.setEnabled(false); 489 | v2SigningEnabledCheckBox.setSelected(true); 490 | v2SigningEnabledCheckBox.setText("--v2-signing-enabled"); 491 | panel2.add(v2SigningEnabledCheckBox, new GridConstraints(3, 2, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 492 | v1SigningEnabledCheckBox = new JCheckBox(); 493 | v1SigningEnabledCheckBox.setEnabled(false); 494 | v1SigningEnabledCheckBox.setSelected(true); 495 | v1SigningEnabledCheckBox.setText("--v1-signing-enabled"); 496 | panel2.add(v1SigningEnabledCheckBox, new GridConstraints(3, 1, 1, 1, GridConstraints.ANCHOR_WEST, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 497 | final JPanel panel3 = new JPanel(); 498 | panel3.setLayout(new GridLayoutManager(1, 2, new Insets(0, 0, 0, 0), -1, -1)); 499 | top.add(panel3, new GridConstraints(1, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, null, null, null, 0, false)); 500 | signBtn = new JButton(); 501 | signBtn.setText(" 4.签名 "); 502 | panel3.add(signBtn, new GridConstraints(0, 1, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_NONE, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_CAN_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 503 | progressBar1 = new JProgressBar(); 504 | progressBar1.setString("点击\"4.签名\"按钮开始 >>>>"); 505 | progressBar1.setStringPainted(true); 506 | panel3.add(progressBar1, new GridConstraints(0, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_HORIZONTAL, GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_FIXED, null, null, null, 0, false)); 507 | final JScrollPane scrollPane1 = new JScrollPane(); 508 | top.add(scrollPane1, new GridConstraints(2, 0, 1, 1, GridConstraints.ANCHOR_CENTER, GridConstraints.FILL_BOTH, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, GridConstraints.SIZEPOLICY_CAN_SHRINK | GridConstraints.SIZEPOLICY_WANT_GROW, null, null, null, 0, false)); 509 | loggingTA = new JTextArea(); 510 | loggingTA.setDoubleBuffered(true); 511 | loggingTA.setEditable(true); 512 | loggingTA.setInheritsPopupMenu(true); 513 | loggingTA.setLineWrap(true); 514 | loggingTA.setText(" 点击“4.签名”按钮开始签名..."); 515 | scrollPane1.setViewportView(loggingTA); 516 | label1.setLabelFor(inPathTF); 517 | label2.setLabelFor(ksPathTF); 518 | label3.setLabelFor(ksPassPF); 519 | label4.setLabelFor(ksPassPF); 520 | label5.setLabelFor(outPathTF); 521 | label7.setLabelFor(keyAliasCB); 522 | label8.setLabelFor(keyPassPF); 523 | } 524 | 525 | /** 526 | * @noinspection ALL 527 | */ 528 | public JComponent $$$getRootComponent$$$() { 529 | return top; 530 | } 531 | 532 | } 533 | -------------------------------------------------------------------------------- /signer/src/main/java/fake/security/Signature.java: -------------------------------------------------------------------------------- 1 | /** 2 | * dx-signer 3 | * 4 | * Copyright 2022 北京顶象技术有限公司 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package fake.security; 19 | 20 | import dx.channel.FixSha1WithDsaProvider; 21 | 22 | import java.security.NoSuchAlgorithmException; 23 | import java.security.NoSuchProviderException; 24 | import java.security.Provider; 25 | 26 | public class Signature { 27 | private static final Provider fix = new FixSha1WithDsaProvider(); 28 | 29 | public static java.security.Signature getInstance(String algorithm) 30 | throws NoSuchAlgorithmException { 31 | if ("SHA1WITHDSA".equalsIgnoreCase(algorithm)) { 32 | return java.security.Signature.getInstance(algorithm, fix); 33 | } 34 | return java.security.Signature.getInstance(algorithm); 35 | } 36 | 37 | public static java.security.Signature getInstance(String algorithm, String provider) 38 | throws NoSuchAlgorithmException, NoSuchProviderException { 39 | if ("SHA1WITHDSA".equalsIgnoreCase(algorithm)) { 40 | return java.security.Signature.getInstance(algorithm, fix); 41 | } 42 | return java.security.Signature.getInstance(algorithm, provider); 43 | } 44 | 45 | public static java.security.Signature getInstance(String algorithm, Provider provider) 46 | throws NoSuchAlgorithmException { 47 | if ("SHA1WITHDSA".equalsIgnoreCase(algorithm)) { 48 | return java.security.Signature.getInstance(algorithm, fix); 49 | } 50 | return java.security.Signature.getInstance(algorithm, provider); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /signer/src/main/resources/dx/signer/LICENSE.txt: -------------------------------------------------------------------------------- 1 | dx-signer 2 | 3 | Copyright 2022 北京顶象技术有限公司 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. --------------------------------------------------------------------------------