Skip to content

Commit

Permalink
feat(gui): APK signature check v1/v2 using the apksig library from Go…
Browse files Browse the repository at this point in the history
…ogle (#431)

* feat: APK signature check v1/v2 using the apksig library from Google
* fix: proposed changes implemented
  • Loading branch information
jpstotz authored and skylot committed Jan 18, 2019
1 parent 618b014 commit d1af751
Show file tree
Hide file tree
Showing 9 changed files with 301 additions and 32 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ allprojects {
mavenLocal()
mavenCentral()
jcenter()
google()
}

jacoco {
Expand Down
2 changes: 2 additions & 0 deletions jadx-gui/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
184 changes: 184 additions & 0 deletions jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java
Original file line number Diff line number Diff line change
@@ -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("<h1>APK signature verification result:</h1>");

builder.append("<p><b>");
if (result.isVerified()) {
builder.escape(NLS.str("apkSignature.verificationSuccess"));
} else {
builder.escape(NLS.str("apkSignature.verificationFailed"));
}
builder.append("</b></p>");

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("<h2>");
builder.escape(String.format(result.isVerifiedUsingV1Scheme() ? sigSucc : sigFail, 1));
builder.append("</h2>\n");

builder.append("<blockquote>");
for (ApkVerifier.Result.V1SchemeSignerInfo signer : result.getV1SchemeSigners()) {
builder.append("<h3>");
builder.escape(NLS.str("apkSignature.signer"));
builder.append(" ");
builder.escape(signer.getName());
builder.append(" (");
builder.escape(signer.getSignatureFileName());
builder.append(")");
builder.append("</h3>");
writeCertificate(builder, signer.getCertificate());
writeIssues(builder, err, signer.getErrors());
writeIssues(builder, warn, signer.getWarnings());
}
builder.append("</blockquote>");
}
if (result.getV2SchemeSigners().size() > 0) {
builder.append("<h2>");
builder.escape(String.format(result.isVerifiedUsingV2Scheme() ? sigSucc : sigFail, 2));
builder.append("</h2>\n");

builder.append("<blockquote>");
for (ApkVerifier.Result.V2SchemeSignerInfo signer : result.getV2SchemeSigners()) {
builder.append("<h3>");
builder.escape(NLS.str("apkSignature.signer"));
builder.append(" ");
builder.append(Integer.toString(signer.getIndex() + 1));
builder.append("</h3>");
writeCertificate(builder, signer.getCertificate());
writeIssues(builder, err, signer.getErrors());
writeIssues(builder, warn, signer.getWarnings());
}
builder.append("</blockquote>");
}
this.content = builder.toString();
} catch (Exception e) {
log.error(e.getMessage(), e);
StringEscapeUtils.Builder builder = StringEscapeUtils.builder(StringEscapeUtils.ESCAPE_HTML4);
builder.append("<h1>");
builder.escape(NLS.str("apkSignature.exception"));
builder.append("</h1><pre>");
builder.escape(ExceptionUtils.getStackTrace(e));
builder.append("</pre>");
return builder.toString();
}
return this.content;
}

private void writeCertificate(StringEscapeUtils.Builder builder, Certificate cert) {
CertificateManager certMgr = new CertificateManager(cert);
builder.append("<blockquote><pre>");
builder.escape(certMgr.generateHeader());
builder.append("</pre><pre>");
builder.escape(certMgr.generatePublicKey());
builder.append("</pre><pre>");
builder.escape(certMgr.generateSignature());
builder.append("</pre><pre>");
builder.append(certMgr.generateFingerprint());
builder.append("</pre></blockquote>");
}

private void writeIssues(StringEscapeUtils.Builder builder, String issueType, List<ApkVerifier.IssueWithParams> issueList) {
if (issueList.size() > 0) {
builder.append("<h3>");
builder.escape(issueType);
builder.append("</h3>");
builder.append("<blockquote>");
// Unprotected Zip entry issues are very common, handle them separately
List<ApkVerifier.IssueWithParams> unprotIssues = issueList.stream().filter(i ->
i.getIssue() == ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY).collect(Collectors.toList());
if (unprotIssues.size() > 0) {
builder.append("<h4>");
builder.escape(NLS.str("apkSignature.unprotectedEntry"));
builder.append("</h4><blockquote>");
for (ApkVerifier.IssueWithParams issue : unprotIssues) {
builder.escape((String) issue.getParams()[0]);
builder.append("<br>");
}
builder.append("</blockquote>");
}
List<ApkVerifier.IssueWithParams> remainingIssues = issueList.stream().filter(i ->
i.getIssue() != ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY).collect(Collectors.toList());
if (remainingIssues.size() > 0) {
builder.append("<pre>\n");
for (ApkVerifier.IssueWithParams issue : remainingIssues) {
builder.escape(issue.toString());
builder.append("\n");
}
builder.append("</pre>\n");
}
builder.append("</blockquote>");
}

}


}
5 changes: 5 additions & 0 deletions jadx-gui/src/main/java/jadx/gui/treemodel/JRoot.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ public final void update() {
add(jRes);
}

ApkSignature signature = ApkSignature.getApkSignature(wrapper);
if (signature != null) {
add(signature);
}

JCertificate certificate = getCertificate(wrapper.getResources());
if (certificate != null) {
add(certificate);
Expand Down
58 changes: 58 additions & 0 deletions jadx-gui/src/main/java/jadx/gui/ui/HtmlPanel.java
Original file line number Diff line number Diff line change
@@ -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();
}
}

}
}
12 changes: 3 additions & 9 deletions jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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;
Expand All @@ -39,12 +40,6 @@
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;
Expand Down Expand Up @@ -296,9 +291,8 @@ private void treeClickAction() {
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();
Expand Down
46 changes: 29 additions & 17 deletions jadx-gui/src/main/java/jadx/gui/ui/TabbedPane.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -27,6 +13,29 @@
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 {

Expand Down Expand Up @@ -93,8 +102,8 @@ public void showResource(JResource res) {
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;
}
Expand Down Expand Up @@ -170,6 +179,9 @@ private ContentPanel makeContentPanel(JNode node) {
return null;
}
}
if (node instanceof ApkSignature) {
return new HtmlPanel(this, node);
}
if (node instanceof JCertificate) {
return new CertificatePanel(this, node);
}
Expand Down
Loading

0 comments on commit d1af751

Please sign in to comment.