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
This commit is contained in:
@@ -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>");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user