├── .github └── workflows │ └── tests.yml ├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── pom.xml └── src ├── main └── java │ └── name │ └── neykov │ └── secrets │ ├── AgentAttach.java │ ├── AgentMain.java │ ├── AttachHelper.java │ ├── MasterSecretCallback.java │ ├── MessageException.java │ └── Transformer.java └── test ├── docker ├── Dockerfile.tomcat ├── Dockerfile.utils ├── clean.sh ├── server.xml ├── test.sh └── test_errors.sh └── java └── name └── neykov └── secrets ├── TestHttpClient.java └── TestURLConnection.java /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Integration Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Set up JDK 1.8 13 | uses: actions/setup-java@v1 14 | with: 15 | java-version: 1.8 16 | - name: Build and run tests 17 | run: mvn -B verify 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .classpath 2 | .project 3 | .settings 4 | target 5 | dependency-reduced-pom.xml 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Svetoslav Neykov 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # extract-tls-secrets 2 | 3 | Decrypt HTTPS/TLS connections on-the-fly. Extract the shared secrets from 4 | secure TLS connections for use with [Wireshark](https://www.wireshark.org/). 5 | Attach to a Java process on either side of the connection to start decrypting. 6 | 7 | ## Usage 8 | 9 | Download from [extract-tls-secrets-4.0.0.jar](https://repo1.maven.org/maven2/name/neykov/extract-tls-secrets/4.0.0/extract-tls-secrets-4.0.0.jar). 10 | Then attach to a Java process in one of two ways: 11 | 12 | ### Attach on startup 13 | 14 | Add a startup argument to the JVM options: `-javaagent:/extract-tls-secrets-4.0.0.jar=` 15 | 16 | For example to launch an application from a jar file run: 17 | 18 | ```shell script 19 | java -javaagent:~/Downloads/extract-tls-secrets-4.0.0.jar=/tmp/secrets.log -jar MyApp.jar 20 | ``` 21 | 22 | To launch in Tomcat add the parameter to `CATALINA_OPTS`: 23 | 24 | ```shell script 25 | CATALINA_OPTS=-javaagent:~/Downloads/extract-tls-secrets-4.0.0.jar=/tmp/secrets.log bin/catalina.sh run 26 | ``` 27 | 28 | ### Attach to a running process 29 | 30 | Attaching to an existing Java process requires a JDK install with `JAVA_HOME` 31 | pointing to it. 32 | 33 | To list the available process IDs run: 34 | 35 | ``` 36 | java -jar ~/Downloads/extract-tls-secrets-4.0.0.jar list 37 | ``` 38 | 39 | Next attach to the process by executing: 40 | 41 | ``` 42 | java -jar ~/Downloads/extract-tls-secrets-4.0.0.jar /tmp/secrets.log 43 | ``` 44 | 45 | ### Decrypt the capture in Wireshark 46 | 47 | To decrypt the capture you need to let Wireshark know where the secrets file is. 48 | Configure the path in 49 | `Preferences > Protocols > TLS (SSL for older versions) > (Pre)-Master-Secret log filename`. 50 | 51 | Alternatively start Wireshark with: 52 | 53 | ``` 54 | wireshark -o tls.keylog_file:/tmp/secrets.log 55 | ``` 56 | 57 | The packets will be decrypted in real-time. 58 | 59 | For a step by step tutorial of using the secrets log file (SSLKEYLOGFILE as referenced usually) 60 | refer to the Peter Wu's [Debugging TLS issues with Wireshark](https://lekensteyn.nl/files/wireshark-tls-debugging-sharkfest19eu.pdf) 61 | presentation. Even more information can be found at the [Wireshark TLS](https://wiki.wireshark.org/TLS) page. 62 | 63 | ## Requirements 64 | 65 | Requires at least Oracle/OpenJDK Java 6. Does not support IBM Java and custom 66 | security providers like Bouncy Castle, Conscrypt. 67 | 68 | ## Building 69 | 70 | ``` 71 | git clone https://github.com/neykov/extract-tls-secrets.git 72 | cd extract-tls-secrets 73 | mvn clean package 74 | ``` 75 | 76 | Running the integration tests requires Docker to be installed on the system: 77 | 78 | ```shell script 79 | mvn verify 80 | ``` 81 | 82 | ## Troubleshooting 83 | 84 | If you get an empty window after selecting "Follow/TLS Stream" from the context menu 85 | or are not seeing HTTP protocol packets in the packet list then you can fix this by either: 86 | * Save the capture as a file and open it again 87 | * In the Wireshark settings in "Procotols/TLS" toggle "Reassemble TLS Application Data spanning multiple SSL records". 88 | The exact state of the checkbox doesn't matter, but it will force a reload which will force proper decryption of the packets. 89 | 90 | The bug seems to be related to the UI side of wireshark as the TLS debug logs show the message successfully being decrypted. 91 | 92 | Reports of the problem: 93 | * https://ask.wireshark.org/questions/33879/ssl-decrypt-shows-ok-in-ssl-debug-file-but-not-in-wireshark 94 | * https://bugs.wireshark.org/bugzilla/show_bug.cgi?id=9154 95 | 96 | 97 | If "Follow/TLS Stream" is not enabled the server is probably on a non-standard port so Wireshark can't infer that the 98 | packets contain TLS traffic. To hint it that it should be decoding the packets as TLS 99 | right click on any of the packets to open the context menu, select "Decode As" and add 100 | the server port, select "TLS" protocol in the "Current" column. If it's still not able 101 | to decrypt try the same by saving the capture in a file and re-opening it. 102 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | name.neykov 6 | extract-tls-secrets 7 | 4.1.0-SNAPSHOT 8 | 9 | Extract TLS Master Secret 10 | 11 | Decrypt HTTPS/TLS connections on-the-fly with Wireshark. 12 | 13 | Extracts the shared master key used in secure connections (SSL & TLS) 14 | for use with Wireshark. Works with connections established with the 15 | (Java provided) javax.net.ssl.SSLSocket API. 16 | 17 | https://github.com/neykov/extract-tls-secrets 18 | 19 | 20 | 21 | The Apache License, Version 2.0 22 | http://www.apache.org/licenses/LICENSE-2.0.txt 23 | 24 | 25 | 26 | 27 | 28 | Svetoslav Neykov 29 | svetoslav@neykov.name 30 | 31 | 32 | 33 | 34 | scm:git:https://github.com/neykov/extract-tls-secrets.git 35 | scm:git:ssh://github.com:neykov/extract-tls-secrets.git 36 | http://github.com/neykov/extract-tls-secrets 37 | 38 | 39 | 40 | UTF-8 41 | 1.8 42 | 1.8 43 | 44 | 45 | 46 | 47 | org.javassist 48 | javassist 49 | 3.23.2-GA 50 | 51 | 52 | 53 | 54 | 55 | 56 | org.apache.maven.plugins 57 | maven-jar-plugin 58 | 3.2.0 59 | 60 | 61 | 62 | name.neykov.secrets.AgentAttach 63 | name.neykov.secrets.AgentMain 64 | name.neykov.secrets.AgentMain 65 | true 66 | true 67 | 68 | 69 | 70 | 71 | 72 | org.apache.maven.plugins 73 | maven-shade-plugin 74 | 3.2.1 75 | 76 | 77 | package 78 | 79 | shade 80 | 81 | 82 | 83 | 84 | javassist 85 | name.neykov.secrets.internal.javassist 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | org.codehaus.mojo 94 | exec-maven-plugin 95 | 1.6.0 96 | 97 | 98 | run-test-script 99 | integration-test 100 | 101 | exec 102 | 103 | 104 | src/test/docker/test.sh 105 | 106 | attach 107 | target/${project.artifactId}-${project.version}.jar 108 | 109 | 110 | 111 | 112 | run-clean-script 113 | clean 114 | 115 | exec 116 | 117 | 118 | src/test/docker/clean.sh 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 134 | 135 | jdk-profile 136 | 137 | 138 | ${java.home}/lib/tools.jar 139 | 140 | 141 | 142 | ${java.home}/lib/tools.jar 143 | 144 | 145 | 146 | jre-profile 147 | 148 | 149 | ${java.home}/../lib/tools.jar 150 | 151 | 152 | 153 | ${java.home}/../lib/tools.jar 154 | 155 | 156 | 157 | apple-jdk 158 | 159 | 160 | ${java.home}/../Classes/classes.jar 161 | 162 | 163 | 164 | ${java.home}/../Classes/classes.jar 165 | 166 | 167 | 168 | tools 169 | 170 | 171 | [,1.9) 172 | 173 | 174 | 175 | com.sun 176 | tools 177 | system 178 | 1.6 179 | ${tools.path} 180 | 181 | 182 | 183 | 184 | 185 | Release 186 | 187 | 188 | 189 | maven-source-plugin 190 | 3.2.0 191 | 192 | 193 | attach-sources 194 | 195 | jar-no-fork 196 | 197 | 198 | 199 | 200 | 201 | maven-javadoc-plugin 202 | 3.1.1 203 | 204 | 205 | attach-javadocs 206 | 207 | jar 208 | 209 | 210 | 211 | 212 | 213 | maven-gpg-plugin 214 | 1.6 215 | 216 | 217 | sign-artifacts 218 | verify 219 | 220 | sign 221 | 222 | 223 | 224 | 225 | 226 | org.sonatype.plugins 227 | nexus-staging-maven-plugin 228 | 1.6.8 229 | true 230 | 231 | ossrh 232 | https://oss.sonatype.org/ 233 | true 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /src/main/java/name/neykov/secrets/AgentAttach.java: -------------------------------------------------------------------------------- 1 | package name.neykov.secrets; 2 | 3 | import java.io.File; 4 | import java.lang.reflect.InvocationTargetException; 5 | import java.lang.reflect.Method; 6 | import java.net.URL; 7 | import java.net.URLClassLoader; 8 | 9 | public class AgentAttach { 10 | // Called on "java -jar" execution. Will attach self to the target process. 11 | public static void main(String[] args) throws Exception { 12 | URL jarUrl = AgentAttach.class.getProtectionDomain().getCodeSource().getLocation(); 13 | File jarFile = new File(jarUrl.toURI()); 14 | if (!jarFile.getName().endsWith(".jar")) { 15 | System.err.println("The agent is not running from a jar file. Attachment will likely fail."); 16 | } 17 | 18 | if (args.length == 0 || args.length > 2 || (args.length == 2 && args[0].equals("list"))) { 19 | System.err.println("Missing required argument: pid (the process to attach to)"); 20 | System.out.println(); 21 | System.out.println("Usage: java -jar " + jarFile.getName() + " []"); 22 | System.out.println(" java -jar " + jarFile.getName() + " list"); 23 | System.out.println(); 24 | System.out.println("Options:"); 25 | System.out.println(" * list - shows available Java processes to attach to"); 26 | System.out.println(" * pid - the process ID to attach to (required)"); 27 | System.out.println(" * secrets_file - file path to log the shared secrets to (optional);"); 28 | System.out.println(" if a relative path is used it's resolved against the target process working folder;"); 29 | System.out.println(" default value is '" + AgentMain.DEFAULT_SECRETS_FILE + "'"); 30 | System.out.println(); 31 | System.out.println("Note: The location of the secrets file will be logged at INFO level in the target process."); 32 | System.exit(1); 33 | } 34 | 35 | String pid = args[0]; 36 | String logFile = null; 37 | if (args.length == 2) { 38 | logFile = args[1]; 39 | } 40 | 41 | try { 42 | attach(jarUrl, jarFile, pid, logFile); 43 | } catch (MessageException e) { 44 | for (String line : e.msg) { 45 | System.err.println(line); 46 | } 47 | System.exit(1); 48 | } 49 | } 50 | 51 | public static void attach(URL jarUrl, File jarFile, String pid, String logFile) throws Exception { 52 | if (isAttachApiAvailable()) { 53 | // Either Java 9 or tools.jar already on classpath 54 | AttachHelper.handle(jarFile.getAbsolutePath(), pid, logFile); 55 | } else { 56 | File toolsFile = getToolsFile(); 57 | URL toolsUrl = toolsFile.toURI().toURL(); 58 | URL[] cp = new URL[] {jarUrl, toolsUrl}; 59 | URLClassLoader classLoader = new URLClassLoader(cp, null); 60 | Thread.currentThread().setContextClassLoader(classLoader); 61 | Class helper = classLoader.loadClass("name.neykov.secrets.AttachHelper"); 62 | 63 | Method handleMethod = helper.getMethod("handle", String.class, String.class, String.class); 64 | handleMethod.invoke(null, jarFile.getAbsolutePath(), pid, logFile); 65 | } 66 | 67 | } 68 | 69 | private static File getToolsFile() throws MessageException { 70 | File javaHome = getJavaHome(); 71 | 72 | // javaHome is a JDK 73 | File toolsFile = new File(javaHome, "lib/tools.jar"); 74 | if (toolsFile.exists()) { 75 | return toolsFile; 76 | } 77 | 78 | // javaHome is the jre subfolder **inside** a JDK home 79 | File toolsFileAlt = new File(javaHome, "../lib/tools.jar"); 80 | if (toolsFileAlt.exists()) { 81 | return toolsFileAlt; 82 | } 83 | 84 | // Apple packaged Java 85 | File classesFile = new File(javaHome, "../Classes/classes.jar"); 86 | if (classesFile.exists()) { 87 | return classesFile; 88 | } 89 | 90 | // Someone decided to copy the tools.jar from a JDK inside working dir 91 | File localToolsFile = new File("tools.jar"); 92 | if (localToolsFile.exists()) { 93 | return localToolsFile; 94 | } 95 | 96 | // Java version format: 97 | // * Java 8 and lower: 1.X.0 (e.x. 1.6.0, 1.7.0, 1.8.0) 98 | // * Java 9 and higher: X.0.0 (e.x. 9.0.0, 11.0.0) 99 | if (System.getProperty("java.version").startsWith("1.")) { 100 | // JAVA_HOME required 101 | throw new MessageException( 102 | "Invalid JAVA_HOME environment variable '" + javaHome.getAbsolutePath() + "'.", 103 | "Must point to a local JDK installation containing a 'lib/tools.jar' file." 104 | ); 105 | } else { 106 | // No need for JAVA_HOME. Not executed from a JDK java executable. 107 | throw new MessageException( 108 | "No access to JDK classes. Make sure to use the java executable from a JDK install." 109 | ); 110 | } 111 | } 112 | 113 | private static File getJavaHome() throws MessageException { 114 | String javaHomeEnv = System.getenv("JAVA_HOME"); 115 | if (javaHomeEnv != null) { 116 | return new File(javaHomeEnv); 117 | } 118 | 119 | throw new MessageException("No JAVA_HOME environment variable found. Must point to a local JDK installation."); 120 | } 121 | 122 | private static boolean isAttachApiAvailable() { 123 | try { 124 | AgentAttach.class.getClassLoader().loadClass("com.sun.tools.attach.VirtualMachine"); 125 | return true; 126 | } catch (ClassNotFoundException e) { 127 | return false; 128 | } 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /src/main/java/name/neykov/secrets/AgentMain.java: -------------------------------------------------------------------------------- 1 | package name.neykov.secrets; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.lang.instrument.Instrumentation; 6 | import java.lang.reflect.InvocationTargetException; 7 | import java.lang.reflect.Method; 8 | import java.net.MalformedURLException; 9 | import java.net.URISyntaxException; 10 | import java.net.URL; 11 | import java.util.Arrays; 12 | import java.util.Collections; 13 | import java.util.HashMap; 14 | import java.util.HashSet; 15 | import java.util.Map; 16 | import java.util.Set; 17 | import java.util.jar.JarFile; 18 | import java.util.logging.Level; 19 | import java.util.logging.Logger; 20 | 21 | import javax.net.ssl.SSLEngine; 22 | 23 | // Loaded in the App class loader 24 | public class AgentMain { 25 | private static final Logger log = Logger.getLogger(AgentMain.class.getName()); 26 | 27 | // Created in process working directory 28 | public static final String DEFAULT_SECRETS_FILE = "ssl-master-secrets.txt"; 29 | 30 | // Called from inside the target process when using "-javaagent:" option. 31 | public static void premain(String agentArgs, Instrumentation inst) { 32 | File jarFile = getJarFile(); 33 | initClassPath(inst, jarFile); 34 | main(agentArgs, inst, jarFile); 35 | } 36 | 37 | // Called from inside the target process when attaching at runtime. 38 | public static void agentmain(String agentArgs, Instrumentation inst) { 39 | File jarFile = getJarFile(); 40 | initClassPath(inst, jarFile); 41 | main(agentArgs, inst, jarFile); 42 | reloadClasses(inst); 43 | } 44 | 45 | /** 46 | * The agent is loaded in the App class loader. Instrumented 47 | * classes are in the boot class loader so can't see "MasterSecretCallback" 48 | * by default. Adding self to the boot class loader will make 49 | * MasterSecretCallback visible to core classes. Note that this leads 50 | * to a split-brain state where some classes of the jar are loaded 51 | * by the App class loader and some in the boot class loader. 52 | */ 53 | private static void initClassPath(Instrumentation inst, File jarFile) { 54 | try { 55 | inst.appendToBootstrapClassLoaderSearch(new JarFile(jarFile)); 56 | } catch (IOException e) { 57 | log.log(Level.WARNING, "Failed attaching to process. Can't access jar file " + jarFile, e); 58 | throw new IllegalStateException(e); 59 | } 60 | } 61 | 62 | public static URL getJarFileOrClassFolder(Class clz){ 63 | URL path = clz.getProtectionDomain().getCodeSource().getLocation(); 64 | log.log(Level.INFO, "get class source location is " + path); 65 | if (null == path) 66 | { 67 | //the class is loaded by Java Extension Class Loader, not System Class Loader, so try parse by full path 68 | String classFullName = clz.getName().replace('.', '/') + ".class"; 69 | URL selfFullResourceUrl = clz.getClassLoader().getResource(classFullName); 70 | log.log(Level.INFO, "selfFullResourceUrl=" + selfFullResourceUrl); 71 | 72 | if (null != selfFullResourceUrl){ 73 | String strFullUrlPath = selfFullResourceUrl.toString(); 74 | 75 | int classNameIndex = strFullUrlPath.indexOf(classFullName); 76 | if (classNameIndex > 0) { 77 | //sample: jar:file:/E:/CodeGithub/xxx/xxx.jar!/ , so need remove "jar:" and "!/" 78 | String strRealUrl = strFullUrlPath.substring(0, classNameIndex); 79 | 80 | int startIndex = 0; 81 | int endIndex = strRealUrl.length(); 82 | if(strRealUrl.startsWith("jar:")){ 83 | startIndex = 4; 84 | } 85 | if(strRealUrl.endsWith("!/")) { 86 | endIndex = strRealUrl.length() - 2; 87 | } 88 | strRealUrl = strRealUrl.substring(startIndex, endIndex); 89 | log.log(Level.INFO, "selfFullResourceUrl=" + selfFullResourceUrl); 90 | try { 91 | path = new URL(strRealUrl); 92 | } catch (MalformedURLException e) { 93 | log.log(Level.WARNING, "generate URL failed, strRealUrl = " + strRealUrl, e); 94 | path = null; 95 | } 96 | } 97 | } 98 | } 99 | //URLDecoder.decode(path.getPath(), StandardCharsets.UTF_8); 100 | return path; 101 | } 102 | 103 | private static File getJarFile() { 104 | URL jarUrl = getJarFileOrClassFolder(AgentMain.class); 105 | try { 106 | return new File(jarUrl.toURI()); 107 | } catch (URISyntaxException e) { 108 | log.log(Level.WARNING, "Failed attaching to process. Can't convert jar to a local path " + jarUrl, e); 109 | throw new IllegalStateException(e); 110 | } 111 | } 112 | 113 | // When attaching to a running VM, the classes we are interested 114 | // in might already have been loaded and used. Need to force a reload 115 | // so our transformer kicks in. 116 | private static void reloadClasses(Instrumentation inst) { 117 | for (Class loadedClass : inst.getAllLoadedClasses()) { 118 | if (Transformer.needsTransform(loadedClass.getName())) { 119 | try { 120 | inst.retransformClasses(loadedClass); 121 | } catch (Throwable e) { 122 | log.log(Level.WARNING, "Failed instrumenting " + loadedClass.getName() + ". Shared secret extraction might fail.", e); 123 | if (e instanceof InterruptedException) { 124 | Thread.currentThread().interrupt(); 125 | return; 126 | } 127 | throw new IllegalStateException(e); 128 | } 129 | } 130 | } 131 | } 132 | 133 | private static void main(String agentArgs, Instrumentation inst, File jarFile) { 134 | openBaseModule(inst); 135 | 136 | String canonicalSecretsPath = getCanonicalSecretsPath(agentArgs); 137 | 138 | // MasterSecretCallback is loaded in boot class loader 139 | MasterSecretCallback.setSecretsFileName(canonicalSecretsPath); 140 | inst.addTransformer(new Transformer(jarFile), true); 141 | 142 | log.info("Successfully attached agent " + jarFile + ". Logging to " + canonicalSecretsPath + ". "); 143 | } 144 | 145 | private static void openBaseModule(Instrumentation inst) { 146 | Method getModule; 147 | try { 148 | getModule = Class.class.getMethod("getModule"); 149 | } catch (NoSuchMethodException e) { 150 | // No modules available, no need to open them (< Java 9) 151 | return; 152 | } catch (SecurityException e) { 153 | // No modules available, no need to open them (< Java 9) 154 | return; 155 | } 156 | 157 | try { 158 | Map> extraOpens = new HashMap>(); 159 | extraOpens.put("sun.security.ssl", new HashSet(Arrays.asList(getModule.invoke(MasterSecretCallback.class)))); 160 | 161 | Method redefineModule = Instrumentation.class.getMethod("redefineModule", 162 | getModule.getReturnType(), Set.class, Map.class, 163 | Map.class, Set.class, Map.class); 164 | 165 | redefineModule.invoke(inst, 166 | getModule.invoke(SSLEngine.class), 167 | Collections.EMPTY_SET, 168 | Collections.EMPTY_MAP, 169 | extraOpens, 170 | Collections.EMPTY_SET, 171 | Collections.EMPTY_MAP); 172 | } catch (IllegalAccessException e) { 173 | log.log(Level.WARNING, "Failed opening modules.", e); 174 | throw new IllegalStateException(e); 175 | } catch (IllegalArgumentException e) { 176 | log.log(Level.WARNING, "Failed opening modules.", e); 177 | throw new IllegalStateException(e); 178 | } catch (InvocationTargetException e) { 179 | log.log(Level.WARNING, "Failed opening modules.", e); 180 | throw new IllegalStateException(e); 181 | } catch (NoSuchMethodException e) { 182 | log.log(Level.WARNING, "Failed opening modules.", e); 183 | throw new IllegalStateException(e); 184 | } catch (SecurityException e) { 185 | log.log(Level.WARNING, "Failed opening modules.", e); 186 | throw new IllegalStateException(e); 187 | } 188 | } 189 | 190 | private static String getCanonicalSecretsPath(String agentArgs) { 191 | String secretsPath; 192 | if (agentArgs != null && !agentArgs.isEmpty()) { 193 | secretsPath = agentArgs; 194 | } else { 195 | secretsPath = DEFAULT_SECRETS_FILE; 196 | } 197 | 198 | File secretsFile = new File(secretsPath); 199 | if (!secretsFile.isAbsolute()) { 200 | secretsFile = new File(System.getProperty("user.dir"), secretsPath); 201 | } 202 | 203 | try { 204 | return secretsFile.getCanonicalPath(); 205 | } catch (IOException e) { 206 | log.log(Level.WARNING, "Failed getting the canonical path for " + secretsFile, e); 207 | throw new IllegalStateException(e); 208 | } 209 | } 210 | 211 | } 212 | -------------------------------------------------------------------------------- /src/main/java/name/neykov/secrets/AttachHelper.java: -------------------------------------------------------------------------------- 1 | package name.neykov.secrets; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | 6 | import com.sun.tools.attach.AgentInitializationException; 7 | import com.sun.tools.attach.AgentLoadException; 8 | import com.sun.tools.attach.AttachNotSupportedException; 9 | import com.sun.tools.attach.VirtualMachine; 10 | import com.sun.tools.attach.VirtualMachineDescriptor; 11 | 12 | //Alternative when tools.jar not available 13 | //https://github.com/apangin/jattach 14 | // 15 | //Byte Buddy (https://github.com/raphw/byte-buddy) abstracts 16 | //the API, including a fallback implementing the attach api. 17 | public class AttachHelper { 18 | public static void handle(String jarPath, String pid, String logFile) throws MessageException { 19 | if (pid.equals("list")) { 20 | System.out.print(AttachHelper.list()); 21 | } else { 22 | try { 23 | AttachHelper.attach(pid, jarPath, logFile); 24 | System.out.println("Successfully attached to process ID " + pid + "."); 25 | } catch (IllegalStateException e) { 26 | String msg = e.getMessage() != null ? e.getMessage() : "Failed attaching to java process " + pid; 27 | throw new MessageException(msg); 28 | } 29 | } 30 | 31 | } 32 | private static void attach(String pid, String jarPath, String options) { 33 | try { 34 | VirtualMachine vm = VirtualMachine.attach(pid); 35 | vm.loadAgent(jarPath, options); 36 | vm.detach(); 37 | } catch (AgentLoadException e) { 38 | throw error(pid, e); 39 | } catch (AgentInitializationException e) { 40 | throw error(pid, e); 41 | } catch (IOException e) { 42 | throw error(pid, e); 43 | } catch (AttachNotSupportedException e) { 44 | throw error(pid, e); 45 | } 46 | } 47 | 48 | private static String list() { 49 | StringBuilder msg = new StringBuilder(); 50 | for (VirtualMachineDescriptor vm : VirtualMachine.list()) { 51 | msg.append(" ").append(vm.id()).append(" ").append(vm.displayName()).append("\n"); 52 | } 53 | return msg.toString(); 54 | } 55 | 56 | private static IllegalStateException error(String pid, Exception e) { 57 | StringBuilder msg = new StringBuilder("Failed to attach to java process ").append(pid).append("."); 58 | if (!pidExists(pid)) { 59 | msg.append("\n\nNo Java process with ID ").append(pid).append(" found. Running Java processes:\n"); 60 | msg.append(list()); 61 | } else { 62 | msg.append(" Cause: " + e.getMessage()); 63 | } 64 | return new IllegalStateException(msg.toString(), e); 65 | } 66 | 67 | private static boolean pidExists(String pid) { 68 | for (VirtualMachineDescriptor vm : VirtualMachine.list()) { 69 | if (vm.id().equals(pid)) { 70 | return true; 71 | } 72 | } 73 | return false; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/name/neykov/secrets/MasterSecretCallback.java: -------------------------------------------------------------------------------- 1 | package name.neykov.secrets; 2 | 3 | import java.io.FileWriter; 4 | import java.io.IOException; 5 | import java.io.Writer; 6 | import java.lang.reflect.Field; 7 | import java.security.Key; 8 | import java.util.Collections; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.logging.Level; 12 | import java.util.logging.Logger; 13 | 14 | import javax.crypto.SecretKey; 15 | import java.security.cert.X509Certificate; 16 | import java.security.PrivateKey; 17 | import javax.net.ssl.SSLSession; 18 | import java.util.Base64; 19 | 20 | import java.util.Date; 21 | import java.text.SimpleDateFormat; 22 | 23 | //Secrets file format: 24 | //https://github.com/boundary/wireshark/blob/d029f48e4fd74b09848fc309630e5dfdc5d602f2/epan/dissectors/packet-ssl-utils.c#L4164-L4182 25 | public class MasterSecretCallback { 26 | private static final Logger log = Logger.getLogger(MasterSecretCallback.class.getName()); 27 | private static final String NL = System.getProperty("line.separator"); 28 | private static final String LINE_SEPARATOR = System.getProperty("line.separator"); 29 | private static final Base64.Encoder b64Encoder = Base64.getMimeEncoder(64, LINE_SEPARATOR.getBytes()); 30 | private static SimpleDateFormat DATE_FMT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSSXXX"); 31 | private static String secretsFileName; 32 | public static void setSecretsFileName(String secretsFileName) { 33 | MasterSecretCallback.secretsFileName = secretsFileName; 34 | } 35 | 36 | private static String getConnectionDetails(SSLSession sslSession) { 37 | String dateNow; 38 | synchronized (DATE_FMT) { 39 | dateNow = DATE_FMT.format(new Date()); 40 | } 41 | 42 | String peerHost = sslSession.getPeerHost(); 43 | int portHost = sslSession.getPeerPort(); 44 | String protocol = sslSession.getProtocol(); 45 | String cipherSuite = sslSession.getCipherSuite(); 46 | String peerHostSection = ""; 47 | if (peerHost != null) { 48 | peerHostSection = "Peer: " + peerHost + ":" + portHost + ", "; 49 | } 50 | String connectionDetails = 51 | "# " + dateNow + " " + peerHostSection + 52 | "CipherSuite: " + cipherSuite + ", Protocol: " + protocol; 53 | return connectionDetails; 54 | } 55 | 56 | public static void onMasterSecret(SSLSession sslSession, Key masterSecret) { 57 | try { 58 | String connectionDetails = getConnectionDetails(sslSession); 59 | String sessionKey = bytesToHex(sslSession.getId()); 60 | String masterKey = bytesToHex(masterSecret.getEncoded()); 61 | write( 62 | connectionDetails, 63 | "RSA Session-ID:" + sessionKey + " Master-Key:" + masterKey 64 | ); 65 | } catch (Exception e) { 66 | log.log(Level.WARNING, "Error retrieving master secret from " + sslSession, e); 67 | } 68 | } 69 | 70 | public static void onSetLocalPrivateKey(SSLSession sslSession, PrivateKey privateKey) { 71 | try { 72 | //String masterKey = bytesToHex(privateKey.getEncoded()); 73 | String masterKey = b64Encoder.encodeToString(privateKey.getEncoded()); 74 | writePrivateKey(masterKey); 75 | } catch (Exception e) { 76 | log.log(Level.WARNING, "Error retrieving master secret from " + sslSession, e); 77 | } 78 | } 79 | 80 | public static void onSetLocalCertificates(SSLSession sslSession, X509Certificate[] certs) { 81 | try { 82 | for(int i = 0; i TLS13_SECRET_NAMES; 107 | static { 108 | Map secrets = new HashMap(); 109 | 110 | // TLS 1.1 111 | secrets.put("TlsMasterSecret", "CLIENT_RANDOM"); 112 | 113 | // TLS 1.3 114 | secrets.put("TlsClientEarlyTrafficSecret", "CLIENT_EARLY_TRAFFIC_SECRET"); 115 | secrets.put("TlsEarlyExporterMasterSecret", "EARLY_EXPORTER_SECRET"); 116 | secrets.put("TlsClientHandshakeTrafficSecret", "CLIENT_HANDSHAKE_TRAFFIC_SECRET"); 117 | secrets.put("TlsServerHandshakeTrafficSecret", "SERVER_HANDSHAKE_TRAFFIC_SECRET"); 118 | secrets.put("TlsClientAppTrafficSecret", "CLIENT_TRAFFIC_SECRET_0"); 119 | secrets.put("TlsServerAppTrafficSecret", "SERVER_TRAFFIC_SECRET_0"); 120 | secrets.put("TlsExporterMasterSecret", "EXPORTER_SECRET"); 121 | 122 | TLS13_SECRET_NAMES = Collections.unmodifiableMap(secrets); 123 | } 124 | 125 | public static void onKeyDerivation(Object context, SecretKey key) { 126 | String secretName = TLS13_SECRET_NAMES.get(key.getAlgorithm()); 127 | if (secretName == null) { 128 | return; 129 | } 130 | try { 131 | SSLSession sslSession = (SSLSession) get(context, "handshakeSession"); 132 | String connectionDetails = getConnectionDetails(sslSession); 133 | Object clientRandom = get(context, "clientHelloRandom"); 134 | String clientRandomBytes = bytesToHex((byte[]) get(clientRandom, "randomBytes")); 135 | write(connectionDetails, secretName + " " + clientRandomBytes + " " + bytesToHex(key.getEncoded())); 136 | } catch (Exception e) { 137 | log.log(Level.WARNING, "Error retrieving client random secret from " + context, e); 138 | } 139 | } 140 | 141 | private static synchronized void write(String... secrets) throws IOException { 142 | Writer out = new FileWriter(secretsFileName, true); 143 | for (String secret : secrets) { 144 | out.write(secret); 145 | out.write(NL); 146 | } 147 | out.close(); 148 | } 149 | private static synchronized void writePrivateKey(String privateKey) throws IOException { 150 | Writer out = new FileWriter(secretsFileName+".key", true); 151 | out.write("-----BEGIN PRIVATE KEY-----\n"); 152 | out.write(privateKey); 153 | out.write(NL); 154 | out.write("-----END PRIVATE KEY-----"); 155 | out.write(NL); 156 | out.close(); 157 | } 158 | private static synchronized void writeCert(String cert) throws IOException { 159 | Writer out = new FileWriter(secretsFileName+".crt", true); 160 | out.write("-----BEGIN CERTIFICATE-----\n"); 161 | out.write(cert); 162 | out.write(NL); 163 | out.write("-----END CERTIFICATE-----\n"); 164 | out.close(); 165 | } 166 | 167 | private final static char[] hexArray = "0123456789ABCDEF".toCharArray(); 168 | private static String bytesToHex(byte[] bytes) { 169 | char[] hexChars = new char[bytes.length * 2]; 170 | for (int j = 0; j < bytes.length; j++) { 171 | int v = bytes[j] & 0xFF; 172 | hexChars[j * 2] = hexArray[v >>> 4]; 173 | hexChars[j * 2 + 1] = hexArray[v & 0x0F]; 174 | } 175 | return new String(hexChars); 176 | } 177 | 178 | private static Object get(Object newObj, String field) throws IllegalAccessException, NoSuchFieldException { 179 | Class type = newObj.getClass(); 180 | while (type != null) { 181 | try { 182 | Field f = type.getDeclaredField(field); 183 | f.setAccessible(true); 184 | return f.get(newObj); 185 | } catch (NoSuchFieldException e) { 186 | type = type.getSuperclass(); 187 | } 188 | } 189 | throw new NoSuchFieldException(field); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/name/neykov/secrets/MessageException.java: -------------------------------------------------------------------------------- 1 | package name.neykov.secrets; 2 | 3 | class MessageException extends Exception { 4 | String[] msg; 5 | 6 | protected MessageException(String... msg) { 7 | this.msg = msg; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/name/neykov/secrets/Transformer.java: -------------------------------------------------------------------------------- 1 | package name.neykov.secrets; 2 | 3 | import java.io.ByteArrayInputStream; 4 | import java.io.File; 5 | import java.lang.instrument.ClassFileTransformer; 6 | import java.lang.instrument.IllegalClassFormatException; 7 | import java.security.ProtectionDomain; 8 | import java.util.logging.Level; 9 | import java.util.logging.Logger; 10 | 11 | import javassist.CannotCompileException; 12 | import javassist.ClassClassPath; 13 | import javassist.ClassPool; 14 | import javassist.CtClass; 15 | import javassist.CtMethod; 16 | import javassist.NotFoundException; 17 | 18 | public class Transformer implements ClassFileTransformer { 19 | private static final Logger log = Logger.getLogger(Transformer.class.getName()); 20 | 21 | private static abstract class InjectCallback { 22 | private String[] handledClasses; 23 | 24 | public InjectCallback(String[] handledClasses) { 25 | this.handledClasses = handledClasses; 26 | } 27 | 28 | public boolean handles(String className) { 29 | for (String cls : handledClasses) { 30 | if (className.equals(cls)) { 31 | return true; 32 | } 33 | } 34 | return false; 35 | } 36 | 37 | public byte[] transform(String className, byte[] classfileBuffer, String jarFile) { 38 | if (handles(className)) { 39 | try { 40 | ClassPool pool = new ClassPool(); 41 | pool.appendSystemPath(); 42 | // Needed for Java 9+ 43 | pool.insertClassPath(new ClassClassPath(Transformer.class)); 44 | CtClass instrumentedClass = pool.makeClass(new ByteArrayInputStream(classfileBuffer)); 45 | instrumentClass(instrumentedClass); 46 | return instrumentedClass.toBytecode(); 47 | } catch (Throwable e) { 48 | log.log(Level.WARNING, "Failed instrumenting " + className, e); 49 | if (e instanceof InterruptedException) { 50 | Thread.currentThread().interrupt(); 51 | } 52 | } 53 | } 54 | return classfileBuffer; 55 | } 56 | 57 | protected abstract void instrumentClass(CtClass instrumentedClass) throws CannotCompileException, NotFoundException; 58 | } 59 | 60 | private static class SessionInjectCallback extends InjectCallback { 61 | public SessionInjectCallback() { 62 | super(new String[] {"sun.security.ssl.SSLSessionImpl", "com.sun.net.ssl.internal.ssl.SSLSessionImpl"}); 63 | } 64 | 65 | @Override 66 | protected void instrumentClass(CtClass instrumentedClass) throws CannotCompileException, NotFoundException { 67 | CtMethod method = instrumentedClass.getDeclaredMethod("setMasterSecret"); 68 | method.insertAfter(MasterSecretCallback.class.getName() + ".onMasterSecret(this, $1);"); 69 | 70 | CtMethod method2 = instrumentedClass.getDeclaredMethod("setLocalPrivateKey"); 71 | method2.insertAfter(MasterSecretCallback.class.getName() + ".onSetLocalPrivateKey(this, $1);"); 72 | 73 | CtMethod method3 = instrumentedClass.getDeclaredMethod("setLocalCertificates"); 74 | method3.insertAfter(MasterSecretCallback.class.getName() + ".onSetLocalCertificates(this, $1);"); 75 | 76 | } 77 | } 78 | 79 | private static class HandshakerInjectCallback extends InjectCallback { 80 | 81 | public HandshakerInjectCallback() { 82 | super(new String [] {"sun.security.ssl.Handshaker", "com.sun.net.ssl.internal.ssl.Handshaker"}); 83 | } 84 | 85 | @Override 86 | protected void instrumentClass(CtClass instrumentedClass) throws CannotCompileException, NotFoundException { 87 | CtMethod method = instrumentedClass.getDeclaredMethod("calculateConnectionKeys"); 88 | method.insertBefore(MasterSecretCallback.class.getName() + ".onCalculateKeys(session, clnt_random, $1);"); 89 | } 90 | 91 | } 92 | 93 | private static class SSLTrafficKeyDerivation extends InjectCallback { 94 | 95 | public SSLTrafficKeyDerivation() { 96 | super(new String[] {"sun.security.ssl.SSLTrafficKeyDerivation"}); 97 | } 98 | 99 | @Override 100 | protected void instrumentClass(CtClass instrumentedClass) throws CannotCompileException, NotFoundException { 101 | CtMethod method = instrumentedClass.getDeclaredMethod("createKeyDerivation"); 102 | method.insertAfter(MasterSecretCallback.class.getName() + ".onKeyDerivation($1, $2);"); 103 | } 104 | 105 | } 106 | 107 | private static final InjectCallback[] TRANSFORMERS = new InjectCallback[] { 108 | new SessionInjectCallback(), 109 | new HandshakerInjectCallback(), 110 | new SSLTrafficKeyDerivation() 111 | }; 112 | private File jarFile; 113 | 114 | 115 | public Transformer(File jarFile) { 116 | this.jarFile = jarFile; 117 | } 118 | 119 | @Override 120 | public byte[] transform( 121 | ClassLoader loader, 122 | String classPath, 123 | Class classBeingRedefined, 124 | ProtectionDomain protectionDomain, 125 | byte[] classfileBuffer) throws IllegalClassFormatException { 126 | String className = classPath.replace("/", "."); 127 | // loader should be null (boot loader), so don't use it 128 | for (InjectCallback ic : TRANSFORMERS) { 129 | if (ic.handles(className)) { 130 | return ic.transform(className, classfileBuffer, jarFile.getAbsolutePath()); 131 | } 132 | } 133 | return classfileBuffer; 134 | } 135 | 136 | public static boolean needsTransform(String className) { 137 | for (InjectCallback ic : TRANSFORMERS) { 138 | if (ic.handles(className)) { 139 | return true; 140 | } 141 | } 142 | return false; 143 | } 144 | 145 | } 146 | -------------------------------------------------------------------------------- /src/test/docker/Dockerfile.tomcat: -------------------------------------------------------------------------------- 1 | ARG JAVA_IMAGE_TAG=8 2 | ARG TOMCAT_MAJOR_VERSION=7 3 | 4 | FROM alpine:latest 5 | ARG TOMCAT_MAJOR_VERSION 6 | 7 | RUN set -x && \ 8 | export DOWNLOAD_ROOT_URL="https://www.apache.org/dyn/closer.cgi?action=download&filename=tomcat/tomcat-$TOMCAT_MAJOR_VERSION/" && \ 9 | export TOMCAT_VERSION=$(wget -qO - "$DOWNLOAD_ROOT_URL" | grep "v$TOMCAT_MAJOR_VERSION" | sed "s@.*v\($TOMCAT_MAJOR_VERSION[^/]*\).*@\1@" | tail -n1)&& \ 10 | export DOWNLOAD_URL="$DOWNLOAD_ROOT_URL/v$TOMCAT_VERSION/bin/apache-tomcat-$TOMCAT_VERSION.tar.gz" && \ 11 | mkdir apache-tomcat && \ 12 | wget "$DOWNLOAD_URL" -O apache-tomcat.tar.gz && \ 13 | tar -xzvf apache-tomcat.tar.gz --strip 1 -C apache-tomcat && \ 14 | rm -r apache-tomcat/webapps && \ 15 | mkdir -p /apache-tomcat/root && \ 16 | echo "PLAIN TEXT" > /apache-tomcat/root/secret.txt 17 | 18 | FROM openjdk:$JAVA_IMAGE_TAG 19 | ARG JAVA_IMAGE_TAG 20 | 21 | COPY --from=0 /apache-tomcat /apache-tomcat 22 | # Enable TLSv1.1 23 | RUN ( `# Java 12+` && [ -f "/usr/java/openjdk-$JAVA_IMAGE_TAG/conf/security/java.security" ] && ( echo "jdk.tls.disabledAlgorithms=" >> /usr/java/openjdk-$JAVA_IMAGE_TAG/conf/security/java.security ) ) || \ 24 | ( `# Java 11` && [ -f "/usr/local/openjdk-$JAVA_IMAGE_TAG/conf/security/java.security" ] && ( echo "jdk.tls.disabledAlgorithms=" >> /usr/local/openjdk-$JAVA_IMAGE_TAG/conf/security/java.security ) ) || \ 25 | ( `# Java 8` && [ -f "/usr/local/openjdk-$JAVA_IMAGE_TAG/jre/lib/security/java.security" ] && ( echo "jdk.tls.disabledAlgorithms=" >> /usr/local/openjdk-$JAVA_IMAGE_TAG/jre/lib/security/java.security ) ) || \ 26 | ( `# Java 6, 7, 9, 10` && [ -f "/etc/java-$JAVA_IMAGE_TAG-openjdk/security/java.security" ] && ( echo "jdk.tls.disabledAlgorithms=" >> /etc/java-$JAVA_IMAGE_TAG-openjdk/security/java.security ) ) || \ 27 | ( echo "Unable to locate java.security" && false ) 28 | 29 | VOLUME /secrets /project 30 | -------------------------------------------------------------------------------- /src/test/docker/Dockerfile.utils: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | VOLUME ["/dump"] 4 | 5 | RUN apk --no-cache add tcpdump tshark 6 | -------------------------------------------------------------------------------- /src/test/docker/clean.sh: -------------------------------------------------------------------------------- 1 | docker image rm ssl-secrets-utils | true 2 | docker image rm ssl-secrets-tomcat | true 3 | -------------------------------------------------------------------------------- /src/test/docker/server.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/test/docker/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | set -x 5 | 6 | echo $@ 7 | 8 | case "$1" in 9 | "agent") INJECT_TYPE="agent";; 10 | "attach") INJECT_TYPE="attach";; 11 | *) echo "Invalid parameter"; exit 1 12 | esac 13 | 14 | JAR_PATH=$2 15 | 16 | CWD="$( cd "$(dirname "$0")" ; pwd -P )" 17 | ROOT=$( cd "$CWD/../../.." && pwd ) 18 | TEST_TMP="$ROOT/target/test/temp" 19 | SECRETS_VOLUME="$TEST_TMP/secrets" 20 | JAVA_VERSIONS="21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6" 21 | 22 | $CWD/test_errors.sh $JAR_PATH 23 | 24 | rm -r $TEST_TMP || true 25 | mkdir -p $SECRETS_VOLUME 26 | 27 | docker network create ssl-secrets || true 28 | 29 | docker build -f $CWD/Dockerfile.utils $CWD -t ssl-secrets-utils 30 | 31 | # Passing "-deststoretype pkcs12" breaks Java 6 32 | cat < /dev/null; 70 | do 71 | sleep 1; 72 | done 73 | 74 | if [ "$INJECT_TYPE" = "attach" ]; then 75 | docker exec ssl-secrets-tomcat java -jar /project/$JAR_PATH list 76 | docker exec ssl-secrets-tomcat java -jar /project/$JAR_PATH 1 /secrets/server.keys 77 | fi 78 | docker logs ssl-secrets-tomcat 79 | 80 | # Start tcpdump listening on the tomcat port 81 | docker run -d --name ssl-secrets-tcpdump --rm --network container:ssl-secrets-tomcat \ 82 | -v $SECRETS_VOLUME:/secrets ssl-secrets-utils \ 83 | tcpdump 'port 443' -Uw /secrets/secrets.pcap 84 | 85 | 86 | for PROTO in "TLSv1.3" "TLSv1.1"; do 87 | 88 | case "$PROTO-$JAVA_IMAGE_TAG" in 89 | "TLSv1.3-6"|"TLSv1.3-7"|"TLSv1.3-9"|"TLSv1.3-10") continue;; 90 | esac 91 | 92 | echo -e "\n" \ 93 | "=============================================\n" \ 94 | " Java $JAVA_IMAGE_TAG - $PROTO\n" \ 95 | "=============================================\n\n" 96 | 97 | rm $SECRETS_VOLUME/client.keys $SECRETS_VOLUME/server.keys || true 98 | 99 | # Run a test request 100 | docker run --network ssl-secrets --rm \ 101 | -v $ROOT:/project -v $SECRETS_VOLUME:/secrets \ 102 | ssl-secrets-tomcat java -cp /project/target/test-classes \ 103 | -Djavax.net.ssl.trustStore=/secrets/keystore -Djavax.net.ssl.trustStorePassword=password \ 104 | -Dhttps.protocols=$PROTO \ 105 | -Djdk.tls.client.protocols=$PROTO \ 106 | -javaagent:/project/$JAR_PATH=/secrets/client.keys \ 107 | name.neykov.secrets.TestURLConnection https://ssl-secrets-tomcat/secret.txt 108 | 109 | # Show captured keys 110 | docker run --rm --network none \ 111 | -v $SECRETS_VOLUME:/secrets \ 112 | ssl-secrets-utils \ 113 | cat /secrets/server.keys /secrets/client.keys 114 | 115 | LAST_STREAM_ID=$(docker run --rm --network none \ 116 | -v $SECRETS_VOLUME:/secrets \ 117 | ssl-secrets-utils tshark \ 118 | -nr /secrets/secrets.pcap \ 119 | -T fields -e tcp.stream | sort -n | tail -1 ) 120 | 121 | # Check we can decrypt the capture using the server keys 122 | docker run --rm --network none \ 123 | -v $SECRETS_VOLUME:/secrets \ 124 | ssl-secrets-utils tshark \ 125 | -o "tls.keylog_file:/secrets/server.keys" \ 126 | -nr /secrets/secrets.pcap -q -z follow,http,ascii,$LAST_STREAM_ID | \ 127 | grep 'PLAIN TEXT' 128 | 129 | # Check we can decrypt the capture using the client keys 130 | docker run --rm --network none \ 131 | -v $SECRETS_VOLUME:/secrets \ 132 | ssl-secrets-utils tshark \ 133 | -o "tls.keylog_file:/secrets/client.keys" \ 134 | -nr /secrets/secrets.pcap -q -z follow,http,ascii,$LAST_STREAM_ID | \ 135 | grep 'PLAIN TEXT' 136 | 137 | done 138 | done 139 | 140 | docker rm -f $(docker ps -qa) 141 | -------------------------------------------------------------------------------- /src/test/docker/test_errors.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | echo $@ 6 | 7 | JAR_PATH=$1 8 | 9 | CWD="$( cd "$(dirname "$0")" ; pwd -P )" 10 | ROOT=$( cd "$CWD/../../.." && pwd ) 11 | 12 | docker image pull openjdk:8 13 | docker image pull openjdk:8-jre 14 | docker image pull openjdk:11-jre 15 | 16 | OUT=$(docker run --rm --network none \ 17 | -v $ROOT:/project \ 18 | openjdk:8 \ 19 | java -jar /project/$JAR_PATH 2>&1) 20 | 21 | [[ "$OUT" == *"Missing required argument"* ]] || exit 1 22 | [[ "$OUT" == *"Usage"* ]] || exit 1 23 | 24 | OUT=$(docker run --rm --network none \ 25 | -v $ROOT:/project \ 26 | openjdk:8-jre \ 27 | java -jar /project/$JAR_PATH list 2>&1) 28 | 29 | [[ "$OUT" == *"Invalid JAVA_HOME environment variable"* ]] || exit 1 30 | [[ "$OUT" == *"Must point to a local JDK installation containing a 'lib/tools.jar'"* ]] || exit 1 31 | 32 | OUT=$(docker run --rm --network none \ 33 | -v $ROOT:/project \ 34 | openjdk:11-jre \ 35 | java -jar /project/$JAR_PATH list 2>&1) 36 | 37 | [[ "$OUT" == *"No access to JDK classes. Make sure to use the java executable from a JDK install."* ]] || exit 1 38 | 39 | OUT=$(docker run --rm --network none \ 40 | -v $ROOT:/project \ 41 | openjdk:8 \ 42 | bash -c "unset JAVA_HOME; java -jar /project/$JAR_PATH list 2>&1") 43 | 44 | [[ "$OUT" == *"No JAVA_HOME environment variable found. Must point to a local JDK installation"* ]] || exit 1 45 | 46 | OUT=$(docker run --rm --network none \ 47 | -v $ROOT:/project \ 48 | openjdk:8 \ 49 | bash -c "JAVA_HOME=/tmp java -jar /project/$JAR_PATH list 2>&1") 50 | 51 | [[ "$OUT" == *"Invalid JAVA_HOME environment variable"* ]] || exit 1 52 | [[ "$OUT" == *"Must point to a local JDK installation containing a 'lib/tools.jar'"* ]] || exit 1 53 | -------------------------------------------------------------------------------- /src/test/java/name/neykov/secrets/TestHttpClient.java: -------------------------------------------------------------------------------- 1 | //package name.neykov.secrets; 2 | // 3 | //import java.net.URI; 4 | //import java.net.http.HttpClient; 5 | //import java.net.http.HttpRequest; 6 | //import java.net.http.HttpResponse; 7 | // 8 | //public class TestHttpClient { 9 | //// Java 9 10 | //// javac module-info.java name\neykov\secrets\TestExtract.java 11 | //// java -cp . --add-modules jdk.incubator.httpclient name.neykov.secrets.Main 12 | //// module extract-ssl-secrets { 13 | //// requires jdk.incubator.httpclient; 14 | //// } 15 | // public static void main(String[] args) throws Exception { 16 | // HttpClient client = HttpClient.newHttpClient(); 17 | // HttpRequest req = HttpRequest.newBuilder(new URI(args[0])).GET().build(); 18 | // int status = client.send(req, HttpResponse.BodyHandlers.ofString()).statusCode(); 19 | // if (status != 200) { 20 | // System.exit(1); 21 | // } 22 | // } 23 | //} 24 | -------------------------------------------------------------------------------- /src/test/java/name/neykov/secrets/TestURLConnection.java: -------------------------------------------------------------------------------- 1 | package name.neykov.secrets; 2 | 3 | import java.net.HttpURLConnection; 4 | import java.net.URL; 5 | 6 | public class TestURLConnection { 7 | public static void main(String[] args) throws Exception { 8 | URL url = new URL(args[0]); 9 | HttpURLConnection connection = (HttpURLConnection)url.openConnection(); 10 | connection.setRequestMethod("GET"); 11 | connection.connect(); 12 | int code = connection.getResponseCode(); 13 | if (code != 200) { 14 | System.err.println("Unexpected response code " + code); 15 | System.exit(1); 16 | } 17 | } 18 | } 19 | --------------------------------------------------------------------------------