Project: jupload
//
// $Id: InteractiveTrustManager.java 441 2008-04-16 07:58:02Z etienne_sf $ 
// 
// jupload - A file upload applet. 
// 
// Copyright 2007 The JUpload Team 
// 
// Created: 30.05.2007 
// Creator: felfert 
// Last modified: $Date: 2008-04-16 00:58:02 -0700 (Wed, 16 Apr 2008) $ 
// 
// This program is free software; you can redistribute it and/or modify 
// it under the terms of the GNU General Public License as published by 
// the Free Software Foundation; either version 2 of the License, or 
// (at your option) any later version. 
// 
// This program is distributed in the hope that it will be useful, 
// but WITHOUT ANY WARRANTY; without even the implied warranty of 
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 
// GNU General Public License for more details. 
// 
// You should have received a copy of the GNU General Public License 
// along with this program; if not, write to the Free Software 
// Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
 
package wjhk.jupload2.upload; 
 
import java.awt.BorderLayout; 
import java.io.File; 
import java.io.FileInputStream; 
import java.io.FileOutputStream; 
import java.io.IOException; 
import java.security.KeyStore; 
import java.security.KeyStoreException; 
import java.security.MessageDigest; 
import java.security.NoSuchAlgorithmException; 
import java.security.UnrecoverableKeyException; 
import java.security.cert.CertificateException; 
import java.security.cert.CertificateExpiredException; 
import java.security.cert.CertificateNotYetValidException; 
import java.security.cert.X509Certificate; 
import java.util.Iterator; 
import java.util.StringTokenizer; 
import java.util.Vector; 
 
import javax.crypto.BadPaddingException; 
import javax.net.ssl.KeyManager; 
import javax.net.ssl.KeyManagerFactory; 
import javax.net.ssl.TrustManager; 
import javax.net.ssl.TrustManagerFactory; 
import javax.net.ssl.X509TrustManager; 
import javax.security.auth.callback.Callback; 
import javax.security.auth.callback.CallbackHandler; 
import javax.security.auth.callback.PasswordCallback; 
import javax.security.auth.callback.UnsupportedCallbackException; 
import javax.swing.BorderFactory; 
import javax.swing.JButton; 
import javax.swing.JEditorPane; 
import javax.swing.JLabel; 
import javax.swing.JOptionPane; 
import javax.swing.JPanel; 
import javax.swing.JPasswordField; 
 
import wjhk.jupload2.policies.UploadPolicy; 
 
/**
 * An implementation of {@link javax.net.ssl.X509TrustManager} which can operate 
 * in different modes. If mode is {@link #NONE}, then any server certificate is 
 * accepted and no certificate-based client authentication is performed. If mode 
 * is SERVER, then server certificates are verified and if verification is 
 * unsuccessful, a dialog is presented to the user, which allows accepting a 
 * certificate temporarily or permanently. If mode is CLIENT, then 
 * certificate-based client authentication is performed. Finally, there is a 
 * mode STRICT, which combines both SERVER and CLIENT modes. 
 *  
 * @author felfert 
 */
 
