From d1af75122694aef279d102e60376025f2e95c0c9 Mon Sep 17 00:00:00 2001 From: Jan S Date: Fri, 18 Jan 2019 10:26:22 +0100 Subject: [PATCH] feat(gui): APK signature check v1/v2 using the apksig library from Google (#431) * feat: APK signature check v1/v2 using the apksig library from Google * fix: proposed changes implemented --- build.gradle | 1 + jadx-gui/build.gradle | 2 + .../java/jadx/gui/treemodel/ApkSignature.java | 184 ++++++++++++++++++ .../main/java/jadx/gui/treemodel/JRoot.java | 5 + .../src/main/java/jadx/gui/ui/HtmlPanel.java | 58 ++++++ .../src/main/java/jadx/gui/ui/MainWindow.java | 12 +- .../src/main/java/jadx/gui/ui/TabbedPane.java | 46 +++-- .../jadx/gui/utils/CertificateManager.java | 14 +- .../resources/i18n/Messages_en_US.properties | 11 ++ 9 files changed, 301 insertions(+), 32 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java diff --git a/build.gradle b/build.gradle index d0aef43b7..fee10c7d3 100644 --- a/build.gradle +++ b/build.gradle @@ -48,6 +48,7 @@ allprojects { mavenLocal() mavenCentral() jcenter() + google() } jacoco { diff --git a/jadx-gui/build.gradle b/jadx-gui/build.gradle index 65b3dd065..f3c23a35f 100644 --- a/jadx-gui/build.gradle +++ b/jadx-gui/build.gradle @@ -16,9 +16,11 @@ dependencies { compile 'hu.kazocsaba:image-viewer:1.2.3' compile 'org.apache.commons:commons-lang3:3.8.1' + compile 'org.apache.commons:commons-text:1.6' compile 'io.reactivex.rxjava2:rxjava:2.2.5' compile "com.github.akarnokd:rxjava2-swing:0.3.3" + compile 'com.android.tools.build:apksig:2.3.0' } applicationDistribution.with { diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java new file mode 100644 index 000000000..d59136bd7 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java @@ -0,0 +1,184 @@ +package jadx.gui.treemodel; + +import com.android.apksig.ApkVerifier; +import jadx.api.ResourceType; +import jadx.gui.JadxWrapper; +import jadx.gui.utils.CertificateManager; +import jadx.gui.utils.NLS; +import jadx.gui.utils.Utils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.*; +import java.io.File; +import java.security.cert.Certificate; +import java.util.List; +import java.util.stream.Collectors; + +public class ApkSignature extends JNode { + + private static final Logger log = LoggerFactory.getLogger(ApkSignature.class); + private static final ImageIcon CERTIFICATE_ICON = Utils.openIcon("certificate_obj"); + + private final transient File openFile; + private String content = null; + + public static ApkSignature getApkSignature(JadxWrapper wrapper) { + // Only show the ApkSignature node if an AndroidManifest.xml is present. + // Without a manifest the Google ApkVerifier refuses to work. + if (!wrapper.getResources().stream().anyMatch(r -> "AndroidManifest.xml".equals(r.getName()))) { + return null; + } + File openFile = wrapper.getOpenFile(); + return new ApkSignature(openFile); + } + + public ApkSignature(File openFile) { + this.openFile = openFile; + } + + @Override + public JClass getJParent() { + return null; + } + + @Override + public Icon getIcon() { + return CERTIFICATE_ICON; + } + + @Override + public String makeString() { + return "APK signature"; + } + + @Override + public String getContent() { + if (content != null) + return this.content; + ApkVerifier verifier = new ApkVerifier.Builder(openFile).build(); + try { + ApkVerifier.Result result = verifier.verify(); + StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4); + builder.append("

APK signature verification result:

"); + + builder.append("

"); + if (result.isVerified()) { + builder.escape(NLS.str("apkSignature.verificationSuccess")); + } else { + builder.escape(NLS.str("apkSignature.verificationFailed")); + } + builder.append("

"); + + final String err = NLS.str("apkSignature.errors"); + final String warn = NLS.str("apkSignature.warnings"); + final String sigSucc = NLS.str("apkSignature.signatureSuccess"); + final String sigFail = NLS.str("apkSignature.signatureFailed"); + + writeIssues(builder, err, result.getErrors()); + writeIssues(builder, warn, result.getWarnings()); + + if (result.getV1SchemeSigners().size() > 0) { + builder.append("

"); + builder.escape(String.format(result.isVerifiedUsingV1Scheme() ? sigSucc : sigFail, 1)); + builder.append("

\n"); + + builder.append("
"); + for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) { + builder.append("

"); + builder.escape(NLS.str("apkSignature.signer")); + builder.append(" "); + builder.escape(signer.getName()); + builder.append(" ("); + builder.escape(signer.getSignatureFileName()); + builder.append(")"); + builder.append("

"); + writeCertificate(builder, signer.getCertificate()); + writeIssues(builder, err, signer.getErrors()); + writeIssues(builder, warn, signer.getWarnings()); + } + builder.append("
"); + } + if (result.getV2SchemeSigners().size() > 0) { + builder.append("

"); + builder.escape(String.format(result.isVerifiedUsingV2Scheme() ? sigSucc : sigFail, 2)); + builder.append("

\n"); + + builder.append("
"); + for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) { + builder.append("

"); + builder.escape(NLS.str("apkSignature.signer")); + builder.append(" "); + builder.append(Integer.toString(signer.getIndex() + 1)); + builder.append("

"); + writeCertificate(builder, signer.getCertificate()); + writeIssues(builder, err, signer.getErrors()); + writeIssues(builder, warn, signer.getWarnings()); + } + builder.append("
"); + } + this.content = builder.toString(); + } catch (Exception e) { + log.error(e.getMessage(), e); + StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4); + builder.append("

"); + builder.escape(NLS.str("apkSignature.exception")); + builder.append("

");
+			builder.escape(ExceptionUtils.getStackTrace(e));
+			builder.append("
"); + return builder.toString(); + } + return this.content; + } + + private void writeCertificate(StringEscapeUtils.Builder builder, Certificate cert) { + CertificateManager certMgr = new CertificateManager(cert); + builder.append("
");
+		builder.escape(certMgr.generateHeader());
+		builder.append("
");
+		builder.escape(certMgr.generatePublicKey());
+		builder.append("
");
+		builder.escape(certMgr.generateSignature());
+		builder.append("
");
+		builder.append(certMgr.generateFingerprint());
+		builder.append("
"); + } + + private void writeIssues(StringEscapeUtils.Builder builder, String issueType, List issueList) { + if (issueList.size() > 0) { + builder.append("

"); + builder.escape(issueType); + builder.append("

"); + builder.append("
"); + // Unprotected Zip entry issues are very common, handle them separately + List unprotIssues = issueList.stream().filter(i -> + i.getIssue() == ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY).collect(Collectors.toList()); + if (unprotIssues.size() > 0) { + builder.append("

"); + builder.escape(NLS.str("apkSignature.unprotectedEntry")); + builder.append("

"); + for (ApkVerifier.IssueWithParams issue : unprotIssues) { + builder.escape((String) issue.getParams()[0]); + builder.append("
"); + } + builder.append("
"); + } + List remainingIssues = issueList.stream().filter(i -> + i.getIssue() != ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY).collect(Collectors.toList()); + if (remainingIssues.size() > 0) { + builder.append("
\n");
+				for (ApkVerifier.IssueWithParams issue : remainingIssues) {
+					builder.escape(issue.toString());
+					builder.append("\n");
+				}
+				builder.append("
\n"); + } + builder.append("
"); + } + + } + + +} diff --git a/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java b/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java index e21fc0bf6..e90335d11 100644 --- a/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java +++ b/jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java @@ -37,6 +37,11 @@ public class JRoot extends JNode { add(jRes); } + ApkSignature signature = ApkSignature.getApkSignature(wrapper); + if (signature != null) { + add(signature); + } + JCertificate certificate = getCertificate(wrapper.getResources()); if (certificate != null) { add(certificate); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java new file mode 100644 index 000000000..8a93034f7 --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java @@ -0,0 +1,58 @@ +package jadx.gui.ui; + +import jadx.gui.settings.JadxSettings; +import jadx.gui.treemodel.JNode; +import jadx.gui.ui.codearea.CodeArea; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; + +import javax.swing.*; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.plaf.PanelUI; +import java.awt.*; +import java.awt.event.KeyEvent; +import java.awt.event.KeyListener; + +public final class HtmlPanel extends ContentPanel { + private static final long serialVersionUID = -6251262855835426245L; + + private final JHtmlPane textArea; + + public HtmlPanel(TabbedPane panel, JNode jnode) { + super(panel, jnode); + setLayout(new BorderLayout()); + textArea = new JHtmlPane(); + loadSettings(); + textArea.setText(jnode.getContent()); + textArea.setCaretPosition(0); // otherwise the start view will be the last line + textArea.setEditable(false); + JScrollPane sp = new JScrollPane(textArea); + add(sp); + } + + @Override + public void loadSettings() { + JadxSettings settings = getTabbedPane().getMainWindow().getSettings(); + textArea.setFont(settings.getFont()); + } + + private static class JHtmlPane extends JEditorPane { + + boolean antiAliasingEnabled; + + public JHtmlPane() { + setContentType("text/html"); + } + + public void paint(Graphics g) { + Graphics2D g2d = (Graphics2D) g.create(); + try { + g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + super.paint(g2d); + } finally { + g2d.dispose(); + } + } + + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java index 7315cc828..e6280950b 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java @@ -28,6 +28,7 @@ import java.util.Arrays; import java.util.Timer; import java.util.TimerTask; +import jadx.gui.treemodel.*; import org.fife.ui.rsyntaxtextarea.Theme; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,12 +40,6 @@ import jadx.gui.jobs.DecompileJob; import jadx.gui.jobs.IndexJob; import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettingsWindow; -import jadx.gui.treemodel.JCertificate; -import jadx.gui.treemodel.JClass; -import jadx.gui.treemodel.JLoadableNode; -import jadx.gui.treemodel.JNode; -import jadx.gui.treemodel.JResource; -import jadx.gui.treemodel.JRoot; import jadx.gui.update.JadxUpdate; import jadx.gui.update.JadxUpdate.IUpdateCallback; import jadx.gui.update.data.Release; @@ -296,9 +291,8 @@ public class MainWindow extends JFrame { if (resFile != null && JResource.isSupportedForView(resFile.getType())) { tabbedPane.showResource(res); } - } else if (obj instanceof JCertificate) { - JCertificate cert = (JCertificate) obj; - tabbedPane.showCertificate(cert); + } else if ((obj instanceof JCertificate) || (obj instanceof ApkSignature)) { + tabbedPane.showSimpleNode((JNode) obj); } else if (obj instanceof JNode) { JNode node = (JNode) obj; JClass cls = node.getRootClass(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java index 08c6c226b..1fc5b1e06 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java @@ -1,22 +1,8 @@ package jadx.gui.ui; -import javax.swing.*; -import javax.swing.plaf.basic.BasicButtonUI; -import javax.swing.text.BadLocationException; -import java.awt.*; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import jadx.api.ResourceFile; import jadx.api.ResourceType; +import jadx.gui.treemodel.ApkSignature; import jadx.gui.treemodel.JCertificate; import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JNode; @@ -27,6 +13,29 @@ import jadx.gui.utils.JumpManager; import jadx.gui.utils.JumpPosition; import jadx.gui.utils.NLS; import jadx.gui.utils.Utils; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.swing.BorderFactory; +import javax.swing.ImageIcon; +import javax.swing.JButton; +import javax.swing.JLabel; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JTabbedPane; +import javax.swing.SwingUtilities; +import javax.swing.plaf.basic.BasicButtonUI; +import javax.swing.text.BadLocationException; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; public class TabbedPane extends JTabbedPane { @@ -93,8 +102,8 @@ public class TabbedPane extends JTabbedPane { SwingUtilities.invokeLater(() -> setSelectedComponent(contentPanel)); } - public void showCertificate(JCertificate cert) { - final ContentPanel contentPanel = getContentPanel(cert); + public void showSimpleNode(JNode node) { + final ContentPanel contentPanel = getContentPanel(node); if (contentPanel == null) { return; } @@ -170,6 +179,9 @@ public class TabbedPane extends JTabbedPane { return null; } } + if (node instanceof ApkSignature) { + return new HtmlPanel(this, node); + } if (node instanceof JCertificate) { return new CertificatePanel(this, node); } diff --git a/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java b/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java index b4069539d..29928aa7b 100755 --- a/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java +++ b/jadx-gui/src/main/java/jadx/gui/utils/CertificateManager.java @@ -54,7 +54,7 @@ public class CertificateManager { } } - String generateHeader() { + public String generateHeader() { StringBuilder builder = new StringBuilder(); append(builder, NLS.str("certificate.cert_type"), x509cert.getType()); append(builder, NLS.str("certificate.serialSigVer"), ((Integer) x509cert.getVersion()).toString()); @@ -70,14 +70,14 @@ public class CertificateManager { return builder.toString(); } - String generateSignature() { + public String generateSignature() { StringBuilder builder = new StringBuilder(); append(builder, NLS.str("certificate.serialSigType"), x509cert.getSigAlgName()); append(builder, NLS.str("certificate.serialSigOID"), x509cert.getSigAlgOID()); return builder.toString(); } - String generateFingerprint() { + public String generateFingerprint() { StringBuilder builder = new StringBuilder(); try { append(builder, NLS.str("certificate.serialMD5"), getThumbPrint(x509cert, "MD5")); @@ -89,7 +89,7 @@ public class CertificateManager { return builder.toString(); } - String generatePublicKey() { + public String generatePublicKey() { PublicKey publicKey = x509cert.getPublicKey(); if (publicKey instanceof RSAPublicKey) { return generateRSAPublicKey(); @@ -106,6 +106,8 @@ public class CertificateManager { append(builder, NLS.str("certificate.serialPubKeyType"), pub.getAlgorithm()); append(builder, NLS.str("certificate.serialPubKeyExponent"), pub.getPublicExponent().toString(10)); + append(builder, NLS.str("certificate.serialPubKeyModulusSize"), Integer.toString( + pub.getModulus().toString(2).length())); append(builder, NLS.str("certificate.serialPubKeyModulus"), pub.getModulus().toString(10)); return builder.toString(); @@ -120,7 +122,7 @@ public class CertificateManager { return builder.toString(); } - String generateTextForX509() { + public String generateTextForX509() { StringBuilder builder = new StringBuilder(); if (x509cert != null) { builder.append(generateHeader()); @@ -136,7 +138,7 @@ public class CertificateManager { return builder.toString(); } - private String generateText() { + public String generateText() { StringBuilder str = new StringBuilder(); String type = cert.getType(); if (type.equals(CERTIFICATE_TYPE_NAME)) { diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 839443e44..c1e3aba38 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -136,9 +136,20 @@ certificate.serialValidUntil=Valid until certificate.serialPubKeyType=Public key type certificate.serialPubKeyExponent=Exponent certificate.serialPubKeyModulus=Modulus +certificate.serialPubKeyModulusSize=Modulus size (bits) certificate.serialSigType=Signature type certificate.serialSigOID=Signature OID certificate.serialMD5=MD5 Fingerprint certificate.serialSHA1=SHA-1 Fingerprint certificate.serialSHA256=SHA-256 Fingerprint certificate.serialPubKeyY=Y + +apkSignature.signer=Signer +apkSignature.verificationSuccess=Signature verification succeeded +apkSignature.verificationFailed=Signature verification succeeded +apkSignature.signatureSuccess=Valid APK signature v%d found +apkSignature.signatureFailed=Invalid APK signature v%d found +apkSignature.errors=Errors +apkSignature.warnings=Warnings +apkSignature.exception=APK verification failed +apkSignature.unprotectedEntry=Files that are not protected by signature. Unauthorized modifications to this JAR entry will not be detected. \ No newline at end of file