├── testkey.pk8 ├── tests └── assets │ └── tiny.apk ├── .gitignore ├── sign.bat ├── license ├── io_utils_notice │ └── NOTICE.txt └── LICENSE-2.0.txt ├── test.sh ├── testkey.x509.pem ├── .travis.yml ├── README.md ├── src ├── s │ ├── IOUtils.java │ └── Sign.java └── orig │ └── SignApk.java └── pom.xml /testkey.pk8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium-boneyard/sign/HEAD/testkey.pk8 -------------------------------------------------------------------------------- /tests/assets/tiny.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/appium-boneyard/sign/HEAD/tests/assets/tiny.apk -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | target/ 3 | .settings 4 | .project 5 | .classpath 6 | *.log 7 | .idea/ 8 | *.iml 9 | -------------------------------------------------------------------------------- /sign.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | if "%PATH_BASE%" == "" set PATH_BASE=%PATH% 3 | set PATH=%CD%;%PATH_BASE%; 4 | chcp 65001 2>nul >nul 5 | java -jar -Duser.language=en -Dfile.encoding=UTF8 "%~dp0\sign.jar" %* -------------------------------------------------------------------------------- /license/io_utils_notice/NOTICE.txt: -------------------------------------------------------------------------------- 1 | Apache Commons IO 2 | Copyright 2002-2012 The Apache Software Foundation 3 | 4 | This product includes software developed by 5 | The Apache Software Foundation (http://www.apache.org/). 6 | 7 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | tmpfile=$(mktemp /tmp/out.XXXXXX) 6 | jarsigner -verify -verbose "./tests/assets/tiny.apk" > "$tmpfile" 7 | if grep -q 'jar verified' "$tmpfile"; then 8 | exit 1 9 | fi 10 | java -jar ./target/sign*.jar "./tests/assets/tiny.apk" 11 | jarsigner -verify -verbose "./tests/assets/tiny.s.apk" > "$tmpfile" 12 | if ! grep -q 'jar verified' "$tmpfile"; then 13 | exit 1 14 | fi 15 | -------------------------------------------------------------------------------- /testkey.x509.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEqDCCA5CgAwIBAgIJAJNurL4H8gHfMA0GCSqGSIb3DQEBBQUAMIGUMQswCQYD 3 | VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4g 4 | VmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UE 5 | AxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAe 6 | Fw0wODAyMjkwMTMzNDZaFw0zNTA3MTcwMTMzNDZaMIGUMQswCQYDVQQGEwJVUzET 7 | MBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4G 8 | A1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9p 9 | ZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZI 10 | hvcNAQEBBQADggENADCCAQgCggEBANaTGQTexgskse3HYuDZ2CU+Ps1s6x3i/waM 11 | qOi8qM1r03hupwqnbOYOuw+ZNVn/2T53qUPn6D1LZLjk/qLT5lbx4meoG7+yMLV4 12 | wgRDvkxyGLhG9SEVhvA4oU6Jwr44f46+z4/Kw9oe4zDJ6pPQp8PcSvNQIg1QCAcy 13 | 4ICXF+5qBTNZ5qaU7Cyz8oSgpGbIepTYOzEJOmc3Li9kEsBubULxWBjf/gOBzAzU 14 | RNps3cO4JFgZSAGzJWQTT7/emMkod0jb9WdqVA2BVMi7yge54kdVMxHEa5r3b97s 15 | zI5p58ii0I54JiCUP5lyfTwE/nKZHZnfm644oLIXf6MdW2r+6R8CAQOjgfwwgfkw 16 | HQYDVR0OBBYEFEhZAFY9JyxGrhGGBaR0GawJyowRMIHJBgNVHSMEgcEwgb6AFEhZ 17 | AFY9JyxGrhGGBaR0GawJyowRoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UE 18 | CBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMH 19 | QW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAG 20 | CSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJAJNurL4H8gHfMAwGA1Ud 21 | EwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAHqvlozrUMRBBVEY0NqrrwFbinZa 22 | J6cVosK0TyIUFf/azgMJWr+kLfcHCHJsIGnlw27drgQAvilFLAhLwn62oX6snb4Y 23 | LCBOsVMR9FXYJLZW2+TcIkCRLXWG/oiVHQGo/rWuWkJgU134NDEFJCJGjDbiLCpe 24 | +ZTWHdcwauTJ9pUbo8EvHRkU3cYfGmLaLfgn9gP+pWA7LFQNvXwBnDa6sppCccEX 25 | 31I828XzgXpJ4O+mDL1/dBd+ek8ZPUP0IgdyZm5MTYPhvVqGCHzzTy3sIeJFymwr 26 | sBbmg2OAUNLEMO6nwmocSdN2ClirfxqCzJOLSDE4QyS9BAH6EhY6UFcOaE0= 27 | -----END CERTIFICATE----- 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | sudo: false 3 | jdk: openjdk8 4 | 5 | jobs: 6 | include: 7 | - stage: 8 | name: JDK 8 9 | before_script: 10 | - | 11 | if [[ -n "$TRAVIS_TAG" ]]; then 12 | ./build.sh "$TRAVIS_TAG" 13 | else 14 | ./build.sh 15 | fi 16 | script: "./test.sh" 17 | - stage: 18 | name: JDK 9 19 | jdk: openjdk9 20 | before_script: "./build.sh" 21 | script: "./test.sh" 22 | - stage: 23 | name: JDK 10 24 | jdk: openjdk10 25 | before_script: "./build.sh" 26 | script: "./test.sh" 27 | - stage: 28 | name: JDK 11 29 | jdk: openjdk11 30 | before_script: "./build.sh" 31 | script: "./test.sh" 32 | - stage: 33 | name: JDK 12 34 | jdk: openjdk12 35 | before_script: "./build.sh" 36 | script: "./test.sh" 37 | deploy: 38 | provider: releases 39 | api_key: 40 | secure: S389qkxYxHbqlv2/KOLzhwSuz36m35cY7yafHRd2y2bsY4nwJ+jaqzggBLoSY6bN+UOxxaZOgw9GO5TvZUMGcSC8xt1f7DnKeAYsHfvAd8mh7hXQBc9B7XK6OEcWatsLpnuyNoGN8bssDTY1xiJVsGyyz6XozqhnKGzfZFiPEp+eTIKzUT1YpRjOfXFZ152u42GugANX4G9TgVipz0jp33CJrrYgAEHOsx4In3Ib1i+Tmpg5XnCsMsBDI/75eegGFl/d34sV1jN3rsijGVMzHCwyL89uw7Duo1Bt3E/NwehayXBiiK3DWP7u276kDY8+XVUFIcKnH8FyGawrYh6IJfkQUzTARrmXpzHDjFGAQqRnqRR/DYMYpEtfZp6Uuidjmbr4P8XsoGlTEMGiAc+6obqeju0yCtx/WewBTx6pMjrGIX50fpGt+yMc4LB4jsvlMAAl370xYeTiaRakIrK2OzAi4UdbS99NHXQvlDrFXVGJCy4RANrbA5kVwhTBwF25QbE12W3+8tCMOxft+gcWz/M3abhEusiL9J3+W9pYyg1X3PKr7sSrGhwd7hrM7ZPglrysdEs+oOJKfxeQbFLdJ+WwLJdag05CHlPzfFUkdIL3LWFvtdbjbFWB3mi+Y2qIcBg4s2fPdoAX+rOpfCBpXCoNvwCo67oz2vNVZE2GfRU= 41 | file_glob: true 42 | file: target/*.jar 43 | skip_cleanup: true 44 | on: 45 | jdk: openjdk8 46 | tags: true 47 | repo: appium/sign 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apk Sign 2 | 3 | Automatically sign an apk with the Android test certificate. The public and private keys are embedded within the jar. 4 | 5 | 6 | ### Development 7 | 8 | Building: 9 | 10 | ```bash 11 | ./build.sh 12 | ``` 13 | 14 | Testing: 15 | 16 | ```bash 17 | ./test.sh 18 | ``` 19 | 20 | 21 | ### Usage 22 | 23 | - `sign my.apk` 24 | 25 | `my.s.apk` is created next to `my.apk` 26 | 27 | - `sign my.apk --override` 28 | 29 | `my.apk` is replaced with a signed version 30 | 31 | Verify signature. 32 | 33 | `jarsigner -verify my.s.apk` 34 | 35 | 36 | ### Release 37 | 38 | New releases are published to GitHub automatically by CI agent. 39 | It is only necessary to push a new version tag to `master`: 40 | 41 | ```bash 42 | git tag -a 1.0 -m 1.0 43 | git push --tags origin master 44 | git push --tags remote master 45 | ``` 46 | 47 | 48 | ### License 49 | 50 | Released under the Apache 2.0 License (the same as Android's [SignApk.java](https://github.com/android/platform_build/blob/master/tools/signapk/SignApk.java)). 51 | 52 | 53 | ### Based on the following AOSP 4.1.1 files & sources 54 | 55 | ``` 56 | https://github.com/android/platform_build/blob/master/tools/signapk/SignApk.java 57 | http://androidxref.com/4.1.1/xref/build/tools/signapk/ 58 | http://androidxref.com/4.1.1/xref/build/tools/signapk/test/run 59 | http://androidxref.com/4.1.1/xref/cts/tests/assets/otacerts.zip 60 | http://androidxref.com/4.1.1/xref/external/quake/tools/packagesharedlib#11 61 | http://androidxref.com/4.1.1/raw/build/target/product/security/testkey.pk8 62 | http://androidxref.com/4.1.1/xref/build/target/product/security/ 63 | ``` 64 | 65 | The following commands are equivalent. 66 | 67 | `sign my.apk` 68 | 69 | `java -classpath sign.jar orig.SignApk testkey.x509.pem testkey.pk8 my.apk my.s.apk` 70 | 71 | `java -jar SignApk.jar testkey.x509.pem testkey.pk8 my.apk my.s.apk` 72 | 73 | 74 | ### Similar Projects 75 | 76 | [ApkSign](http://code.google.com/p/dex2jar/source/browse/dex-tools/src/main/java/com/googlecode/dex2jar/tools/ApkSign.java) by Panxiaobo. dex2jar's ApkSign has many dependencies and does not fit into one source file. While the name `ApkSign` is similar to `apks`, no source from dex2jar is used in this project. 77 | 78 | [Tiny Sign](http://code.google.com/p/tiny-sign/) by Panxiaobo. Simple jar signing that can run on Android. 79 | -------------------------------------------------------------------------------- /src/s/IOUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to the Apache Software Foundation (ASF) under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * The ASF licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. 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. 16 | */ 17 | package s; 18 | 19 | import java.io.ByteArrayOutputStream; 20 | import java.io.IOException; 21 | import java.io.InputStream; 22 | import java.io.OutputStream; 23 | 24 | public class IOUtils { 25 | 26 | /** 27 | * Get the contents of an InputStream as a byte[]. 28 | *

29 | * This method buffers the input internally, so there is no need to use a 30 | * BufferedInputStream. 31 | * 32 | * @param input 33 | * the InputStream to read from 34 | * @return the requested byte array 35 | */ 36 | public static byte[] toByteArray(InputStream input) { 37 | try { 38 | ByteArrayOutputStream output = new ByteArrayOutputStream(); 39 | copy(input, output); 40 | return output.toByteArray(); 41 | } catch (Exception e) { 42 | e.printStackTrace(); 43 | return null; 44 | } 45 | } 46 | 47 | private static int copy(InputStream input, OutputStream output) 48 | throws IOException { 49 | long count = copyLarge(input, output); 50 | if (count > Integer.MAX_VALUE) { 51 | return -1; 52 | } 53 | return (int) count; 54 | } 55 | 56 | private static long copyLarge(InputStream input, OutputStream output) 57 | throws IOException { 58 | byte[] buffer = new byte[4096]; 59 | long count = 0; 60 | int n = 0; 61 | while (-1 != (n = input.read(buffer))) { 62 | output.write(buffer, 0, n); 63 | count += n; 64 | } 65 | return count; 66 | } 67 | 68 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | apks 5 | apks 6 | 0.0.1-SNAPSHOT 7 | jar 8 | 9 | UTF-8 10 | s.Sign 11 | sign 12 | 13 | 14 | 15 | 16 | ${basedir} 17 | false 18 | 19 | testkey.x509.pem 20 | testkey.pk8 21 | license/LICENSE-2.0.txt 22 | 23 | 24 | 25 | 26 | src 27 | 28 | 29 | maven-compiler-plugin 30 | 2.5.1 31 | 32 | 1.7 33 | 1.7 34 | 35 | 36 | 37 | org.apache.maven.plugins 38 | maven-resources-plugin 39 | 2.5 40 | 41 | UTF-8 42 | 43 | 44 | 45 | org.apache.maven.plugins 46 | maven-jar-plugin 47 | 2.4 48 | 49 | ${namePrefix}-${project.version} 50 | 51 | 52 | true 53 | ${mainClass} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /license/LICENSE-2.0.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 | -------------------------------------------------------------------------------- /src/s/Sign.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2008 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package s; 18 | 19 | import java.io.ByteArrayInputStream; 20 | import java.io.ByteArrayOutputStream; 21 | import java.io.File; 22 | import java.io.FileOutputStream; 23 | import java.io.FilterOutputStream; 24 | import java.io.IOException; 25 | import java.io.InputStream; 26 | import java.io.OutputStream; 27 | import java.io.PrintStream; 28 | import java.security.DigestOutputStream; 29 | import java.security.GeneralSecurityException; 30 | import java.security.KeyFactory; 31 | import java.security.MessageDigest; 32 | import java.security.PrivateKey; 33 | import java.security.Signature; 34 | import java.security.SignatureException; 35 | import java.security.cert.CertificateFactory; 36 | import java.security.cert.X509Certificate; 37 | import java.security.spec.InvalidKeySpecException; 38 | import java.security.spec.KeySpec; 39 | import java.security.spec.PKCS8EncodedKeySpec; 40 | import java.util.ArrayList; 41 | import java.util.Collections; 42 | import java.util.Date; 43 | import java.util.Enumeration; 44 | import java.util.List; 45 | import java.util.Map; 46 | import java.util.TreeMap; 47 | import java.util.jar.Attributes; 48 | import java.util.jar.JarEntry; 49 | import java.util.jar.JarFile; 50 | import java.util.jar.JarOutputStream; 51 | import java.util.jar.Manifest; 52 | import java.util.regex.Pattern; 53 | 54 | import java.util.Base64; 55 | import sun.security.pkcs.ContentInfo; 56 | import sun.security.pkcs.PKCS7; 57 | import sun.security.pkcs.SignerInfo; 58 | import sun.security.x509.AlgorithmId; 59 | import sun.security.x509.X500Name; 60 | 61 | /** 62 | * Command line tool to sign JAR files (including APKs and OTA updates) in a way 63 | * compatible with the mincrypt verifier, using SHA1 and RSA keys. 64 | */ 65 | @SuppressWarnings("restriction") 66 | class Sign { 67 | private static final String CERT_SF_NAME = "META-INF/CERT.SF"; 68 | private static final String CERT_RSA_NAME = "META-INF/CERT.RSA"; 69 | 70 | private static final String OTACERT_NAME = "META-INF/com/android/otacert"; 71 | 72 | // Files matching this pattern are not copied to the output. 73 | private static Pattern stripPattern = Pattern 74 | .compile("^META-INF/(.*)[.](SF|RSA|DSA)$"); 75 | 76 | private static X509Certificate readPublicKey() throws IOException, 77 | GeneralSecurityException { 78 | final InputStream publicKeyFileIS = new ByteArrayInputStream(publicBytes); 79 | final CertificateFactory cf = CertificateFactory.getInstance("X.509"); 80 | return (X509Certificate) cf.generateCertificate(publicKeyFileIS); 81 | } 82 | 83 | /** Read a PKCS 8 format private key. */ 84 | private static PrivateKey readPrivateKey() throws IOException, 85 | GeneralSecurityException { 86 | KeySpec spec = new PKCS8EncodedKeySpec(privateBytes); 87 | 88 | try { 89 | return KeyFactory.getInstance("RSA").generatePrivate(spec); 90 | } catch (InvalidKeySpecException ex) { 91 | return KeyFactory.getInstance("DSA").generatePrivate(spec); 92 | } 93 | 94 | } 95 | 96 | /** Add the SHA1 of every file to the manifest, creating it if necessary. */ 97 | private static Manifest addDigestsToManifest(JarFile jar) throws IOException, 98 | GeneralSecurityException { 99 | Manifest input = jar.getManifest(); 100 | Manifest output = new Manifest(); 101 | Attributes main = output.getMainAttributes(); 102 | if (input != null) { 103 | main.putAll(input.getMainAttributes()); 104 | } else { 105 | main.putValue("Manifest-Version", "1.0"); 106 | } 107 | 108 | Base64.Encoder base64 = Base64.getEncoder().withoutPadding(); 109 | MessageDigest md = MessageDigest.getInstance("SHA1"); 110 | byte[] buffer = new byte[4096]; 111 | int num; 112 | 113 | // We sort the input entries by name, and add them to the 114 | // output manifest in sorted order. We expect that the output 115 | // map will be deterministic. 116 | 117 | TreeMap byName = new TreeMap(); 118 | 119 | for (Enumeration e = jar.entries(); e.hasMoreElements();) { 120 | JarEntry entry = e.nextElement(); 121 | byName.put(entry.getName(), entry); 122 | } 123 | 124 | for (JarEntry entry : byName.values()) { 125 | String name = entry.getName(); 126 | if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) 127 | && !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) 128 | && !name.equals(OTACERT_NAME) 129 | && (stripPattern == null || !stripPattern.matcher(name).matches())) { 130 | InputStream data = jar.getInputStream(entry); 131 | while ((num = data.read(buffer)) > 0) { 132 | md.update(buffer, 0, num); 133 | } 134 | 135 | Attributes attr = null; 136 | if (input != null) 137 | attr = input.getAttributes(name); 138 | attr = attr != null ? new Attributes(attr) : new Attributes(); 139 | attr.putValue("SHA1-Digest", base64.encodeToString(md.digest())); 140 | output.getEntries().put(name, attr); 141 | } 142 | } 143 | 144 | return output; 145 | } 146 | 147 | /** 148 | * Add a copy of the public key to the archive; this should exactly match one 149 | * of the files in /system/etc/security/otacerts.zip on the device. (The same 150 | * cert can be extracted from the CERT.RSA file but this is much easier to get 151 | * at.) 152 | */ 153 | private static void addOtacert(JarOutputStream outputJar, long timestamp, 154 | Manifest manifest) throws IOException, GeneralSecurityException { 155 | final InputStream input = new ByteArrayInputStream(publicBytes); 156 | 157 | Base64.Encoder base64 = Base64.getEncoder().withoutPadding(); 158 | MessageDigest md = MessageDigest.getInstance("SHA1"); 159 | 160 | JarEntry je = new JarEntry(OTACERT_NAME); 161 | je.setTime(timestamp); 162 | outputJar.putNextEntry(je); 163 | 164 | byte[] b = new byte[4096]; 165 | int read; 166 | while ((read = input.read(b)) != -1) { 167 | outputJar.write(b, 0, read); 168 | md.update(b, 0, read); 169 | } 170 | input.close(); 171 | 172 | Attributes attr = new Attributes(); 173 | attr.putValue("SHA1-Digest", base64.encodeToString(md.digest())); 174 | manifest.getEntries().put(OTACERT_NAME, attr); 175 | } 176 | 177 | /** Write to another stream and also feed it to the Signature object. */ 178 | private static class SignatureOutputStream extends FilterOutputStream { 179 | private Signature mSignature; 180 | private int mCount; 181 | 182 | public SignatureOutputStream(OutputStream out, Signature sig) { 183 | super(out); 184 | mSignature = sig; 185 | mCount = 0; 186 | } 187 | 188 | @Override 189 | public void write(int b) throws IOException { 190 | try { 191 | mSignature.update((byte) b); 192 | } catch (SignatureException e) { 193 | throw new IOException("SignatureException: " + e); 194 | } 195 | super.write(b); 196 | mCount++; 197 | } 198 | 199 | @Override 200 | public void write(byte[] b, int off, int len) throws IOException { 201 | try { 202 | mSignature.update(b, off, len); 203 | } catch (SignatureException e) { 204 | throw new IOException("SignatureException: " + e); 205 | } 206 | super.write(b, off, len); 207 | mCount += len; 208 | } 209 | 210 | public int size() { 211 | return mCount; 212 | } 213 | } 214 | 215 | /** Write a .SF file with a digest of the specified manifest. */ 216 | private static void writeSignatureFile(Manifest manifest, 217 | SignatureOutputStream out) throws IOException, GeneralSecurityException { 218 | Manifest sf = new Manifest(); 219 | Attributes main = sf.getMainAttributes(); 220 | main.putValue("Signature-Version", "1.0"); 221 | 222 | Base64.Encoder base64 = Base64.getEncoder().withoutPadding(); 223 | MessageDigest md = MessageDigest.getInstance("SHA1"); 224 | PrintStream print = new PrintStream(new DigestOutputStream( 225 | new ByteArrayOutputStream(), md), true, "UTF-8"); 226 | 227 | // Digest of the entire manifest 228 | manifest.write(print); 229 | print.flush(); 230 | main.putValue("SHA1-Digest-Manifest", base64.encodeToString(md.digest())); 231 | 232 | Map entries = manifest.getEntries(); 233 | for (Map.Entry entry : entries.entrySet()) { 234 | // Digest of the manifest stanza for this entry. 235 | print.print("Name: " + entry.getKey() + "\r\n"); 236 | for (Map.Entry att : entry.getValue().entrySet()) { 237 | print.print(att.getKey() + ": " + att.getValue() + "\r\n"); 238 | } 239 | print.print("\r\n"); 240 | print.flush(); 241 | 242 | Attributes sfAttr = new Attributes(); 243 | sfAttr.putValue("SHA1-Digest", base64.encodeToString(md.digest())); 244 | sf.getEntries().put(entry.getKey(), sfAttr); 245 | } 246 | 247 | sf.write(out); 248 | 249 | // A bug in the java.util.jar implementation of Android platforms 250 | // up to version 1.6 will cause a spurious IOException to be thrown 251 | // if the length of the signature file is a multiple of 1024 bytes. 252 | // As a workaround, add an extra CRLF in this case. 253 | if ((out.size() % 1024) == 0) { 254 | out.write('\r'); 255 | out.write('\n'); 256 | } 257 | } 258 | 259 | /** Write a .RSA file with a digital signature. */ 260 | private static void writeSignatureBlock(Signature signature, 261 | X509Certificate publicKey, OutputStream out) throws IOException, 262 | GeneralSecurityException { 263 | SignerInfo signerInfo = new SignerInfo(new X500Name(publicKey 264 | .getIssuerX500Principal().getName()), publicKey.getSerialNumber(), 265 | AlgorithmId.get("SHA1"), AlgorithmId.get("RSA"), signature.sign()); 266 | 267 | PKCS7 pkcs7 = new PKCS7(new AlgorithmId[] { AlgorithmId.get("SHA1") }, 268 | new ContentInfo(ContentInfo.DATA_OID, null), 269 | new X509Certificate[] { publicKey }, new SignerInfo[] { signerInfo }); 270 | 271 | pkcs7.encodeSignedData(out); 272 | } 273 | 274 | private static void signWholeOutputFile(byte[] zipData, 275 | OutputStream outputStream, X509Certificate publicKey, 276 | PrivateKey privateKey) throws IOException, GeneralSecurityException { 277 | 278 | // For a zip with no archive comment, the 279 | // end-of-central-directory record will be 22 bytes long, so 280 | // we expect to find the EOCD marker 22 bytes from the end. 281 | if (zipData[zipData.length - 22] != 0x50 282 | || zipData[zipData.length - 21] != 0x4b 283 | || zipData[zipData.length - 20] != 0x05 284 | || zipData[zipData.length - 19] != 0x06) { 285 | throw new IllegalArgumentException( 286 | "zip data already has an archive comment"); 287 | } 288 | 289 | Signature signature = Signature.getInstance("SHA1withRSA"); 290 | signature.initSign(privateKey); 291 | signature.update(zipData, 0, zipData.length - 2); 292 | 293 | ByteArrayOutputStream temp = new ByteArrayOutputStream(); 294 | 295 | // put a readable message and a null char at the start of the 296 | // archive comment, so that tools that display the comment 297 | // (hopefully) show something sensible. 298 | // TODO: anything more useful we can put in this message? 299 | byte[] message = "signed by SignApk".getBytes("UTF-8"); 300 | temp.write(message); 301 | temp.write(0); 302 | writeSignatureBlock(signature, publicKey, temp); 303 | int total_size = temp.size() + 6; 304 | if (total_size > 0xffff) { 305 | throw new IllegalArgumentException( 306 | "signature is too big for ZIP file comment"); 307 | } 308 | // signature starts this many bytes from the end of the file 309 | int signature_start = total_size - message.length - 1; 310 | temp.write(signature_start & 0xff); 311 | temp.write((signature_start >> 8) & 0xff); 312 | // Why the 0xff bytes? In a zip file with no archive comment, 313 | // bytes [-6:-2] of the file are the little-endian offset from 314 | // the start of the file to the central directory. So for the 315 | // two high bytes to be 0xff 0xff, the archive would have to 316 | // be nearly 4GB in side. So it's unlikely that a real 317 | // commentless archive would have 0xffs here, and lets us tell 318 | // an old signed archive from a new one. 319 | temp.write(0xff); 320 | temp.write(0xff); 321 | temp.write(total_size & 0xff); 322 | temp.write((total_size >> 8) & 0xff); 323 | temp.flush(); 324 | 325 | // Signature verification checks that the EOCD header is the 326 | // last such sequence in the file (to avoid minzip finding a 327 | // fake EOCD appended after the signature in its scan). The 328 | // odds of producing this sequence by chance are very low, but 329 | // let's catch it here if it does. 330 | byte[] b = temp.toByteArray(); 331 | for (int i = 0; i < b.length - 3; ++i) { 332 | if (b[i] == 0x50 && b[i + 1] == 0x4b && b[i + 2] == 0x05 333 | && b[i + 3] == 0x06) { 334 | throw new IllegalArgumentException("found spurious EOCD header at " + i); 335 | } 336 | } 337 | 338 | outputStream.write(zipData, 0, zipData.length - 2); 339 | outputStream.write(total_size & 0xff); 340 | outputStream.write((total_size >> 8) & 0xff); 341 | temp.writeTo(outputStream); 342 | } 343 | 344 | /** 345 | * Copy all the files in a manifest from input to output. We set the 346 | * modification times in the output to a fixed time, so as to reduce variation 347 | * in the output file and make incremental OTAs more efficient. 348 | */ 349 | private static void copyFiles(Manifest manifest, JarFile in, 350 | JarOutputStream out, long timestamp) throws IOException { 351 | byte[] buffer = new byte[4096]; 352 | int num; 353 | 354 | Map entries = manifest.getEntries(); 355 | List names = new ArrayList(entries.keySet()); 356 | Collections.sort(names); 357 | for (String name : names) { 358 | JarEntry inEntry = in.getJarEntry(name); 359 | JarEntry outEntry = null; 360 | if (inEntry.getMethod() == JarEntry.STORED) { 361 | // Preserve the STORED method of the input entry. 362 | outEntry = new JarEntry(inEntry); 363 | } else { 364 | // Create a new entry so that the compressed len is recomputed. 365 | outEntry = new JarEntry(name); 366 | } 367 | outEntry.setTime(timestamp); 368 | out.putNextEntry(outEntry); 369 | 370 | InputStream data = in.getInputStream(inEntry); 371 | while ((num = data.read(buffer)) > 0) { 372 | out.write(buffer, 0, num); 373 | } 374 | out.flush(); 375 | } 376 | } 377 | 378 | /** 379 | * Invokes file.delete() and if that fails, file.deleteOnExit(). Immediately 380 | * returns if file is null. 381 | **/ 382 | public static void delete(final File file) { 383 | if (file == null) { 384 | return; 385 | } 386 | 387 | if (!file.delete()) { 388 | file.deleteOnExit(); 389 | } 390 | } 391 | 392 | // Public key. 393 | private static final byte[] publicBytes = IOUtils.toByteArray(Sign.class 394 | .getResourceAsStream("/testkey.x509.pem")); 395 | // Private key. 396 | private static final byte[] privateBytes = IOUtils.toByteArray(Sign.class 397 | .getResourceAsStream("/testkey.pk8")); 398 | 399 | // Only compile the pattern once. 400 | private static Pattern endApk = Pattern.compile("\\.apk$"); 401 | 402 | public static void sign(String inputApkPath, boolean override) { 403 | String outputApkPath = endApk.matcher(inputApkPath).replaceAll("") 404 | + ".s.apk"; 405 | 406 | final File input = new File(inputApkPath); 407 | 408 | if (!input.exists() || !input.isFile()) { 409 | throw new RuntimeException("Input is not an existing file. " + inputApkPath); 410 | } 411 | 412 | File renamedInput = null; 413 | 414 | if (override) { 415 | outputApkPath = inputApkPath; 416 | 417 | renamedInput = new File(input.getParentFile(), 418 | new Date().getTime() + ".tmp"); 419 | 420 | if (!input.renameTo(renamedInput)) { 421 | throw new RuntimeException("Unable to rename input apk. " 422 | + inputApkPath); 423 | } 424 | 425 | inputApkPath = renamedInput.getAbsolutePath(); 426 | } 427 | 428 | boolean signWholeFile = false; 429 | 430 | JarFile inputJar = null; 431 | JarOutputStream outputJar = null; 432 | FileOutputStream outputFile = null; 433 | 434 | try { 435 | X509Certificate publicKey = readPublicKey(); 436 | 437 | // Assume the certificate is valid for at least an hour. 438 | long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; 439 | 440 | PrivateKey privateKey = readPrivateKey(); 441 | inputJar = new JarFile(new File(inputApkPath), false); // Don't verify. 442 | 443 | OutputStream outputStream = null; 444 | if (signWholeFile) { 445 | outputStream = new ByteArrayOutputStream(); 446 | } else { 447 | outputStream = outputFile = new FileOutputStream(new File(outputApkPath)); 448 | } 449 | outputJar = new JarOutputStream(outputStream); 450 | outputJar.setLevel(9); 451 | 452 | JarEntry je; 453 | 454 | Manifest manifest = addDigestsToManifest(inputJar); 455 | 456 | // Everything else 457 | copyFiles(manifest, inputJar, outputJar, timestamp); 458 | 459 | // otacert 460 | if (signWholeFile) { 461 | addOtacert(outputJar, timestamp, manifest); 462 | } 463 | 464 | // MANIFEST.MF 465 | je = new JarEntry(JarFile.MANIFEST_NAME); 466 | je.setTime(timestamp); 467 | outputJar.putNextEntry(je); 468 | manifest.write(outputJar); 469 | 470 | // CERT.SF 471 | Signature signature = Signature.getInstance("SHA1withRSA"); 472 | signature.initSign(privateKey); 473 | je = new JarEntry(CERT_SF_NAME); 474 | je.setTime(timestamp); 475 | outputJar.putNextEntry(je); 476 | writeSignatureFile(manifest, new SignatureOutputStream(outputJar, 477 | signature)); 478 | 479 | // CERT.RSA 480 | je = new JarEntry(CERT_RSA_NAME); 481 | je.setTime(timestamp); 482 | outputJar.putNextEntry(je); 483 | writeSignatureBlock(signature, publicKey, outputJar); 484 | 485 | outputJar.close(); 486 | outputJar = null; 487 | outputStream.flush(); 488 | 489 | if (signWholeFile) { 490 | outputFile = new FileOutputStream(outputApkPath); 491 | signWholeOutputFile( 492 | ((ByteArrayOutputStream) outputStream).toByteArray(), outputFile, 493 | publicKey, privateKey); 494 | } 495 | } catch (Exception e) { 496 | e.printStackTrace(); 497 | System.exit(1); 498 | } finally { 499 | try { 500 | if (renamedInput != null) { 501 | delete(renamedInput); 502 | } 503 | if (inputJar != null) { 504 | inputJar.close(); 505 | } 506 | if (outputFile != null) { 507 | outputFile.close(); 508 | } 509 | } catch (IOException e) { 510 | e.printStackTrace(); 511 | System.exit(1); 512 | } 513 | } 514 | } 515 | 516 | public static void main(String[] args) { 517 | if (args.length < 1) { 518 | System.out.println("Usage: java -jar sign.jar my_1.apk [my_2.apk ...] [--override]"); 519 | System.exit(0); 520 | } 521 | 522 | boolean override = false; 523 | for (final String arg : args) { 524 | if (arg.toLowerCase().equals("--override")) { 525 | override = true; 526 | break; 527 | } 528 | } 529 | 530 | for (final String apk : args) { 531 | if (apk.toLowerCase().endsWith(".apk")) { 532 | sign(apk, override); 533 | } 534 | } 535 | } 536 | } -------------------------------------------------------------------------------- /src/orig/SignApk.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2008 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package orig; 18 | 19 | import java.io.BufferedReader; 20 | import java.io.ByteArrayOutputStream; 21 | import java.io.DataInputStream; 22 | import java.io.File; 23 | import java.io.FileInputStream; 24 | import java.io.FileOutputStream; 25 | import java.io.FilterOutputStream; 26 | import java.io.IOException; 27 | import java.io.InputStream; 28 | import java.io.InputStreamReader; 29 | import java.io.OutputStream; 30 | import java.io.PrintStream; 31 | import java.security.DigestOutputStream; 32 | import java.security.GeneralSecurityException; 33 | import java.security.Key; 34 | import java.security.KeyFactory; 35 | import java.security.MessageDigest; 36 | import java.security.PrivateKey; 37 | import java.security.Signature; 38 | import java.security.SignatureException; 39 | import java.security.cert.CertificateFactory; 40 | import java.security.cert.X509Certificate; 41 | import java.security.spec.InvalidKeySpecException; 42 | import java.security.spec.KeySpec; 43 | import java.security.spec.PKCS8EncodedKeySpec; 44 | import java.util.ArrayList; 45 | import java.util.Collections; 46 | import java.util.Enumeration; 47 | import java.util.List; 48 | import java.util.Map; 49 | import java.util.TreeMap; 50 | import java.util.jar.Attributes; 51 | import java.util.jar.JarEntry; 52 | import java.util.jar.JarFile; 53 | import java.util.jar.JarOutputStream; 54 | import java.util.jar.Manifest; 55 | import java.util.regex.Pattern; 56 | 57 | import javax.crypto.Cipher; 58 | import javax.crypto.EncryptedPrivateKeyInfo; 59 | import javax.crypto.SecretKeyFactory; 60 | import javax.crypto.spec.PBEKeySpec; 61 | 62 | import java.util.Base64; 63 | import sun.security.pkcs.ContentInfo; 64 | import sun.security.pkcs.PKCS7; 65 | import sun.security.pkcs.SignerInfo; 66 | import sun.security.x509.AlgorithmId; 67 | import sun.security.x509.X500Name; 68 | 69 | /** 70 | * Command line tool to sign JAR files (including APKs and OTA updates) in 71 | * a way compatible with the mincrypt verifier, using SHA1 and RSA keys. 72 | */ 73 | @SuppressWarnings("restriction") 74 | class SignApk { 75 | private static final String CERT_SF_NAME = "META-INF/CERT.SF"; 76 | private static final String CERT_RSA_NAME = "META-INF/CERT.RSA"; 77 | 78 | private static final String OTACERT_NAME = "META-INF/com/android/otacert"; 79 | 80 | // Files matching this pattern are not copied to the output. 81 | private static Pattern stripPattern = 82 | Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA)$"); 83 | 84 | private static X509Certificate readPublicKey(File file) 85 | throws IOException, GeneralSecurityException { 86 | FileInputStream input = new FileInputStream(file); 87 | try { 88 | CertificateFactory cf = CertificateFactory.getInstance("X.509"); 89 | return (X509Certificate) cf.generateCertificate(input); 90 | } finally { 91 | input.close(); 92 | } 93 | } 94 | 95 | /** 96 | * Reads the password from stdin and returns it as a string. 97 | * 98 | * @param keyFile The file containing the private key. Used to prompt the user. 99 | */ 100 | private static String readPassword(File keyFile) { 101 | // TODO: use Console.readPassword() when it's available. 102 | System.out.print("Enter password for " + keyFile + " (password will not be hidden): "); 103 | System.out.flush(); 104 | BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in)); 105 | try { 106 | return stdin.readLine(); 107 | } catch (IOException ex) { 108 | return null; 109 | } 110 | } 111 | 112 | /** 113 | * Decrypt an encrypted PKCS 8 format private key. 114 | * 115 | * Based on ghstark's post on Aug 6, 2006 at 116 | * http://forums.sun.com/thread.jspa?threadID=758133&messageID=4330949 117 | * 118 | * @param encryptedPrivateKey The raw data of the private key 119 | * @param keyFile The file containing the private key 120 | */ 121 | private static KeySpec decryptPrivateKey(byte[] encryptedPrivateKey, File keyFile) 122 | throws GeneralSecurityException { 123 | EncryptedPrivateKeyInfo epkInfo; 124 | try { 125 | epkInfo = new EncryptedPrivateKeyInfo(encryptedPrivateKey); 126 | } catch (IOException ex) { 127 | // Probably not an encrypted key. 128 | return null; 129 | } 130 | 131 | char[] password = readPassword(keyFile).toCharArray(); 132 | 133 | SecretKeyFactory skFactory = SecretKeyFactory.getInstance(epkInfo.getAlgName()); 134 | Key key = skFactory.generateSecret(new PBEKeySpec(password)); 135 | 136 | Cipher cipher = Cipher.getInstance(epkInfo.getAlgName()); 137 | cipher.init(Cipher.DECRYPT_MODE, key, epkInfo.getAlgParameters()); 138 | 139 | try { 140 | return epkInfo.getKeySpec(cipher); 141 | } catch (InvalidKeySpecException ex) { 142 | System.err.println("signapk: Password for " + keyFile + " may be bad."); 143 | throw ex; 144 | } 145 | } 146 | 147 | /** Read a PKCS 8 format private key. */ 148 | private static PrivateKey readPrivateKey(File file) 149 | throws IOException, GeneralSecurityException { 150 | DataInputStream input = new DataInputStream(new FileInputStream(file)); 151 | try { 152 | byte[] bytes = new byte[(int) file.length()]; 153 | input.read(bytes); 154 | 155 | KeySpec spec = decryptPrivateKey(bytes, file); 156 | if (spec == null) { 157 | spec = new PKCS8EncodedKeySpec(bytes); 158 | } 159 | 160 | try { 161 | return KeyFactory.getInstance("RSA").generatePrivate(spec); 162 | } catch (InvalidKeySpecException ex) { 163 | return KeyFactory.getInstance("DSA").generatePrivate(spec); 164 | } 165 | } finally { 166 | input.close(); 167 | } 168 | } 169 | 170 | /** Add the SHA1 of every file to the manifest, creating it if necessary. */ 171 | private static Manifest addDigestsToManifest(JarFile jar) 172 | throws IOException, GeneralSecurityException { 173 | Manifest input = jar.getManifest(); 174 | Manifest output = new Manifest(); 175 | Attributes main = output.getMainAttributes(); 176 | if (input != null) { 177 | main.putAll(input.getMainAttributes()); 178 | } else { 179 | main.putValue("Manifest-Version", "1.0"); 180 | } 181 | 182 | Base64.Encoder base64 = Base64.getEncoder().withoutPadding(); 183 | MessageDigest md = MessageDigest.getInstance("SHA1"); 184 | byte[] buffer = new byte[4096]; 185 | int num; 186 | 187 | // We sort the input entries by name, and add them to the 188 | // output manifest in sorted order. We expect that the output 189 | // map will be deterministic. 190 | 191 | TreeMap byName = new TreeMap(); 192 | 193 | for (Enumeration e = jar.entries(); e.hasMoreElements(); ) { 194 | JarEntry entry = e.nextElement(); 195 | byName.put(entry.getName(), entry); 196 | } 197 | 198 | for (JarEntry entry: byName.values()) { 199 | String name = entry.getName(); 200 | if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) && 201 | !name.equals(CERT_SF_NAME) && !name.equals(CERT_RSA_NAME) && 202 | !name.equals(OTACERT_NAME) && 203 | (stripPattern == null || 204 | !stripPattern.matcher(name).matches())) { 205 | InputStream data = jar.getInputStream(entry); 206 | while ((num = data.read(buffer)) > 0) { 207 | md.update(buffer, 0, num); 208 | } 209 | 210 | Attributes attr = null; 211 | if (input != null) attr = input.getAttributes(name); 212 | attr = attr != null ? new Attributes(attr) : new Attributes(); 213 | attr.putValue("SHA1-Digest", base64.encodeToString(md.digest())); 214 | output.getEntries().put(name, attr); 215 | } 216 | } 217 | 218 | return output; 219 | } 220 | 221 | /** 222 | * Add a copy of the public key to the archive; this should 223 | * exactly match one of the files in 224 | * /system/etc/security/otacerts.zip on the device. (The same 225 | * cert can be extracted from the CERT.RSA file but this is much 226 | * easier to get at.) 227 | */ 228 | private static void addOtacert(JarOutputStream outputJar, 229 | File publicKeyFile, 230 | long timestamp, 231 | Manifest manifest) 232 | throws IOException, GeneralSecurityException { 233 | Base64.Encoder base64 = Base64.getEncoder().withoutPadding(); 234 | MessageDigest md = MessageDigest.getInstance("SHA1"); 235 | 236 | JarEntry je = new JarEntry(OTACERT_NAME); 237 | je.setTime(timestamp); 238 | outputJar.putNextEntry(je); 239 | FileInputStream input = new FileInputStream(publicKeyFile); 240 | byte[] b = new byte[4096]; 241 | int read; 242 | while ((read = input.read(b)) != -1) { 243 | outputJar.write(b, 0, read); 244 | md.update(b, 0, read); 245 | } 246 | input.close(); 247 | 248 | Attributes attr = new Attributes(); 249 | attr.putValue("SHA1-Digest", base64.encodeToString(md.digest())); 250 | manifest.getEntries().put(OTACERT_NAME, attr); 251 | } 252 | 253 | 254 | /** Write to another stream and also feed it to the Signature object. */ 255 | private static class SignatureOutputStream extends FilterOutputStream { 256 | private Signature mSignature; 257 | private int mCount; 258 | 259 | public SignatureOutputStream(OutputStream out, Signature sig) { 260 | super(out); 261 | mSignature = sig; 262 | mCount = 0; 263 | } 264 | 265 | @Override 266 | public void write(int b) throws IOException { 267 | try { 268 | mSignature.update((byte) b); 269 | } catch (SignatureException e) { 270 | throw new IOException("SignatureException: " + e); 271 | } 272 | super.write(b); 273 | mCount++; 274 | } 275 | 276 | @Override 277 | public void write(byte[] b, int off, int len) throws IOException { 278 | try { 279 | mSignature.update(b, off, len); 280 | } catch (SignatureException e) { 281 | throw new IOException("SignatureException: " + e); 282 | } 283 | super.write(b, off, len); 284 | mCount += len; 285 | } 286 | 287 | public int size() { 288 | return mCount; 289 | } 290 | } 291 | 292 | /** Write a .SF file with a digest of the specified manifest. */ 293 | private static void writeSignatureFile(Manifest manifest, SignatureOutputStream out) 294 | throws IOException, GeneralSecurityException { 295 | Manifest sf = new Manifest(); 296 | Attributes main = sf.getMainAttributes(); 297 | main.putValue("Signature-Version", "1.0"); 298 | 299 | Base64.Encoder base64 = Base64.getEncoder().withoutPadding(); 300 | MessageDigest md = MessageDigest.getInstance("SHA1"); 301 | PrintStream print = new PrintStream( 302 | new DigestOutputStream(new ByteArrayOutputStream(), md), 303 | true, "UTF-8"); 304 | 305 | // Digest of the entire manifest 306 | manifest.write(print); 307 | print.flush(); 308 | main.putValue("SHA1-Digest-Manifest", base64.encodeToString(md.digest())); 309 | 310 | Map entries = manifest.getEntries(); 311 | for (Map.Entry entry : entries.entrySet()) { 312 | // Digest of the manifest stanza for this entry. 313 | print.print("Name: " + entry.getKey() + "\r\n"); 314 | for (Map.Entry att : entry.getValue().entrySet()) { 315 | print.print(att.getKey() + ": " + att.getValue() + "\r\n"); 316 | } 317 | print.print("\r\n"); 318 | print.flush(); 319 | 320 | Attributes sfAttr = new Attributes(); 321 | sfAttr.putValue("SHA1-Digest", base64.encodeToString(md.digest())); 322 | sf.getEntries().put(entry.getKey(), sfAttr); 323 | } 324 | 325 | sf.write(out); 326 | 327 | // A bug in the java.util.jar implementation of Android platforms 328 | // up to version 1.6 will cause a spurious IOException to be thrown 329 | // if the length of the signature file is a multiple of 1024 bytes. 330 | // As a workaround, add an extra CRLF in this case. 331 | if ((out.size() % 1024) == 0) { 332 | out.write('\r'); 333 | out.write('\n'); 334 | } 335 | } 336 | 337 | /** Write a .RSA file with a digital signature. */ 338 | private static void writeSignatureBlock( 339 | Signature signature, X509Certificate publicKey, OutputStream out) 340 | throws IOException, GeneralSecurityException { 341 | SignerInfo signerInfo = new SignerInfo( 342 | new X500Name(publicKey.getIssuerX500Principal().getName()), 343 | publicKey.getSerialNumber(), 344 | AlgorithmId.get("SHA1"), 345 | AlgorithmId.get("RSA"), 346 | signature.sign()); 347 | 348 | PKCS7 pkcs7 = new PKCS7( 349 | new AlgorithmId[] { AlgorithmId.get("SHA1") }, 350 | new ContentInfo(ContentInfo.DATA_OID, null), 351 | new X509Certificate[] { publicKey }, 352 | new SignerInfo[] { signerInfo }); 353 | 354 | pkcs7.encodeSignedData(out); 355 | } 356 | 357 | private static void signWholeOutputFile(byte[] zipData, 358 | OutputStream outputStream, 359 | X509Certificate publicKey, 360 | PrivateKey privateKey) 361 | throws IOException, GeneralSecurityException { 362 | 363 | // For a zip with no archive comment, the 364 | // end-of-central-directory record will be 22 bytes long, so 365 | // we expect to find the EOCD marker 22 bytes from the end. 366 | if (zipData[zipData.length-22] != 0x50 || 367 | zipData[zipData.length-21] != 0x4b || 368 | zipData[zipData.length-20] != 0x05 || 369 | zipData[zipData.length-19] != 0x06) { 370 | throw new IllegalArgumentException("zip data already has an archive comment"); 371 | } 372 | 373 | Signature signature = Signature.getInstance("SHA1withRSA"); 374 | signature.initSign(privateKey); 375 | signature.update(zipData, 0, zipData.length-2); 376 | 377 | ByteArrayOutputStream temp = new ByteArrayOutputStream(); 378 | 379 | // put a readable message and a null char at the start of the 380 | // archive comment, so that tools that display the comment 381 | // (hopefully) show something sensible. 382 | // TODO: anything more useful we can put in this message? 383 | byte[] message = "signed by SignApk".getBytes("UTF-8"); 384 | temp.write(message); 385 | temp.write(0); 386 | writeSignatureBlock(signature, publicKey, temp); 387 | int total_size = temp.size() + 6; 388 | if (total_size > 0xffff) { 389 | throw new IllegalArgumentException("signature is too big for ZIP file comment"); 390 | } 391 | // signature starts this many bytes from the end of the file 392 | int signature_start = total_size - message.length - 1; 393 | temp.write(signature_start & 0xff); 394 | temp.write((signature_start >> 8) & 0xff); 395 | // Why the 0xff bytes? In a zip file with no archive comment, 396 | // bytes [-6:-2] of the file are the little-endian offset from 397 | // the start of the file to the central directory. So for the 398 | // two high bytes to be 0xff 0xff, the archive would have to 399 | // be nearly 4GB in side. So it's unlikely that a real 400 | // commentless archive would have 0xffs here, and lets us tell 401 | // an old signed archive from a new one. 402 | temp.write(0xff); 403 | temp.write(0xff); 404 | temp.write(total_size & 0xff); 405 | temp.write((total_size >> 8) & 0xff); 406 | temp.flush(); 407 | 408 | // Signature verification checks that the EOCD header is the 409 | // last such sequence in the file (to avoid minzip finding a 410 | // fake EOCD appended after the signature in its scan). The 411 | // odds of producing this sequence by chance are very low, but 412 | // let's catch it here if it does. 413 | byte[] b = temp.toByteArray(); 414 | for (int i = 0; i < b.length-3; ++i) { 415 | if (b[i] == 0x50 && b[i+1] == 0x4b && b[i+2] == 0x05 && b[i+3] == 0x06) { 416 | throw new IllegalArgumentException("found spurious EOCD header at " + i); 417 | } 418 | } 419 | 420 | outputStream.write(zipData, 0, zipData.length-2); 421 | outputStream.write(total_size & 0xff); 422 | outputStream.write((total_size >> 8) & 0xff); 423 | temp.writeTo(outputStream); 424 | } 425 | 426 | /** 427 | * Copy all the files in a manifest from input to output. We set 428 | * the modification times in the output to a fixed time, so as to 429 | * reduce variation in the output file and make incremental OTAs 430 | * more efficient. 431 | */ 432 | private static void copyFiles(Manifest manifest, 433 | JarFile in, JarOutputStream out, long timestamp) throws IOException { 434 | byte[] buffer = new byte[4096]; 435 | int num; 436 | 437 | Map entries = manifest.getEntries(); 438 | List names = new ArrayList(entries.keySet()); 439 | Collections.sort(names); 440 | for (String name : names) { 441 | JarEntry inEntry = in.getJarEntry(name); 442 | JarEntry outEntry = null; 443 | if (inEntry.getMethod() == JarEntry.STORED) { 444 | // Preserve the STORED method of the input entry. 445 | outEntry = new JarEntry(inEntry); 446 | } else { 447 | // Create a new entry so that the compressed len is recomputed. 448 | outEntry = new JarEntry(name); 449 | } 450 | outEntry.setTime(timestamp); 451 | out.putNextEntry(outEntry); 452 | 453 | InputStream data = in.getInputStream(inEntry); 454 | while ((num = data.read(buffer)) > 0) { 455 | out.write(buffer, 0, num); 456 | } 457 | out.flush(); 458 | } 459 | } 460 | 461 | public static void main(String[] args) { 462 | if (args.length != 4 && args.length != 5) { 463 | System.err.println("Usage: signapk [-w] " + 464 | "publickey.x509[.pem] privatekey.pk8 " + 465 | "input.jar output.jar"); 466 | System.exit(2); 467 | } 468 | 469 | boolean signWholeFile = false; 470 | int argstart = 0; 471 | if (args[0].equals("-w")) { 472 | signWholeFile = true; 473 | argstart = 1; 474 | } 475 | 476 | JarFile inputJar = null; 477 | JarOutputStream outputJar = null; 478 | FileOutputStream outputFile = null; 479 | 480 | try { 481 | File publicKeyFile = new File(args[argstart+0]); 482 | X509Certificate publicKey = readPublicKey(publicKeyFile); 483 | 484 | // Assume the certificate is valid for at least an hour. 485 | long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000; 486 | 487 | PrivateKey privateKey = readPrivateKey(new File(args[argstart+1])); 488 | inputJar = new JarFile(new File(args[argstart+2]), false); // Don't verify. 489 | 490 | OutputStream outputStream = null; 491 | if (signWholeFile) { 492 | outputStream = new ByteArrayOutputStream(); 493 | } else { 494 | outputStream = outputFile = new FileOutputStream(args[argstart+3]); 495 | } 496 | outputJar = new JarOutputStream(outputStream); 497 | outputJar.setLevel(9); 498 | 499 | JarEntry je; 500 | 501 | Manifest manifest = addDigestsToManifest(inputJar); 502 | 503 | // Everything else 504 | copyFiles(manifest, inputJar, outputJar, timestamp); 505 | 506 | // otacert 507 | if (signWholeFile) { 508 | addOtacert(outputJar, publicKeyFile, timestamp, manifest); 509 | } 510 | 511 | // MANIFEST.MF 512 | je = new JarEntry(JarFile.MANIFEST_NAME); 513 | je.setTime(timestamp); 514 | outputJar.putNextEntry(je); 515 | manifest.write(outputJar); 516 | 517 | // CERT.SF 518 | Signature signature = Signature.getInstance("SHA1withRSA"); 519 | signature.initSign(privateKey); 520 | je = new JarEntry(CERT_SF_NAME); 521 | je.setTime(timestamp); 522 | outputJar.putNextEntry(je); 523 | writeSignatureFile(manifest, 524 | new SignatureOutputStream(outputJar, signature)); 525 | 526 | // CERT.RSA 527 | je = new JarEntry(CERT_RSA_NAME); 528 | je.setTime(timestamp); 529 | outputJar.putNextEntry(je); 530 | writeSignatureBlock(signature, publicKey, outputJar); 531 | 532 | outputJar.close(); 533 | outputJar = null; 534 | outputStream.flush(); 535 | 536 | if (signWholeFile) { 537 | outputFile = new FileOutputStream(args[argstart+3]); 538 | signWholeOutputFile(((ByteArrayOutputStream)outputStream).toByteArray(), 539 | outputFile, publicKey, privateKey); 540 | } 541 | } catch (Exception e) { 542 | e.printStackTrace(); 543 | System.exit(1); 544 | } finally { 545 | try { 546 | if (inputJar != null) inputJar.close(); 547 | if (outputFile != null) outputFile.close(); 548 | } catch (IOException e) { 549 | e.printStackTrace(); 550 | System.exit(1); 551 | } 552 | } 553 | } 554 | } 555 | --------------------------------------------------------------------------------