public class InteractiveTrustManager implements X509TrustManager, 
        CallbackHandler { 
 
    /**
     * Mode for accepting any certificate. 
     */
 
    public final static int NONE = 0
 
    /**
     * Mode for verifying server certificate chains. 
     */
 
    public final static int SERVER = 1
 
    /**
     * Mode for using client certificates. 
     */
 
    public final static int CLIENT = 2
 
    /**
     * Mode for performing both client authentication and server cert 
     * verification. 
     */
 
    public final static int STRICT = SERVER + CLIENT; 
 
    private UploadPolicy uploadPolicy; 
 
    private int mode = STRICT; 
 
    private String hostname; 
 
    private final static String TS = ".truststore"
 
    private final static String TSKEY = "javax.net.ssl.trustStore"
 
    private final static String USERTS = System.getProperty("user.home"
            + File.separator + TS; 
 
    /**
     * Absolute path of the truststore to use. 
     */
 
    private String tsname = null
 
    private String tspasswd = null
 
    private TrustManagerFactory tmf = null
 
    private static KeyManagerFactory kmf = null
 
    /**
     * The truststore for validation of server certificates 
     */
 
    private static KeyStore ts = null
 
    /**
     * The keystore for client certificates. 
     */
 
    private KeyStore ks = null
 
    private String getPassword(String storename) { 
        JPasswordField pwf = new JPasswordField(16); 
        JLabel l = new JLabel(String.format(this.uploadPolicy 
                .getString("itm_prompt_pass"), storename)); 
        l.setLabelFor(pwf); 
        JPanel p = new JPanel(new BorderLayout(100)); 
        p.setBorder(BorderFactory.createEmptyBorder(510510)); 
        p.add(l, BorderLayout.LINE_START); 
        p.add(pwf, BorderLayout.LINE_END); 
        int res = JOptionPane.showConfirmDialog(null, p, String.format( 
                this.uploadPolicy.getString("itm_title_pass"), storename), 
                JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); 
        if (res == JOptionPane.OK_OPTION) 
            return new String(pwf.getPassword()); 
        return null
    } 
 
    /**
     * @see javax.security.auth.callback.CallbackHandler#handle(javax.security.auth.callback.Callback[]) 
     */
 
    public void handle(Callback[] callbacks) 
            throws UnsupportedCallbackException { 
 
        for (int i = 0; i < callbacks.length; i++) { 
            if (callbacks[i] instanceof PasswordCallback) { 
                // prompt the user for sensitive information 
                PasswordCallback pc = (PasswordCallback) callbacks[i]; 
                String pw = getPassword(pc.getPrompt()); 
                pc.setPassword((pw == null) ? null : pw.toCharArray()); 
                pw = null
            } else { 
                throw new UnsupportedCallbackException(callbacks[i], 
                        "Unrecognized Callback"); 
            } 
        } 
    } 
 
    /**
     * Create a new instance. 
     *  
     * @param p The UploadPolicy to use for this instance. 
     * @param hostname  
     * @param passwd An optional password for the truststore. 
     * @throws NoSuchAlgorithmException 
     * @throws KeyStoreException 
     * @throws CertificateException 
     * @throws IllegalArgumentException  
     * @throws UnrecoverableKeyException 
     */
 
    public InteractiveTrustManager(UploadPolicy p, String hostname, 
            String passwd) throws NoSuchAlgorithmException, KeyStoreException, 
            CertificateException, IllegalArgumentException, 
            UnrecoverableKeyException { 
        this.mode = p.getSslVerifyCert(); 
        this.uploadPolicy = p; 
        if ((this.mode & SERVER) != 0) { 
            if (null == passwd) 
                // The default password as distributed by Sun. 
                passwd = "changeit"
            this.tsname = System.getProperty(TSKEY); 
            if (null == this.tsname) { 
                // The default system-wide truststore 
                this.tsname = System.getProperty("java.home") + File.separator 
                        + "lib" + File.separator + "security" + File.separator 
                        + "cacerts"
                // If the a user-specific truststore exists, it has precedence. 
                if (new File(USERTS).exists()) 
                    this.tsname = USERTS; 
            } 
            if (null == hostname || hostname.length() == 0
                throw new IllegalArgumentException( 
                        "hostname may not be null or empty."); 
            this.hostname = hostname; 
            // Initialize the keystore only once, so that we can 
            // reuse it during the session 
            if (null == ts) { 
                ts = KeyStore.getInstance(KeyStore.getDefaultType()); 
                while (true) { 
                    try { 
                        FileInputStream is = new FileInputStream(this.tsname); 
                        ts.load(is, passwd.toCharArray()); 
                        is.close(); 
                        // need it later for eventual storing. 
                        this.tspasswd = passwd; 
                        break
                    } catch (IOException e) { 
                        if (e 
                                .getMessage() 
                                .equals( 
                                        "Keystore was tampered with, or password was incorrect")) { 
                            passwd = getPassword(this.uploadPolicy 
                                    .getString("itm_tstore")); 
                            if (null != passwd) 
                                continue
                        } 
                        throw new KeyStoreException("Could not load truststore"); 
                    } 
                } 
            } 
            this.tmf = TrustManagerFactory.getInstance(TrustManagerFactory 
                    .getDefaultAlgorithm()); 
            this.tmf.init(ts); 
        } 
        if ((this.mode & CLIENT) != 0) { 
            String ksname = System.getProperty("javax.net.ssl.keyStore"); 
            if (null == ksname) 
                ksname = System.getProperty("user.home") + File.separator 
                        + ".keystore"
            String cpass = "changeit"
            File f = new File(ksname); 
            if (!(f.exists() && f.isFile())) 
                throw new KeyStoreException("Keystore " + ksname 
                        + " does not exist."); 
            if (null == kmf) { 
                String kstype = ksname.toLowerCase().endsWith(".p12") ? "PKCS12" 
                        : KeyStore.getDefaultType(); 
                this.ks = KeyStore.getInstance(kstype); 
                while (true) { 
                    try { 
                        FileInputStream is = new FileInputStream(ksname); 
                        this.ks.load(is, cpass.toCharArray()); 
                        is.close(); 
                        break
                    } catch (IOException e) { 
                        if ((e.getCause() instanceof BadPaddingException) 
                                || (e.getMessage() 
                                        .equals("Keystore was tampered with, or password was incorrect"))) { 
                            cpass = getPassword("Keystore"); 
                            if (null != cpass) 
                                continue
                        } 
                        throw new KeyStoreException("Could not load keystore: " 
                                + e.getMessage()); 
                    } 
                } 
                kmf = KeyManagerFactory.getInstance(KeyManagerFactory 
                        .getDefaultAlgorithm()); 
                kmf.init(this.ks, cpass.toCharArray()); 
            } 
        } 
 
    } 
 
    /**
     * Retrieve key managers. 
     *  
     * @return The current array of key managers. 
     */
 
    public KeyManager[] getKeyManagers() { 
        return ((this.mode & CLIENT) == 0) ? null : kmf.getKeyManagers(); 
    } 
 
    /**
     * Retrieve trust managers. 
     *  
     * @return The current array of trust managers 
     */
 
    public X509TrustManager[] getTrustManagers() { 
        return new X509TrustManager[] { 
            this 
        }; 
    } 
 
    /**
     * As this class is used on the client side only, The implementation of this 
     * method does nothing. 
     *  
     * @see javax.net.ssl.X509TrustManager#checkClientTrusted(java.security.cert.X509Certificate[], 
     *      java.lang.String) 
     */
 
    public void checkClientTrusted(@SuppressWarnings("unused"
    X509Certificate[] arg0, @SuppressWarnings("unused"
    String arg1) { 
        // Nothing to do. 
    } 
 
    /**
     * Format a DN. This method formats a DN (Distinguished Name) string as 
     * returned from {@link javax.security.auth.x500.X500Principal#getName()} to 
     * HTML table columns. 
     *  
     * @param dn The DN to format. 
     * @param cn An optional CN (Common Name) to match against the CN in the DN. 
     *            If this parameter is non null and the CN, encoded in the DN 
     *            does not match the CN specified, it is considered an error and 
     *            the CN is printed accordingly (red). 
     * @param reason A vector of error-strings. If the CN-comparison fails, an 
     *            explanation is added to this vector. 
     * @return A string, containing the HTML code rendering the given DN in a 
     *         table. 
     */
 
    private String formatDN(String dn, String cn, Vector<String> reason) { 
        StringBuffer ret = new StringBuffer(); 
        StringTokenizer t = new StringTokenizer(dn, ","); 
        while (t.hasMoreTokens()) { 
            String tok = t.nextToken(); 
            while (tok.endsWith("\\")) 
                tok += t.nextToken(); 
            String kv[] = tok.split("="2); 
            if (kv.length == 2) { 
                if (kv[0].equals("C")) 
                    ret.append("<tr><td>").append( 
                            this.uploadPolicy.getString("itm_cert_C")).append( 
                            "</td><td>").append(kv[1]).append("</td></tr>\n"); 
                if (kv[0].equals("CN")) { 
                    boolean ok = true
                    if (null != cn) 
                        ok = cn.equals(kv[1]); 
                    ret.append("<tr><td>").append( 
                            this.uploadPolicy.getString("itm_cert_CN")).append( 
                            "</td><td"); 
                    ret.append(ok ? ">" : " class=\"err\">").append(kv[1]) 
                            .append("</td></tr>\n"); 
                    if (!ok) 
                        reason.add(String.format(this.uploadPolicy 
                                .getString("itm_reason_cnmatch"), cn)); 
                } 
                if (kv[0].equals("L")) 
                    ret.append("<tr><td>").append( 
                            this.uploadPolicy.getString("itm_cert_L")).append( 
                            "</td><td>").append(kv[1]).append("</td></tr>\n"); 
                if (kv[0].equals("ST")) 
                    ret.append("<tr><td>").append( 
                            this.uploadPolicy.getString("itm_cert_ST")).append( 
                            "</td><td>").append(kv[1]).append("</td></tr>\n"); 
                if (kv[0].equals("O")) 
                    ret.append("<tr><td>").append( 
                            this.uploadPolicy.getString("itm_cert_O")).append( 
                            "</td><td>").append(kv[1]).append("</td></tr>\n"); 
                if (kv[0].equals("OU")) 
                    ret.append("<tr><td>").append( 
                            this.uploadPolicy.getString("itm_cert_OU")).append( 
                            "</td><td>").append(kv[1]).append("</td></tr>\n"); 
            } 
        } 
        return ret.toString(); 
    } 
 
    private void CertDialog(X509Certificate c) throws CertificateException { 
        int i; 
        boolean expired = false
        boolean notyet = false
        Vector<String> reason = new Vector<String>(); 
        reason.add(this.uploadPolicy.getString("itm_reason_itrust")); 
        try { 
            c.checkValidity(); 
        } catch (CertificateExpiredException e1) { 
            expired = true
            reason.add(this.uploadPolicy.getString("itm_reason_expired")); 
        } catch (CertificateNotYetValidException e2) { 
            notyet = true
            reason.add(this.uploadPolicy.getString("itm_reason_notyet")); 
        } 
 
        StringBuffer msg = new StringBuffer(); 
        msg.append("<html><head>"); 
        msg.append("<style type=\"text/css\">\n"); 
        msg.append("td, th, p, body { "); 
        msg.append("font-family: Arial, Helvetica, sans-serif; "); 
        msg.append("font-size: 12pt; "); 
        // PLAF hassle. The PLAF renders controls with different text colors, 
        // but 
        // does not set SystemColor.controlText. So we create a dummy button and 
        // retrieve its text color. 
        Integer ii = new Integer( 
                new JButton(".").getForeground().getRGB() & 0x00ffffff); 
        msg.append("color: ").append(String.format("#%06x", ii)).append(" }\n"); 
        msg.append("th { text-align: left; }\n"); 
        msg.append("td { margin-left: 20; }\n"); 
        msg.append(".err { color: red; }\n"); 
        msg.append("</style>\n"); 
        msg.append("</head><body>"); 
        msg.append("<h3>").append( 
                this.uploadPolicy.getString("itm_fail_verify")).append("</h3>"); 
        msg.append("<h4>").append( 
                this.uploadPolicy.getString("itm_cert_details")) 
                .append("</h4>"); 
        msg.append("<table>"); 
        msg.append("<tr><th colspan=2>").append( 
                this.uploadPolicy.getString("itm_cert_subject")).append( 
                "</th></tr>"); 
        msg.append(formatDN(c.getSubjectX500Principal().getName(), 
                this.hostname, reason)); 
        msg.append("<tr><td>").append( 
                this.uploadPolicy.getString("itm_cert_nbefore")) 
                .append("</td>"); 
        msg.append(notyet ? "<td class=\"err\">" : "<td>").append( 
                c.getNotBefore()).append("</td></tr>\n"); 
        msg.append("<tr><td>").append( 
                this.uploadPolicy.getString("itm_cert_nafter")).append("</td>"); 
        msg.append(expired ? "<td class=\"err\">" : "<td>").append( 
                c.getNotAfter()).append("</td></tr>\n"); 
        msg.append("<tr><td>").append( 
                this.uploadPolicy.getString("itm_cert_serial")).append( 
                "</td><td>"); 
        msg.append(c.getSerialNumber()); 
        msg.append("</td></tr>\n"); 
        msg.append("<tr><td>").append( 
                String.format(this.uploadPolicy.getString("itm_cert_fprint"), 
                        "SHA1")).append("</td><td>"); 
        MessageDigest d; 
        StringBuffer fp = new StringBuffer(); 
        try { 
            d = MessageDigest.getInstance("SHA1"); 
        } catch (NoSuchAlgorithmException e) { 
            throw new CertificateException( 
                    "Unable to calculate certificate SHA1 fingerprint: " 
                            + e.getMessage()); 
        } 
        byte[] sha1sum = d.digest(c.getEncoded()); 
        for (i = 0; i < sha1sum.length; i++) { 
            if (i > 0
                fp.append(":"); 
            fp.append(Integer.toHexString((sha1sum[i] >> 4) & 0x0f)); 
            fp.append(Integer.toHexString(sha1sum[i] & 0x0f)); 
        } 
        msg.append(fp).append("</td></tr>\n"); 
        fp.setLength(0); 
        msg.append("<tr><td>").append( 
                String.format(this.uploadPolicy.getString("itm_cert_fprint"), 
                        "MD5")).append("</td><td>"); 
        try { 
            d = MessageDigest.getInstance("MD5"); 
        } catch (NoSuchAlgorithmException e) { 
            throw new CertificateException( 
                    "Unable to calculate certificate MD5 fingerprint: " 
                            + e.getMessage()); 
        } 
        byte[] md5sum = d.digest(c.getEncoded()); 
        for (i = 0; i < md5sum.length; i++) { 
            if (i > 0
                fp.append(":"); 
            fp.append(Integer.toHexString((md5sum[i] >> 4) & 0x0f)); 
            fp.append(Integer.toHexString(md5sum[i] & 0x0f)); 
        } 
        msg.append(fp).append("</td></tr>\n"); 
        msg.append("</table><table>"); 
        msg.append("<tr><th colspan=2>").append( 
                this.uploadPolicy.getString("itm_cert_issuer")).append( 
                "</th></tr>"); 
        msg 
                .append(formatDN(c.getIssuerX500Principal().getName(), null
                        reason)); 
        msg.append("</table>"); 
        msg.append("<p><b>").append(this.uploadPolicy.getString("itm_reasons")) 
                .append("</b><br><ul>"); 
        Iterator<String> it = reason.iterator(); 
        while (it.hasNext()) { 
            msg.append("<li>" + it.next() + "</li>\n"); 
        } 
        msg.append("</ul></p>"); 
        msg.append("<p><b>").append( 
                this.uploadPolicy.getString("itm_accept_prompt")).append( 
                "</b></p>"); 
        msg.append("</body></html>"); 
 
        JPanel p = new JPanel(); 
        p.setLayout(new BorderLayout()); 
        JEditorPane ep = new JEditorPane("text/html", msg.toString()); 
        ep.setEditable(false); 
        ep.setBackground(p.getBackground()); 
        p.add(ep, BorderLayout.CENTER); 
 
        String no = this.uploadPolicy.getString("itm_accept_no"); 
        int ans = JOptionPane.showOptionDialog(null, p, 
                "SSL Certificate Alert", JOptionPane.YES_NO_CANCEL_OPTION, 
                JOptionPane.WARNING_MESSAGE, nullnew String[] { 
                        this.uploadPolicy.getString("itm_accept_always"), 
                        this.uploadPolicy.getString("itm_accept_now"), no 
                }, no); 
        switch (ans) { 
            case JOptionPane.CANCEL_OPTION: 
            case JOptionPane.CLOSED_OPTION: 
                throw new CertificateException("Server certificate rejected."); 
            case JOptionPane.NO_OPTION: 
            case JOptionPane.YES_OPTION: 
                // Add certificate to truststore 
                try { 
                    ts.setCertificateEntry(fp.toString(), c); 
                } catch (KeyStoreException e) { 
                    throw new CertificateException( 
                            "Unable to add certificate: " + e.getMessage()); 
                } 
                if (ans == JOptionPane.YES_OPTION) { 
                    // Save truststore for permanent acceptance. 
                    // If not explicitely specified, we save to a 
                    // user-truststore. 
                    if (null == System.getProperty(TSKEY)) 
                        this.tsname = USERTS; 
                    while (true) { 
                        try { 
                            File f = new File(this.tsname); 
                            boolean old = false
                            if (f.exists()) { 
                                if (!f.renameTo(new File(this.tsname + ".old"))) 
                                    throw new IOException( 
                                            "Could not rename truststore"); 
                                old = true
                            } else { 
                                // New truststore, get a new password. 
                                this.tspasswd = this 
                                        .getPassword(this.uploadPolicy 
                                                .getString("itm_new_tstore")); 
                                if (null == this.tspasswd) 
                                    this.tspasswd = "changeit"
                            } 
                            FileOutputStream os = new FileOutputStream( 
                                    this.tsname); 
                            ts.store(os, this.tspasswd.toCharArray()); 
                            os.close(); 
                            if (old && (!f.delete())) 
                                throw new IOException( 
                                        "Could not delete old truststore"); 
                            // Must re-initialize TrustManagerFactory 
                            this.tmf.init(ts); 
                            System.out.println("Saved cert to " + this.tsname); 
                            break
                        } catch (Exception e) { 
                            if (this.tsname.equals(USERTS)) 
                                throw new CertificateException(e); 
                            this.tsname = USERTS; 
                        } 
                    } 
                } 
        } 
    } 
 
    /**
     * @see javax.net.ssl.X509TrustManager#checkServerTrusted(java.security.cert.X509Certificate[], 
     *      java.lang.String) 
     */
 
    public void checkServerTrusted(X509Certificate[] chain, String authType) 
            throws CertificateException { 
        if ((this.mode & SERVER) != 0) { 
            if (null == chain || chain.length == 0
                throw new IllegalArgumentException( 
                        "Certificate chain is null or empty"); 
 
            int i; 
            TrustManager[] mgrs = this.tmf.getTrustManagers(); 
            for (i = 0; i < mgrs.length; i++) { 
                if (mgrs[i] instanceof X509TrustManager) { 
                    X509TrustManager m = (X509TrustManager) (mgrs[i]); 
                    try { 
                        m.checkServerTrusted(chain, authType); 
                        return
                    } catch (Exception e) { 
                        // try next 
                    } 
                } 
            } 
 
            // If we get here, the certificate could not be verified. 
            // Ask the user what to do. 
            CertDialog(chain[0]); 
        } 
        // In dummy mode: Nothing to do. 
    } 
 
    /**
     * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers() 
     */
 
    public X509Certificate[] getAcceptedIssuers() { 
        System.out.println("getAcceptedIssuers"); 
        return new X509Certificate[0]; 
    } 
}