Commit d1af7512 authored by Jan S's avatar Jan S Committed by skylot

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
parent 618b014b
...@@ -48,6 +48,7 @@ allprojects { ...@@ -48,6 +48,7 @@ allprojects {
mavenLocal() mavenLocal()
mavenCentral() mavenCentral()
jcenter() jcenter()
google()
} }
jacoco { jacoco {
......
...@@ -16,9 +16,11 @@ dependencies { ...@@ -16,9 +16,11 @@ dependencies {
compile 'hu.kazocsaba:image-viewer:1.2.3' compile 'hu.kazocsaba:image-viewer:1.2.3'
compile 'org.apache.commons:commons-lang3:3.8.1' 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 'io.reactivex.rxjava2:rxjava:2.2.5'
compile "com.github.akarnokd:rxjava2-swing:0.3.3" compile "com.github.akarnokd:rxjava2-swing:0.3.3"
compile 'com.android.tools.build:apksig:2.3.0'
} }
applicationDistribution.with { applicationDistribution.with {
......
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>");
}
}
}
...@@ -37,6 +37,11 @@ public class JRoot extends JNode { ...@@ -37,6 +37,11 @@ public class JRoot extends JNode {
add(jRes); add(jRes);
} }
ApkSignature signature = ApkSignature.getApkSignature(wrapper);
if (signature != null) {
add(signature);
}
JCertificate certificate = getCertificate(wrapper.getResources()); JCertificate certificate = getCertificate(wrapper.getResources());
if (certificate != null) { if (certificate != null) {
add(certificate); add(certificate);
......
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();
}
}
}
}
...@@ -28,6 +28,7 @@ import java.util.Arrays; ...@@ -28,6 +28,7 @@ import java.util.Arrays;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import jadx.gui.treemodel.*;
import org.fife.ui.rsyntaxtextarea.Theme; import org.fife.ui.rsyntaxtextarea.Theme;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
...@@ -39,12 +40,6 @@ import jadx.gui.jobs.DecompileJob; ...@@ -39,12 +40,6 @@ import jadx.gui.jobs.DecompileJob;
import jadx.gui.jobs.IndexJob; import jadx.gui.jobs.IndexJob;
import jadx.gui.settings.JadxSettings; import jadx.gui.settings.JadxSettings;
import jadx.gui.settings.JadxSettingsWindow; 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;
import jadx.gui.update.JadxUpdate.IUpdateCallback; import jadx.gui.update.JadxUpdate.IUpdateCallback;
import jadx.gui.update.data.Release; import jadx.gui.update.data.Release;
...@@ -296,9 +291,8 @@ public class MainWindow extends JFrame { ...@@ -296,9 +291,8 @@ public class MainWindow extends JFrame {
if (resFile != null && JResource.isSupportedForView(resFile.getType())) { if (resFile != null && JResource.isSupportedForView(resFile.getType())) {
tabbedPane.showResource(res); tabbedPane.showResource(res);
} }
} else if (obj instanceof JCertificate) { } else if ((obj instanceof JCertificate) || (obj instanceof ApkSignature)) {
JCertificate cert = (JCertificate) obj; tabbedPane.showSimpleNode((JNode) obj);
tabbedPane.showCertificate(cert);
} else if (obj instanceof JNode) { } else if (obj instanceof JNode) {
JNode node = (JNode) obj; JNode node = (JNode) obj;
JClass cls = node.getRootClass(); JClass cls = node.getRootClass();
......
package jadx.gui.ui; 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.ResourceFile;
import jadx.api.ResourceType; import jadx.api.ResourceType;
import jadx.gui.treemodel.ApkSignature;
import jadx.gui.treemodel.JCertificate; import jadx.gui.treemodel.JCertificate;
import jadx.gui.treemodel.JClass; import jadx.gui.treemodel.JClass;
import jadx.gui.treemodel.JNode; import jadx.gui.treemodel.JNode;
...@@ -27,6 +13,29 @@ import jadx.gui.utils.JumpManager; ...@@ -27,6 +13,29 @@ import jadx.gui.utils.JumpManager;
import jadx.gui.utils.JumpPosition; import jadx.gui.utils.JumpPosition;
import jadx.gui.utils.NLS; import jadx.gui.utils.NLS;
import jadx.gui.utils.Utils; 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 { public class TabbedPane extends JTabbedPane {
...@@ -93,8 +102,8 @@ public class TabbedPane extends JTabbedPane { ...@@ -93,8 +102,8 @@ public class TabbedPane extends JTabbedPane {
SwingUtilities.invokeLater(() -> setSelectedComponent(contentPanel)); SwingUtilities.invokeLater(() -> setSelectedComponent(contentPanel));
} }
public void showCertificate(JCertificate cert) { public void showSimpleNode(JNode node) {
final ContentPanel contentPanel = getContentPanel(cert); final ContentPanel contentPanel = getContentPanel(node);
if (contentPanel == null) { if (contentPanel == null) {
return; return;
} }
...@@ -170,6 +179,9 @@ public class TabbedPane extends JTabbedPane { ...@@ -170,6 +179,9 @@ public class TabbedPane extends JTabbedPane {
return null; return null;
} }
} }
if (node instanceof ApkSignature) {
return new HtmlPanel(this, node);
}
if (node instanceof JCertificate) { if (node instanceof JCertificate) {
return new CertificatePanel(this, node); return new CertificatePanel(this, node);
} }
......
...@@ -54,7 +54,7 @@ public class CertificateManager { ...@@ -54,7 +54,7 @@ public class CertificateManager {
} }
} }
String generateHeader() { public String generateHeader() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
append(builder, NLS.str("certificate.cert_type"), x509cert.getType()); append(builder, NLS.str("certificate.cert_type"), x509cert.getType());
append(builder, NLS.str("certificate.serialSigVer"), ((Integer) x509cert.getVersion()).toString()); append(builder, NLS.str("certificate.serialSigVer"), ((Integer) x509cert.getVersion()).toString());
...@@ -70,14 +70,14 @@ public class CertificateManager { ...@@ -70,14 +70,14 @@ public class CertificateManager {
return builder.toString(); return builder.toString();
} }
String generateSignature() { public String generateSignature() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
append(builder, NLS.str("certificate.serialSigType"), x509cert.getSigAlgName()); append(builder, NLS.str("certificate.serialSigType"), x509cert.getSigAlgName());
append(builder, NLS.str("certificate.serialSigOID"), x509cert.getSigAlgOID()); append(builder, NLS.str("certificate.serialSigOID"), x509cert.getSigAlgOID());
return builder.toString(); return builder.toString();
} }
String generateFingerprint() { public String generateFingerprint() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
try { try {
append(builder, NLS.str("certificate.serialMD5"), getThumbPrint(x509cert, "MD5")); append(builder, NLS.str("certificate.serialMD5"), getThumbPrint(x509cert, "MD5"));
...@@ -89,7 +89,7 @@ public class CertificateManager { ...@@ -89,7 +89,7 @@ public class CertificateManager {
return builder.toString(); return builder.toString();
} }
String generatePublicKey() { public String generatePublicKey() {
PublicKey publicKey = x509cert.getPublicKey(); PublicKey publicKey = x509cert.getPublicKey();
if (publicKey instanceof RSAPublicKey) { if (publicKey instanceof RSAPublicKey) {
return generateRSAPublicKey(); return generateRSAPublicKey();
...@@ -106,6 +106,8 @@ public class CertificateManager { ...@@ -106,6 +106,8 @@ public class CertificateManager {
append(builder, NLS.str("certificate.serialPubKeyType"), pub.getAlgorithm()); append(builder, NLS.str("certificate.serialPubKeyType"), pub.getAlgorithm());
append(builder, NLS.str("certificate.serialPubKeyExponent"), pub.getPublicExponent().toString(10)); 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)); append(builder, NLS.str("certificate.serialPubKeyModulus"), pub.getModulus().toString(10));
return builder.toString(); return builder.toString();
...@@ -120,7 +122,7 @@ public class CertificateManager { ...@@ -120,7 +122,7 @@ public class CertificateManager {
return builder.toString(); return builder.toString();
} }
String generateTextForX509() { public String generateTextForX509() {
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
if (x509cert != null) { if (x509cert != null) {
builder.append(generateHeader()); builder.append(generateHeader());
...@@ -136,7 +138,7 @@ public class CertificateManager { ...@@ -136,7 +138,7 @@ public class CertificateManager {
return builder.toString(); return builder.toString();
} }
private String generateText() { public String generateText() {
StringBuilder str = new StringBuilder(); StringBuilder str = new StringBuilder();
String type = cert.getType(); String type = cert.getType();
if (type.equals(CERTIFICATE_TYPE_NAME)) { if (type.equals(CERTIFICATE_TYPE_NAME)) {
......
...@@ -136,9 +136,20 @@ certificate.serialValidUntil=Valid until ...@@ -136,9 +136,20 @@ certificate.serialValidUntil=Valid until
certificate.serialPubKeyType=Public key type certificate.serialPubKeyType=Public key type
certificate.serialPubKeyExponent=Exponent certificate.serialPubKeyExponent=Exponent
certificate.serialPubKeyModulus=Modulus certificate.serialPubKeyModulus=Modulus
certificate.serialPubKeyModulusSize=Modulus size (bits)
certificate.serialSigType=Signature type certificate.serialSigType=Signature type
certificate.serialSigOID=Signature OID certificate.serialSigOID=Signature OID
certificate.serialMD5=MD5 Fingerprint certificate.serialMD5=MD5 Fingerprint
certificate.serialSHA1=SHA-1 Fingerprint certificate.serialSHA1=SHA-1 Fingerprint
certificate.serialSHA256=SHA-256 Fingerprint certificate.serialSHA256=SHA-256 Fingerprint
certificate.serialPubKeyY=Y 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
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment