├── .gitignore ├── COPYING ├── LICENSES ├── Apache-2.0 ├── BSD-3-Clause ├── GPL-3.0-or-later └── MIT ├── README.md ├── SERVICES.md ├── app ├── .gitignore ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── io │ │ └── github │ │ └── muntashirakon │ │ └── adb │ │ └── testapp │ │ ├── AdbConnectionManager.java │ │ ├── App.java │ │ ├── MainActivity.java │ │ └── MainViewModel.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ ├── ic_add_link.xml │ ├── ic_launcher_background.xml │ ├── ic_link.xml │ └── ic_link_off.xml │ ├── layout │ ├── activity_main.xml │ └── dialog_input.xml │ ├── menu │ └── actions_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ └── strings.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── libadb ├── .gitignore ├── build.gradle └── src │ ├── main │ ├── AndroidManifest.xml │ └── java │ │ └── io │ │ └── github │ │ └── muntashirakon │ │ └── adb │ │ ├── AbsAdbConnectionManager.java │ │ ├── AdbAuthenticationFailedException.java │ │ ├── AdbConnection.java │ │ ├── AdbInputStream.java │ │ ├── AdbOutputStream.java │ │ ├── AdbPairingRequiredException.java │ │ ├── AdbProtocol.java │ │ ├── AdbStream.java │ │ ├── AndroidPubkey.java │ │ ├── ByteArrayNoThrowOutputStream.java │ │ ├── KeyPair.java │ │ ├── LocalServices.java │ │ ├── PRNGFixes.java │ │ ├── PairingAuthCtx.java │ │ ├── PairingConnectionCtx.java │ │ ├── SslUtils.java │ │ ├── StringCompat.java │ │ └── android │ │ ├── AdbMdns.java │ │ ├── AndroidUtils.java │ │ └── package.html │ └── test │ └── java │ └── io │ └── github │ └── muntashirakon │ └── adb │ └── AndroidPubkeyTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .gradle 3 | .DS_Store 4 | /build 5 | local.properties -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | LibADB Android is provided under: 2 | 3 | SPDX-License-Identifier: GPL-3.0-or-later or Apache-2.0 4 | 5 | Being under the terms of the GNU General Public License version 3 or 6 | later, according with: 7 | 8 | LICENSES/GPL-3.0-or-later 9 | 10 | or Apache License, Version 2.0, according with: 11 | 12 | LICENSES/Apache-2.0 13 | 14 | In addition, other licenses may also apply. Please navigate to: 15 | 16 | LICENSES/ 17 | 18 | to see all the licenses used in this project. 19 | 20 | All contributions to the LibADB Android are subject to this COPYING 21 | file. 22 | -------------------------------------------------------------------------------- /LICENSES/Apache-2.0: -------------------------------------------------------------------------------- 1 | Valid-License-Identifier: Apache-2.0 2 | SPDX-URL: https://spdx.org/licenses/Apache-2.0.html 3 | Usage-Guide: 4 | To use the Apache License version 2.0 put the following SPDX tag/value 5 | pair into a comment according to the placement guidelines in the 6 | licensing rules documentation: 7 | SPDX-License-Identifier: Apache-2.0 8 | Do NOT use this license unless the files are copied from another work 9 | under the same license. In such cases, use "AND GPL-3.0-or-later" so 10 | that your contributions are under GPL-3.0+ license. 11 | License-Text: 12 | 13 | Apache License 14 | 15 | Version 2.0, January 2004 16 | 17 | http://www.apache.org/licenses/ 18 | 19 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 20 | 21 | 1. Definitions. 22 | 23 | "License" shall mean the terms and conditions for use, reproduction, and 24 | distribution as defined by Sections 1 through 9 of this document. 25 | 26 | "Licensor" shall mean the copyright owner or entity authorized by the 27 | copyright owner that is granting the License. 28 | 29 | "Legal Entity" shall mean the union of the acting entity and all other 30 | entities that control, are controlled by, or are under common control with 31 | that entity. For the purposes of this definition, "control" means (i) the 32 | power, direct or indirect, to cause the direction or management of such 33 | entity, whether by contract or otherwise, or (ii) ownership of fifty 34 | percent (50%) or more of the outstanding shares, or (iii) beneficial 35 | ownership of such entity. 36 | 37 | "You" (or "Your") shall mean an individual or Legal Entity exercising 38 | permissions granted by this License. 39 | 40 | "Source" form shall mean the preferred form for making modifications, 41 | including but not limited to software source code, documentation source, 42 | and configuration files. 43 | 44 | "Object" form shall mean any form resulting from mechanical transformation 45 | or translation of a Source form, including but not limited to compiled 46 | object code, generated documentation, and conversions to other media types. 47 | 48 | "Work" shall mean the work of authorship, whether in Source or Object form, 49 | made available under the License, as indicated by a copyright notice that 50 | is included in or attached to the work (an example is provided in the 51 | Appendix below). 52 | 53 | "Derivative Works" shall mean any work, whether in Source or Object form, 54 | that is based on (or derived from) the Work and for which the editorial 55 | revisions, annotations, elaborations, or other modifications represent, as 56 | a whole, an original work of authorship. For the purposes of this License, 57 | Derivative Works shall not include works that remain separable from, or 58 | merely link (or bind by name) to the interfaces of, the Work and Derivative 59 | Works thereof. 60 | 61 | "Contribution" shall mean any work of authorship, including the original 62 | version of the Work and any modifications or additions to that Work or 63 | Derivative Works thereof, that is intentionally submitted to Licensor for 64 | inclusion in the Work by the copyright owner or by an individual or Legal 65 | Entity authorized to submit on behalf of the copyright owner. For the 66 | purposes of this definition, "submitted" means any form of electronic, 67 | verbal, or written communication sent to the Licensor or its 68 | representatives, including but not limited to communication on electronic 69 | mailing lists, source code control systems, and issue tracking systems that 70 | are managed by, or on behalf of, the Licensor for the purpose of discussing 71 | and improving the Work, but excluding communication that is conspicuously 72 | marked or otherwise designated in writing by the copyright owner as "Not a 73 | Contribution." 74 | 75 | "Contributor" shall mean Licensor and any individual or Legal Entity on 76 | behalf of whom a Contribution has been received by Licensor and 77 | subsequently incorporated within the Work. 78 | 79 | 2. Grant of Copyright License. Subject to the terms and conditions of this 80 | License, each Contributor hereby grants to You a perpetual, worldwide, 81 | non-exclusive, no-charge, royalty-free, irrevocable copyright license to 82 | reproduce, prepare Derivative Works of, publicly display, publicly 83 | perform, sublicense, and distribute the Work and such Derivative Works 84 | in Source or Object form. 85 | 86 | 3. Grant of Patent License. Subject to the terms and conditions of this 87 | License, each Contributor hereby grants to You a perpetual, worldwide, 88 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in 89 | this section) patent license to make, have made, use, offer to sell, 90 | sell, import, and otherwise transfer the Work, where such license 91 | applies only to those patent claims licensable by such Contributor that 92 | are necessarily infringed by their Contribution(s) alone or by 93 | combination of their Contribution(s) with the Work to which such 94 | Contribution(s) was submitted. If You institute patent litigation 95 | against any entity (including a cross-claim or counterclaim in a 96 | lawsuit) alleging that the Work or a Contribution incorporated within 97 | the Work constitutes direct or contributory patent infringement, then 98 | any patent licenses granted to You under this License for that Work 99 | shall terminate as of the date such litigation is filed. 100 | 101 | 4. Redistribution. You may reproduce and distribute copies of the Work or 102 | Derivative Works thereof in any medium, with or without modifications, 103 | and in Source or Object form, provided that You meet the following 104 | conditions: 105 | 106 | a. You must give any other recipients of the Work or Derivative Works a 107 | copy of this License; and 108 | 109 | b. You must cause any modified files to carry prominent notices stating 110 | that You changed the files; and 111 | 112 | c. You must retain, in the Source form of any Derivative Works that You 113 | distribute, all copyright, patent, trademark, and attribution notices 114 | from the Source form of the Work, excluding those notices that do not 115 | pertain to any part of the Derivative Works; and 116 | 117 | d. If the Work includes a "NOTICE" text file as part of its 118 | distribution, then any Derivative Works that You distribute must 119 | include a readable copy of the attribution notices contained within 120 | such NOTICE file, excluding those notices that do not pertain to any 121 | part of the Derivative Works, in at least one of the following 122 | places: within a NOTICE text file distributed as part of the 123 | Derivative Works; within the Source form or documentation, if 124 | provided along with the Derivative Works; or, within a display 125 | generated by the Derivative Works, if and wherever such third-party 126 | notices normally appear. The contents of the NOTICE file are for 127 | informational purposes only and do not modify the License. You may 128 | add Your own attribution notices within Derivative Works that You 129 | distribute, alongside or as an addendum to the NOTICE text from the 130 | Work, provided that such additional attribution notices cannot be 131 | construed as modifying the License. 132 | 133 | You may add Your own copyright statement to Your modifications and may 134 | provide additional or different license terms and conditions for use, 135 | reproduction, or distribution of Your modifications, or for any such 136 | Derivative Works as a whole, provided Your use, reproduction, and 137 | distribution of the Work otherwise complies with the conditions stated 138 | in this License. 139 | 140 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 141 | Contribution intentionally submitted for inclusion in the Work by You to 142 | the Licensor shall be under the terms and conditions of this License, 143 | without any additional terms or conditions. Notwithstanding the above, 144 | nothing herein shall supersede or modify the terms of any separate 145 | license agreement you may have executed with Licensor regarding such 146 | Contributions. 147 | 148 | 6. Trademarks. This License does not grant permission to use the trade 149 | names, trademarks, service marks, or product names of the Licensor, 150 | except as required for reasonable and customary use in describing the 151 | origin of the Work and reproducing the content of the NOTICE file. 152 | 153 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to 154 | in writing, Licensor provides the Work (and each Contributor provides 155 | its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS 156 | OF ANY KIND, either express or implied, including, without limitation, 157 | any warranties or conditions of TITLE, NON-INFRINGEMENT, 158 | MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely 159 | responsible for determining the appropriateness of using or 160 | redistributing the Work and assume any risks associated with Your 161 | exercise of permissions under this License. 162 | 163 | 8. Limitation of Liability. In no event and under no legal theory, whether 164 | in tort (including negligence), contract, or otherwise, unless required 165 | by applicable law (such as deliberate and grossly negligent acts) or 166 | agreed to in writing, shall any Contributor be liable to You for 167 | damages, including any direct, indirect, special, incidental, or 168 | consequential damages of any character arising as a result of this 169 | License or out of the use or inability to use the Work (including but 170 | not limited to damages for loss of goodwill, work stoppage, computer 171 | failure or malfunction, or any and all other commercial damages or 172 | losses), even if such Contributor has been advised of the possibility of 173 | such damages. 174 | 175 | 9. Accepting Warranty or Additional Liability. While redistributing the 176 | Work or Derivative Works thereof, You may choose to offer, and charge a 177 | fee for, acceptance of support, warranty, indemnity, or other liability 178 | obligations and/or rights consistent with this License. However, in 179 | accepting such obligations, You may act only on Your own behalf and on 180 | Your sole responsibility, not on behalf of any other Contributor, and 181 | only if You agree to indemnify, defend, and hold each Contributor 182 | harmless for any liability incurred by, or claims asserted against, such 183 | Contributor by reason of your accepting any such warranty or additional 184 | liability. 185 | 186 | END OF TERMS AND CONDITIONS -------------------------------------------------------------------------------- /LICENSES/BSD-3-Clause: -------------------------------------------------------------------------------- 1 | Valid-License-Identifier: BSD-3-Clause 2 | SPDX-URL: https://spdx.org/licenses/BSD-3-Clause.html 3 | Usage-Guide: 4 | To use the BSD 3-clause "New" or "Revised" License put the following SPDX 5 | tag/value pair into a comment according to the placement guidelines in 6 | the licensing rules documentation: 7 | SPDX-License-Identifier: BSD-3-Clause 8 | License-Text: 9 | 10 | Copyright (c) . All rights reserved. 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modification, are permitted provided that the following conditions are met: 14 | 15 | 1. Redistributions of source code must retain the above copyright notice, 16 | this list of conditions and the following disclaimer. 17 | 18 | 2. Redistributions in binary form must reproduce the above copyright 19 | notice, this list of conditions and the following disclaimer in the 20 | documentation and/or other materials provided with the distribution. 21 | 22 | 3. Neither the name of the copyright holder nor the names of its 23 | contributors may be used to endorse or promote products derived from this 24 | software without specific prior written permission. 25 | 26 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 27 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 28 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 29 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE 30 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 31 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 32 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 33 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 34 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 35 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 36 | POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /LICENSES/MIT: -------------------------------------------------------------------------------- 1 | Valid-License-Identifier: MIT 2 | SPDX-URL: https://spdx.org/licenses/MIT.html 3 | Usage-Guide: 4 | To use the MIT License put the following SPDX tag/value pair into a 5 | comment according to the placement guidelines in the licensing rules 6 | documentation: 7 | SPDX-License-Identifier: MIT 8 | Do NOT use this license unless the files are copied from another work 9 | under the same license. In such cases, use "AND GPL-3.0-or-later" so 10 | that your contributions are under GPL-3.0+ license. 11 | License-Text: 12 | 13 | MIT License 14 | 15 | Copyright (c) 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a 18 | copy of this software and associated documentation files (the "Software"), 19 | to deal in the Software without restriction, including without limitation 20 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 21 | and/or sell copies of the Software, and to permit persons to whom the 22 | Software is furnished to do so, subject to the following conditions: 23 | 24 | The above copyright notice and this permission notice shall be included in 25 | all copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 32 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 33 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LibADB Android 2 | 3 | ADB library for Android. It enables an app to connect to the ADB daemon (`adbd` process) belonging to the same or a 4 | different device and execute arbitrary services or commands (via `shell:` service). 5 | 6 | **Disclaimer:** This library has never gone through a security audit. Please, proceed with caution if security is 7 | crucial for your app. Avoid using the APIs for reasons other than connecting or using ADB. For the safety of your app 8 | and its users, open a remote service instead of using ADB and ask the user to disconnect Wireless debugging. 9 | 10 | ## Getting Started 11 | ### Adding Dependencies 12 | LibADB Android is available via JitPack. 13 | 14 | ```groovy 15 | // Top level build file 16 | repositories { 17 | mavenCentral() 18 | maven { url "https://jitpack.io" } 19 | } 20 | 21 | // Add to dependencies section 22 | dependencies { 23 | // Add this library 24 | implementation 'com.github.MuntashirAkon:libadb-android:1.0.1' 25 | 26 | // Library to generate X509Certificate. You can also use BouncyCastle for 27 | // this. See example for use-case. 28 | // implementation 'com.github.MuntashirAkon:sun-security-android:1.1' 29 | 30 | // Bypass hidden API if you want to use the Android default Conscrypt in 31 | // Android 9 (Pie) or later. It also requires additional steps. See 32 | // https://github.com/LSPosed/AndroidHiddenApiBypass to find out more about 33 | // this. 34 | // implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:2.0' 35 | 36 | // Use custom Conscrypt library. If you want to connect to a remote ADB 37 | // daemon instead of the device the app is currently running or do not want 38 | // to bypass hidden API, this is the recommended choice. 39 | implementation 'org.conscrypt:conscrypt-android:2.5.2' 40 | } 41 | ``` 42 | 43 | If you're using the custom Conscrypt library in order to connect to a remote ADB daemon and the app targets Android 44 | version below 4.4, you have to extend `android.app.Application` to apply fixes for the random number generation: 45 | ```java 46 | public class MyAwesomeApp extends Application { 47 | @Override 48 | public void onCreate() { 49 | super.onCreate(); 50 | // Fix random number generation in Android versions below 4.4. 51 | PRNGFixes.apply(); 52 | } 53 | 54 | @Override 55 | protected void attachBaseContext(Context base) { 56 | super.attachBaseContext(base); 57 | // Uncomment the following line if you want to bypass hidden API as 58 | // described above. 59 | // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 60 | // HiddenApiBypass.addHiddenApiExemptions("L"); 61 | // } 62 | } 63 | } 64 | ``` 65 | 66 | **Notice:** Conscrypt supports only API 9 (Gingerbread) or later, meaning you cannot use ADB pairing or any TLSv1.3 67 | features in API less than 9. The corresponding methods are already annotated properly. So, you don't have to worry about 68 | compatibility issues that may arise when your app's minimum SDK is set to one of the unsupported versions. 69 | 70 | ### Configuring ADB 71 | Instead of doing everything manually, you can create a concrete implementation of the `AbsAdbConnectionManager` class. 72 | Example: 73 | 74 | ```java 75 | public class AdbConnectionManager extends AbsAdbConnectionManager { 76 | private static AbsAdbConnectionManager INSTANCE; 77 | 78 | public static AbsAdbConnectionManager getInstance() throws Exception { 79 | if (INSTANCE == null) { 80 | INSTANCE = new AdbConnectionManager(); 81 | } 82 | return INSTANCE; 83 | } 84 | 85 | private PrivateKey mPrivateKey; 86 | private Certificate mCertificate; 87 | 88 | private AdbConnectionManager() throws Exception { 89 | // Set the API version whose `adbd` is running 90 | setApi(Build.VERSION.SDK_INT); 91 | // TODO: Load private key and certificate (along with public key) from 92 | // some place such as KeyStore or file system. 93 | mPrivateKey = ...; 94 | mCertificate = ...; 95 | if (mPrivateKey == null) { 96 | // Generate a new key pair 97 | int keySize = 2048; 98 | KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); 99 | keyPairGenerator.initialize(keySize, SecureRandom.getInstance("SHA1PRNG")); 100 | KeyPair generateKeyPair = keyPairGenerator.generateKeyPair(); 101 | PublicKey publicKey = generateKeyPair.getPublic(); 102 | mPrivateKey = generateKeyPair.getPrivate(); 103 | // Generate a certificate 104 | // On Android, it requires sun.security-android library as mentioned 105 | // above. 106 | String subject = "CN=My Awesome App"; 107 | String algorithmName = "SHA512withRSA"; 108 | long expiryDate = System.currentTimeMillis() + 86400000; 109 | CertificateExtensions certificateExtensions = new CertificateExtensions(); 110 | certificateExtensions.set("SubjectKeyIdentifier", new SubjectKeyIdentifierExtension( 111 | new KeyIdentifier(publicKey).getIdentifier())); 112 | X500Name x500Name = new X500Name(subject); 113 | Date notBefore = new Date(); 114 | Date notAfter = new Date(expiryDate); 115 | certificateExtensions.set("PrivateKeyUsage", new PrivateKeyUsageExtension(notBefore, notAfter)); 116 | CertificateValidity certificateValidity = new CertificateValidity(notBefore, notAfter); 117 | X509CertInfo x509CertInfo = new X509CertInfo(); 118 | x509CertInfo.set("version", new CertificateVersion(2)); 119 | x509CertInfo.set("serialNumber", new CertificateSerialNumber(new Random().nextInt() & Integer.MAX_VALUE)); 120 | x509CertInfo.set("algorithmID", new CertificateAlgorithmId(AlgorithmId.get(algorithmName))); 121 | x509CertInfo.set("subject", new CertificateSubjectName(x500Name)); 122 | x509CertInfo.set("key", new CertificateX509Key(publicKey)); 123 | x509CertInfo.set("validity", certificateValidity); 124 | x509CertInfo.set("issuer", new CertificateIssuerName(x500Name)); 125 | x509CertInfo.set("extensions", certificateExtensions); 126 | X509CertImpl x509CertImpl = new X509CertImpl(x509CertInfo); 127 | x509CertImpl.sign(mPrivateKey, algorithmName); 128 | mCertificate = x509CertImpl; 129 | // TODO: Store the key pair to some place else. 130 | } 131 | } 132 | 133 | @NonNull 134 | @Override 135 | protected PrivateKey getPrivateKey() { 136 | return mPrivateKey; 137 | } 138 | 139 | @NonNull 140 | @Override 141 | protected Certificate getCertificate() { 142 | return mCertificate; 143 | } 144 | 145 | @NonNull 146 | @Override 147 | protected String getDeviceName() { 148 | return "MyAwesomeApp"; 149 | } 150 | } 151 | ``` 152 | 153 | ### Connecting to ADB 154 | 155 | You can connect to ADB in several ways from the `AbsAdbConnectionManager`: 156 | 157 | | Method | Description | 158 | |---------------------------------|---------------------------------------------------------------------------------------------------------------| 159 | | `connect(host, port)` | Connect using a host address and a port number | 160 | | `connect(port)` | Connect using a host address set by `setHostAddress()` and a port number | 161 | | `connectTcp(Context, timeout)` | (SDK 16+) Discover host address and port number automatically for ADB over TCP and connect to it | 162 | | `connectTls(Context, timeout)` | (SDK 16+) Discover host address and port number automatically for TLS (from Android 9) and connect to it | 163 | | `autoConnect(Context, timeout)` | (SDK 16+) Discover host address and port number automatically for both ADB over TCP and TLS and connect to it | 164 | 165 | ### Wireless Debugging 166 | Internally, ADB over TCP and Wireless Debugging are very similar except Wireless Debugging requires an extra step of 167 | _pairing_ the device. In order to pair a new device, you can simply invoke `AdbConnectionManager.getInstance().pair(host, port, pairingCode)`. 168 | After the pairing, you can connect to ADB via the usual `connect()` methods without any additional steps. 169 | 170 | ### Opening ADB Shell for Executing Arbitrary Commands 171 | Simply use `AdbConnectionManager.getInstance().openStream("shell:")`. This will return an `AdbStream` which can be used 172 | to read/write to the ADB shell via `AdbStream#openInputStream()` and `AdbStream#openOutputStream()` methods 173 | respectively like a normal Java `Process`. While it is possible to read/write in the same thread (first write and then 174 | read), this is not recommended because the shell might be stuck indefinitely for commands such as `top`. 175 | 176 | **NOTE:** If you want to create a full-featured terminal emulator, this approach isn't recommended. Instead, you should 177 | create a remote service via `app_process` or start an SSH server and connect to it. 178 | 179 | ### Other services 180 | You can also use other services via the `AdbConnectionManager#openStream()` methods. See [SERVICES.md](./SERVICES.md) 181 | for more information. 182 | 183 | ## For Java (non-Android) Projects 184 | It is possible to modify this library to work on non-Android project. But it isn't supported because Spake2-Java only 185 | provides stable releases for Android. However, you can incorporate this library in your project by manually compiling 186 | Spake2 library for your platforms. 187 | 188 | ## Contributing 189 | By contributing to this project, you permit your work to be released under the terms of GNU General Public License, 190 | Version 3 or later **or** Apache License, Version 2.0. 191 | 192 | ## License 193 | Copyright 2021 © Muntashir Al-Islam 194 | 195 | Dual licensed under the terms of [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html) or 196 | [Apache-2.0 license](https://www.apache.org/licenses/LICENSE-2.0.html). Use whatever license you need for your project. 197 | 198 | _Note regarding the Apache-2.0 license, this library has an LGPL dependency which may go against the policy of some 199 | organizations such as ASF._ 200 | -------------------------------------------------------------------------------- /SERVICES.md: -------------------------------------------------------------------------------- 1 | # Services 2 | 3 | LibADB only supports local services which can be requested through `AbsAdbConnectionManager#openStream(String)` or 4 | `AdbConnection#open(String)`. 5 | 6 | - `shell:command arg1 arg2 ...` 7 | 8 | Run `command arg1 arg2 ...` in a shell on the device, and return its output and error streams. Note that arguments 9 | must be separated by spaces. If an argument contains a space, it must be quoted with double-quotes. Arguments cannot 10 | contain double quotes or things will go very wrong. 11 | 12 | - `shell:` 13 | 14 | Start an interactive shell session on the device. Redirect stdin/stdout/stderr as appropriate. 15 | 16 | - `dev:` 17 | 18 | Opens a device file and connects the client directly to it for read/write purposes. Useful for debugging, but may 19 | require special privileges and thus may not run on all devices. `` is a full path from the root of the 20 | filesystem. 21 | 22 | - `tcp:` 23 | 24 | Tries to connect to tcp port `` on localhost. 25 | 26 | - `tcp::` 27 | 28 | Tries to connect to tcp port `` on machine `` from the device. This can be useful to debug some 29 | networking/proxy issues that can only be revealed on the device itself. 30 | 31 | - `local:` 32 | 33 | Tries to connect to a Unix domain socket `` on the device. 34 | 35 | - `localreserved:`/ 36 | `localabstract:`/ 37 | `localfilesystem:` 38 | 39 | Variants of `local:` that are used to access other Android socket namespaces. 40 | 41 | - `sync:` 42 | 43 | This starts the file synchronization service, used to implement "adb push" and "adb pull". Since this service is 44 | pretty complex, it will be detailed in a companion document named SYNC.TXT 45 | 46 | - `reverse:` 47 | 48 | This implements the 'adb reverse' feature, i.e. the ability to reverse socket connections from a device to the host. 49 | `` is one of the forwarding commands that are described above, as in: 50 | 51 | list-forward 52 | forward:; 53 | forward:norebind:; 54 | killforward-all 55 | killforward: 56 | 57 | Note that in this case, corresponds to the socket on the device 58 | and corresponds to the socket on the host. 59 | 60 | The output of reverse:list-forward is the same as host:list-forward 61 | except that will be just 'host'. 62 | 63 | ## Reference 64 | - [SERVICES.TXT](https://android.googlesource.com/platform/packages/modules/adb/+/6a85258511fb13ebbbedba4e36616db4c6e970fb/SERVICES.TXT) 65 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | plugins { 4 | id 'com.android.application' 5 | } 6 | 7 | android { 8 | compileSdk 34 9 | namespace "io.github.muntashirakon.adb.testapp" 10 | 11 | defaultConfig { 12 | applicationId "io.github.muntashirakon.adb.testapp" 13 | minSdk 21 14 | targetSdk 34 15 | versionCode 7 16 | versionName "2.2.2" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled false 22 | } 23 | } 24 | compileOptions { 25 | sourceCompatibility JavaVersion.VERSION_1_8 26 | targetCompatibility JavaVersion.VERSION_1_8 27 | } 28 | } 29 | 30 | dependencies { 31 | implementation project(path: ':libadb') 32 | implementation 'androidx.appcompat:appcompat:1.6.1' 33 | implementation 'com.google.android.material:material:1.11.0' 34 | 35 | // Library to generate X509Certificate. You can also use BouncyCastle for this. 36 | implementation 'com.github.MuntashirAkon:sun-security-android:1.1' 37 | 38 | // Bypass hidden API if you want to use Android default conscrypt. It also requires additional steps. 39 | // See https://github.com/LSPosed/AndroidHiddenApiBypass to find out more about this. 40 | // Comment out the line below if you do not need this. 41 | implementation 'org.lsposed.hiddenapibypass:hiddenapibypass:4.3' 42 | 43 | // Uncomment the line below if you want to use the custom conscrypt. 44 | // implementation 'org.conscrypt:conscrypt-android:2.5.2' 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/muntashirakon/adb/testapp/AdbConnectionManager.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb.testapp; 4 | 5 | import android.content.Context; 6 | import android.os.Build; 7 | import android.sun.misc.BASE64Encoder; 8 | import android.sun.security.provider.X509Factory; 9 | import android.sun.security.x509.AlgorithmId; 10 | import android.sun.security.x509.CertificateAlgorithmId; 11 | import android.sun.security.x509.CertificateExtensions; 12 | import android.sun.security.x509.CertificateIssuerName; 13 | import android.sun.security.x509.CertificateSerialNumber; 14 | import android.sun.security.x509.CertificateSubjectName; 15 | import android.sun.security.x509.CertificateValidity; 16 | import android.sun.security.x509.CertificateVersion; 17 | import android.sun.security.x509.CertificateX509Key; 18 | import android.sun.security.x509.KeyIdentifier; 19 | import android.sun.security.x509.PrivateKeyUsageExtension; 20 | import android.sun.security.x509.SubjectKeyIdentifierExtension; 21 | import android.sun.security.x509.X500Name; 22 | import android.sun.security.x509.X509CertImpl; 23 | import android.sun.security.x509.X509CertInfo; 24 | 25 | import androidx.annotation.NonNull; 26 | import androidx.annotation.Nullable; 27 | 28 | import java.io.File; 29 | import java.io.FileInputStream; 30 | import java.io.FileOutputStream; 31 | import java.io.IOException; 32 | import java.io.InputStream; 33 | import java.io.OutputStream; 34 | import java.nio.charset.StandardCharsets; 35 | import java.security.KeyFactory; 36 | import java.security.KeyPair; 37 | import java.security.KeyPairGenerator; 38 | import java.security.NoSuchAlgorithmException; 39 | import java.security.PrivateKey; 40 | import java.security.PublicKey; 41 | import java.security.SecureRandom; 42 | import java.security.cert.Certificate; 43 | import java.security.cert.CertificateEncodingException; 44 | import java.security.cert.CertificateException; 45 | import java.security.cert.CertificateFactory; 46 | import java.security.spec.EncodedKeySpec; 47 | import java.security.spec.InvalidKeySpecException; 48 | import java.security.spec.PKCS8EncodedKeySpec; 49 | import java.util.Date; 50 | import java.util.Random; 51 | 52 | import io.github.muntashirakon.adb.AbsAdbConnectionManager; 53 | 54 | public class AdbConnectionManager extends AbsAdbConnectionManager { 55 | private static AbsAdbConnectionManager INSTANCE; 56 | 57 | public static AbsAdbConnectionManager getInstance(@NonNull Context context) throws Exception { 58 | if (INSTANCE == null) { 59 | INSTANCE = new AdbConnectionManager(context); 60 | } 61 | return INSTANCE; 62 | } 63 | 64 | private PrivateKey mPrivateKey; 65 | private Certificate mCertificate; 66 | 67 | private AdbConnectionManager(@NonNull Context context) throws Exception { 68 | setApi(Build.VERSION.SDK_INT); 69 | mPrivateKey = readPrivateKeyFromFile(context); 70 | mCertificate = readCertificateFromFile(context); 71 | if (mPrivateKey == null) { 72 | // Generate a new key pair 73 | int keySize = 2048; 74 | KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); 75 | keyPairGenerator.initialize(keySize, SecureRandom.getInstance("SHA1PRNG")); 76 | KeyPair generateKeyPair = keyPairGenerator.generateKeyPair(); 77 | PublicKey publicKey = generateKeyPair.getPublic(); 78 | mPrivateKey = generateKeyPair.getPrivate(); 79 | // Generate a new certificate 80 | String subject = "CN=My Awesome App"; 81 | String algorithmName = "SHA512withRSA"; 82 | long expiryDate = System.currentTimeMillis() + 86400000; 83 | CertificateExtensions certificateExtensions = new CertificateExtensions(); 84 | certificateExtensions.set("SubjectKeyIdentifier", new SubjectKeyIdentifierExtension( 85 | new KeyIdentifier(publicKey).getIdentifier())); 86 | X500Name x500Name = new X500Name(subject); 87 | Date notBefore = new Date(); 88 | Date notAfter = new Date(expiryDate); 89 | certificateExtensions.set("PrivateKeyUsage", new PrivateKeyUsageExtension(notBefore, notAfter)); 90 | CertificateValidity certificateValidity = new CertificateValidity(notBefore, notAfter); 91 | X509CertInfo x509CertInfo = new X509CertInfo(); 92 | x509CertInfo.set("version", new CertificateVersion(2)); 93 | x509CertInfo.set("serialNumber", new CertificateSerialNumber(new Random().nextInt() & Integer.MAX_VALUE)); 94 | x509CertInfo.set("algorithmID", new CertificateAlgorithmId(AlgorithmId.get(algorithmName))); 95 | x509CertInfo.set("subject", new CertificateSubjectName(x500Name)); 96 | x509CertInfo.set("key", new CertificateX509Key(publicKey)); 97 | x509CertInfo.set("validity", certificateValidity); 98 | x509CertInfo.set("issuer", new CertificateIssuerName(x500Name)); 99 | x509CertInfo.set("extensions", certificateExtensions); 100 | X509CertImpl x509CertImpl = new X509CertImpl(x509CertInfo); 101 | x509CertImpl.sign(mPrivateKey, algorithmName); 102 | mCertificate = x509CertImpl; 103 | // Write files 104 | writePrivateKeyToFile(context, mPrivateKey); 105 | writeCertificateToFile(context, mCertificate); 106 | } 107 | } 108 | 109 | @NonNull 110 | @Override 111 | protected PrivateKey getPrivateKey() { 112 | return mPrivateKey; 113 | } 114 | 115 | @NonNull 116 | @Override 117 | protected Certificate getCertificate() { 118 | return mCertificate; 119 | } 120 | 121 | @NonNull 122 | @Override 123 | protected String getDeviceName() { 124 | return "MyAwesomeApp"; 125 | } 126 | 127 | @Nullable 128 | private static Certificate readCertificateFromFile(@NonNull Context context) 129 | throws IOException, CertificateException { 130 | File certFile = new File(context.getFilesDir(), "cert.pem"); 131 | if (!certFile.exists()) return null; 132 | try (InputStream cert = new FileInputStream(certFile)) { 133 | return CertificateFactory.getInstance("X.509").generateCertificate(cert); 134 | } 135 | } 136 | 137 | private static void writeCertificateToFile(@NonNull Context context, @NonNull Certificate certificate) 138 | throws CertificateEncodingException, IOException { 139 | File certFile = new File(context.getFilesDir(), "cert.pem"); 140 | BASE64Encoder encoder = new BASE64Encoder(); 141 | try (OutputStream os = new FileOutputStream(certFile)) { 142 | os.write(X509Factory.BEGIN_CERT.getBytes(StandardCharsets.UTF_8)); 143 | os.write('\n'); 144 | encoder.encode(certificate.getEncoded(), os); 145 | os.write('\n'); 146 | os.write(X509Factory.END_CERT.getBytes(StandardCharsets.UTF_8)); 147 | } 148 | } 149 | 150 | @Nullable 151 | private static PrivateKey readPrivateKeyFromFile(@NonNull Context context) 152 | throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { 153 | File privateKeyFile = new File(context.getFilesDir(), "private.key"); 154 | if (!privateKeyFile.exists()) return null; 155 | byte[] privKeyBytes = new byte[(int) privateKeyFile.length()]; 156 | try (InputStream is = new FileInputStream(privateKeyFile)) { 157 | is.read(privKeyBytes); 158 | } 159 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 160 | EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privKeyBytes); 161 | return keyFactory.generatePrivate(privateKeySpec); 162 | } 163 | 164 | private static void writePrivateKeyToFile(@NonNull Context context, @NonNull PrivateKey privateKey) 165 | throws IOException { 166 | File privateKeyFile = new File(context.getFilesDir(), "private.key"); 167 | try (OutputStream os = new FileOutputStream(privateKeyFile)) { 168 | os.write(privateKey.getEncoded()); 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/muntashirakon/adb/testapp/App.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb.testapp; 4 | 5 | import android.app.Application; 6 | import android.content.Context; 7 | import android.os.Build; 8 | 9 | import org.lsposed.hiddenapibypass.HiddenApiBypass; 10 | 11 | import io.github.muntashirakon.adb.PRNGFixes; 12 | 13 | public class App extends Application { 14 | @Override 15 | public void onCreate() { 16 | super.onCreate(); 17 | PRNGFixes.apply(); 18 | } 19 | 20 | @Override 21 | protected void attachBaseContext(Context base) { 22 | super.attachBaseContext(base); 23 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 24 | HiddenApiBypass.addHiddenApiExemptions("L"); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/muntashirakon/adb/testapp/MainActivity.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb.testapp; 4 | 5 | import android.os.Build; 6 | import android.os.Bundle; 7 | import android.text.InputType; 8 | import android.text.TextUtils; 9 | import android.view.Menu; 10 | import android.view.MenuItem; 11 | import android.view.View; 12 | import android.view.inputmethod.EditorInfo; 13 | import android.view.inputmethod.InputMethodManager; 14 | import android.widget.ScrollView; 15 | import android.widget.Toast; 16 | 17 | import androidx.annotation.NonNull; 18 | import androidx.appcompat.app.AppCompatActivity; 19 | import androidx.appcompat.widget.AppCompatEditText; 20 | import androidx.appcompat.widget.AppCompatTextView; 21 | import androidx.lifecycle.ViewModelProvider; 22 | 23 | import com.google.android.material.dialog.MaterialAlertDialogBuilder; 24 | import com.google.android.material.textfield.TextInputEditText; 25 | 26 | import java.util.Objects; 27 | 28 | public class MainActivity extends AppCompatActivity { 29 | private static final int DEFAULT_PORT_ADDRESS = 5555; 30 | 31 | private MenuItem connectAdbMenu; 32 | private MenuItem disconnectAdbMenu; 33 | private MenuItem pairAdbMenu; 34 | private ScrollView scrollView; 35 | 36 | private AppCompatEditText commandInput; 37 | private AppCompatTextView commandOutput; 38 | private MainViewModel viewModel; 39 | private boolean connected = false; 40 | 41 | @Override 42 | protected void onCreate(Bundle savedInstanceState) { 43 | super.onCreate(savedInstanceState); 44 | setContentView(R.layout.activity_main); 45 | setSupportActionBar(findViewById(R.id.toolbar)); 46 | viewModel = new ViewModelProvider(this).get(MainViewModel.class); 47 | scrollView = findViewById(R.id.scrollView); 48 | commandInput = findViewById(R.id.command_input); 49 | commandOutput = findViewById(R.id.command_output); 50 | init(); 51 | } 52 | 53 | private void init() { 54 | commandInput.setOnEditorActionListener((v, actionId, event) -> { 55 | if (actionId == EditorInfo.IME_ACTION_DONE && connected) { 56 | String command = Objects.requireNonNull(commandInput.getText()).toString(); 57 | viewModel.execute(command); 58 | return true; 59 | } 60 | return false; 61 | }); 62 | 63 | // Observers 64 | viewModel.watchConnectAdb().observe(this, isConnected -> { 65 | connected = isConnected; 66 | checkMenus(); 67 | if (isConnected) { 68 | Toast.makeText(this, getString(R.string.connected_to_adb), Toast.LENGTH_SHORT).show(); 69 | openIme(); 70 | } else { 71 | Toast.makeText(this, getString(R.string.disconnected_from_adb), Toast.LENGTH_SHORT).show(); 72 | } 73 | }); 74 | viewModel.watchPairAdb().observe(this, isPaired -> { 75 | if (isPaired) { 76 | Toast.makeText(this, getString(R.string.pairing_successful), Toast.LENGTH_SHORT).show(); 77 | } else { 78 | Toast.makeText(this, getString(R.string.pairing_failed), Toast.LENGTH_SHORT).show(); 79 | } 80 | }); 81 | viewModel.watchAskPairAdb().observe(this, displayDialog -> { 82 | if (displayDialog) { 83 | pairAdb(); 84 | } 85 | }); 86 | viewModel.watchCommandOutput().observe(this, output -> { 87 | commandOutput.setText(output == null ? "" : output); 88 | commandOutput.post(() -> scrollView.scrollTo(0, commandOutput.getHeight())); 89 | }); 90 | 91 | // Try auto-connecting 92 | viewModel.autoConnect(); 93 | } 94 | 95 | @Override 96 | protected void onResume() { 97 | super.onResume(); 98 | openIme(); 99 | } 100 | 101 | @Override 102 | protected void onPause() { 103 | super.onPause(); 104 | if (connected && commandInput != null) { 105 | InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 106 | imm.hideSoftInputFromWindow(commandInput.getWindowToken(), 0); 107 | } 108 | } 109 | 110 | @Override 111 | public boolean onCreateOptionsMenu(Menu menu) { 112 | getMenuInflater().inflate(R.menu.actions_main, menu); 113 | connectAdbMenu = menu.findItem(R.id.action_connect); 114 | disconnectAdbMenu = menu.findItem(R.id.action_disconnect); 115 | pairAdbMenu = menu.findItem(R.id.action_pair); 116 | return super.onCreateOptionsMenu(menu); 117 | } 118 | 119 | @Override 120 | public boolean onPrepareOptionsMenu(Menu menu) { 121 | if (pairAdbMenu != null) { 122 | pairAdbMenu.setEnabled(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R); 123 | pairAdbMenu.setVisible(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R); 124 | } 125 | checkMenus(); 126 | return super.onPrepareOptionsMenu(menu); 127 | } 128 | 129 | @Override 130 | public boolean onOptionsItemSelected(@NonNull MenuItem item) { 131 | int id = item.getItemId(); 132 | if (id == R.id.action_connect) { 133 | connectAdb(); 134 | return true; 135 | } 136 | if (id == R.id.action_disconnect) { 137 | connectAdb(); 138 | return true; 139 | } 140 | if (id == R.id.action_pair) { 141 | pairAdb(); 142 | return true; 143 | } 144 | return super.onOptionsItemSelected(item); 145 | } 146 | 147 | private void openIme() { 148 | if (connected && commandInput != null && !commandInput.isFocused()) { 149 | commandInput.requestFocus(); 150 | InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); 151 | imm.showSoftInput(commandInput, InputMethodManager.SHOW_IMPLICIT); 152 | } 153 | } 154 | 155 | private void checkMenus() { 156 | if (connectAdbMenu != null) { 157 | connectAdbMenu.setEnabled(!connected); 158 | connectAdbMenu.setVisible(!connected); 159 | } 160 | if (disconnectAdbMenu != null) { 161 | disconnectAdbMenu.setEnabled(connected); 162 | disconnectAdbMenu.setVisible(connected); 163 | } 164 | } 165 | 166 | private void connectAdb() { 167 | if (connected) { 168 | viewModel.disconnect(); 169 | return; 170 | } 171 | AppCompatEditText editText = new AppCompatEditText(this); 172 | editText.setInputType(InputType.TYPE_CLASS_NUMBER); 173 | editText.setText(String.valueOf(DEFAULT_PORT_ADDRESS)); 174 | new MaterialAlertDialogBuilder(this) 175 | .setTitle(R.string.connect_adb) 176 | .setView(editText) 177 | .setPositiveButton(R.string.connect_adb, (dialog, which) -> { 178 | CharSequence portString = editText.getText(); 179 | if (portString != null && TextUtils.isDigitsOnly(portString)) { 180 | int port = Integer.parseInt(portString.toString()); 181 | viewModel.connect(port); 182 | } 183 | }) 184 | .setNegativeButton(android.R.string.cancel, null) 185 | .show(); 186 | } 187 | 188 | private void pairAdb() { 189 | View view = getLayoutInflater().inflate(R.layout.dialog_input, null); 190 | TextInputEditText pairingCodeEditText = view.findViewById(R.id.pairing_code); 191 | TextInputEditText portNumberEditText = view.findViewById(R.id.port_number); 192 | viewModel.watchPairingPort().observe(this, port -> { 193 | if (port != -1) { 194 | portNumberEditText.setText(String.valueOf(port)); 195 | } else { 196 | portNumberEditText.setText(null); 197 | } 198 | }); 199 | viewModel.getPairingPort(); 200 | new MaterialAlertDialogBuilder(this) 201 | .setTitle(R.string.pair_adb) 202 | .setView(view) 203 | .setPositiveButton(R.string.pair_adb, (dialog, which) -> { 204 | CharSequence pairingCode = pairingCodeEditText.getText(); 205 | CharSequence portNumberString = portNumberEditText.getText(); 206 | if (pairingCode != null && pairingCode.length() == 6 && portNumberString != null 207 | && TextUtils.isDigitsOnly(portNumberString)) { 208 | int port = Integer.parseInt(portNumberString.toString()); 209 | viewModel.pair(port, pairingCode.toString()); 210 | } 211 | }) 212 | .setNegativeButton(android.R.string.cancel, null) 213 | .setOnDismissListener(dialog -> viewModel.watchPairingPort().removeObservers(this)) 214 | .show(); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /app/src/main/java/io/github/muntashirakon/adb/testapp/MainViewModel.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb.testapp; 4 | 5 | import android.app.Application; 6 | import android.os.Build; 7 | 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | import androidx.annotation.WorkerThread; 11 | import androidx.lifecycle.AndroidViewModel; 12 | import androidx.lifecycle.LiveData; 13 | import androidx.lifecycle.MutableLiveData; 14 | 15 | import java.io.BufferedReader; 16 | import java.io.IOException; 17 | import java.io.InputStreamReader; 18 | import java.io.OutputStream; 19 | import java.nio.charset.StandardCharsets; 20 | import java.util.concurrent.CountDownLatch; 21 | import java.util.concurrent.ExecutorService; 22 | import java.util.concurrent.Executors; 23 | import java.util.concurrent.TimeUnit; 24 | import java.util.concurrent.atomic.AtomicInteger; 25 | 26 | import io.github.muntashirakon.adb.AbsAdbConnectionManager; 27 | import io.github.muntashirakon.adb.AdbPairingRequiredException; 28 | import io.github.muntashirakon.adb.AdbStream; 29 | import io.github.muntashirakon.adb.LocalServices; 30 | import io.github.muntashirakon.adb.android.AdbMdns; 31 | import io.github.muntashirakon.adb.android.AndroidUtils; 32 | 33 | public class MainViewModel extends AndroidViewModel { 34 | private final ExecutorService executor = Executors.newFixedThreadPool(3); 35 | private final MutableLiveData connectAdb = new MutableLiveData<>(); 36 | private final MutableLiveData pairAdb = new MutableLiveData<>(); 37 | private final MutableLiveData askPairAdb = new MutableLiveData<>(); 38 | private final MutableLiveData commandOutput = new MutableLiveData<>(); 39 | private final MutableLiveData pairingPort = new MutableLiveData<>(); 40 | 41 | @Nullable 42 | private AdbStream adbShellStream; 43 | 44 | public MainViewModel(@NonNull Application application) { 45 | super(application); 46 | } 47 | 48 | public LiveData watchConnectAdb() { 49 | return connectAdb; 50 | } 51 | 52 | public LiveData watchPairAdb() { 53 | return pairAdb; 54 | } 55 | 56 | public LiveData watchAskPairAdb() { 57 | return askPairAdb; 58 | } 59 | 60 | public LiveData watchCommandOutput() { 61 | return commandOutput; 62 | } 63 | 64 | public LiveData watchPairingPort() { 65 | return pairingPort; 66 | } 67 | 68 | @Override 69 | protected void onCleared() { 70 | super.onCleared(); 71 | executor.submit(() -> { 72 | try { 73 | if (adbShellStream != null) { 74 | adbShellStream.close(); 75 | } 76 | } catch (Exception e) { 77 | e.printStackTrace(); 78 | } 79 | try { 80 | AdbConnectionManager.getInstance(getApplication()).close(); 81 | } catch (Exception e) { 82 | e.printStackTrace(); 83 | } 84 | }); 85 | executor.shutdown(); 86 | } 87 | 88 | public void connect(int port) { 89 | executor.submit(() -> { 90 | try { 91 | AbsAdbConnectionManager manager = AdbConnectionManager.getInstance(getApplication()); 92 | boolean connectionStatus; 93 | try { 94 | connectionStatus = manager.connect(AndroidUtils.getHostIpAddress(getApplication()), port); 95 | } catch (Throwable th) { 96 | th.printStackTrace(); 97 | connectionStatus = false; 98 | } 99 | connectAdb.postValue(connectionStatus); 100 | } catch (Throwable th) { 101 | th.printStackTrace(); 102 | connectAdb.postValue(false); 103 | } 104 | }); 105 | } 106 | 107 | public void autoConnect() { 108 | executor.submit(this::autoConnectInternal); 109 | } 110 | 111 | public void disconnect() { 112 | executor.submit(() -> { 113 | try { 114 | AbsAdbConnectionManager manager = AdbConnectionManager.getInstance(getApplication()); 115 | manager.disconnect(); 116 | connectAdb.postValue(false); 117 | } catch (Throwable th) { 118 | th.printStackTrace(); 119 | connectAdb.postValue(true); 120 | } 121 | }); 122 | } 123 | 124 | public void getPairingPort() { 125 | executor.submit(() -> { 126 | AtomicInteger atomicPort = new AtomicInteger(-1); 127 | CountDownLatch resolveHostAndPort = new CountDownLatch(1); 128 | 129 | AdbMdns adbMdns = new AdbMdns(getApplication(), AdbMdns.SERVICE_TYPE_TLS_PAIRING, (hostAddress, port) -> { 130 | atomicPort.set(port); 131 | resolveHostAndPort.countDown(); 132 | }); 133 | adbMdns.start(); 134 | 135 | try { 136 | if (!resolveHostAndPort.await(1, TimeUnit.MINUTES)) { 137 | return; 138 | } 139 | } catch (InterruptedException ignore) { 140 | } finally { 141 | adbMdns.stop(); 142 | } 143 | 144 | pairingPort.postValue(atomicPort.get()); 145 | }); 146 | } 147 | 148 | public void pair(int port, String pairingCode) { 149 | executor.submit(() -> { 150 | try { 151 | boolean pairingStatus; 152 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 153 | AbsAdbConnectionManager manager = AdbConnectionManager.getInstance(getApplication()); 154 | pairingStatus = manager.pair(AndroidUtils.getHostIpAddress(getApplication()), port, pairingCode); 155 | } else pairingStatus = false; 156 | pairAdb.postValue(pairingStatus); 157 | autoConnectInternal(); 158 | } catch (Throwable th) { 159 | th.printStackTrace(); 160 | pairAdb.postValue(false); 161 | } 162 | }); 163 | } 164 | 165 | @WorkerThread 166 | private void autoConnectInternal() { 167 | try { 168 | AbsAdbConnectionManager manager = AdbConnectionManager.getInstance(getApplication()); 169 | boolean connected = false; 170 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { 171 | try { 172 | connected = manager.autoConnect(getApplication(), 5000); 173 | } catch (AdbPairingRequiredException e) { 174 | askPairAdb.postValue(true); 175 | return; 176 | } catch (Throwable th) { 177 | th.printStackTrace(); 178 | } 179 | } 180 | if (!connected) { 181 | connected = manager.connect(5555); 182 | } 183 | if (connected) { 184 | connectAdb.postValue(true); 185 | } 186 | } catch (Throwable th) { 187 | th.printStackTrace(); 188 | } 189 | } 190 | 191 | private volatile boolean clearEnabled; 192 | private final Runnable outputGenerator = () -> { 193 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(adbShellStream.openInputStream()))) { 194 | StringBuilder sb = new StringBuilder(); 195 | String s; 196 | while ((s = reader.readLine()) != null) { 197 | if (clearEnabled) { 198 | sb.delete(0, sb.length()); 199 | clearEnabled = false; 200 | } 201 | sb.append(s).append("\n"); 202 | commandOutput.postValue(sb); 203 | } 204 | } catch (IOException e) { 205 | e.printStackTrace(); 206 | } 207 | }; 208 | 209 | public void execute(String command) { 210 | executor.submit(() -> { 211 | try { 212 | if (adbShellStream == null || adbShellStream.isClosed()) { 213 | AbsAdbConnectionManager manager = AdbConnectionManager.getInstance(getApplication()); 214 | adbShellStream = manager.openStream(LocalServices.SHELL); 215 | new Thread(outputGenerator).start(); 216 | } 217 | if (command.equals("clear")) { 218 | clearEnabled = true; 219 | } 220 | try (OutputStream os = adbShellStream.openOutputStream()) { 221 | os.write(String.format("%1$s\n", command).getBytes(StandardCharsets.UTF_8)); 222 | os.flush(); 223 | os.write("\n".getBytes(StandardCharsets.UTF_8)); 224 | } 225 | } catch (Exception e) { 226 | e.printStackTrace(); 227 | } 228 | }); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | 8 | 9 | 10 | 11 | 12 | 13 | 17 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_add_link.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 7 | 9 | 11 | 13 | 15 | 17 | 19 | 21 | 23 | 25 | 27 | 29 | 31 | 33 | 35 | 37 | 39 | 41 | 43 | 45 | 47 | 49 | 51 | 53 | 55 | 57 | 59 | 61 | 63 | 65 | 67 | 69 | 70 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_link.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_link_off.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 12 | 13 | 18 | 19 | 20 | 21 | 27 | 28 | 38 | 39 | 40 | 45 | 46 | 53 | 54 | 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /app/src/main/res/layout/dialog_input.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 18 | 19 | 20 | 21 | 26 | 27 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/menu/actions_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 14 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuntashirAkon/libadb-android/8a906cfedeaaa27d0d906faf9a3cf91abda15c7c/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuntashirAkon/libadb-android/8a906cfedeaaa27d0d906faf9a3cf91abda15c7c/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuntashirAkon/libadb-android/8a906cfedeaaa27d0d906faf9a3cf91abda15c7c/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuntashirAkon/libadb-android/8a906cfedeaaa27d0d906faf9a3cf91abda15c7c/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuntashirAkon/libadb-android/8a906cfedeaaa27d0d906faf9a3cf91abda15c7c/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuntashirAkon/libadb-android/8a906cfedeaaa27d0d906faf9a3cf91abda15c7c/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuntashirAkon/libadb-android/8a906cfedeaaa27d0d906faf9a3cf91abda15c7c/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuntashirAkon/libadb-android/8a906cfedeaaa27d0d906faf9a3cf91abda15c7c/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuntashirAkon/libadb-android/8a906cfedeaaa27d0d906faf9a3cf91abda15c7c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuntashirAkon/libadb-android/8a906cfedeaaa27d0d906faf9a3cf91abda15c7c/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ADB Shell 4 | Connect 5 | Pair 6 | Enter command 7 | Run 8 | Pairing successful! 9 | Pairing failed! 10 | Connected to ADB! 11 | Disconnected from ADB! 12 | Disconnect 13 | Pairing Code 14 | Port Number 15 | 16 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | buildscript { 4 | repositories { 5 | google() 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:8.3.2' 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | wrapper { 16 | distributionType = Wrapper.DistributionType.BIN 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | mavenCentral() 23 | maven { url "https://jitpack.io" } 24 | } 25 | gradle.projectsEvaluated { 26 | tasks.withType(JavaCompile).tap { 27 | configureEach { 28 | options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | android.jetifier.ignorelist=bcprov-jdk15to18 5 | android.nonTransitiveRClass=true 6 | android.nonFinalResIds=false 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MuntashirAkon/libadb-android/8a906cfedeaaa27d0d906faf9a3cf91abda15c7c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=544c35d6bd849ae8a5ed0bcea39ba677dc40f49df7d1835561582da2009b961d 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /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 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk17 3 | before_install: 4 | - sdk install java 17.0.3-tem 5 | - sdk use java 17.0.3-tem 6 | - sdk install maven 7 | - mvn -v -------------------------------------------------------------------------------- /libadb/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /libadb/build.gradle: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | plugins { 4 | id 'com.android.library' 5 | id 'maven-publish' 6 | } 7 | 8 | group = 'io.github.muntashirakon' 9 | version = '3.0.0' 10 | 11 | android { 12 | compileSdk 34 13 | namespace "io.github.muntashirakon.adb" 14 | 15 | defaultConfig { 16 | minSdk 1 17 | targetSdk 34 18 | aarMetadata { 19 | minCompileSdk = 1 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | } 27 | } 28 | 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_1_8 31 | targetCompatibility JavaVersion.VERSION_1_8 32 | } 33 | 34 | publishing { 35 | singleVariant("release") { 36 | withSourcesJar() 37 | withJavadocJar() 38 | } 39 | } 40 | } 41 | 42 | publishing { 43 | publications { 44 | release(MavenPublication) { 45 | artifactId = 'libadb' 46 | afterEvaluate { 47 | from components.release 48 | } 49 | } 50 | } 51 | } 52 | 53 | dependencies { 54 | implementation "androidx.annotation:annotation:1.7.1" 55 | implementation 'org.bouncycastle:bcprov-jdk15to18:1.78' 56 | implementation 'com.github.MuntashirAkon.spake2-java:android:2.0.0' 57 | 58 | testImplementation 'junit:junit:4.13.2' 59 | } 60 | -------------------------------------------------------------------------------- /libadb/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/AbsAdbConnectionManager.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import android.content.Context; 6 | import android.os.Build; 7 | 8 | import androidx.annotation.CallSuper; 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | import androidx.annotation.RequiresApi; 12 | import androidx.annotation.WorkerThread; 13 | 14 | import java.io.Closeable; 15 | import java.io.IOException; 16 | import java.io.UnsupportedEncodingException; 17 | import java.security.PrivateKey; 18 | import java.security.cert.Certificate; 19 | import java.util.Objects; 20 | import java.util.concurrent.CountDownLatch; 21 | import java.util.concurrent.TimeUnit; 22 | import java.util.concurrent.atomic.AtomicInteger; 23 | import java.util.concurrent.atomic.AtomicReference; 24 | 25 | import javax.security.auth.DestroyFailedException; 26 | 27 | import io.github.muntashirakon.adb.android.AdbMdns; 28 | 29 | @SuppressWarnings("unused") 30 | public abstract class AbsAdbConnectionManager implements Closeable { 31 | private final Object mLock = new Object(); 32 | @Nullable 33 | private AdbConnection mAdbConnection; 34 | private String mHostAddress = "127.0.0.1"; 35 | private int mApi = Build.VERSION_CODES.BASE; 36 | private long mTimeout = Long.MAX_VALUE; 37 | private TimeUnit mTimeoutUnit = TimeUnit.MILLISECONDS; 38 | private boolean mThrowOnUnauthorised = false; 39 | 40 | /** 41 | * Return generated/stored private key. 42 | */ 43 | @NonNull 44 | protected abstract PrivateKey getPrivateKey(); 45 | 46 | /** 47 | * Return public key wrapped around a certificate. 48 | */ 49 | @NonNull 50 | protected abstract Certificate getCertificate(); 51 | 52 | /** 53 | * Return a name for the device. This can be the app label, hostname or user@hostname. 54 | */ 55 | @NonNull 56 | protected abstract String getDeviceName(); 57 | 58 | /** 59 | * Set host address for this connection. On the same device, this should be {@code 127.0.0.1}. 60 | */ 61 | @CallSuper 62 | public void setHostAddress(@NonNull String hostAddress) { 63 | mHostAddress = Objects.requireNonNull(hostAddress); 64 | } 65 | 66 | /** 67 | * Get host address for this connection. Default value is {@code 127.0.0.1}. 68 | */ 69 | @NonNull 70 | public String getHostAddress() { 71 | return mHostAddress; 72 | } 73 | 74 | /** 75 | * Set Android API (i.e. SDK) version for this connection. If the daemon and the client are located in the same 76 | * directory, the value should be {@link Build.VERSION#SDK_INT} in order to improve performance as well as security. 77 | * 78 | * @param api The API version, default is {@link Build.VERSION_CODES#BASE}. 79 | */ 80 | public void setApi(int api) { 81 | this.mApi = api; 82 | } 83 | 84 | /** 85 | * Get Android API (i.e. SDK) version for this connection. Default value is {@link Build.VERSION_CODES#BASE}. 86 | */ 87 | public int getApi() { 88 | return mApi; 89 | } 90 | 91 | /** 92 | * Set time to wait for the connection to be made. 93 | * 94 | * @param timeout Timeout value 95 | * @param unit Timeout unit 96 | */ 97 | @CallSuper 98 | public void setTimeout(long timeout, TimeUnit unit) { 99 | mTimeout = timeout; 100 | mTimeoutUnit = unit; 101 | } 102 | 103 | /** 104 | * Get time to wait for the connection to be made. If not set using {@link #setTimeout(long, TimeUnit)}, the default 105 | * timeout is {@link Long#MAX_VALUE} milliseconds. 106 | * 107 | * @return Timeout in milliseconds 108 | */ 109 | public long getTimeout() { 110 | return mTimeoutUnit.toMillis(mTimeout); 111 | } 112 | 113 | /** 114 | * Get the unit for the timeout. If not set using {@link #setTimeout(long, TimeUnit)}, the default timeout unit is 115 | * {@link TimeUnit#MILLISECONDS}. 116 | */ 117 | @NonNull 118 | public TimeUnit getTimeoutUnit() { 119 | return mTimeoutUnit; 120 | } 121 | 122 | /** 123 | * Set whether to throw {@link AdbAuthenticationFailedException} if the daemon rejects the first authentication 124 | * attempt. 125 | * 126 | * @param throwOnUnauthorised {@code true} to throw {@link AdbAuthenticationFailedException} or {@code false} 127 | * otherwise. 128 | */ 129 | @CallSuper 130 | public void setThrowOnUnauthorised(boolean throwOnUnauthorised) { 131 | mThrowOnUnauthorised = throwOnUnauthorised; 132 | } 133 | 134 | /** 135 | * Get whether to throw {@link AdbAuthenticationFailedException} if the daemon rejects the first authentication 136 | * attempt. 137 | * 138 | * @return {@code true} if the system is configured to throw {@link AdbAuthenticationFailedException} or 139 | * {@code false} otherwise. The default value is {@code false}. 140 | */ 141 | public boolean isThrowOnUnauthorised() { 142 | return mThrowOnUnauthorised; 143 | } 144 | 145 | /** 146 | * Get the {@link AdbConnection} backed by this object. 147 | * 148 | * @return Underlying {@link AdbConnection}, or {@code null} if the connection hasn't been made yet. 149 | */ 150 | @CallSuper 151 | @Nullable 152 | public AdbConnection getAdbConnection() { 153 | synchronized (mLock) { 154 | return mAdbConnection; 155 | } 156 | } 157 | 158 | /** 159 | * Check if it is connected to an ADB daemon. 160 | * 161 | * @return {@code true} if connected, {@code false} otherwise. 162 | */ 163 | public boolean isConnected() { 164 | synchronized (mLock) { 165 | return mAdbConnection != null && mAdbConnection.isConnected() && mAdbConnection.isConnectionEstablished(); 166 | } 167 | } 168 | 169 | /** 170 | * Attempt to connect to ADB by performing an automatic network discovery of TLS host and port. Host address set by 171 | * {@link #setHostAddress(String)} is ignored. 172 | * 173 | * @param context Application context 174 | * @param timeoutMillis Amount of time spent in searching for a host and a port. 175 | * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection 176 | * attempt is unsuccessful, or it has already been made. 177 | * @throws IOException If the socket connection could not be made. 178 | * @throws InterruptedException If timeout has reached. 179 | * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the ADB 180 | * daemon has rejected the first authentication attempt, which indicates 181 | * that the daemon has not saved the public key from a previous connection. 182 | * @throws AdbPairingRequiredException If ADB lacks pairing 183 | */ 184 | @WorkerThread 185 | @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) 186 | public boolean connectTls(@NonNull Context context, long timeoutMillis) 187 | throws IOException, InterruptedException, AdbPairingRequiredException { 188 | return autoConnect(context, AdbMdns.SERVICE_TYPE_TLS_CONNECT, timeoutMillis); 189 | } 190 | 191 | /** 192 | * Attempt to connect to ADB by performing an automatic network discovery of TCP host and port. Host address set by 193 | * {@link #setHostAddress(String)} is ignored. 194 | * 195 | * @param context Application context 196 | * @param timeoutMillis Amount of time spent in searching for a host and a port. 197 | * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection 198 | * attempt is unsuccessful, or it has already been made. 199 | * @throws IOException If the socket connection could not be made. 200 | * @throws InterruptedException If timeout has reached. 201 | * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the ADB 202 | * daemon has rejected the first authentication attempt, which indicates 203 | * that the daemon has not saved the public key from a previous connection. 204 | * @throws AdbPairingRequiredException If ADB lacks pairing 205 | */ 206 | @WorkerThread 207 | @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) 208 | public boolean connectTcp(@NonNull Context context, long timeoutMillis) 209 | throws IOException, InterruptedException, AdbPairingRequiredException { 210 | return autoConnect(context, AdbMdns.SERVICE_TYPE_ADB, timeoutMillis); 211 | } 212 | 213 | /** 214 | * Attempt to connect to ADB by performing an automatic network discovery of host and port. Host address set by 215 | * {@link #setHostAddress(String)} is ignored. 216 | * 217 | * @param context Application context 218 | * @param timeoutMillis Amount of time spent in searching for a host and a port. 219 | * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection 220 | * attempt is unsuccessful, or it has already been made. 221 | * @throws IOException If the socket connection could not be made. 222 | * @throws InterruptedException If timeout has reached. 223 | * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the ADB 224 | * daemon has rejected the first authentication attempt, which indicates 225 | * that the daemon has not saved the public key from a previous connection. 226 | * @throws AdbPairingRequiredException If ADB lacks pairing 227 | */ 228 | @WorkerThread 229 | @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) 230 | public boolean autoConnect(@NonNull Context context, long timeoutMillis) 231 | throws IOException, InterruptedException, AdbPairingRequiredException { 232 | synchronized (mLock) { 233 | AtomicInteger atomicPort = new AtomicInteger(-1); 234 | AtomicReference atomicHostAddress = new AtomicReference<>(null); 235 | CountDownLatch resolveHostAndPort = new CountDownLatch(1); 236 | 237 | AdbMdns adbMdnsTcp = new AdbMdns(context, AdbMdns.SERVICE_TYPE_ADB, (hostAddress, port) -> { 238 | if (hostAddress != null) { 239 | atomicHostAddress.set(hostAddress.getHostAddress()); 240 | atomicPort.set(port); 241 | } 242 | resolveHostAndPort.countDown(); 243 | }); 244 | adbMdnsTcp.start(); 245 | 246 | AdbMdns adbMdnsTls = new AdbMdns(context, AdbMdns.SERVICE_TYPE_TLS_CONNECT, (hostAddress, port) -> { 247 | if (hostAddress != null) { 248 | atomicHostAddress.set(hostAddress.getHostAddress()); 249 | atomicPort.set(port); 250 | } 251 | resolveHostAndPort.countDown(); 252 | }); 253 | adbMdnsTls.start(); 254 | 255 | try { 256 | if (!resolveHostAndPort.await(timeoutMillis, TimeUnit.MILLISECONDS)) { 257 | throw new InterruptedException("Timed out while trying to find a valid host address and port"); 258 | } 259 | } finally { 260 | adbMdnsTcp.stop(); 261 | adbMdnsTls.stop(); 262 | } 263 | 264 | String host = atomicHostAddress.get(); 265 | int port = atomicPort.get(); 266 | 267 | if (host == null || port == -1) { 268 | throw new IOException("Could not find any valid host address or port"); 269 | } 270 | 271 | mHostAddress = host; 272 | mAdbConnection = new AdbConnection.Builder(host, port) 273 | .setApi(mApi) 274 | .setKeyPair(getAdbKeyPair()) 275 | .setDeviceName(Objects.requireNonNull(getDeviceName())) 276 | .build(); 277 | return mAdbConnection.connect(mTimeout, mTimeoutUnit, mThrowOnUnauthorised); 278 | } 279 | } 280 | 281 | @WorkerThread 282 | @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) 283 | private boolean autoConnect(@NonNull Context context, @AdbMdns.ServiceType @NonNull String serviceType, long timeoutMillis) 284 | throws IOException, InterruptedException, AdbPairingRequiredException { 285 | synchronized (mLock) { 286 | AtomicInteger atomicPort = new AtomicInteger(-1); 287 | AtomicReference atomicHostAddress = new AtomicReference<>(null); 288 | CountDownLatch resolveHostAndPort = new CountDownLatch(1); 289 | 290 | AdbMdns adbMdns = new AdbMdns(context, serviceType, (hostAddress, port) -> { 291 | if (hostAddress != null) { 292 | atomicHostAddress.set(hostAddress.getHostAddress()); 293 | atomicPort.set(port); 294 | } 295 | resolveHostAndPort.countDown(); 296 | }); 297 | adbMdns.start(); 298 | 299 | try { 300 | if (!resolveHostAndPort.await(timeoutMillis, TimeUnit.MILLISECONDS)) { 301 | throw new InterruptedException("Timed out while trying to find a valid host address and port"); 302 | } 303 | } finally { 304 | adbMdns.stop(); 305 | } 306 | 307 | String host = atomicHostAddress.get(); 308 | int port = atomicPort.get(); 309 | 310 | if (host == null || port == -1) { 311 | throw new IOException("Could not find any valid host address or port"); 312 | } 313 | 314 | mHostAddress = host; 315 | mAdbConnection = new AdbConnection.Builder(host, port) 316 | .setApi(mApi) 317 | .setKeyPair(getAdbKeyPair()) 318 | .setDeviceName(Objects.requireNonNull(getDeviceName())) 319 | .build(); 320 | return mAdbConnection.connect(mTimeout, mTimeoutUnit, mThrowOnUnauthorised); 321 | } 322 | } 323 | 324 | /** 325 | * Attempt to connect to ADB given a port number. Host address is set via {@link #setHostAddress(String)}. 326 | * 327 | * @param port Port number 328 | * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection 329 | * attempt is unsuccessful, or it has already been made. 330 | * @throws IOException If the socket connection could not be made. 331 | * @throws InterruptedException If timeout has reached. 332 | * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the ADB 333 | * daemon has rejected the first authentication attempt, which indicates 334 | * that the daemon has not saved the public key from a previous connection. 335 | * @throws AdbPairingRequiredException If ADB lacks pairing 336 | */ 337 | @WorkerThread 338 | public boolean connect(int port) throws IOException, InterruptedException, AdbPairingRequiredException { 339 | synchronized (mLock) { 340 | if (isConnected()) { 341 | return false; 342 | } 343 | mAdbConnection = new AdbConnection.Builder(mHostAddress, port) 344 | .setApi(mApi) 345 | .setKeyPair(getAdbKeyPair()) 346 | .setDeviceName(Objects.requireNonNull(getDeviceName())) 347 | .build(); 348 | return mAdbConnection.connect(mTimeout, mTimeoutUnit, mThrowOnUnauthorised); 349 | } 350 | } 351 | 352 | /** 353 | * Attempt to connect to ADB via a host address and a port number. 354 | * 355 | * @param host Host address to use instead of taking it from the {@link #getHostAddress()} 356 | * @param port Port number 357 | * @return {@code true} if and only if the connection is successful. It returns {@code false} if the connection 358 | * attempt is unsuccessful, or it has already been made. 359 | * @throws IOException If the socket connection could not be made. 360 | * @throws InterruptedException If timeout has reached. 361 | * @throws AdbAuthenticationFailedException If {@link #isThrowOnUnauthorised()} is set to {@code true}, and the 362 | * ADB daemon has rejected the first authentication attempt, which 363 | * indicates that the daemon has not saved the public key from a previous 364 | * connection. 365 | * @throws AdbPairingRequiredException If ADB lacks pairing 366 | */ 367 | @WorkerThread 368 | public boolean connect(@NonNull String host, int port) 369 | throws IOException, InterruptedException, AdbPairingRequiredException { 370 | synchronized (mLock) { 371 | if (isConnected()) { 372 | return false; 373 | } 374 | mHostAddress = host; 375 | mAdbConnection = new AdbConnection.Builder(host, port) 376 | .setApi(mApi) 377 | .setKeyPair(getAdbKeyPair()) 378 | .setDeviceName(Objects.requireNonNull(getDeviceName())) 379 | .build(); 380 | return mAdbConnection.connect(mTimeout, mTimeoutUnit, mThrowOnUnauthorised); 381 | } 382 | } 383 | 384 | /** 385 | * Disconnect the underlying {@link AdbConnection}. 386 | * 387 | * @throws IOException If the underlying socket fails to close 388 | */ 389 | public void disconnect() throws IOException { 390 | synchronized (mLock) { 391 | if (mAdbConnection != null) { 392 | mAdbConnection.close(); 393 | mAdbConnection = null; 394 | } 395 | } 396 | } 397 | 398 | /** 399 | * Opens an {@link AdbStream} object corresponding to the specified destination. 400 | * This routine will block until the connection completes. 401 | * 402 | * @param destination The destination to open on the target 403 | * @return {@link AdbStream} object corresponding to the specified destination 404 | * @throws IOException If the steam fails or no connection has been made 405 | * @throws InterruptedException If the stream fails while sending the packet 406 | * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8. 407 | */ 408 | @WorkerThread 409 | @NonNull 410 | public AdbStream openStream(String destination) throws IOException, InterruptedException { 411 | synchronized (mLock) { 412 | if (mAdbConnection != null && mAdbConnection.isConnected()) { 413 | try { 414 | return mAdbConnection.open(destination); 415 | } catch (AdbPairingRequiredException e) { 416 | throw new IllegalStateException(e); 417 | } 418 | } 419 | throw new IOException("Not connected to ADB."); 420 | } 421 | } 422 | 423 | /** 424 | * Opens an {@link AdbStream} object corresponding to the specified destination. 425 | * This routine will block until the connection completes. 426 | * 427 | * @param service The service to open. One of the services under {@link LocalServices.Services}. 428 | * @param args Additional arguments supported by the service (see the corresponding constant to learn more). 429 | * @return AdbStream object corresponding to the specified destination 430 | * @throws UnsupportedEncodingException If the destination cannot be encoded to UTF-8 431 | * @throws IOException If the stream fails while sending the packet 432 | * @throws InterruptedException If we are unable to wait for the connection to finish 433 | */ 434 | @NonNull 435 | public AdbStream openStream(@LocalServices.Services int service, @NonNull String... args) 436 | throws IOException, InterruptedException { 437 | synchronized (mLock) { 438 | if (mAdbConnection != null && mAdbConnection.isConnected()) { 439 | try { 440 | return mAdbConnection.open(service, args); 441 | } catch (AdbPairingRequiredException e) { 442 | throw new IllegalStateException(e); 443 | } 444 | } 445 | throw new IOException("Not connected to ADB."); 446 | } 447 | } 448 | 449 | /** 450 | * Pair with an ADB daemon given port number and pairing code. 451 | * 452 | * @param port Port number 453 | * @param pairingCode The six-digit pairing code as string 454 | * @return {@code true} if the pairing is successful and {@code false} otherwise. 455 | * @throws Exception If pairing failed for some reason. 456 | */ 457 | @WorkerThread 458 | @RequiresApi(Build.VERSION_CODES.GINGERBREAD) 459 | public boolean pair(int port, @NonNull String pairingCode) throws Exception { 460 | return pair(mHostAddress, port, pairingCode); 461 | } 462 | 463 | /** 464 | * Pair with an ADB daemon given host address, port number and pairing code. 465 | * 466 | * @param host Host address to use instead of taking it from the {@link #getHostAddress()} 467 | * @param port Port number 468 | * @param pairingCode The six-digit pairing code as string 469 | * @return {@code true} if the pairing is successful and {@code false} otherwise. 470 | * @throws Exception If pairing failed for some reason. 471 | */ 472 | @WorkerThread 473 | @RequiresApi(Build.VERSION_CODES.GINGERBREAD) 474 | public boolean pair(@NonNull String host, int port, @NonNull String pairingCode) throws Exception { 475 | synchronized (mLock) { 476 | KeyPair keyPair = getAdbKeyPair(); 477 | try (PairingConnectionCtx pairingClient = new PairingConnectionCtx(Objects.requireNonNull(host), port, 478 | StringCompat.getBytes(Objects.requireNonNull(pairingCode), "UTF-8"), keyPair, getDeviceName())) { 479 | // TODO: 5/12/21 Return true/false instead of only exceptions 480 | pairingClient.start(); 481 | } 482 | return true; 483 | } 484 | } 485 | 486 | /** 487 | * Close the underlying {@link AdbConnection} and destroy the private key. 488 | * 489 | * @throws IOException If socket fails to close. 490 | */ 491 | @Override 492 | public void close() throws IOException { 493 | try { 494 | getPrivateKey().destroy(); 495 | } catch (DestroyFailedException | NoSuchMethodError e) { 496 | e.printStackTrace(); 497 | } 498 | if (mAdbConnection != null) { 499 | mAdbConnection.close(); 500 | mAdbConnection = null; 501 | } 502 | } 503 | 504 | @NonNull 505 | private KeyPair getAdbKeyPair() { 506 | return new KeyPair(Objects.requireNonNull(getPrivateKey()), Objects.requireNonNull(getCertificate())); 507 | } 508 | } 509 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/AdbAuthenticationFailedException.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0) 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | /** 6 | * Thrown when the ADB daemon rejects our initial authentication attempt, which typically means that the peer has not 7 | * previously saved our public key. 8 | */ 9 | // Copyright 2020 Sam Palmer 10 | public class AdbAuthenticationFailedException extends RuntimeException { 11 | public AdbAuthenticationFailedException() { 12 | super("Initial authentication attempt rejected by peer."); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/AdbInputStream.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | 8 | public class AdbInputStream extends InputStream { 9 | public AdbStream mAdbStream; 10 | 11 | public AdbInputStream(AdbStream adbStream) { 12 | this.mAdbStream = adbStream; 13 | } 14 | 15 | @Override 16 | public int read() throws IOException { 17 | byte[] bytes = new byte[1]; 18 | if (read(bytes) == -1) { 19 | return -1; 20 | } 21 | return bytes[0]; 22 | } 23 | 24 | @Override 25 | public int read(byte[] b) throws IOException { 26 | return read(b, 0, b.length); 27 | } 28 | 29 | @Override 30 | public int read(byte[] b, int off, int len) throws IOException { 31 | if (mAdbStream.isClosed()) return -1; 32 | return mAdbStream.read(b, off, len); 33 | } 34 | 35 | @Override 36 | public void close() { 37 | } 38 | 39 | @Override 40 | public int available() throws IOException { 41 | return mAdbStream.available(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/AdbOutputStream.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import java.io.IOException; 6 | import java.io.OutputStream; 7 | 8 | public class AdbOutputStream extends OutputStream { 9 | private final AdbStream mAdbStream; 10 | 11 | public AdbOutputStream(AdbStream adbStream) { 12 | this.mAdbStream = adbStream; 13 | } 14 | 15 | @Override 16 | public void write(int b) throws IOException { 17 | write(new byte[]{(byte) (b & 0xFF)}); 18 | } 19 | 20 | @Override 21 | public void write(byte[] b) throws IOException { 22 | write(b, 0, b.length); 23 | } 24 | 25 | @Override 26 | public void write(byte[] b, int off, int len) throws IOException { 27 | mAdbStream.write(b, off, len); 28 | } 29 | 30 | @Override 31 | public void flush() throws IOException { 32 | mAdbStream.flush(); 33 | } 34 | 35 | @Override 36 | public void close() throws IOException { 37 | flush(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/AdbPairingRequiredException.java: -------------------------------------------------------------------------------- 1 | package io.github.muntashirakon.adb; 2 | 3 | public class AdbPairingRequiredException extends Exception { 4 | public AdbPairingRequiredException(String message) { 5 | super(message); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/AdbProtocol.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0) 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import android.os.Build; 6 | 7 | import androidx.annotation.IntDef; 8 | import androidx.annotation.NonNull; 9 | import androidx.annotation.Nullable; 10 | 11 | import java.io.IOException; 12 | import java.io.InputStream; 13 | import java.io.StreamCorruptedException; 14 | import java.lang.annotation.Retention; 15 | import java.lang.annotation.RetentionPolicy; 16 | import java.nio.ByteBuffer; 17 | import java.nio.ByteOrder; 18 | import java.util.Arrays; 19 | 20 | /** 21 | * This class provides useful functions and fields for ADB protocol details. 22 | */ 23 | // Copyright 2013 Cameron Gutman 24 | final class AdbProtocol { 25 | /** 26 | * The length of the ADB message header 27 | */ 28 | public static final int ADB_HEADER_LENGTH = 24; 29 | 30 | /** 31 | * SYNC(online, sequence, "") 32 | * 33 | * @deprecated Obsolete, no longer used. Never used on the client side. 34 | */ 35 | public static final int A_SYNC = 0x434e5953; 36 | 37 | /** 38 | * CNXN is the connect message. No messages (except AUTH) are valid before this message is received. 39 | */ 40 | public static final int A_CNXN = 0x4e584e43; 41 | 42 | /** 43 | * The payload sent with the CONNECT message. 44 | */ 45 | public static final byte[] SYSTEM_IDENTITY_STRING_HOST = StringCompat.getBytes("host::\0", "UTF-8"); 46 | 47 | /** 48 | * AUTH is the authentication message. It is part of the RSA public key authentication added in Android 4.2.2 49 | * ({@link Build.VERSION_CODES#JELLY_BEAN_MR1}). 50 | */ 51 | public static final int A_AUTH = 0x48545541; 52 | 53 | /** 54 | * OPEN is the open stream message. It is sent to open a new stream on the target device. 55 | */ 56 | public static final int A_OPEN = 0x4e45504f; 57 | 58 | /** 59 | * OKAY is a success message. It is sent when a write is processed successfully. 60 | */ 61 | public static final int A_OKAY = 0x59414b4f; 62 | 63 | /** 64 | * CLSE is the close stream message. It is sent to close an existing stream on the target device. 65 | */ 66 | public static final int A_CLSE = 0x45534c43; 67 | 68 | /** 69 | * WRTE is the write stream message. It is sent with a payload that is the data to write to the stream. 70 | */ 71 | public static final int A_WRTE = 0x45545257; 72 | 73 | /** 74 | * STLS is the Stream-based TLS1.3 authentication method, added in Android 9 ({@link Build.VERSION_CODES#P}). 75 | */ 76 | public static final int A_STLS = 0x534c5453; 77 | 78 | @Retention(RetentionPolicy.SOURCE) 79 | @IntDef({A_SYNC, A_CNXN, A_OPEN, A_OKAY, A_CLSE, A_WRTE, A_AUTH, A_STLS}) 80 | private @interface Command { 81 | } 82 | 83 | /** 84 | * Original payload size 85 | */ 86 | public static final int MAX_PAYLOAD_V1 = 4 * 1024; 87 | /** 88 | * Supported payload size since Android 7 (N) 89 | */ 90 | public static final int MAX_PAYLOAD_V2 = 256 * 1024; 91 | /** 92 | * Supported payload size since Android 9 (P) 93 | */ 94 | public static final int MAX_PAYLOAD_V3 = 1024 * 1024; 95 | /** 96 | * Maximum supported payload size is set to the original to support all APIs 97 | */ 98 | public static final int MAX_PAYLOAD = MAX_PAYLOAD_V1; 99 | 100 | /** 101 | * The original version of the ADB protocol 102 | */ 103 | public static final int A_VERSION_MIN = 0x01000000; 104 | /** 105 | * The new version of the ADB protocol introduced in Android 9 (P) with the introduction of TLS 106 | */ 107 | public static final int A_VERSION_SKIP_CHECKSUM = 0x01000001; 108 | public static final int A_VERSION = A_VERSION_MIN; 109 | 110 | /** 111 | * The current version of the Stream-based TLS 112 | */ 113 | public static final int A_STLS_VERSION_MIN = 0x01000000; 114 | public static final int A_STLS_VERSION = A_STLS_VERSION_MIN; 115 | 116 | /** 117 | * This authentication type represents a SHA1 hash to sign. 118 | */ 119 | public static final int ADB_AUTH_TOKEN = 1; 120 | 121 | /** 122 | * This authentication type represents the signed SHA1 hash. 123 | */ 124 | public static final int ADB_AUTH_SIGNATURE = 2; 125 | 126 | /** 127 | * This authentication type represents an RSA public key. 128 | */ 129 | public static final int ADB_AUTH_RSAPUBLICKEY = 3; 130 | 131 | @Retention(RetentionPolicy.SOURCE) 132 | @IntDef({ADB_AUTH_TOKEN, ADB_AUTH_SIGNATURE, ADB_AUTH_RSAPUBLICKEY}) 133 | private @interface AuthType { 134 | } 135 | 136 | public static int getMaxData(int api) { 137 | if (api >= Build.VERSION_CODES.P) { 138 | return MAX_PAYLOAD_V3; 139 | } 140 | if (api >= Build.VERSION_CODES.N) { 141 | return MAX_PAYLOAD_V2; 142 | } 143 | return MAX_PAYLOAD_V1; 144 | } 145 | 146 | public static int getProtocolVersion(int api) { 147 | if (api >= Build.VERSION_CODES.P) { 148 | return A_VERSION_SKIP_CHECKSUM; 149 | } 150 | return A_VERSION_MIN; 151 | } 152 | 153 | /** 154 | * This function performs a checksum on the ADB payload data. 155 | * 156 | * @param data The data 157 | * @return The checksum of the data 158 | */ 159 | private static int getPayloadChecksum(@NonNull byte[] data) { 160 | return getPayloadChecksum(data, 0, data.length); 161 | } 162 | 163 | /** 164 | * This function performs a checksum on the ADB payload data. 165 | * 166 | * @param data The data 167 | * @param offset The start offset in the data 168 | * @param length The number of bytes to take from the data 169 | * @return The checksum of the data 170 | */ 171 | private static int getPayloadChecksum(@NonNull byte[] data, int offset, int length) { 172 | int checksum = 0; 173 | for (int i = offset; i < offset + length; ++i) { 174 | checksum += data[i] & 0xFF; 175 | } 176 | return checksum; 177 | } 178 | 179 | /** 180 | * This function generates an ADB message given the fields. 181 | * 182 | * @param command Command identifier constant 183 | * @param arg0 First argument 184 | * @param arg1 Second argument 185 | * @param data The data 186 | * @return Byte array containing the message 187 | */ 188 | @NonNull 189 | public static byte[] generateMessage(@Command int command, int arg0, int arg1, @Nullable byte[] data) { 190 | return generateMessage(command, arg0, arg1, data, 0, data == null ? 0 : data.length); 191 | } 192 | 193 | /** 194 | * This function generates an ADB message given the fields. 195 | * 196 | * @param command Command identifier constant 197 | * @param arg0 First argument 198 | * @param arg1 Second argument 199 | * @param data The data 200 | * @param offset The start offset in the data 201 | * @param length The number of bytes to take from the data 202 | * @return Byte array containing the message 203 | */ 204 | @NonNull 205 | public static byte[] generateMessage(@Command int command, int arg0, int arg1, @Nullable byte[] data, int offset, int length) { 206 | // Protocol as defined at https://github.com/aosp-mirror/platform_system_core/blob/6072de17cd812daf238092695f26a552d3122f8c/adb/protocol.txt 207 | // struct message { 208 | // unsigned command; // command identifier constant 209 | // unsigned arg0; // first argument 210 | // unsigned arg1; // second argument 211 | // unsigned data_length; // length of payload (0 is allowed) 212 | // unsigned data_check; // checksum of data payload 213 | // unsigned magic; // command ^ 0xffffffff 214 | // }; 215 | 216 | ByteBuffer message; 217 | 218 | if (data != null) { 219 | message = ByteBuffer.allocate(ADB_HEADER_LENGTH + length).order(ByteOrder.LITTLE_ENDIAN); 220 | } else { 221 | message = ByteBuffer.allocate(ADB_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN); 222 | } 223 | 224 | message.putInt(command); 225 | message.putInt(arg0); 226 | message.putInt(arg1); 227 | 228 | if (data != null) { 229 | message.putInt(length); 230 | message.putInt(getPayloadChecksum(data, offset, length)); 231 | } else { 232 | message.putInt(0); 233 | message.putInt(0); 234 | } 235 | 236 | message.putInt(~command); 237 | 238 | if (data != null) { 239 | message.put(data, offset, length); 240 | } 241 | 242 | return message.array(); 243 | } 244 | 245 | /** 246 | * Generates a CONNECT message for a given API. 247 | *

