/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.plugin.desktoputil;
import java.awt.*;
import java.security.*;
import java.security.cert.*;
import java.security.cert.Certificate;
import java.security.interfaces.*;
import java.util.*;
import javax.naming.*;
import javax.naming.ldap.*;
import javax.security.auth.x500.*;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;
import javax.swing.text.*;
import javax.swing.tree.*;
import org.jitsi.service.resources.*;
/**
* Panel that shows the content of an X509Certificate.
*/
public class X509CertificatePanel
extends TransparentPanel
{
private static final long serialVersionUID = -8368302061995971947L;
private final JEditorPane infoTextPane = new JEditorPane();
private final ResourceManagementService R
= DesktopUtilActivator.getResources();
/**
* Constructs a X509 certificate panel from a single certificate.
* If a chain is available instead use the second constructor.
* This constructor is kept for backwards compatibility and for convenience
* when there is only one certificate of interest.
*
* @param certificate <tt>X509Certificate</tt> object
*/
public X509CertificatePanel(Certificate certificate)
{
this(new Certificate[]
{
certificate
});
}
/**
* Constructs a X509 certificate panel.
*
* @param certificates <tt>X509Certificate</tt> objects
*/
public X509CertificatePanel(Certificate[] certificates)
{
setLayout(new BorderLayout(5, 5));
// Certificate chain list
TransparentPanel topPanel = new TransparentPanel(new BorderLayout());
topPanel.add(new JLabel("<html><body><b>"
+ R.getI18NString("service.gui.CERT_INFO_CHAIN")
+ "</b></body></html>"), BorderLayout.NORTH);
DefaultMutableTreeNode top = new DefaultMutableTreeNode();
DefaultMutableTreeNode previous = top;
for (int i = certificates.length - 1; i >= 0; i--)
{
Certificate cert = certificates[i];
DefaultMutableTreeNode next = new DefaultMutableTreeNode(cert);
previous.add(next);
previous = next;
}
JTree tree = new JTree(top);
tree.setBorder(new BevelBorder(BevelBorder.LOWERED));
tree.setRootVisible(false);
tree.setExpandsSelectedPaths(true);
tree.getSelectionModel().setSelectionMode(
TreeSelectionModel.SINGLE_TREE_SELECTION);
tree.setCellRenderer(new DefaultTreeCellRenderer()
{
@Override
public Component getTreeCellRendererComponent(JTree tree,
Object value, boolean sel, boolean expanded, boolean leaf,
int row, boolean hasFocus)
{
JLabel component = (JLabel) super.getTreeCellRendererComponent(
tree, value, sel, expanded, leaf, row, hasFocus);
if (value instanceof DefaultMutableTreeNode)
{
Object o = ((DefaultMutableTreeNode) value).getUserObject();
if (o instanceof X509Certificate)
{
component.setText(
getSimplifiedName((X509Certificate) o));
}
else
{
// We don't know how to represent this certificate type,
// let's use the first 20 characters
String text = o.toString();
if (text.length() > 20)
{
text = text.substring(0, 20);
}
component.setText(text);
}
}
return component;
}
});
tree.getSelectionModel().addTreeSelectionListener(
new TreeSelectionListener()
{
@Override
public void valueChanged(TreeSelectionEvent e)
{
valueChangedPerformed(e);
}
});
tree.setSelectionPath(new TreePath(((
(DefaultTreeModel)tree.getModel()).getPathToRoot(previous))));
topPanel.add(tree, BorderLayout.CENTER);
add(topPanel, BorderLayout.NORTH);
// Certificate details pane
Caret caret = infoTextPane.getCaret();
if (caret instanceof DefaultCaret)
{
((DefaultCaret) caret).setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
}
/*
* Make JEditorPane respect our default font because we will be using it
* to just display text.
*/
infoTextPane.putClientProperty(
JEditorPane.HONOR_DISPLAY_PROPERTIES,
true);
infoTextPane.setOpaque(false);
infoTextPane.setEditable(false);
infoTextPane.setContentType("text/html");
infoTextPane.setText(toString(certificates[0]));
final JScrollPane certScroll = new JScrollPane(infoTextPane);
certScroll.setPreferredSize(new Dimension(300, 500));
add(certScroll, BorderLayout.CENTER);
}
/**
* Creates a String representation of the given object.
* @param certificate to print
* @return the String representation
*/
private String toString(Object certificate)
{
final StringBuilder sb = new StringBuilder();
sb.append("<html><body>\n");
if (certificate instanceof X509Certificate)
{
renderX509(sb, (X509Certificate) certificate);
}
else
{
sb.append("<pre>\n");
sb.append(certificate.toString());
sb.append("</pre>\n");
}
sb.append("</body></html>");
return sb.toString();
}
/**
* Appends an HTML representation of the given X509Certificate.
* @param sb StringBuilder to append to
* @param certificate to print
*/
private void renderX509(StringBuilder sb, X509Certificate certificate)
{
X500Principal issuer = certificate.getIssuerX500Principal();
X500Principal subject = certificate.getSubjectX500Principal();
sb.append("<table cellspacing='1' cellpadding='1'>\n");
// subject
addTitle(sb, R.getI18NString("service.gui.CERT_INFO_ISSUED_TO"));
try
{
for(Rdn name : new LdapName(subject.getName()).getRdns())
{
String nameType = name.getType();
String lblKey = "service.gui.CERT_INFO_" + nameType;
String lbl = R.getI18NString(lblKey);
if ((lbl == null) || ("!" + lblKey + "!").equals(lbl))
lbl = nameType;
final String value;
Object nameValue = name.getValue();
if (nameValue instanceof byte[])
{
byte[] nameValueAsByteArray = (byte[]) nameValue;
value
= getHex(nameValueAsByteArray) + " ("
+ new String(nameValueAsByteArray) + ")";
}
else
value = nameValue.toString();
addField(sb, lbl, value);
}
}
catch (InvalidNameException ine)
{
addField(sb, R.getI18NString("service.gui.CERT_INFO_CN"),
subject.getName());
}
// issuer
addTitle(sb, R.getI18NString("service.gui.CERT_INFO_ISSUED_BY"));
try
{
for(Rdn name : new LdapName(issuer.getName()).getRdns())
{
String nameType = name.getType();
String lblKey = "service.gui.CERT_INFO_" + nameType;
String lbl = R.getI18NString(lblKey);
if ((lbl == null) || ("!" + lblKey + "!").equals(lbl))
lbl = nameType;
final String value;
Object nameValue = name.getValue();
if (nameValue instanceof byte[])
{
byte[] nameValueAsByteArray = (byte[]) nameValue;
value
= getHex(nameValueAsByteArray) + " ("
+ new String(nameValueAsByteArray) + ")";
}
else
value = nameValue.toString();
addField(sb, lbl, value);
}
}
catch (InvalidNameException ine)
{
addField(sb, R.getI18NString("service.gui.CERT_INFO_CN"),
issuer.getName());
}
// validity
addTitle(sb, R.getI18NString("service.gui.CERT_INFO_VALIDITY"));
addField(sb, R.getI18NString("service.gui.CERT_INFO_ISSUED_ON"),
certificate.getNotBefore().toString());
addField(sb, R.getI18NString("service.gui.CERT_INFO_EXPIRES_ON"),
certificate.getNotAfter().toString());
addTitle(sb, R.getI18NString("service.gui.CERT_INFO_FINGERPRINTS"));
try
{
String sha256String = getThumbprint(certificate, "SHA-256");
String sha1String = getThumbprint(certificate, "SHA1");
addField(sb, "SHA256:", sha256String, 48);
addField(sb, "SHA1:", sha1String, 72);
}
catch (CertificateException e)
{
// do nothing as we cannot show this value
}
addTitle(sb, R.getI18NString("service.gui.CERT_INFO_CERT_DETAILS"));
addField(sb, R.getI18NString("service.gui.CERT_INFO_SER_NUM"),
certificate.getSerialNumber().toString());
addField(sb, R.getI18NString("service.gui.CERT_INFO_VER"),
String.valueOf(certificate.getVersion()));
addField(sb, R.getI18NString("service.gui.CERT_INFO_SIGN_ALG"),
String.valueOf(certificate.getSigAlgName()));
addTitle(sb, R.getI18NString("service.gui.CERT_INFO_PUB_KEY_INFO"));
addField(sb, R.getI18NString("service.gui.CERT_INFO_ALG"),
certificate.getPublicKey().getAlgorithm());
if(certificate.getPublicKey().getAlgorithm().equals("RSA"))
{
RSAPublicKey key = (RSAPublicKey)certificate.getPublicKey();
addField(sb,
R.getI18NString("service.gui.CERT_INFO_PUB_KEY"),
R.getI18NString("service.gui.CERT_INFO_KEY_BITS_PRINT",
new String[]{
String.valueOf(
(key.getModulus().toByteArray().length-1)*8)
}),
getHex(key.getModulus().toByteArray()),
48);
addField(sb, R.getI18NString("service.gui.CERT_INFO_EXP"),
key.getPublicExponent().toString());
addField(sb, R.getI18NString("service.gui.CERT_INFO_KEY_SIZE"),
R.getI18NString(
"service.gui.CERT_INFO_KEY_BITS_PRINT",
new String[]{
String.valueOf(key.getModulus().bitLength())}));
}
else if(certificate.getPublicKey().getAlgorithm().equals("DSA"))
{
DSAPublicKey key =
(DSAPublicKey)certificate.getPublicKey();
addField(sb, "Y:", key.getY().toString(16));
}
addField(sb, R.getI18NString("service.gui.CERT_INFO_SIGN"),
R.getI18NString(
"service.gui.CERT_INFO_KEY_BITS_PRINT",
new String[]{
String.valueOf(certificate.getSignature().length*8),
}),
getHex(certificate.getSignature()),
48);
sb.append("</table>\n");
}
/**
* Add a title.
*
* @param sb StringBuilder to append to
* @param title to print
*/
private void addTitle(StringBuilder sb, String title)
{
sb.append("<tr><td colspan='2'")
.append(" style='margin-top: 5pt; white-space: nowrap'><p><b>")
.append(title).append("</b></p></td></tr>\n");
}
/**
* Add a field.
* @param sb StringBuilder to append to
* @param field name of the certificate field
* @param value to print
*/
private void addField(StringBuilder sb, String field, String value)
{
addField(sb, field, value, null, 0);
}
/**
* Add a field.
* @param sb StringBuilder to append to
* @param field name of the certificate field
* @param value to print
* @param wrap force-wrap after number of characters
*/
private void addField(StringBuilder sb, String field, String value,
int wrap)
{
addField(sb, field, value, null, wrap);
}
/**
* Add a field.
* @param sb StringBuilder to append to
* @param field name of the certificate field
* @param value to print (not wrapped)
* @param otherValue second line of value to print (wrapped)
* @param wrap force-wrap after number of characters
*/
private void addField(StringBuilder sb, String field, String value,
String otherValue, int wrap)
{
sb.append("<tr><td style='margin-left: 5pt; margin-right: 25pt;")
.append("white-space: nowrap' valign='top'>")
.append(field).append("</td><td><span");
if (otherValue != null)
{
sb.append('>').append(value).append("</span><br/><span");
value = otherValue;
}
if (wrap > 0)
{
sb.append(" style='font-family:monospace'>");
for (int i = 0; i < value.length(); i++)
{
if (i % wrap == 0 && i > 0)
{
sb.append("<br/>");
}
sb.append(value.charAt(i));
}
}
else
{
sb.append(">");
sb.append(value);
}
sb.append("</span></td></tr>");
}
/**
* Converts the byte array to hex string.
* @param raw the data.
* @return the hex string.
*/
private String getHex( byte [] raw )
{
if (raw == null)
return null;
StringBuilder hex = new StringBuilder(2 * raw.length);
Formatter f = new Formatter(hex);
try
{
for (byte b : raw)
f.format("%02X:", b);
}
finally
{
f.close();
}
return hex.substring(0, hex.length() - 1);
}
/**
* Calculates the hash of the certificate known as the "thumbprint"
* and returns it as a string representation.
*
* @param cert The certificate to hash.
* @param algorithm The hash algorithm to use.
* @return The SHA-1 hash of the certificate.
* @throws CertificateException
*/
private static String getThumbprint(X509Certificate cert, String algorithm)
throws CertificateException
{
MessageDigest digest;
try
{
digest = MessageDigest.getInstance(algorithm);
}
catch (NoSuchAlgorithmException e)
{
throw new CertificateException(e);
}
byte[] encodedCert = cert.getEncoded();
StringBuilder sb = new StringBuilder(encodedCert.length * 2);
Formatter f = new Formatter(sb);
try
{
for (byte b : digest.digest(encodedCert))
f.format("%02X:", b);
}
finally
{
f.close();
}
return sb.substring(0, sb.length() - 1);
}
/**
* Construct a "simplified name" based on the subject DN from the
* certificate. The purpose is to have something shorter to display in the
* list. The name used is one of the following DN parts, if
* available, otherwise the complete DN:
* 'CN', 'OU' or else 'O'.
* @param cert to read subject DN from
* @return the simplified name
*/
private static String getSimplifiedName(X509Certificate cert)
{
final HashMap<String, String> parts = new HashMap<String, String>();
try
{
for (Rdn name : new LdapName(
cert.getSubjectX500Principal().getName()).getRdns())
{
if (name.getType() != null && name.getValue() != null)
{
parts.put(name.getType(), name.getValue().toString());
}
}
}
catch (InvalidNameException ignored) // NOPMD
{
}
String result = parts.get("CN");
if (result == null)
{
result = parts.get("OU");
}
if (result == null)
{
result = parts.get("O");
}
if (result == null)
{
result = cert.getSubjectX500Principal().getName();
}
return result;
}
/**
* Called when the selection changed in the tree.
* Loads the selected certificate.
* @param e the event
*/
private void valueChangedPerformed(TreeSelectionEvent e)
{
Object o = e.getNewLeadSelectionPath().getLastPathComponent();
if (o instanceof DefaultMutableTreeNode)
{
DefaultMutableTreeNode node = (DefaultMutableTreeNode) o;
infoTextPane.setText(toString(node.getUserObject()));
}
}
}