248 | * CONNECT(version, maxdata, "system-identity-string") 249 | * 250 | * @param api API version 251 | * @return Byte array containing the message 252 | */ 253 | @NonNull 254 | public static byte[] generateConnect(int api) { 255 | return generateMessage(A_CNXN, getProtocolVersion(api), getMaxData(api), SYSTEM_IDENTITY_STRING_HOST); 256 | } 257 | 258 | /** 259 | * Generates an AUTH message with the specified type and payload. 260 | *

261 | * AUTH(type, 0, "data") 262 | * 263 | * @param type Authentication type (see ADB_AUTH_* constants) 264 | * @param data The data 265 | * @return Byte array containing the message 266 | */ 267 | @NonNull 268 | public static byte[] generateAuth(@AuthType int type, byte[] data) { 269 | return generateMessage(A_AUTH, type, 0, data); 270 | } 271 | 272 | /** 273 | * Generates an STLS message with default parameters. 274 | *

275 | * STLS(version, 0, "") 276 | * 277 | * @return Byte array containing the message 278 | */ 279 | @NonNull 280 | public static byte[] generateStls() { 281 | return generateMessage(A_STLS, A_STLS_VERSION, 0, null); 282 | } 283 | 284 | /** 285 | * Generates an OPEN stream message with the specified local ID and destination. 286 | *

287 | * OPEN(local-id, 0, "destination") 288 | * 289 | * @param localId A unique local ID identifying the stream 290 | * @param destination The destination of the stream on the target 291 | * @return Byte array containing the message 292 | */ 293 | @NonNull 294 | public static byte[] generateOpen(int localId, @NonNull String destination) { 295 | ByteBuffer bbuf = ByteBuffer.allocate(destination.length() + 1); 296 | bbuf.put(StringCompat.getBytes(destination, "UTF-8")); 297 | bbuf.put((byte) 0); 298 | return generateMessage(A_OPEN, localId, 0, bbuf.array()); 299 | } 300 | 301 | /** 302 | * Generates a WRITE stream message with the specified IDs and payload. 303 | *

304 | * WRITE(local-id, remote-id, "data") 305 | * 306 | * @param localId The unique local ID of the stream 307 | * @param remoteId The unique remote ID of the stream 308 | * @param data The data 309 | * @param offset The start offset in the data 310 | * @param length The number of bytes to take from the data 311 | * @return Byte array containing the message 312 | */ 313 | @NonNull 314 | public static byte[] generateWrite(int localId, int remoteId, byte[] data, int offset, int length) { 315 | return generateMessage(A_WRTE, localId, remoteId, data, offset, length); 316 | } 317 | 318 | /** 319 | * Generates a CLOSE stream message with the specified IDs. 320 | *

321 | * CLOSE(local-id, remote-id, "") 322 | * 323 | * @param localId The unique local ID of the stream 324 | * @param remoteId The unique remote ID of the stream 325 | * @return Byte array containing the message 326 | */ 327 | @NonNull 328 | public static byte[] generateClose(int localId, int remoteId) { 329 | return generateMessage(A_CLSE, localId, remoteId, null); 330 | } 331 | 332 | /** 333 | * Generates an OKAY/READY message with the specified IDs. 334 | *

335 | * READY(local-id, remote-id, "") 336 | * 337 | * @param localId The unique local ID of the stream 338 | * @param remoteId The unique remote ID of the stream 339 | * @return Byte array containing the message 340 | */ 341 | @NonNull 342 | public static byte[] generateReady(int localId, int remoteId) { 343 | return generateMessage(A_OKAY, localId, remoteId, null); 344 | } 345 | 346 | /** 347 | * This class provides an abstraction for the ADB message format. 348 | */ 349 | static final class Message { 350 | /** 351 | * The command field of the message 352 | */ 353 | @Command 354 | public final int command; 355 | /** 356 | * The arg0 field of the message 357 | */ 358 | public final int arg0; 359 | /** 360 | * The arg1 field of the message 361 | */ 362 | public final int arg1; 363 | /** 364 | * The payload length field of the message 365 | */ 366 | public final int dataLength; 367 | /** 368 | * The checksum field of the message 369 | */ 370 | public final int dataCheck; 371 | /** 372 | * The magic field of the message 373 | */ 374 | public final int magic; 375 | /** 376 | * The payload of the message 377 | */ 378 | public byte[] payload; 379 | 380 | /** 381 | * Read and parse an ADB message from the supplied input stream. 382 | *

383 | * Note: If data is corrupted, the connection has to be closed immediately to avoid inconsistencies. 384 | * 385 | * @param in InputStream object to read data from 386 | * @return An AdbMessage object represented the message read 387 | * @throws IOException If the stream fails while reading. 388 | * @throws StreamCorruptedException If data is corrupted. 389 | */ 390 | @NonNull 391 | public static Message parse(@NonNull InputStream in, int protocolVersion, int maxData) throws IOException { 392 | ByteBuffer header = ByteBuffer.allocate(ADB_HEADER_LENGTH).order(ByteOrder.LITTLE_ENDIAN); 393 | 394 | // Read header 395 | int dataRead = 0; 396 | do { 397 | int bytesRead = in.read(header.array(), dataRead, ADB_HEADER_LENGTH - dataRead); 398 | if (bytesRead < 0) { 399 | throw new IOException("Stream closed"); 400 | } else dataRead += bytesRead; 401 | } while (dataRead < ADB_HEADER_LENGTH); 402 | 403 | Message msg = new Message(header); 404 | 405 | // Validate header 406 | if (msg.command != (~msg.magic)) { // magic = cmd ^ 0xFFFFFFFF 407 | throw new StreamCorruptedException(String.format("Invalid header: Invalid magic 0x%x.", msg.magic)); 408 | } 409 | if (msg.command != A_SYNC && msg.command != A_CNXN && msg.command != A_OPEN && msg.command != A_OKAY 410 | && msg.command != A_CLSE && msg.command != A_WRTE && msg.command != A_AUTH 411 | && msg.command != A_STLS) { 412 | throw new StreamCorruptedException(String.format("Invalid header: Invalid command 0x%x.", msg.command)); 413 | } 414 | if (msg.dataLength < 0 || msg.dataLength > maxData) { 415 | throw new StreamCorruptedException(String.format("Invalid header: Invalid data length %d", msg.dataLength)); 416 | } 417 | 418 | if (msg.dataLength == 0) { 419 | // No payload supplied, return immediately 420 | return msg; 421 | } 422 | 423 | // Read payload 424 | msg.payload = new byte[msg.dataLength]; 425 | dataRead = 0; 426 | do { 427 | int bytesRead = in.read(msg.payload, dataRead, msg.dataLength - dataRead); 428 | if (bytesRead < 0) { 429 | throw new IOException("Stream closed"); 430 | } else dataRead += bytesRead; 431 | } while (dataRead < msg.dataLength); 432 | 433 | // Verify payload 434 | if ((protocolVersion <= A_VERSION_MIN || (msg.command == A_CNXN && msg.arg0 <= A_VERSION_MIN)) 435 | && getPayloadChecksum(msg.payload) != msg.dataCheck) { 436 | // Checksum verification failed 437 | throw new StreamCorruptedException("Invalid header: Checksum mismatched."); 438 | } 439 | 440 | return msg; 441 | } 442 | 443 | private Message(@NonNull ByteBuffer header) { 444 | command = header.getInt(); 445 | arg0 = header.getInt(); 446 | arg1 = header.getInt(); 447 | dataLength = header.getInt(); 448 | dataCheck = header.getInt(); 449 | magic = header.getInt(); 450 | } 451 | 452 | @NonNull 453 | @Override 454 | public String toString() { 455 | String tag; 456 | switch (command) { 457 | case A_SYNC: 458 | tag = "SYNC"; 459 | break; 460 | case A_CNXN: 461 | tag = "CNXN"; 462 | break; 463 | case A_OPEN: 464 | tag = "OPEN"; 465 | break; 466 | case A_OKAY: 467 | tag = "OKAY"; 468 | break; 469 | case A_CLSE: 470 | tag = "CLSE"; 471 | break; 472 | case A_WRTE: 473 | tag = "WRTE"; 474 | break; 475 | case A_AUTH: 476 | tag = "AUTH"; 477 | break; 478 | case A_STLS: 479 | tag = "STLS"; 480 | break; 481 | default: 482 | tag = "????"; 483 | break; 484 | } 485 | return "Message{" + 486 | "command=" + tag + 487 | ", arg0=0x" + Integer.toHexString(arg0) + 488 | ", arg1=0x" + Integer.toHexString(arg1) + 489 | ", payloadLength=" + dataLength + 490 | ", checksum=" + dataCheck + 491 | ", magic=0x" + Integer.toHexString(magic) + 492 | ", payload=" + Arrays.toString(payload) + 493 | '}'; 494 | } 495 | } 496 | 497 | } 498 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/AdbStream.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSD-3-Clause AND (GPL-3.0-or-later OR Apache-2.0) 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import java.io.Closeable; 6 | import java.io.IOException; 7 | import java.nio.ByteBuffer; 8 | import java.util.Queue; 9 | import java.util.concurrent.ConcurrentLinkedQueue; 10 | import java.util.concurrent.atomic.AtomicBoolean; 11 | 12 | /** 13 | * This class abstracts the underlying ADB streams 14 | */ 15 | // Copyright 2013 Cameron Gutman 16 | public class AdbStream implements Closeable { 17 | 18 | /** 19 | * The AdbConnection object that the stream communicates over 20 | */ 21 | private final AdbConnection mAdbConnection; 22 | 23 | /** 24 | * The local ID of the stream 25 | */ 26 | private final int mLocalId; 27 | 28 | /** 29 | * The remote ID of the stream 30 | */ 31 | private volatile int mRemoteId; 32 | 33 | /** 34 | * Indicates whether WRTE is currently allowed 35 | */ 36 | private final AtomicBoolean mWriteReady; 37 | 38 | /** 39 | * A queue of data from the target's WRTE packets 40 | */ 41 | private final Queue mReadQueue; 42 | 43 | /** 44 | * Store data received from the first WRTE packet in order to support buffering. 45 | */ 46 | private final ByteBuffer mReadBuffer; 47 | 48 | /** 49 | * Indicates whether the connection is closed already 50 | */ 51 | private volatile boolean mIsClosed; 52 | 53 | /** 54 | * Whether the remote peer has closed but we still have unread data in the queue 55 | */ 56 | private volatile boolean mPendingClose; 57 | 58 | /** 59 | * Creates a new AdbStream object on the specified AdbConnection 60 | * with the given local ID. 61 | * 62 | * @param adbConnection AdbConnection that this stream is running on 63 | * @param localId Local ID of the stream 64 | */ 65 | AdbStream(AdbConnection adbConnection, int localId) 66 | throws IOException, InterruptedException, AdbPairingRequiredException { 67 | this.mAdbConnection = adbConnection; 68 | this.mLocalId = localId; 69 | this.mReadQueue = new ConcurrentLinkedQueue<>(); 70 | this.mReadBuffer = (ByteBuffer) ByteBuffer.allocate(adbConnection.getMaxData()).flip(); 71 | this.mWriteReady = new AtomicBoolean(false); 72 | this.mIsClosed = false; 73 | } 74 | 75 | public AdbInputStream openInputStream() { 76 | return new AdbInputStream(this); 77 | } 78 | 79 | public AdbOutputStream openOutputStream() { 80 | return new AdbOutputStream(this); 81 | } 82 | 83 | /** 84 | * Called by the connection thread to indicate newly received data. 85 | * 86 | * @param payload Data inside the WRTE message 87 | */ 88 | void addPayload(byte[] payload) { 89 | synchronized (mReadQueue) { 90 | mReadQueue.add(payload); 91 | mReadQueue.notifyAll(); 92 | } 93 | } 94 | 95 | /** 96 | * Called by the connection thread to send an OKAY packet, allowing the 97 | * other side to continue transmission. 98 | * 99 | * @throws IOException If the connection fails while sending the packet 100 | */ 101 | void sendReady() throws IOException { 102 | // Generate and send a OKAY packet 103 | mAdbConnection.sendPacket(AdbProtocol.generateReady(mLocalId, mRemoteId)); 104 | } 105 | 106 | /** 107 | * Called by the connection thread to update the remote ID for this stream 108 | * 109 | * @param remoteId New remote ID 110 | */ 111 | void updateRemoteId(int remoteId) { 112 | this.mRemoteId = remoteId; 113 | } 114 | 115 | /** 116 | * Called by the connection thread to indicate the stream is okay to send data. 117 | */ 118 | void readyForWrite() { 119 | mWriteReady.set(true); 120 | } 121 | 122 | /** 123 | * Called by the connection thread to notify that the stream was closed by the peer. 124 | */ 125 | void notifyClose(boolean closedByPeer) { 126 | // We don't call close() because it sends another CLSE 127 | if (closedByPeer && !mReadQueue.isEmpty()) { 128 | // The remote peer closed the stream, but we haven't finished reading the remaining data 129 | mPendingClose = true; 130 | } else { 131 | mIsClosed = true; 132 | } 133 | 134 | // Notify readers and writers 135 | synchronized (this) { 136 | notifyAll(); 137 | } 138 | synchronized (mReadQueue) { 139 | mReadQueue.notifyAll(); 140 | } 141 | } 142 | 143 | /** 144 | * Read bytes from the ADB daemon. 145 | * 146 | * @return the next byte of data, or {@code -1} if the end of the stream is reached. 147 | * @throws IOException If the stream fails while waiting 148 | */ 149 | public int read(byte[] bytes, int offset, int length) throws IOException { 150 | if (mReadBuffer.hasRemaining()) { 151 | return readBuffer(bytes, offset, length); 152 | } 153 | // Buffer has no data, grab from the queue 154 | synchronized (mReadQueue) { 155 | byte[] data; 156 | // Wait for the connection to close or data to be received 157 | while ((data = mReadQueue.poll()) == null && !mIsClosed) { 158 | try { 159 | mReadQueue.wait(); 160 | } catch (InterruptedException e) { 161 | //noinspection UnnecessaryInitCause 162 | throw (IOException) new IOException().initCause(e); 163 | } 164 | } 165 | // Add data to the buffer 166 | if (data != null) { 167 | mReadBuffer.clear(); 168 | mReadBuffer.put(data); 169 | mReadBuffer.flip(); 170 | if (mReadBuffer.hasRemaining()) { 171 | return readBuffer(bytes, offset, length); 172 | } 173 | } 174 | 175 | if (mIsClosed) { 176 | throw new IOException("Stream closed."); 177 | } 178 | 179 | if (mPendingClose && mReadQueue.isEmpty()) { 180 | // The peer closed the stream, and we've finished reading the stream data, so this stream is finished 181 | mIsClosed = true; 182 | } 183 | } 184 | 185 | return -1; 186 | } 187 | 188 | private int readBuffer(byte[] bytes, int offset, int length) { 189 | int count = 0; 190 | for (int i = offset; i < offset + length; ++i) { 191 | if (mReadBuffer.hasRemaining()) { 192 | bytes[i] = mReadBuffer.get(); 193 | ++count; 194 | } 195 | } 196 | return count; 197 | } 198 | 199 | /** 200 | * Sends a WRTE packet with a given byte array payload. It does not flush the stream. 201 | * 202 | * @param bytes Payload in the form of a byte array 203 | * @throws IOException If the stream fails while sending data 204 | */ 205 | public void write(byte[] bytes, int offset, int length) throws IOException { 206 | synchronized (this) { 207 | // Make sure we're ready for a WRTE 208 | while (!mIsClosed && !mWriteReady.compareAndSet(true, false)) { 209 | try { 210 | wait(); 211 | } catch (InterruptedException e) { 212 | //noinspection UnnecessaryInitCause 213 | throw (IOException) new IOException().initCause(e); 214 | } 215 | } 216 | 217 | if (mIsClosed) { 218 | throw new IOException("Stream closed"); 219 | } 220 | } 221 | // Split and send data as WRTE packet 222 | // TODO: A WRITE message may not be sent until a READY message is received. 223 | // Once a WRITE message is sent, an additional WRITE message may not be 224 | // sent until another READY message has been received. Recipients of 225 | // a WRITE message that is in violation of this requirement will CLOSE 226 | // the connection. 227 | int maxData; 228 | try { 229 | maxData = mAdbConnection.getMaxData(); 230 | } catch (InterruptedException | AdbPairingRequiredException e) { 231 | //noinspection UnnecessaryInitCause 232 | throw (IOException) new IOException().initCause(e); 233 | } 234 | while (length != 0) { 235 | if (length <= maxData) { 236 | mAdbConnection.sendPacket(AdbProtocol.generateWrite(mLocalId, mRemoteId, bytes, offset, length)); 237 | offset = offset + length; 238 | length = 0; 239 | } else { // if (length > maxData) { 240 | mAdbConnection.sendPacket(AdbProtocol.generateWrite(mLocalId, mRemoteId, bytes, offset, maxData)); 241 | offset = offset + maxData; 242 | length = length - maxData; 243 | } 244 | } 245 | } 246 | 247 | public void flush() throws IOException { 248 | if (mIsClosed) { 249 | throw new IOException("Stream closed"); 250 | } 251 | mAdbConnection.flushPacket(); 252 | } 253 | 254 | /** 255 | * Closes the stream. This sends a close message to the peer. 256 | * 257 | * @throws IOException If the stream fails while sending the close message. 258 | */ 259 | @Override 260 | public void close() throws IOException { 261 | synchronized (this) { 262 | // This may already be closed by the remote host 263 | if (mIsClosed) 264 | return; 265 | 266 | // Notify readers/writers that we've closed 267 | notifyClose(false); 268 | } 269 | 270 | mAdbConnection.sendPacket(AdbProtocol.generateClose(mLocalId, mRemoteId)); 271 | } 272 | 273 | /** 274 | * Returns whether the stream is closed or not 275 | * 276 | * @return True if the stream is close, false if not 277 | */ 278 | public boolean isClosed() { 279 | return mIsClosed; 280 | } 281 | 282 | /** 283 | * Returns an estimate of available data. 284 | * 285 | * @return an estimate of the number of bytes that can be read from this stream without blocking. 286 | * @throws IOException if the stream is close. 287 | */ 288 | public int available() throws IOException { 289 | synchronized (this) { 290 | if (mIsClosed) { 291 | throw new IOException("Stream closed."); 292 | } 293 | if (mReadBuffer.hasRemaining()) { 294 | return mReadBuffer.remaining(); 295 | } 296 | byte[] data = mReadQueue.peek(); 297 | return data == null ? 0 : data.length; 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/AndroidPubkey.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import androidx.annotation.NonNull; 6 | import androidx.annotation.Nullable; 7 | import androidx.annotation.VisibleForTesting; 8 | 9 | import org.bouncycastle.util.encoders.Base64; 10 | 11 | import java.math.BigInteger; 12 | import java.nio.ByteBuffer; 13 | import java.nio.ByteOrder; 14 | import java.security.GeneralSecurityException; 15 | import java.security.InvalidKeyException; 16 | import java.security.KeyFactory; 17 | import java.security.NoSuchAlgorithmException; 18 | import java.security.PrivateKey; 19 | import java.security.interfaces.RSAPublicKey; 20 | import java.security.spec.InvalidKeySpecException; 21 | import java.security.spec.RSAPublicKeySpec; 22 | import java.util.Objects; 23 | 24 | import javax.crypto.Cipher; 25 | 26 | final class AndroidPubkey { 27 | /** 28 | * Size of an RSA modulus such as an encrypted block or a signature. 29 | */ 30 | public static final int ANDROID_PUBKEY_MODULUS_SIZE = 2048 / 8; 31 | 32 | /** 33 | * Size of an encoded RSA key. 34 | */ 35 | public static final int ANDROID_PUBKEY_ENCODED_SIZE = 3 * 4 + 2 * ANDROID_PUBKEY_MODULUS_SIZE; 36 | 37 | /** 38 | * Size of the RSA modulus in words. 39 | */ 40 | public static final int ANDROID_PUBKEY_MODULUS_SIZE_WORDS = ANDROID_PUBKEY_MODULUS_SIZE / 4; 41 | 42 | /** 43 | * The RSA signature padding as an int array. 44 | */ 45 | private static final int[] SIGNATURE_PADDING_AS_INT = new int[]{ 46 | 0x00, 0x01, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 47 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 48 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 49 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 50 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 51 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 52 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 53 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 54 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 55 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 56 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 57 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 58 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 59 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 60 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 61 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 62 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 63 | 0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00, 64 | 0x04, 0x14 65 | }; 66 | 67 | /** 68 | * The RSA signature padding as a byte array 69 | */ 70 | private static final byte[] RSA_SHA_PKCS1_SIGNATURE_PADDING; 71 | 72 | static { 73 | RSA_SHA_PKCS1_SIGNATURE_PADDING = new byte[SIGNATURE_PADDING_AS_INT.length]; 74 | 75 | for (int i = 0; i < RSA_SHA_PKCS1_SIGNATURE_PADDING.length; i++) 76 | RSA_SHA_PKCS1_SIGNATURE_PADDING[i] = (byte) SIGNATURE_PADDING_AS_INT[i]; 77 | } 78 | 79 | /** 80 | * Signs the ADB SHA1 payload with the private key of this object. 81 | * 82 | * @param privateKey Private key to sign with 83 | * @param payload SHA1 payload to sign 84 | * @return Signed SHA1 payload 85 | * @throws GeneralSecurityException If signing fails 86 | */ 87 | // Taken from adb_auth_sign 88 | @NonNull 89 | public static byte[] adbAuthSign(@NonNull PrivateKey privateKey, byte[] payload) 90 | throws GeneralSecurityException { 91 | Cipher c = Cipher.getInstance("RSA/ECB/NoPadding"); 92 | c.init(Cipher.ENCRYPT_MODE, privateKey); 93 | c.update(RSA_SHA_PKCS1_SIGNATURE_PADDING); 94 | return c.doFinal(payload); 95 | } 96 | 97 | /** 98 | * Converts a standard RSAPublicKey object to the special ADB format. Available since 4.2.2. 99 | * 100 | * @param publicKey RSAPublicKey object to convert 101 | * @param name Name without null terminator 102 | * @return Byte array containing the converted RSAPublicKey object 103 | */ 104 | @NonNull 105 | public static byte[] encodeWithName(@NonNull RSAPublicKey publicKey, @NonNull String name) 106 | throws InvalidKeyException { 107 | int pkeySize = 4 * (int) Math.ceil(ANDROID_PUBKEY_ENCODED_SIZE / 3.0); 108 | try (ByteArrayNoThrowOutputStream bos = new ByteArrayNoThrowOutputStream(pkeySize + name.length() + 2)) { 109 | bos.write(Base64.encode(encode(publicKey))); 110 | bos.write(getUserInfo(name)); 111 | return bos.toByteArray(); 112 | } 113 | } 114 | 115 | // Taken from get_user_info except that a custom name is used instead of host@user 116 | @VisibleForTesting 117 | @NonNull 118 | static byte[] getUserInfo(@NonNull String name) { 119 | return StringCompat.getBytes(String.format(" %s\u0000", name), "UTF-8"); 120 | } 121 | 122 | // https://android.googlesource.com/platform/system/core/+/e797a5c75afc17024d0f0f488c130128fcd704e2/libcrypto_utils/android_pubkey.cpp 123 | // typedef struct RSAPublicKey { 124 | // uint32_t modulus_size_words; // Modulus length. This must be ANDROID_PUBKEY_MODULUS_SIZE. 125 | // uint32_t n0inv; // Precomputed montgomery parameter: -1 / n[0] mod 2^32 126 | // uint8_t modulus[ANDROID_PUBKEY_MODULUS_SIZE]; // RSA modulus as a little-endian array. 127 | // uint8_t rr[ANDROID_PUBKEY_MODULUS_SIZE]; // Montgomery parameter R^2 as a little-endian array. 128 | // uint32_t exponent; // RSA modulus: 3 or 65537 129 | // } RSAPublicKey; 130 | 131 | /** 132 | * Allocates a new {@link RSAPublicKey} object, decodes a public RSA key stored in Android's custom binary format, 133 | * and sets the key parameters. The resulting key can be used with the standard Java cryptography API to perform 134 | * public operations. 135 | * 136 | * @param androidPubkey Public RSA key in Android's custom binary format. The size of the key must be at least 137 | * {@link #ANDROID_PUBKEY_ENCODED_SIZE} 138 | * @return {@link RSAPublicKey} object 139 | */ 140 | @NonNull 141 | public static RSAPublicKey decode(@NonNull byte[] androidPubkey) 142 | throws InvalidKeyException, NoSuchAlgorithmException, InvalidKeySpecException { 143 | BigInteger n; 144 | BigInteger e; 145 | 146 | // Check size is large enough and the modulus size is correct. 147 | if (androidPubkey.length < ANDROID_PUBKEY_ENCODED_SIZE) { 148 | throw new InvalidKeyException("Invalid key length"); 149 | } 150 | ByteBuffer keyStruct = ByteBuffer.wrap(androidPubkey).order(ByteOrder.LITTLE_ENDIAN); 151 | int modulusSize = keyStruct.getInt(); 152 | if (modulusSize != ANDROID_PUBKEY_MODULUS_SIZE_WORDS) { 153 | throw new InvalidKeyException("Invalid modulus length."); 154 | } 155 | 156 | // Convert the modulus to big-endian byte order as expected by BN_bin2bn. 157 | byte[] modulus = new byte[ANDROID_PUBKEY_MODULUS_SIZE]; 158 | keyStruct.position(8); 159 | keyStruct.get(modulus); 160 | n = new BigInteger(1, swapEndianness(modulus)); 161 | 162 | // Read the exponent. 163 | keyStruct.position(520); 164 | e = BigInteger.valueOf(keyStruct.getInt()); 165 | 166 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 167 | RSAPublicKeySpec publicKeySpec = new RSAPublicKeySpec(n, e); 168 | return (RSAPublicKey) keyFactory.generatePublic(publicKeySpec); 169 | } 170 | 171 | /** 172 | * Encodes the given key in the Android RSA public key binary format. 173 | * 174 | * @return Public RSA key in Android's custom binary format. The size of the key should be at least 175 | * {@link #ANDROID_PUBKEY_ENCODED_SIZE} 176 | */ 177 | @NonNull 178 | public static byte[] encode(@NonNull RSAPublicKey publicKey) throws InvalidKeyException { 179 | BigInteger r32; 180 | BigInteger n0inv; 181 | BigInteger rr; 182 | 183 | if (publicKey.getModulus().toByteArray().length < ANDROID_PUBKEY_MODULUS_SIZE) { 184 | throw new InvalidKeyException("Invalid key length " + publicKey.getModulus().toByteArray().length); 185 | } 186 | 187 | ByteBuffer keyStruct = ByteBuffer.allocate(ANDROID_PUBKEY_ENCODED_SIZE).order(ByteOrder.LITTLE_ENDIAN); 188 | // Store the modulus size. 189 | keyStruct.putInt(ANDROID_PUBKEY_MODULUS_SIZE_WORDS); // modulus_size_words 190 | 191 | // Compute and store n0inv = -1 / N[0] mod 2^32. 192 | r32 = BigInteger.ZERO.setBit(32); // r32 = 2^32 193 | n0inv = publicKey.getModulus().mod(r32); // n0inv = N[0] mod 2^32 194 | n0inv = n0inv.modInverse(r32); // n0inv = 1/n0inv mod 2^32 195 | n0inv = r32.subtract(n0inv); // n0inv = 2^32 - n0inv 196 | keyStruct.putInt(n0inv.intValue()); // n0inv 197 | 198 | // Store the modulus. 199 | keyStruct.put(Objects.requireNonNull(BigEndianToLittleEndianPadded(ANDROID_PUBKEY_MODULUS_SIZE, publicKey.getModulus()))); 200 | 201 | // Compute and store rr = (2^(rsa_size)) ^ 2 mod N. 202 | rr = BigInteger.ZERO.setBit(ANDROID_PUBKEY_MODULUS_SIZE * 8); // rr = 2^(rsa_size) 203 | rr = rr.modPow(BigInteger.valueOf(2), publicKey.getModulus()); // rr = rr^2 mod N 204 | keyStruct.put(Objects.requireNonNull(BigEndianToLittleEndianPadded(ANDROID_PUBKEY_MODULUS_SIZE, rr))); 205 | 206 | // Store the exponent. 207 | keyStruct.putInt(publicKey.getPublicExponent().intValue()); // exponent 208 | 209 | return keyStruct.array(); 210 | } 211 | 212 | @Nullable 213 | private static byte[] BigEndianToLittleEndianPadded(int len, @NonNull BigInteger in) { 214 | byte[] out = new byte[len]; 215 | byte[] bytes = swapEndianness(in.toByteArray()); // Convert big endian -> little endian 216 | int num_bytes = bytes.length; 217 | if (len < num_bytes) { 218 | if (!fitsInBytes(bytes, num_bytes, len)) { 219 | return null; 220 | } 221 | num_bytes = len; 222 | } 223 | System.arraycopy(bytes, 0, out, 0, num_bytes); 224 | return out; 225 | } 226 | 227 | static boolean fitsInBytes(@NonNull byte[] bytes, int num_bytes, int len) { 228 | byte mask = 0; 229 | for (int i = len; i < num_bytes; i++) { 230 | mask |= bytes[i]; 231 | } 232 | return mask == 0; 233 | } 234 | 235 | @NonNull 236 | private static byte[] swapEndianness(@NonNull byte[] bytes) { 237 | int len = bytes.length; 238 | byte[] out = new byte[len]; 239 | for (int i = 0; i < len; ++i) { 240 | out[i] = bytes[len - i - 1]; 241 | } 242 | return out; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/ByteArrayNoThrowOutputStream.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import java.io.ByteArrayOutputStream; 6 | 7 | class ByteArrayNoThrowOutputStream extends ByteArrayOutputStream { 8 | public ByteArrayNoThrowOutputStream() { 9 | super(); 10 | } 11 | 12 | public ByteArrayNoThrowOutputStream(int size) { 13 | super(size); 14 | } 15 | 16 | @Override 17 | public void write(byte[] b) { 18 | write(b, 0, b.length); 19 | } 20 | 21 | @Override 22 | public void close() { 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/KeyPair.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import java.security.PrivateKey; 6 | import java.security.PublicKey; 7 | import java.security.cert.Certificate; 8 | 9 | import javax.security.auth.DestroyFailedException; 10 | 11 | final class KeyPair { 12 | private final PrivateKey mPrivateKey; 13 | private final Certificate mCertificate; 14 | 15 | public KeyPair(PrivateKey privateKey, Certificate certificate) { 16 | mPrivateKey = privateKey; 17 | mCertificate = certificate; 18 | } 19 | 20 | public PrivateKey getPrivateKey() { 21 | return mPrivateKey; 22 | } 23 | 24 | public PublicKey getPublicKey() { 25 | return mCertificate.getPublicKey(); 26 | } 27 | 28 | public Certificate getCertificate() { 29 | return mCertificate; 30 | } 31 | 32 | public void destroy() throws DestroyFailedException { 33 | try { 34 | mPrivateKey.destroy(); 35 | } catch (NoSuchMethodError ignore) { 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/LocalServices.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import android.text.TextUtils; 6 | 7 | import androidx.annotation.IntDef; 8 | import androidx.annotation.NonNull; 9 | 10 | import java.lang.annotation.Retention; 11 | import java.lang.annotation.RetentionPolicy; 12 | import java.util.Objects; 13 | 14 | 15 | /** 16 | * Local services extracted from the ADB client 17 | * for easy access. 18 | */ 19 | public class LocalServices { 20 | static final int SERVICE_FIRST = 1; 21 | 22 | public static final int SHELL = 1; 23 | /** 24 | * Remount the device's filesystem in read-write mode, instead of read-only. This is usually necessary before 25 | * performing an {@link #SYNC} request. This request may not succeed on certain builds which do not allow that. 26 | *

27 | * This essentially executes {@code /system/bin/remount} command. Additional arguments such as {@code -R} can be 28 | * passed too. 29 | */ 30 | public static final int REMOUNT = 2; 31 | public static final int FILE = 3; 32 | public static final int TCP_CONNECT = 4; 33 | public static final int LOCAL_UNIX_SOCKET = 5; 34 | public static final int LOCAL_UNIX_SOCKET_RESERVED = 6; 35 | public static final int LOCAL_UNIX_SOCKET_ABSTRACT = 7; 36 | public static final int LOCAL_UNIX_SOCKET_FILE_SYSTEM = 8; 37 | /** 38 | * Receive snapshots of the framebuffer. It requires sufficient privileges (or the connection is closed immediately) 39 | * but works as follows: 40 | *

41 | * After an {@link AdbStream} is opened, ADB daemon sends a 16-byte binary structure containing the following fields 42 | * (little-endian format): 43 | *

 44 |      * uint32_t depth;     // framebuffer depth = 16
 45 |      * uint32_t size;      // framebuffer size in bytes = 2 * width * height
 46 |      * uint32_t width;     // framebuffer width in pixels
 47 |      * uint32_t height;    // framebuffer height in pixels
 48 |      * 
49 | * After that, each time a snapshot is wanted, one byte should be sent through the channel, which will trigger the 50 | * daemon to send {@code size} bytes of framebuffer data. 51 | */ 52 | public static final int FRAMEBUFFER = 9; 53 | /** 54 | * Connects to the JDWP thread running in the VM of process PID (specified as an argument). 55 | */ 56 | public static final int CONNECT_JDWP = 10; 57 | /** 58 | * Receive the list of JDWP PIDs periodically. The format of the returned data is the following (in order): 59 | *
    60 | *
  1. {@code hex4}: The length of all content as a 4-char hexadecimal string i.e. {@code %04zx}. 61 | *
  2. {@code content}: A series of ASCII lines of the following format: 62 | *
     63 |      *  <pid> "\n"
     64 |      *  
    65 | *
66 | * This service is used by DDMS to know which debuggable processes are running on the device/emulator. 67 | *

68 | * Note that there is no single-shot service to retrieve the list only once. 69 | */ 70 | public static final int TRACK_JDWP = 11; 71 | public static final int SYNC = 12; 72 | /** 73 | * Reverse socket connections from the device running ADB daemon to this client. This should not be used if both 74 | * the ADB daemon and the client are in the same device. 75 | *

76 | * It takes an additional argument called {@code forward-command}. It can be one of the following: 77 | *

    78 | *
  • {@code list-forward}: List all forwarded connections from the device 79 | * This returns something that looks like the following: 80 | *
      81 | *
    1. {@code hex4}: The length of the payload, as 4 hexadecimal chars i.e. {@code %04zx}. 82 | *
    2. {@code payload}: A series of lines of the following format: 83 | *
       84 |      *     host " " <local> " " <remote> "\n"
       85 |      *     
      86 | * Where <local> is the device-specific endpoint (e.g. {@code tcp:9000}), and <remote> is the 87 | * client-specific endpoint. 88 | *
    89 | *
  • forward:; 90 | *
  • forward:norebind:; 91 | *
  • killforward-all 92 | *
  • killforward: 93 | *
94 | */ 95 | public static final int REVERSE = 13; 96 | /** 97 | * Backup some or all packages installed in the device. For this to work, {@code allowBackup=true} must be present 98 | * in the application section of the AndroidManifest.xml of the app. 99 | *

100 | * It takes additional arguments which can be one of the following: 101 | *

    102 | *
  • List of packages (as array) 103 | *
  • {@code -all} 104 | *
  • {@code -shared} 105 | *
106 | * Output is a stream which is in zlib format with 24 bytes at the front (if unencrypted). 107 | */ 108 | public static final int BACKUP = 14; 109 | /** 110 | * Restore a backup. Input is a stream which is in zlib format with 24 bytes at the front (if unencrypted). 111 | */ 112 | public static final int RESTORE = 15; 113 | 114 | static final int SERVICE_LAST = 15; 115 | 116 | @IntDef({ 117 | SHELL, 118 | REMOUNT, 119 | FILE, 120 | TCP_CONNECT, 121 | LOCAL_UNIX_SOCKET, 122 | LOCAL_UNIX_SOCKET_RESERVED, 123 | LOCAL_UNIX_SOCKET_ABSTRACT, 124 | LOCAL_UNIX_SOCKET_FILE_SYSTEM, 125 | FRAMEBUFFER, 126 | CONNECT_JDWP, 127 | TRACK_JDWP, 128 | SYNC, 129 | REVERSE, 130 | BACKUP, 131 | RESTORE, 132 | }) 133 | @Retention(RetentionPolicy.SOURCE) 134 | public @interface Services { 135 | } 136 | 137 | @NonNull 138 | static String getServiceName(@Services int service) { 139 | switch (service) { 140 | case SHELL: 141 | return "shell:"; 142 | case CONNECT_JDWP: 143 | return "jdwp:"; 144 | case FILE: 145 | return "dev:"; 146 | case FRAMEBUFFER: 147 | return "framebuffer:"; 148 | case LOCAL_UNIX_SOCKET: 149 | return "local:"; 150 | case LOCAL_UNIX_SOCKET_ABSTRACT: 151 | return "localabstract:"; 152 | case LOCAL_UNIX_SOCKET_FILE_SYSTEM: 153 | return "localfilesystem:"; 154 | case LOCAL_UNIX_SOCKET_RESERVED: 155 | return "localreserved:"; 156 | case REMOUNT: 157 | return "remount:"; 158 | case REVERSE: 159 | return "reverse:"; 160 | case SYNC: 161 | return "sync:"; 162 | case TCP_CONNECT: 163 | return "tcp:"; 164 | case TRACK_JDWP: 165 | return "track-jdwp"; 166 | case BACKUP: 167 | return "backup:"; 168 | case RESTORE: 169 | return "restore:"; 170 | default: 171 | throw new IllegalArgumentException("Invalid service: " + service); 172 | } 173 | } 174 | 175 | @NonNull 176 | static String getDestination(@Services int service, @NonNull String... args) { 177 | String serviceName = getServiceName(service); 178 | StringBuilder destination = new StringBuilder(serviceName); 179 | switch (service) { 180 | case SHELL: 181 | for (String arg : args) { 182 | if (arg.contains("\"")) { 183 | throw new IllegalArgumentException("Arguments for inline shell cannot contain double" + 184 | " quotations."); 185 | } 186 | if (arg.contains(" ")) { 187 | destination.append("\"").append(Objects.requireNonNull(arg)).append("\""); 188 | } else destination.append(Objects.requireNonNull(arg)); 189 | } 190 | break; 191 | case FILE: 192 | if (args.length == 0) { 193 | throw new IllegalArgumentException("File name must be specified."); 194 | } else if (args.length != 1) { 195 | throw new IllegalArgumentException("Service expects exactly one argument, " + args.length 196 | + " supplied."); 197 | } 198 | destination.append(Objects.requireNonNull(args[0])); 199 | break; 200 | case TCP_CONNECT: 201 | if (args.length == 0) { 202 | throw new IllegalArgumentException("Port number must be specified."); 203 | } else if (args.length == 1) { 204 | destination.append(args[0]); 205 | } else if (args.length == 2) { 206 | destination.append(Objects.requireNonNull(args[0])) 207 | .append(':') 208 | .append(Objects.requireNonNull(args[1])); 209 | } else { 210 | throw new IllegalArgumentException("Invalid number of arguments supplied."); 211 | } 212 | break; 213 | case LOCAL_UNIX_SOCKET: 214 | case LOCAL_UNIX_SOCKET_ABSTRACT: 215 | case LOCAL_UNIX_SOCKET_FILE_SYSTEM: 216 | case LOCAL_UNIX_SOCKET_RESERVED: 217 | if (args.length == 0) { 218 | throw new IllegalArgumentException("Path must be specified."); 219 | } else if (args.length != 1) { 220 | throw new IllegalArgumentException("Service expects exactly one argument, " + args.length 221 | + " supplied."); 222 | } 223 | destination.append(Objects.requireNonNull(args[0])); 224 | break; 225 | case CONNECT_JDWP: 226 | if (args.length == 0) { 227 | throw new IllegalArgumentException("PID must be specified."); 228 | } else if (args.length != 1) { 229 | throw new IllegalArgumentException("Service expects exactly one argument, " + args.length 230 | + " supplied."); 231 | } 232 | destination.append(Objects.requireNonNull(args[0])); 233 | break; 234 | case REVERSE: 235 | if (args.length == 0) { 236 | throw new IllegalArgumentException("Forward command must be specified."); 237 | } else if (args.length != 1) { 238 | throw new IllegalArgumentException("Service expects exactly one argument, " + args.length 239 | + " supplied."); 240 | } 241 | if (args[0] == null) { 242 | throw new IllegalArgumentException("Forward command is empty"); 243 | } 244 | if ("list-forward".equals(args[0]) || "killforward-all".equals(args[0])) { 245 | destination.append(args[0]); 246 | } else if (args[0].startsWith("forward:") || args[0].startsWith("killforward:")) { 247 | destination.append(args[0]); 248 | } else { 249 | throw new IllegalArgumentException("Invalid forward command."); 250 | } 251 | break; 252 | case BACKUP: 253 | if (args.length == 0) { 254 | throw new IllegalArgumentException("At least one package must be specified or use -shared/-all."); 255 | } 256 | case REMOUNT: 257 | // Additional arguments for the commands 258 | destination.append(TextUtils.join(" ", args)); 259 | break; 260 | case RESTORE: 261 | case FRAMEBUFFER: 262 | case SYNC: 263 | case TRACK_JDWP: 264 | if (args.length != 0) { 265 | throw new IllegalArgumentException("Service expects no arguments."); 266 | } 267 | break; 268 | } 269 | return destination.toString(); 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/PRNGFixes.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT AND (GPL-3.0-or-later OR Apache-2.0) 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import android.os.Build; 6 | import android.os.Process; 7 | import android.util.Log; 8 | 9 | import androidx.annotation.GuardedBy; 10 | 11 | import java.io.ByteArrayOutputStream; 12 | import java.io.DataInputStream; 13 | import java.io.DataOutputStream; 14 | import java.io.File; 15 | import java.io.FileInputStream; 16 | import java.io.FileOutputStream; 17 | import java.io.IOException; 18 | import java.io.OutputStream; 19 | import java.io.UnsupportedEncodingException; 20 | import java.security.NoSuchAlgorithmException; 21 | import java.security.Provider; 22 | import java.security.SecureRandom; 23 | import java.security.SecureRandomSpi; 24 | import java.security.Security; 25 | 26 | /** 27 | * Fixes for the output of the default PRNG having low entropy. 28 | *

29 | * The fixes need to be applied via {@link #apply()} before any use of Java 30 | * Cryptography Architecture primitives. A good place to invoke them is in the 31 | * application's {@code onCreate}. 32 | */ 33 | // Copyright 2013 Google Inc. 34 | public final class PRNGFixes { 35 | private static final byte[] BUILD_FINGERPRINT_AND_DEVICE_SERIAL = getBuildFingerprintAndDeviceSerial(); 36 | 37 | /** 38 | * Hidden constructor to prevent instantiation. 39 | */ 40 | private PRNGFixes() { 41 | } 42 | 43 | /** 44 | * Applies all fixes. 45 | * 46 | * @throws SecurityException if a fix is needed but could not be applied. 47 | */ 48 | public static void apply() { 49 | applyOpenSSLFix(); 50 | installLinuxPRNGSecureRandom(); 51 | } 52 | 53 | /** 54 | * Applies the fix for OpenSSL PRNG having low entropy. Does nothing if the 55 | * fix is not needed. 56 | * 57 | * @throws SecurityException if the fix is needed but could not be applied. 58 | */ 59 | private static void applyOpenSSLFix() throws SecurityException { 60 | if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) 61 | || (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2)) { 62 | // No need to apply the fix 63 | return; 64 | } 65 | 66 | try { 67 | // Mix in the device- and invocation-specific seed. 68 | Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") 69 | .getMethod("RAND_seed", byte[].class) 70 | .invoke(null, generateSeed()); 71 | 72 | // Mix output of Linux PRNG into OpenSSL's PRNG 73 | int bytesRead = (Integer) Class.forName("org.apache.harmony.xnet.provider.jsse.NativeCrypto") 74 | .getMethod("RAND_load_file", String.class, long.class) 75 | .invoke(null, "/dev/urandom", 1024); 76 | if (bytesRead != 1024) { 77 | throw new IOException("Unexpected number of bytes read from Linux PRNG: " + bytesRead); 78 | } 79 | } catch (Exception e) { 80 | throw new SecurityException("Failed to seed OpenSSL PRNG", e); 81 | } 82 | } 83 | 84 | /** 85 | * Installs a Linux PRNG-backed {@code SecureRandom} implementation as the 86 | * default. Does nothing if the implementation is already the default or if 87 | * there is not need to install the implementation. 88 | * 89 | * @throws SecurityException if the fix is needed but could not be applied. 90 | */ 91 | private static void installLinuxPRNGSecureRandom() 92 | throws SecurityException { 93 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN_MR2) { 94 | // No need to apply the fix 95 | return; 96 | } 97 | 98 | // Install a Linux PRNG-based SecureRandom implementation as the 99 | // default, if not yet installed. 100 | Provider[] secureRandomProviders = Security.getProviders("SecureRandom.SHA1PRNG"); 101 | if ((secureRandomProviders == null) 102 | || (secureRandomProviders.length < 1) 103 | || (!LinuxPRNGSecureRandomProvider.class.equals(secureRandomProviders[0].getClass()))) { 104 | Security.insertProviderAt(new LinuxPRNGSecureRandomProvider(), 1); 105 | } 106 | 107 | // Assert that new SecureRandom() and 108 | // SecureRandom.getInstance("SHA1PRNG") return a SecureRandom backed 109 | // by the Linux PRNG-based SecureRandom implementation. 110 | SecureRandom rng1 = new SecureRandom(); 111 | if (!LinuxPRNGSecureRandomProvider.class.equals(rng1.getProvider().getClass())) { 112 | throw new SecurityException("new SecureRandom() backed by wrong Provider: " 113 | + rng1.getProvider().getClass()); 114 | } 115 | 116 | SecureRandom rng2; 117 | try { 118 | rng2 = SecureRandom.getInstance("SHA1PRNG"); 119 | } catch (NoSuchAlgorithmException e) { 120 | throw new SecurityException("SHA1PRNG not available", e); 121 | } 122 | if (!LinuxPRNGSecureRandomProvider.class.equals( 123 | rng2.getProvider().getClass())) { 124 | throw new SecurityException("SecureRandom.getInstance(\"SHA1PRNG\") backed by wrong provider: " 125 | + rng2.getProvider().getClass()); 126 | } 127 | } 128 | 129 | /** 130 | * {@code Provider} of {@code SecureRandom} engines which pass through 131 | * all requests to the Linux PRNG. 132 | */ 133 | private static class LinuxPRNGSecureRandomProvider extends Provider { 134 | 135 | public LinuxPRNGSecureRandomProvider() { 136 | super("LinuxPRNG", 1.0, "A Linux-specific random number provider that uses /dev/urandom"); 137 | // Although /dev/urandom is not a SHA-1 PRNG, some apps 138 | // explicitly request a SHA1PRNG SecureRandom and we thus need to 139 | // prevent them from getting the default implementation whose output 140 | // may have low entropy. 141 | put("SecureRandom.SHA1PRNG", LinuxPRNGSecureRandom.class.getName()); 142 | put("SecureRandom.SHA1PRNG ImplementedIn", "Software"); 143 | } 144 | } 145 | 146 | /** 147 | * {@link SecureRandomSpi} which passes all requests to the Linux PRNG 148 | * ({@code /dev/urandom}). 149 | */ 150 | public static class LinuxPRNGSecureRandom extends SecureRandomSpi { 151 | 152 | /* 153 | * IMPLEMENTATION NOTE: Requests to generate bytes and to mix in a seed 154 | * are passed through to the Linux PRNG (/dev/urandom). Instances of 155 | * this class seed themselves by mixing in the current time, PID, UID, 156 | * build fingerprint, and hardware serial number (where available) into 157 | * Linux PRNG. 158 | * 159 | * Concurrency: Read requests to the underlying Linux PRNG are 160 | * serialized (on sLock) to ensure that multiple threads do not get 161 | * duplicated PRNG output. 162 | */ 163 | 164 | private static final File URANDOM_FILE = new File("/dev/urandom"); 165 | 166 | private static final Object sLock = new Object(); 167 | 168 | /** 169 | * Input stream for reading from Linux PRNG or {@code null} if not yet 170 | * opened. 171 | */ 172 | @GuardedBy("sLock") 173 | private static DataInputStream sUrandomIn; 174 | 175 | /** 176 | * Output stream for writing to Linux PRNG or {@code null} if not yet 177 | * opened. 178 | */ 179 | @GuardedBy("sLock") 180 | private static OutputStream sUrandomOut; 181 | 182 | /** 183 | * Whether this engine instance has been seeded. This is needed because 184 | * each instance needs to seed itself if the client does not explicitly 185 | * seed it. 186 | */ 187 | private boolean mSeeded; 188 | 189 | @Override 190 | protected void engineSetSeed(byte[] bytes) { 191 | try { 192 | OutputStream out; 193 | synchronized (sLock) { 194 | out = getUrandomOutputStream(); 195 | } 196 | out.write(bytes); 197 | out.flush(); 198 | } catch (IOException e) { 199 | // On a small fraction of devices /dev/urandom is not writable. 200 | // Log and ignore. 201 | Log.w(PRNGFixes.class.getSimpleName(), 202 | "Failed to mix seed into " + URANDOM_FILE); 203 | } finally { 204 | mSeeded = true; 205 | } 206 | } 207 | 208 | @Override 209 | protected void engineNextBytes(byte[] bytes) { 210 | if (!mSeeded) { 211 | // Mix in the device- and invocation-specific seed. 212 | engineSetSeed(generateSeed()); 213 | } 214 | 215 | try { 216 | DataInputStream in; 217 | synchronized (sLock) { 218 | in = getUrandomInputStream(); 219 | } 220 | synchronized (in) { 221 | in.readFully(bytes); 222 | } 223 | } catch (IOException e) { 224 | throw new SecurityException( 225 | "Failed to read from " + URANDOM_FILE, e); 226 | } 227 | } 228 | 229 | @Override 230 | protected byte[] engineGenerateSeed(int size) { 231 | byte[] seed = new byte[size]; 232 | engineNextBytes(seed); 233 | return seed; 234 | } 235 | 236 | private DataInputStream getUrandomInputStream() { 237 | synchronized (sLock) { 238 | if (sUrandomIn == null) { 239 | // NOTE: Consider inserting a BufferedInputStream between 240 | // DataInputStream and FileInputStream if you need higher 241 | // PRNG output performance and can live with future PRNG 242 | // output being pulled into this process prematurely. 243 | try { 244 | sUrandomIn = new DataInputStream( 245 | new FileInputStream(URANDOM_FILE)); 246 | } catch (IOException e) { 247 | throw new SecurityException("Failed to open " 248 | + URANDOM_FILE + " for reading", e); 249 | } 250 | } 251 | return sUrandomIn; 252 | } 253 | } 254 | 255 | private OutputStream getUrandomOutputStream() throws IOException { 256 | synchronized (sLock) { 257 | if (sUrandomOut == null) { 258 | sUrandomOut = new FileOutputStream(URANDOM_FILE); 259 | } 260 | return sUrandomOut; 261 | } 262 | } 263 | } 264 | 265 | /** 266 | * Generates a device- and invocation-specific seed to be mixed into the 267 | * Linux PRNG. 268 | */ 269 | private static byte[] generateSeed() { 270 | try { 271 | ByteArrayOutputStream seedBuffer = new ByteArrayOutputStream(); 272 | DataOutputStream seedBufferOut = 273 | new DataOutputStream(seedBuffer); 274 | seedBufferOut.writeLong(System.currentTimeMillis()); 275 | seedBufferOut.writeLong(System.nanoTime()); 276 | seedBufferOut.writeInt(Process.myPid()); 277 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BASE_1_1) { 278 | seedBufferOut.writeInt(Process.myUid()); 279 | } 280 | seedBufferOut.write(BUILD_FINGERPRINT_AND_DEVICE_SERIAL); 281 | seedBufferOut.close(); 282 | return seedBuffer.toByteArray(); 283 | } catch (IOException e) { 284 | throw new SecurityException("Failed to generate seed", e); 285 | } 286 | } 287 | 288 | /** 289 | * Gets the hardware serial number of this device. 290 | * 291 | * @return serial number or {@code null} if not available. 292 | */ 293 | private static String getDeviceSerialNumber() { 294 | // We're using the Reflection API because Build.SERIAL is only available 295 | // since API Level 9 (Gingerbread, Android 2.3). 296 | try { 297 | return (String) Build.class.getField("SERIAL").get(null); 298 | } catch (Exception ignored) { 299 | return null; 300 | } 301 | } 302 | 303 | private static byte[] getBuildFingerprintAndDeviceSerial() { 304 | StringBuilder result = new StringBuilder(); 305 | String fingerprint = Build.FINGERPRINT; 306 | if (fingerprint != null) { 307 | result.append(fingerprint); 308 | } 309 | String serial = getDeviceSerialNumber(); 310 | if (serial != null) { 311 | result.append(serial); 312 | } 313 | try { 314 | return result.toString().getBytes("UTF-8"); 315 | } catch (UnsupportedEncodingException e) { 316 | throw new RuntimeException("UTF-8 encoding not supported"); 317 | } 318 | } 319 | } -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/PairingAuthCtx.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import android.os.Build; 6 | 7 | import androidx.annotation.NonNull; 8 | import androidx.annotation.Nullable; 9 | import androidx.annotation.RequiresApi; 10 | import androidx.annotation.VisibleForTesting; 11 | 12 | import org.bouncycastle.crypto.InvalidCipherTextException; 13 | import org.bouncycastle.crypto.digests.SHA256Digest; 14 | import org.bouncycastle.crypto.engines.AESEngine; 15 | import org.bouncycastle.crypto.generators.HKDFBytesGenerator; 16 | import org.bouncycastle.crypto.modes.GCMBlockCipher; 17 | import org.bouncycastle.crypto.modes.GCMModeCipher; 18 | import org.bouncycastle.crypto.params.AEADParameters; 19 | import org.bouncycastle.crypto.params.HKDFParameters; 20 | import org.bouncycastle.crypto.params.KeyParameter; 21 | 22 | import java.nio.ByteBuffer; 23 | import java.nio.ByteOrder; 24 | import java.util.Arrays; 25 | 26 | import javax.security.auth.Destroyable; 27 | 28 | import io.github.muntashirakon.crypto.spake2.Spake2Context; 29 | import io.github.muntashirakon.crypto.spake2.Spake2Role; 30 | 31 | @RequiresApi(Build.VERSION_CODES.GINGERBREAD) 32 | class PairingAuthCtx implements Destroyable { 33 | // The following values are taken from the following source and are subjected to change 34 | // https://github.com/aosp-mirror/platform_system_core/blob/android-11.0.0_r1/adb/pairing_auth/pairing_auth.cpp 35 | private static final byte[] CLIENT_NAME = StringCompat.getBytes("adb pair client\u0000", "UTF-8"); 36 | private static final byte[] SERVER_NAME = StringCompat.getBytes("adb pair server\u0000", "UTF-8"); 37 | 38 | // The following values are taken from the following source and are subjected to change 39 | // https://github.com/aosp-mirror/platform_system_core/blob/android-11.0.0_r1/adb/pairing_auth/aes_128_gcm.cpp 40 | private static final byte[] INFO = StringCompat.getBytes("adb pairing_auth aes-128-gcm key", "UTF-8"); 41 | private static final int HKDF_KEY_LENGTH = 128 / 8; 42 | public static final int GCM_IV_LENGTH = 12; // in bytes 43 | 44 | private final byte[] mMsg; 45 | private final Spake2Context mSpake2Ctx; 46 | private final byte[] mSecretKey = new byte[HKDF_KEY_LENGTH]; 47 | private long mDecIv = 0; 48 | private long mEncIv = 0; 49 | private boolean mIsDestroyed = false; 50 | 51 | @Nullable 52 | public static PairingAuthCtx createAlice(byte[] password) { 53 | Spake2Context spake25519 = new Spake2Context(Spake2Role.Alice, CLIENT_NAME, SERVER_NAME); 54 | try { 55 | return new PairingAuthCtx(spake25519, password); 56 | } catch (IllegalArgumentException | IllegalStateException e) { 57 | return null; 58 | } 59 | } 60 | 61 | @VisibleForTesting 62 | @Nullable 63 | public static PairingAuthCtx createBob(byte[] password) { 64 | Spake2Context spake25519 = new Spake2Context(Spake2Role.Bob, SERVER_NAME, CLIENT_NAME); 65 | try { 66 | return new PairingAuthCtx(spake25519, password); 67 | } catch (IllegalArgumentException | IllegalStateException e) { 68 | return null; 69 | } 70 | } 71 | 72 | private PairingAuthCtx(Spake2Context spake25519, byte[] password) 73 | throws IllegalArgumentException, IllegalStateException { 74 | mSpake2Ctx = spake25519; 75 | mMsg = mSpake2Ctx.generateMessage(password); 76 | } 77 | 78 | public byte[] getMsg() { 79 | return mMsg; 80 | } 81 | 82 | public boolean initCipher(byte[] theirMsg) throws IllegalArgumentException, IllegalStateException { 83 | if (mIsDestroyed) return false; 84 | byte[] keyMaterial = mSpake2Ctx.processMessage(theirMsg); 85 | if (keyMaterial == null) return false; 86 | HKDFBytesGenerator hkdf = new HKDFBytesGenerator(new SHA256Digest()); 87 | hkdf.init(new HKDFParameters(keyMaterial, null, INFO)); 88 | hkdf.generateBytes(mSecretKey, 0, mSecretKey.length); 89 | return true; 90 | } 91 | 92 | @Nullable 93 | public byte[] encrypt(@NonNull byte[] in) { 94 | return encryptDecrypt(true, in, ByteBuffer.allocate(GCM_IV_LENGTH) 95 | .order(ByteOrder.LITTLE_ENDIAN).putLong(mEncIv++).array()); 96 | } 97 | 98 | @Nullable 99 | public byte[] decrypt(@NonNull byte[] in) { 100 | return encryptDecrypt(false, in, ByteBuffer.allocate(GCM_IV_LENGTH) 101 | .order(ByteOrder.LITTLE_ENDIAN).putLong(mDecIv++).array()); 102 | } 103 | 104 | @Override 105 | public boolean isDestroyed() { 106 | return mIsDestroyed; 107 | } 108 | 109 | @Override 110 | public void destroy() { 111 | mIsDestroyed = true; 112 | Arrays.fill(mSecretKey, (byte) 0); 113 | mSpake2Ctx.destroy(); 114 | } 115 | 116 | @Nullable 117 | private byte[] encryptDecrypt(boolean forEncryption, @NonNull byte[] in, @NonNull byte[] iv) { 118 | if (mIsDestroyed) return null; 119 | AEADParameters spec = new AEADParameters(new KeyParameter(mSecretKey), mSecretKey.length * 8, iv); 120 | GCMModeCipher cipher = GCMBlockCipher.newInstance(AESEngine.newInstance()); 121 | cipher.init(forEncryption, spec); 122 | byte[] out = new byte[cipher.getOutputSize(in.length)]; 123 | int newOffset = cipher.processBytes(in, 0, in.length, out, 0); 124 | try { 125 | cipher.doFinal(out, newOffset); 126 | } catch (InvalidCipherTextException e) { 127 | return null; 128 | } 129 | return out; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/PairingConnectionCtx.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import android.annotation.SuppressLint; 6 | import android.os.Build; 7 | import android.util.Log; 8 | 9 | import androidx.annotation.NonNull; 10 | import androidx.annotation.Nullable; 11 | import androidx.annotation.RequiresApi; 12 | 13 | import java.io.Closeable; 14 | import java.io.DataInputStream; 15 | import java.io.DataOutputStream; 16 | import java.io.IOException; 17 | import java.lang.reflect.Method; 18 | import java.net.Socket; 19 | import java.nio.ByteBuffer; 20 | import java.nio.ByteOrder; 21 | import java.security.InvalidKeyException; 22 | import java.security.KeyManagementException; 23 | import java.security.NoSuchAlgorithmException; 24 | import java.security.PrivateKey; 25 | import java.security.cert.Certificate; 26 | import java.security.interfaces.RSAPublicKey; 27 | import java.util.Arrays; 28 | import java.util.Objects; 29 | 30 | import javax.net.ssl.SSLContext; 31 | import javax.net.ssl.SSLException; 32 | import javax.net.ssl.SSLServerSocket; 33 | import javax.net.ssl.SSLSocket; 34 | 35 | // https://github.com/aosp-mirror/platform_system_core/blob/android-11.0.0_r1/adb/pairing_connection/pairing_connection.cpp 36 | // Also based on Shizuku's implementation 37 | @RequiresApi(Build.VERSION_CODES.GINGERBREAD) 38 | public final class PairingConnectionCtx implements Closeable { 39 | public static final String TAG = PairingConnectionCtx.class.getSimpleName(); 40 | 41 | public static final String EXPORTED_KEY_LABEL = "adb-label\u0000"; 42 | public static final int EXPORT_KEY_SIZE = 64; 43 | 44 | private enum State { 45 | Ready, 46 | ExchangingMsgs, 47 | ExchangingPeerInfo, 48 | Stopped 49 | } 50 | 51 | enum Role { 52 | Client, 53 | Server, 54 | } 55 | 56 | private final String mHost; 57 | private final int mPort; 58 | private final byte[] mPswd; 59 | private final PeerInfo mPeerInfo; 60 | private final SSLContext mSslContext; 61 | private final Role mRole = Role.Client; 62 | 63 | private DataInputStream mInputStream; 64 | private DataOutputStream mOutputStream; 65 | private PairingAuthCtx mPairingAuthCtx; 66 | private State mState = State.Ready; 67 | 68 | public PairingConnectionCtx(@NonNull String host, int port, @NonNull byte[] pswd, @NonNull KeyPair keyPair, 69 | @NonNull String deviceName) 70 | throws NoSuchAlgorithmException, KeyManagementException, InvalidKeyException { 71 | this.mHost = Objects.requireNonNull(host); 72 | this.mPort = port; 73 | this.mPswd = Objects.requireNonNull(pswd); 74 | this.mPeerInfo = new PeerInfo(PeerInfo.ADB_RSA_PUB_KEY, AndroidPubkey.encodeWithName((RSAPublicKey) 75 | keyPair.getPublicKey(), Objects.requireNonNull(deviceName))); 76 | this.mSslContext = SslUtils.getSslContext(keyPair); 77 | } 78 | 79 | public PairingConnectionCtx(@NonNull String host, int port, @NonNull byte[] pswd, @NonNull PrivateKey privateKey, 80 | @NonNull Certificate certificate, @NonNull String deviceName) 81 | throws NoSuchAlgorithmException, KeyManagementException, InvalidKeyException { 82 | this(host, port, pswd, new KeyPair(Objects.requireNonNull(privateKey), Objects.requireNonNull(certificate)), 83 | deviceName); 84 | } 85 | 86 | public void start() throws IOException { 87 | if (mState != State.Ready) { 88 | throw new IOException("Connection is not ready yet."); 89 | } 90 | 91 | mState = State.ExchangingMsgs; 92 | 93 | // Start worker 94 | setupTlsConnection(); 95 | 96 | for (; ; ) { 97 | switch (mState) { 98 | case ExchangingMsgs: 99 | if (!doExchangeMsgs()) { 100 | notifyResult(); 101 | throw new IOException("Exchanging message wasn't successful."); 102 | } 103 | mState = State.ExchangingPeerInfo; 104 | break; 105 | case ExchangingPeerInfo: 106 | if (!doExchangePeerInfo()) { 107 | notifyResult(); 108 | throw new IOException("Could not exchange peer info."); 109 | } 110 | notifyResult(); 111 | return; 112 | case Ready: 113 | case Stopped: 114 | throw new IOException("Connection closed with errors."); 115 | } 116 | } 117 | } 118 | 119 | private void notifyResult() { 120 | mState = State.Stopped; 121 | } 122 | 123 | private void setupTlsConnection() throws IOException { 124 | Socket socket; 125 | if (mRole == Role.Server) { 126 | SSLServerSocket sslServerSocket = (SSLServerSocket) mSslContext.getServerSocketFactory().createServerSocket(mPort); 127 | socket = sslServerSocket.accept(); 128 | // TODO: Write automated test scripts after removing Conscrypt dependency. 129 | } else { // role == Role.Client 130 | socket = new Socket(mHost, mPort); 131 | } 132 | socket.setTcpNoDelay(true); 133 | 134 | // We use custom SSLContext to allow any SSL certificates 135 | SSLSocket sslSocket = (SSLSocket) mSslContext.getSocketFactory().createSocket(socket, mHost, mPort, true); 136 | sslSocket.startHandshake(); 137 | Log.d(TAG, "Handshake succeeded."); 138 | 139 | mInputStream = new DataInputStream(sslSocket.getInputStream()); 140 | mOutputStream = new DataOutputStream(sslSocket.getOutputStream()); 141 | 142 | // To ensure the connection is not stolen while we do the PAKE, append the exported key material from the 143 | // tls connection to the password. 144 | byte[] keyMaterial = exportKeyingMaterial(sslSocket, EXPORT_KEY_SIZE); 145 | byte[] passwordBytes = new byte[mPswd.length + keyMaterial.length]; 146 | System.arraycopy(mPswd, 0, passwordBytes, 0, mPswd.length); 147 | System.arraycopy(keyMaterial, 0, passwordBytes, mPswd.length, keyMaterial.length); 148 | 149 | PairingAuthCtx pairingAuthCtx = PairingAuthCtx.createAlice(passwordBytes); 150 | if (pairingAuthCtx == null) { 151 | throw new IOException("Unable to create PairingAuthCtx."); 152 | } 153 | this.mPairingAuthCtx = pairingAuthCtx; 154 | } 155 | 156 | @SuppressLint("PrivateApi") // Conscrypt is a stable private API 157 | private byte[] exportKeyingMaterial(SSLSocket sslSocket, int length) throws SSLException { 158 | // Conscrypt#exportKeyingMaterial(SSLSocket socket, String label, byte[] context, int length): byte[] 159 | // throws SSLException 160 | try { 161 | Class conscryptClass; 162 | if (SslUtils.isCustomConscrypt()) { 163 | conscryptClass = Class.forName("org.conscrypt.Conscrypt"); 164 | } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { 165 | // Although support for conscrypt has been added in Android 5.0 (Lollipop), 166 | // TLS1.3 isn't supported until Android 9 (Pie). 167 | throw new SSLException("TLSv1.3 isn't supported on your platform. Use custom Conscrypt library instead."); 168 | } else { 169 | conscryptClass = Class.forName("com.android.org.conscrypt.Conscrypt"); 170 | } 171 | Method exportKeyingMaterial = conscryptClass.getMethod("exportKeyingMaterial", SSLSocket.class, 172 | String.class, byte[].class, int.class); 173 | return (byte[]) exportKeyingMaterial.invoke(null, sslSocket, EXPORTED_KEY_LABEL, null, length); 174 | } catch (SSLException e) { 175 | throw e; 176 | } catch (Throwable th) { 177 | throw new SSLException(th); 178 | } 179 | } 180 | 181 | private void writeHeader(@NonNull PairingPacketHeader header, @NonNull byte[] payload) throws IOException { 182 | ByteBuffer buffer = ByteBuffer.allocate(PairingPacketHeader.PAIRING_PACKET_HEADER_SIZE) 183 | .order(ByteOrder.BIG_ENDIAN); 184 | header.writeTo(buffer); 185 | 186 | mOutputStream.write(buffer.array()); 187 | mOutputStream.write(payload); 188 | } 189 | 190 | @Nullable 191 | private PairingPacketHeader readHeader() throws IOException { 192 | byte[] bytes = new byte[PairingPacketHeader.PAIRING_PACKET_HEADER_SIZE]; 193 | mInputStream.readFully(bytes); 194 | ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN); 195 | return PairingPacketHeader.readFrom(buffer); 196 | } 197 | 198 | @NonNull 199 | private PairingPacketHeader createHeader(byte type, int payloadSize) { 200 | return new PairingPacketHeader(PairingPacketHeader.CURRENT_KEY_HEADER_VERSION, type, payloadSize); 201 | } 202 | 203 | private boolean checkHeaderType(byte expected, byte actual) { 204 | if (expected != actual) { 205 | Log.e(TAG, "Unexpected header type (expected=" + expected + " actual=" + actual + ")"); 206 | return false; 207 | } 208 | return true; 209 | } 210 | 211 | private boolean doExchangeMsgs() throws IOException { 212 | byte[] msg = mPairingAuthCtx.getMsg(); 213 | 214 | PairingPacketHeader ourHeader = createHeader(PairingPacketHeader.SPAKE2_MSG, msg.length); 215 | // Write our SPAKE2 msg 216 | writeHeader(ourHeader, msg); 217 | 218 | // Read the peer's SPAKE2 msg header 219 | PairingPacketHeader theirHeader = readHeader(); 220 | if (theirHeader == null || !checkHeaderType(PairingPacketHeader.SPAKE2_MSG, theirHeader.type)) return false; 221 | 222 | // Read the SPAKE2 msg payload and initialize the cipher for encrypting the PeerInfo and certificate. 223 | byte[] theirMsg = new byte[theirHeader.payloadSize]; 224 | mInputStream.readFully(theirMsg); 225 | 226 | try { 227 | return mPairingAuthCtx.initCipher(theirMsg); 228 | } catch (Exception e) { 229 | Log.e(TAG, "Unable to initialize pairing cipher"); 230 | //noinspection UnnecessaryInitCause 231 | throw (IOException) new IOException().initCause(e); 232 | } 233 | } 234 | 235 | private boolean doExchangePeerInfo() throws IOException { 236 | // Encrypt PeerInfo 237 | ByteBuffer buffer = ByteBuffer.allocate(PeerInfo.MAX_PEER_INFO_SIZE).order(ByteOrder.BIG_ENDIAN); 238 | mPeerInfo.writeTo(buffer); 239 | byte[] outBuffer = mPairingAuthCtx.encrypt(buffer.array()); 240 | if (outBuffer == null) { 241 | Log.e(TAG, "Failed to encrypt peer info"); 242 | return false; 243 | } 244 | 245 | // Write out the packet header 246 | PairingPacketHeader ourHeader = createHeader(PairingPacketHeader.PEER_INFO, outBuffer.length); 247 | // Write out the encrypted payload 248 | writeHeader(ourHeader, outBuffer); 249 | 250 | // Read in the peer's packet header 251 | PairingPacketHeader theirHeader = readHeader(); 252 | if (theirHeader == null || !checkHeaderType(PairingPacketHeader.PEER_INFO, theirHeader.type)) return false; 253 | 254 | // Read in the encrypted peer certificate 255 | byte[] theirMsg = new byte[theirHeader.payloadSize]; 256 | mInputStream.readFully(theirMsg); 257 | 258 | // Try to decrypt the certificate 259 | byte[] decryptedMsg = mPairingAuthCtx.decrypt(theirMsg); 260 | if (decryptedMsg == null) { 261 | Log.e(TAG, "Unsupported payload while decrypting peer info."); 262 | return false; 263 | } 264 | 265 | // The decrypted message should contain the PeerInfo. 266 | if (decryptedMsg.length != PeerInfo.MAX_PEER_INFO_SIZE) { 267 | Log.e(TAG, "Got size=" + decryptedMsg.length + " PeerInfo.size=" + PeerInfo.MAX_PEER_INFO_SIZE); 268 | return false; 269 | } 270 | 271 | PeerInfo theirPeerInfo = PeerInfo.readFrom(ByteBuffer.wrap(decryptedMsg)); 272 | Log.d(TAG, theirPeerInfo.toString()); 273 | return true; 274 | } 275 | 276 | @Override 277 | public void close() { 278 | Arrays.fill(mPswd, (byte) 0); 279 | try { 280 | mInputStream.close(); 281 | } catch (IOException ignore) { 282 | } 283 | try { 284 | mOutputStream.close(); 285 | } catch (IOException ignore) { 286 | } 287 | if (mState != State.Ready) { 288 | mPairingAuthCtx.destroy(); 289 | } 290 | } 291 | 292 | private static class PeerInfo { 293 | public static final int MAX_PEER_INFO_SIZE = 1 << 13; 294 | 295 | public static final byte ADB_RSA_PUB_KEY = 0; 296 | public static final byte ADB_DEVICE_GUID = 0; 297 | 298 | @NonNull 299 | public static PeerInfo readFrom(@NonNull ByteBuffer buffer) { 300 | byte type = buffer.get(); 301 | byte[] data = new byte[MAX_PEER_INFO_SIZE - 1]; 302 | buffer.get(data); 303 | return new PeerInfo(type, data); 304 | } 305 | 306 | private final byte type; 307 | private final byte[] data = new byte[MAX_PEER_INFO_SIZE - 1]; 308 | 309 | public PeerInfo(byte type, byte[] data) { 310 | this.type = type; 311 | System.arraycopy(data, 0, this.data, 0, Math.min(data.length, MAX_PEER_INFO_SIZE - 1)); 312 | } 313 | 314 | public void writeTo(@NonNull ByteBuffer buffer) { 315 | buffer.put(type).put(data); 316 | } 317 | 318 | @NonNull 319 | @Override 320 | public String toString() { 321 | return "PeerInfo{" + 322 | "type=" + type + 323 | ", data=" + Arrays.toString(data) + 324 | '}'; 325 | } 326 | } 327 | 328 | private static class PairingPacketHeader { 329 | public static final byte CURRENT_KEY_HEADER_VERSION = 1; 330 | public static final byte MIN_SUPPORTED_KEY_HEADER_VERSION = 1; 331 | public static final byte MAX_SUPPORTED_KEY_HEADER_VERSION = 1; 332 | 333 | public static final int MAX_PAYLOAD_SIZE = 2 * PeerInfo.MAX_PEER_INFO_SIZE; 334 | public static final byte PAIRING_PACKET_HEADER_SIZE = 6; 335 | 336 | public static final byte SPAKE2_MSG = 0; 337 | public static final byte PEER_INFO = 1; 338 | 339 | @Nullable 340 | public static PairingPacketHeader readFrom(@NonNull ByteBuffer buffer) { 341 | byte version = buffer.get(); 342 | byte type = buffer.get(); 343 | int payload = buffer.getInt(); 344 | if (version < MIN_SUPPORTED_KEY_HEADER_VERSION || version > MAX_SUPPORTED_KEY_HEADER_VERSION) { 345 | Log.e(TAG, "PairingPacketHeader version mismatch (us=" + CURRENT_KEY_HEADER_VERSION 346 | + " them=" + version + ")"); 347 | return null; 348 | } 349 | if (type != SPAKE2_MSG && type != PEER_INFO) { 350 | Log.e(TAG, "Unknown PairingPacket type " + type); 351 | return null; 352 | } 353 | if (payload <= 0 || payload > MAX_PAYLOAD_SIZE) { 354 | Log.e(TAG, "Header payload not within a safe payload size (size=" + payload + ")"); 355 | return null; 356 | } 357 | return new PairingPacketHeader(version, type, payload); 358 | } 359 | 360 | private final byte version; 361 | private final byte type; 362 | private final int payloadSize; 363 | 364 | public PairingPacketHeader(byte version, byte type, int payloadSize) { 365 | this.version = version; 366 | this.type = type; 367 | this.payloadSize = payloadSize; 368 | } 369 | 370 | public void writeTo(@NonNull ByteBuffer buffer) { 371 | buffer.put(version).put(type).putInt(payloadSize); 372 | } 373 | 374 | @NonNull 375 | @Override 376 | public String toString() { 377 | return "PairingPacketHeader{" + 378 | "version=" + version + 379 | ", type=" + type + 380 | ", payloadSize=" + payloadSize + 381 | '}'; 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/SslUtils.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import android.annotation.SuppressLint; 6 | import android.os.Build; 7 | 8 | import androidx.annotation.NonNull; 9 | 10 | import java.net.Socket; 11 | import java.security.KeyManagementException; 12 | import java.security.NoSuchAlgorithmException; 13 | import java.security.Principal; 14 | import java.security.PrivateKey; 15 | import java.security.Provider; 16 | import java.security.SecureRandom; 17 | import java.security.cert.X509Certificate; 18 | 19 | import javax.net.ssl.KeyManager; 20 | import javax.net.ssl.SSLContext; 21 | import javax.net.ssl.X509ExtendedKeyManager; 22 | import javax.net.ssl.X509TrustManager; 23 | 24 | final class SslUtils { 25 | private static boolean customConscrypt = false; 26 | private static SSLContext sslContext; 27 | 28 | public static boolean isCustomConscrypt() { 29 | return customConscrypt; 30 | } 31 | 32 | @SuppressLint("TrulyRandom") // The users are already instructed to fix this issue 33 | @NonNull 34 | public static SSLContext getSslContext(KeyPair keyPair) throws NoSuchAlgorithmException, KeyManagementException { 35 | if (sslContext != null) { 36 | return sslContext; 37 | } 38 | try { 39 | Class providerClass = Class.forName("org.conscrypt.OpenSSLProvider"); 40 | Provider openSslProvder = (Provider) providerClass.getDeclaredConstructor().newInstance(); 41 | sslContext = SSLContext.getInstance("TLSv1.3", openSslProvder); 42 | customConscrypt = true; 43 | } catch (NoSuchAlgorithmException e) { 44 | throw e; 45 | } catch (Throwable e) { 46 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { 47 | // Custom error message to inform user that they should use custom Conscrypt library. 48 | throw new NoSuchAlgorithmException("TLSv1.3 isn't supported on your platform. Use custom Conscrypt library instead."); 49 | } 50 | sslContext = SSLContext.getInstance("TLSv1.3"); 51 | customConscrypt = false; 52 | } 53 | System.out.println("Using " + (customConscrypt ? "custom" : "default") + " TLSv1.3 provider..."); 54 | sslContext.init(new KeyManager[]{getKeyManager(keyPair)}, 55 | new X509TrustManager[]{getAllAcceptingTrustManager()}, 56 | new SecureRandom()); 57 | return sslContext; 58 | } 59 | 60 | @NonNull 61 | private static KeyManager getKeyManager(KeyPair keyPair) { 62 | return new X509ExtendedKeyManager() { 63 | private final String mAlias = "key"; 64 | 65 | @Override 66 | public String[] getClientAliases(String keyType, Principal[] issuers) { 67 | return null; 68 | } 69 | 70 | @Override 71 | public String chooseClientAlias(String[] keyTypes, Principal[] issuers, Socket socket) { 72 | for (String keyType : keyTypes) { 73 | if (keyType.equals("RSA")) return mAlias; 74 | } 75 | return null; 76 | } 77 | 78 | @Override 79 | public String[] getServerAliases(String keyType, Principal[] issuers) { 80 | return null; 81 | } 82 | 83 | @Override 84 | public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) { 85 | return null; 86 | } 87 | 88 | @Override 89 | public X509Certificate[] getCertificateChain(String alias) { 90 | if (this.mAlias.equals(alias)) { 91 | return new X509Certificate[]{(X509Certificate) keyPair.getCertificate()}; 92 | } 93 | return null; 94 | } 95 | 96 | @Override 97 | public PrivateKey getPrivateKey(String alias) { 98 | if (this.mAlias.equals(alias)) { 99 | return keyPair.getPrivateKey(); 100 | } 101 | return null; 102 | } 103 | }; 104 | } 105 | 106 | @SuppressLint("TrustAllX509TrustManager") // Accept all certificates 107 | @NonNull 108 | private static X509TrustManager getAllAcceptingTrustManager() { 109 | return new X509TrustManager() { 110 | @Override 111 | public void checkClientTrusted(X509Certificate[] chain, String authType) { 112 | } 113 | 114 | @Override 115 | public void checkServerTrusted(X509Certificate[] chain, String authType) { 116 | } 117 | 118 | @Override 119 | public X509Certificate[] getAcceptedIssuers() { 120 | return new X509Certificate[0]; 121 | } 122 | }; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/StringCompat.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb; 4 | 5 | import android.os.Build; 6 | 7 | import androidx.annotation.NonNull; 8 | 9 | import java.io.UnsupportedEncodingException; 10 | import java.nio.charset.Charset; 11 | import java.nio.charset.IllegalCharsetNameException; 12 | 13 | final class StringCompat { 14 | @NonNull 15 | public static byte[] getBytes(@NonNull String text, @NonNull String charsetName) { 16 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { 17 | return text.getBytes(Charset.forName(charsetName)); 18 | } 19 | try { 20 | return text.getBytes(charsetName); 21 | } catch (UnsupportedEncodingException e) { 22 | throw (IllegalCharsetNameException) new IllegalCharsetNameException("Illegal charset " + charsetName) 23 | .initCause(e); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/android/AdbMdns.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb.android; 4 | 5 | import android.content.Context; 6 | import android.net.nsd.NsdManager; 7 | import android.net.nsd.NsdServiceInfo; 8 | import android.os.Build; 9 | 10 | import androidx.annotation.NonNull; 11 | import androidx.annotation.Nullable; 12 | import androidx.annotation.RequiresApi; 13 | import androidx.annotation.StringDef; 14 | 15 | import java.io.IOException; 16 | import java.lang.annotation.Retention; 17 | import java.lang.annotation.RetentionPolicy; 18 | import java.net.InetAddress; 19 | import java.net.InetSocketAddress; 20 | import java.net.NetworkInterface; 21 | import java.net.ServerSocket; 22 | import java.net.SocketException; 23 | import java.util.Collections; 24 | import java.util.Objects; 25 | 26 | /** 27 | * Automatic discovery of ADB daemons. 28 | */ 29 | // Copyright 2020 南宫雪珊 30 | // Copyright 2022 Muntashir Al-Islam 31 | // Based on https://android.googlesource.com/platform/packages/modules/adb/+/eddd2d3a386a83f5d1e14f87a318adef4c2f1a9d/adb_mdns.cpp 32 | @RequiresApi(Build.VERSION_CODES.JELLY_BEAN) 33 | public class AdbMdns { 34 | public static final String SERVICE_TYPE_ADB = "adb"; 35 | public static final String SERVICE_TYPE_TLS_PAIRING = "adb-tls-pairing"; 36 | public static final String SERVICE_TYPE_TLS_CONNECT = "adb-tls-connect"; 37 | 38 | @StringDef({ 39 | SERVICE_TYPE_ADB, 40 | SERVICE_TYPE_TLS_PAIRING, 41 | SERVICE_TYPE_TLS_CONNECT, 42 | }) 43 | @Retention(RetentionPolicy.SOURCE) 44 | public @interface ServiceType { 45 | } 46 | 47 | public interface OnAdbDaemonDiscoveredListener { 48 | void onPortChanged(@Nullable InetAddress hostAddress, int port); 49 | } 50 | 51 | @NonNull 52 | private final Context mContext; 53 | @NonNull 54 | private final String mServiceType; 55 | @NonNull 56 | private final OnAdbDaemonDiscoveredListener mAdbDaemonDiscoveredListener; 57 | private final NsdManager.DiscoveryListener mDiscoveryListener; 58 | private final NsdManager mNsdManager; 59 | 60 | private boolean mRegistered; 61 | private boolean mRunning; 62 | @Nullable 63 | private String mServiceName; 64 | 65 | public AdbMdns(@NonNull Context context, @ServiceType @NonNull String serviceType, 66 | @NonNull OnAdbDaemonDiscoveredListener portChangeListener) { 67 | mContext = Objects.requireNonNull(context); 68 | mServiceType = String.format("_%s._tcp", Objects.requireNonNull(serviceType)); 69 | mAdbDaemonDiscoveredListener = Objects.requireNonNull(portChangeListener); 70 | mNsdManager = (NsdManager) context.getSystemService(Context.NSD_SERVICE); 71 | mDiscoveryListener = new DiscoveryListener(this); 72 | } 73 | 74 | public void start() { 75 | if (mRunning) return; 76 | mRunning = true; 77 | if (!mRegistered) { 78 | mNsdManager.discoverServices(mServiceType, NsdManager.PROTOCOL_DNS_SD, mDiscoveryListener); 79 | } 80 | } 81 | 82 | public void stop() { 83 | if (!mRunning) return; 84 | mRunning = false; 85 | if (mRegistered) { 86 | mNsdManager.stopServiceDiscovery(mDiscoveryListener); 87 | } 88 | } 89 | 90 | private void onDiscoveryStart() { 91 | mRegistered = true; 92 | } 93 | 94 | private void onDiscoverStop() { 95 | mRegistered = false; 96 | } 97 | 98 | private void onServiceFound(NsdServiceInfo serviceInfo) { 99 | mNsdManager.resolveService(serviceInfo, new ResolveListener(this)); 100 | } 101 | 102 | private void onServiceLost(NsdServiceInfo serviceInfo) { 103 | if (mServiceName != null && mServiceName.equals(serviceInfo.getServiceName())) { 104 | mAdbDaemonDiscoveredListener.onPortChanged(serviceInfo.getHost(), -1); 105 | } 106 | } 107 | 108 | private void onServiceResolved(NsdServiceInfo serviceInfo) { 109 | if (!mRunning) return; 110 | try { 111 | for (NetworkInterface networkInterface : Collections.list(NetworkInterface.getNetworkInterfaces())) { 112 | for (InetAddress inetAddress : Collections.list(networkInterface.getInetAddresses())) { 113 | String inetHost = inetAddress.getHostAddress(); 114 | if (inetHost != null && inetHost.equals(serviceInfo.getHost().getHostAddress()) 115 | && isPortAvailable(serviceInfo.getPort())) { 116 | mServiceName = serviceInfo.getServiceName(); 117 | mAdbDaemonDiscoveredListener.onPortChanged(serviceInfo.getHost(), serviceInfo.getPort()); 118 | } 119 | } 120 | } 121 | } catch (SocketException e) { 122 | e.printStackTrace(); 123 | } 124 | } 125 | 126 | private boolean isPortAvailable(int port) { 127 | try (ServerSocket socket = new ServerSocket()) { 128 | socket.bind(new InetSocketAddress(AndroidUtils.getHostIpAddress(mContext), port), 1); 129 | return false; 130 | } catch (IOException e) { 131 | return true; 132 | } 133 | } 134 | 135 | private static class DiscoveryListener implements NsdManager.DiscoveryListener { 136 | @NonNull 137 | private final AdbMdns mAdbMdns; 138 | 139 | private DiscoveryListener(@NonNull AdbMdns adbMdns) { 140 | mAdbMdns = adbMdns; 141 | } 142 | 143 | @Override 144 | public void onDiscoveryStarted(String serviceType) { 145 | mAdbMdns.onDiscoveryStart(); 146 | } 147 | 148 | @Override 149 | public void onStartDiscoveryFailed(String serviceType, int errorCode) { 150 | } 151 | 152 | @Override 153 | public void onDiscoveryStopped(String serviceType) { 154 | mAdbMdns.onDiscoverStop(); 155 | } 156 | 157 | @Override 158 | public void onStopDiscoveryFailed(String serviceType, int errorCode) { 159 | } 160 | 161 | @Override 162 | public void onServiceFound(NsdServiceInfo serviceInfo) { 163 | mAdbMdns.onServiceFound(serviceInfo); 164 | } 165 | 166 | @Override 167 | public void onServiceLost(NsdServiceInfo serviceInfo) { 168 | mAdbMdns.onServiceLost(serviceInfo); 169 | } 170 | } 171 | 172 | private static class ResolveListener implements NsdManager.ResolveListener { 173 | @NonNull 174 | private final AdbMdns mAdbMdns; 175 | 176 | private ResolveListener(@NonNull AdbMdns adbMdns) { 177 | mAdbMdns = adbMdns; 178 | } 179 | 180 | @Override 181 | public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode) { 182 | } 183 | 184 | @Override 185 | public void onServiceResolved(NsdServiceInfo serviceInfo) { 186 | mAdbMdns.onServiceResolved(serviceInfo); 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/android/AndroidUtils.java: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: GPL-3.0-or-later OR Apache-2.0 2 | 3 | package io.github.muntashirakon.adb.android; 4 | 5 | import android.annotation.SuppressLint; 6 | import android.content.Context; 7 | import android.os.Build; 8 | import android.provider.Settings; 9 | 10 | import androidx.annotation.NonNull; 11 | 12 | import java.net.InetAddress; 13 | import java.net.UnknownHostException; 14 | 15 | public class AndroidUtils { 16 | // https://github.com/firebase/firebase-android-sdk/blob/7d86138304a6573cbe2c61b66b247e930fa05767/firebase-crashlytics/src/main/java/com/google/firebase/crashlytics/internal/common/CommonUtils.java#L402 17 | private static final String GOLDFISH = "goldfish"; 18 | private static final String RANCHU = "ranchu"; 19 | private static final String SDK = "sdk"; 20 | 21 | public static boolean isEmulator(@NonNull Context context) { 22 | if (Build.PRODUCT.contains(SDK)) { 23 | return true; 24 | } 25 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO 26 | && (Build.HARDWARE.contains(GOLDFISH) || Build.HARDWARE.contains(RANCHU))) { 27 | return true; 28 | } 29 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CUPCAKE) { 30 | @SuppressLint("HardwareIds") 31 | String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); 32 | return androidId == null; 33 | } 34 | return false; 35 | } 36 | 37 | 38 | @NonNull 39 | public static String getHostIpAddress(@NonNull Context context) { 40 | if (AndroidUtils.isEmulator(context)) { 41 | return "10.0.2.2"; 42 | } 43 | String ipAddress; 44 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 45 | ipAddress = InetAddress.getLoopbackAddress().getHostAddress(); 46 | } else { 47 | try { 48 | ipAddress = InetAddress.getLocalHost().getHostAddress(); 49 | } catch (UnknownHostException e) { 50 | ipAddress = null; 51 | } 52 | } 53 | if (ipAddress == null || ipAddress.equals("::1")) { 54 | return "127.0.0.1"; 55 | } 56 | return ipAddress; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /libadb/src/main/java/io/github/muntashirakon/adb/android/package.html: -------------------------------------------------------------------------------- 1 |

All Android dependencies are kept under this package for easy reference.

-------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':libadb' 2 | include ':app' 3 | rootProject.name = "libadb-android" 4 | project(':libadb').name = 'libadb' 5 | --------------------------------------------------------------------------------