View Javadoc
1   //
2   // $Id: InteractiveTrustManager.java 918 2010-01-08 22:21:44Z etienne_sf $
3   //
4   // jupload - A file upload applet.
5   //
6   // Copyright 2007 The JUpload Team
7   //
8   // Created: 30.05.2007
9   // Creator: felfert
10  // Last modified: $Date: 2010-01-08 23:21:44 +0100 (ven., 08 janv. 2010) $
11  //
12  // This program is free software; you can redistribute it and/or modify
13  // it under the terms of the GNU General Public License as published by
14  // the Free Software Foundation; either version 2 of the License, or
15  // (at your option) any later version.
16  //
17  // This program is distributed in the hope that it will be useful,
18  // but WITHOUT ANY WARRANTY; without even the implied warranty of
19  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20  // GNU General Public License for more details.
21  //
22  // You should have received a copy of the GNU General Public License
23  // along with this program; if not, write to the Free Software
24  // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
25  
26  package wjhk.jupload2.upload.helper;
27  
28  import java.awt.BorderLayout;
29  import java.io.File;
30  import java.io.FileInputStream;
31  import java.io.FileOutputStream;
32  import java.io.IOException;
33  import java.security.KeyStore;
34  import java.security.KeyStoreException;
35  import java.security.MessageDigest;
36  import java.security.NoSuchAlgorithmException;
37  import java.security.UnrecoverableKeyException;
38  import java.security.cert.CertificateException;
39  import java.security.cert.CertificateExpiredException;
40  import java.security.cert.CertificateNotYetValidException;
41  import java.security.cert.X509Certificate;
42  import java.util.Iterator;
43  import java.util.StringTokenizer;
44  import java.util.Vector;
45  
46  import javax.crypto.BadPaddingException;
47  import javax.net.ssl.KeyManager;
48  import javax.net.ssl.KeyManagerFactory;
49  import javax.net.ssl.TrustManager;
50  import javax.net.ssl.TrustManagerFactory;
51  import javax.net.ssl.X509TrustManager;
52  import javax.security.auth.callback.Callback;
53  import javax.security.auth.callback.CallbackHandler;
54  import javax.security.auth.callback.PasswordCallback;
55  import javax.security.auth.callback.UnsupportedCallbackException;
56  import javax.swing.BorderFactory;
57  import javax.swing.JButton;
58  import javax.swing.JEditorPane;
59  import javax.swing.JLabel;
60  import javax.swing.JOptionPane;
61  import javax.swing.JPanel;
62  import javax.swing.JPasswordField;
63  
64  import wjhk.jupload2.policies.UploadPolicy;
65  
66  /**
67   * An implementation of {@link javax.net.ssl.X509TrustManager} which can operate
68   * in different modes. If mode is {@link #NONE}, then any server certificate is
69   * accepted and no certificate-based client authentication is performed. If mode
70   * is SERVER, then server certificates are verified and if verification is
71   * unsuccessful, a dialog is presented to the user, which allows accepting a
72   * certificate temporarily or permanently. If mode is CLIENT, then
73   * certificate-based client authentication is performed. Finally, there is a
74   * mode STRICT, which combines both SERVER and CLIENT modes.
75   * 
76   * @author felfert
77   */
78  public class InteractiveTrustManager implements X509TrustManager,
79          CallbackHandler {
80  
81      /**
82       * Mode for accepting any certificate.
83       */
84      public final static int NONE = 0;
85  
86      /**
87       * Mode for verifying server certificate chains.
88       */
89      public final static int SERVER = 1;
90  
91      /**
92       * Mode for using client certificates.
93       */
94      public final static int CLIENT = 2;
95  
96      /**
97       * Mode for performing both client authentication and server cert
98       * verification.
99       */
100     public final static int STRICT = SERVER + CLIENT;
101 
102     private UploadPolicy uploadPolicy;
103 
104     private int mode = STRICT;
105 
106     private String hostname;
107 
108     private final static String TS = ".truststore";
109 
110     private final static String TSKEY = "javax.net.ssl.trustStore";
111 
112     private final static String USERTS = System.getProperty("user.home")
113             + File.separator + TS;
114 
115     /**
116      * Absolute path of the truststore to use.
117      */
118     private String tsname = null;
119 
120     private String tspasswd = null;
121 
122     private TrustManagerFactory tmf = null;
123 
124     private KeyManagerFactory kmf = null;
125 
126     /**
127      * The truststore for validation of server certificates
128      */
129     private KeyStore ts = null;
130 
131     /**
132      * The keystore for client certificates.
133      */
134     private KeyStore ks = null;
135 
136     private String getPassword(String storename) {
137         JPasswordField pwf = new JPasswordField(16);
138         JLabel l = new JLabel(this.uploadPolicy.getLocalizedString(
139                 "itm_prompt_pass", storename));
140         l.setLabelFor(pwf);
141         JPanel p = new JPanel(new BorderLayout(10, 0));
142         p.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10));
143         p.add(l, BorderLayout.LINE_START);
144         p.add(pwf, BorderLayout.LINE_END);
145         int res = JOptionPane.showConfirmDialog(null, p, this.uploadPolicy
146                 .getLocalizedString("itm_title_pass", storename),
147                 JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
148         if (res == JOptionPane.OK_OPTION)
149             return new String(pwf.getPassword());
150         return null;
151     }
152 
153     /**
154      * @see javax.security.auth.callback.CallbackHandler#handle(javax.security.auth.callback.Callback[])
155      */
156     public void handle(Callback[] callbacks)
157             throws UnsupportedCallbackException {
158 
159         for (int i = 0; i < callbacks.length; i++) {
160             if (callbacks[i] instanceof PasswordCallback) {
161                 // prompt the user for sensitive information
162                 PasswordCallback pc = (PasswordCallback) callbacks[i];
163                 String pw = getPassword(pc.getPrompt());
164                 pc.setPassword((pw == null) ? null : pw.toCharArray());
165                 pw = null;
166             } else {
167                 throw new UnsupportedCallbackException(callbacks[i],
168                         "Unrecognized Callback");
169             }
170         }
171     }
172 
173     /**
174      * Create a new instance.
175      * 
176      * @param p The UploadPolicy to use for this instance.
177      * @param hostname
178      * @param passwd An optional password for the truststore.
179      * @throws NoSuchAlgorithmException
180      * @throws KeyStoreException
181      * @throws CertificateException
182      * @throws IllegalArgumentException
183      * @throws UnrecoverableKeyException
184      */
185     public InteractiveTrustManager(UploadPolicy p, String hostname,
186             String passwd) throws NoSuchAlgorithmException, KeyStoreException,
187             CertificateException, IllegalArgumentException,
188             UnrecoverableKeyException {
189         this.mode = p.getSslVerifyCert();
190         this.uploadPolicy = p;
191         if ((this.mode & SERVER) != 0) {
192             if (null == passwd)
193                 // The default password as distributed by Sun.
194                 passwd = "changeit";
195             this.tsname = System.getProperty(TSKEY);
196             if (null == this.tsname) {
197                 // The default system-wide truststore
198                 this.tsname = System.getProperty("java.home") + File.separator
199                         + "lib" + File.separator + "security" + File.separator
200                         + "cacerts";
201                 // If the a user-specific truststore exists, it has precedence.
202                 if (new File(USERTS).exists())
203                     this.tsname = USERTS;
204             }
205             if (null == hostname || hostname.length() == 0)
206                 throw new IllegalArgumentException(
207                         "hostname may not be null or empty.");
208             this.hostname = hostname;
209             // Initialize the keystore only once, so that we can
210             // reuse it during the session
211             if (null == this.ts) {
212                 this.ts = KeyStore.getInstance(KeyStore.getDefaultType());
213                 while (true) {
214                     try {
215                         FileInputStream is = new FileInputStream(this.tsname);
216                         this.ts.load(is, passwd.toCharArray());
217                         is.close();
218                         // need it later for eventual storing.
219                         this.tspasswd = passwd;
220                         break;
221                     } catch (IOException e) {
222                         if (e
223                                 .getMessage()
224                                 .equals(
225                                         "Keystore was tampered with, or password was incorrect")) {
226                             passwd = getPassword(this.uploadPolicy
227                                     .getLocalizedString("itm_tstore"));
228                             if (null != passwd)
229                                 continue;
230                         }
231                         throw new KeyStoreException("Could not load truststore");
232                     }
233                 }
234             }
235             this.tmf = TrustManagerFactory.getInstance(TrustManagerFactory
236                     .getDefaultAlgorithm());
237             this.tmf.init(this.ts);
238         }
239         if ((this.mode & CLIENT) != 0) {
240             String ksname = System.getProperty("javax.net.ssl.keyStore");
241             if (null == ksname)
242                 ksname = System.getProperty("user.home") + File.separator
243                         + ".keystore";
244             String cpass = "changeit";
245             File f = new File(ksname);
246             if (!(f.exists() && f.isFile()))
247                 throw new KeyStoreException("Keystore " + ksname
248                         + " does not exist.");
249             if (null == this.kmf) {
250                 String kstype = ksname.toLowerCase().endsWith(".p12") ? "PKCS12"
251                         : KeyStore.getDefaultType();
252                 this.ks = KeyStore.getInstance(kstype);
253                 while (true) {
254                     try {
255                         FileInputStream is = new FileInputStream(ksname);
256                         this.ks.load(is, cpass.toCharArray());
257                         is.close();
258                         break;
259                     } catch (IOException e) {
260                         if ((e.getCause() instanceof BadPaddingException)
261                                 || (e.getMessage()
262                                         .equals("Keystore was tampered with, or password was incorrect"))) {
263                             cpass = getPassword("Keystore");
264                             if (null != cpass)
265                                 continue;
266                         }
267                         throw new KeyStoreException("Could not load keystore: "
268                                 + e.getMessage());
269                     }
270                 }
271                 this.kmf = KeyManagerFactory.getInstance(KeyManagerFactory
272                         .getDefaultAlgorithm());
273                 this.kmf.init(this.ks, cpass.toCharArray());
274             }
275         }
276 
277     }
278 
279     /**
280      * Retrieve key managers.
281      * 
282      * @return The current array of key managers.
283      */
284     public KeyManager[] getKeyManagers() {
285         return ((this.mode & CLIENT) == 0) ? null : this.kmf.getKeyManagers();
286     }
287 
288     /**
289      * Retrieve trust managers.
290      * 
291      * @return The current array of trust managers
292      */
293     public X509TrustManager[] getTrustManagers() {
294         return new X509TrustManager[] {
295             this
296         };
297     }
298 
299     /**
300      * As this class is used on the client side only, The implementation of this
301      * method does nothing.
302      * 
303      * @see javax.net.ssl.X509TrustManager#checkClientTrusted(java.security.cert.X509Certificate[],
304      *      java.lang.String)
305      */
306     public void checkClientTrusted(X509Certificate[] arg0, String arg1) {
307         // Nothing to do.
308     }
309 
310     /**
311      * Format a DN. This method formats a DN (Distinguished Name) string as
312      * returned from {@link javax.security.auth.x500.X500Principal#getName()} to
313      * HTML table columns.
314      * 
315      * @param dn The DN to format.
316      * @param cn An optional CN (Common Name) to match against the CN in the DN.
317      *            If this parameter is non null and the CN, encoded in the DN
318      *            does not match the CN specified, it is considered an error and
319      *            the CN is printed accordingly (red).
320      * @param reason A vector of error-strings. If the CN-comparison fails, an
321      *            explanation is added to this vector.
322      * @return A string, containing the HTML code rendering the given DN in a
323      *         table.
324      */
325     private String formatDN(String dn, String cn, Vector<String> reason) {
326         StringBuffer ret = new StringBuffer();
327         StringTokenizer t = new StringTokenizer(dn, ",");
328         while (t.hasMoreTokens()) {
329             String tok = t.nextToken();
330             while (tok.endsWith("\\"))
331                 tok += t.nextToken();
332             String kv[] = tok.split("=", 2);
333             if (kv.length == 2) {
334                 if (kv[0].equals("C"))
335                     ret.append("<tr><td>").append(
336                             this.uploadPolicy.getLocalizedString("itm_cert_C"))
337                             .append("</td><td>").append(kv[1]).append(
338                                     "</td></tr>\n");
339                 if (kv[0].equals("CN")) {
340                     boolean ok = true;
341                     if (null != cn)
342                         ok = cn.equals(kv[1]);
343                     ret.append("<tr><td>")
344                             .append(
345                                     this.uploadPolicy
346                                             .getLocalizedString("itm_cert_CN"))
347                             .append("</td><td");
348                     ret.append(ok ? ">" : " class=\"err\">").append(kv[1])
349                             .append("</td></tr>\n");
350                     if (!ok)
351                         reason.add(this.uploadPolicy.getLocalizedString(
352                                 "itm_reason_cnmatch", cn));
353                 }
354                 if (kv[0].equals("L"))
355                     ret.append("<tr><td>").append(
356                             this.uploadPolicy.getLocalizedString("itm_cert_L"))
357                             .append("</td><td>").append(kv[1]).append(
358                                     "</td></tr>\n");
359                 if (kv[0].equals("ST"))
360                     ret.append("<tr><td>")
361                             .append(
362                                     this.uploadPolicy
363                                             .getLocalizedString("itm_cert_ST"))
364                             .append("</td><td>").append(kv[1]).append(
365                                     "</td></tr>\n");
366                 if (kv[0].equals("O"))
367                     ret.append("<tr><td>").append(
368                             this.uploadPolicy.getLocalizedString("itm_cert_O"))
369                             .append("</td><td>").append(kv[1]).append(
370                                     "</td></tr>\n");
371                 if (kv[0].equals("OU"))
372                     ret.append("<tr><td>")
373                             .append(
374                                     this.uploadPolicy
375                                             .getLocalizedString("itm_cert_OU"))
376                             .append("</td><td>").append(kv[1]).append(
377                                     "</td></tr>\n");
378             }
379         }
380         return ret.toString();
381     }
382 
383     private void CertDialog(X509Certificate c) throws CertificateException {
384         int i;
385         boolean expired = false;
386         boolean notyet = false;
387         Vector<String> reason = new Vector<String>();
388         reason.add(this.uploadPolicy.getLocalizedString("itm_reason_itrust"));
389         try {
390             c.checkValidity();
391         } catch (CertificateExpiredException e1) {
392             expired = true;
393             reason.add(this.uploadPolicy
394                     .getLocalizedString("itm_reason_expired"));
395         } catch (CertificateNotYetValidException e2) {
396             notyet = true;
397             reason.add(this.uploadPolicy
398                     .getLocalizedString("itm_reason_notyet"));
399         }
400 
401         StringBuffer msg = new StringBuffer();
402         msg.append("<html><head>");
403         msg.append("<style type=\"text/css\">\n");
404         msg.append("td, th, p, body { ");
405         msg.append("font-family: Arial, Helvetica, sans-serif; ");
406         msg.append("font-size: 12pt; ");
407         // PLAF hassle. The PLAF renders controls with different text colors,
408         // but
409         // does not set SystemColor.controlText. So we create a dummy button and
410         // retrieve its text color.
411         Integer ii = Integer
412                 .valueOf(new JButton(".").getForeground().getRGB() & 0x00ffffff);
413         msg.append("color: ").append(String.format("#%06x", ii)).append(" }\n");
414         msg.append("th { text-align: left; }\n");
415         msg.append("td { margin-left: 20; }\n");
416         msg.append(".err { color: red; }\n");
417         msg.append("</style>\n");
418         msg.append("</head><body>");
419         msg.append("<h3>").append(
420                 this.uploadPolicy.getLocalizedString("itm_fail_verify"))
421                 .append("</h3>");
422         msg.append("<h4>").append(
423                 this.uploadPolicy.getLocalizedString("itm_cert_details"))
424                 .append("</h4>");
425         msg.append("<table>");
426         msg.append("<tr><th colspan=2>").append(
427                 this.uploadPolicy.getLocalizedString("itm_cert_subject"))
428                 .append("</th></tr>");
429         msg.append(formatDN(c.getSubjectX500Principal().getName(),
430                 this.hostname, reason));
431         msg.append("<tr><td>").append(
432                 this.uploadPolicy.getLocalizedString("itm_cert_nbefore"))
433                 .append("</td>");
434         msg.append(notyet ? "<td class=\"err\">" : "<td>").append(
435                 c.getNotBefore()).append("</td></tr>\n");
436         msg.append("<tr><td>").append(
437                 this.uploadPolicy.getLocalizedString("itm_cert_nafter"))
438                 .append("</td>");
439         msg.append(expired ? "<td class=\"err\">" : "<td>").append(
440                 c.getNotAfter()).append("</td></tr>\n");
441         msg.append("<tr><td>").append(
442                 this.uploadPolicy.getLocalizedString("itm_cert_serial"))
443                 .append("</td><td>");
444         msg.append(c.getSerialNumber());
445         msg.append("</td></tr>\n");
446         msg.append("<tr><td>")
447                 .append(
448                         this.uploadPolicy.getLocalizedString("itm_cert_fprint",
449                                 "SHA1")).append("</td><td>");
450         MessageDigest d;
451         StringBuffer fp = new StringBuffer();
452         try {
453             d = MessageDigest.getInstance("SHA1");
454         } catch (NoSuchAlgorithmException e) {
455             throw new CertificateException(
456                     "Unable to calculate certificate SHA1 fingerprint: "
457                             + e.getMessage());
458         }
459         byte[] sha1sum = d.digest(c.getEncoded());
460         for (i = 0; i < sha1sum.length; i++) {
461             if (i > 0)
462                 fp.append(":");
463             fp.append(Integer.toHexString((sha1sum[i] >> 4) & 0x0f));
464             fp.append(Integer.toHexString(sha1sum[i] & 0x0f));
465         }
466         msg.append(fp).append("</td></tr>\n");
467         fp.setLength(0);
468         msg.append("<tr><td>").append(
469                 this.uploadPolicy.getLocalizedString("itm_cert_fprint", "MD5"))
470                 .append("</td><td>");
471         try {
472             d = MessageDigest.getInstance("MD5");
473         } catch (NoSuchAlgorithmException e) {
474             throw new CertificateException(
475                     "Unable to calculate certificate MD5 fingerprint: "
476                             + e.getMessage());
477         }
478         byte[] md5sum = d.digest(c.getEncoded());
479         for (i = 0; i < md5sum.length; i++) {
480             if (i > 0)
481                 fp.append(":");
482             fp.append(Integer.toHexString((md5sum[i] >> 4) & 0x0f));
483             fp.append(Integer.toHexString(md5sum[i] & 0x0f));
484         }
485         msg.append(fp).append("</td></tr>\n");
486         msg.append("</table><table>");
487         msg.append("<tr><th colspan=2>").append(
488                 this.uploadPolicy.getLocalizedString("itm_cert_issuer"))
489                 .append("</th></tr>");
490         msg
491                 .append(formatDN(c.getIssuerX500Principal().getName(), null,
492                         reason));
493         msg.append("</table>");
494         msg.append("<p><b>").append(
495                 this.uploadPolicy.getLocalizedString("itm_reasons")).append(
496                 "</b><br><ul>");
497         Iterator<String> it = reason.iterator();
498         while (it.hasNext()) {
499             msg.append("<li>" + it.next() + "</li>\n");
500         }
501         msg.append("</ul></p>");
502         msg.append("<p><b>").append(
503                 this.uploadPolicy.getLocalizedString("itm_accept_prompt"))
504                 .append("</b></p>");
505         msg.append("</body></html>");
506 
507         JPanel p = new JPanel();
508         p.setLayout(new BorderLayout());
509         JEditorPane ep = new JEditorPane("text/html", msg.toString());
510         ep.setEditable(false);
511         ep.setBackground(p.getBackground());
512         p.add(ep, BorderLayout.CENTER);
513 
514         String no = this.uploadPolicy.getLocalizedString("itm_accept_no");
515         int ans = JOptionPane.showOptionDialog(null, p,
516                 "SSL Certificate Alert", JOptionPane.YES_NO_CANCEL_OPTION,
517                 JOptionPane.WARNING_MESSAGE, null, new String[] {
518                         this.uploadPolicy
519                                 .getLocalizedString("itm_accept_always"),
520                         this.uploadPolicy.getLocalizedString("itm_accept_now"),
521                         no
522                 }, no);
523         switch (ans) {
524             case JOptionPane.CANCEL_OPTION:
525             case JOptionPane.CLOSED_OPTION:
526                 throw new CertificateException("Server certificate rejected.");
527             case JOptionPane.NO_OPTION:
528             case JOptionPane.YES_OPTION:
529                 // Add certificate to truststore
530                 try {
531                     this.ts.setCertificateEntry(fp.toString(), c);
532                 } catch (KeyStoreException e) {
533                     throw new CertificateException(
534                             "Unable to add certificate: " + e.getMessage());
535                 }
536                 if (ans == JOptionPane.YES_OPTION) {
537                     // Save truststore for permanent acceptance.
538                     // If not explicitely specified, we save to a
539                     // user-truststore.
540                     if (null == System.getProperty(TSKEY))
541                         this.tsname = USERTS;
542                     while (true) {
543                         try {
544                             File f = new File(this.tsname);
545                             boolean old = false;
546                             if (f.exists()) {
547                                 if (!f.renameTo(new File(this.tsname + ".old")))
548                                     throw new IOException(
549                                             "Could not rename truststore");
550                                 old = true;
551                             } else {
552                                 // New truststore, get a new password.
553                                 this.tspasswd = this
554                                         .getPassword(this.uploadPolicy
555                                                 .getLocalizedString("itm_new_tstore"));
556                                 if (null == this.tspasswd)
557                                     this.tspasswd = "changeit";
558                             }
559                             FileOutputStream os = new FileOutputStream(
560                                     this.tsname);
561                             this.ts.store(os, this.tspasswd.toCharArray());
562                             os.close();
563                             if (old && (!f.delete()))
564                                 throw new IOException(
565                                         "Could not delete old truststore");
566                             // Must re-initialize TrustManagerFactory
567                             this.tmf.init(this.ts);
568                             System.out.println("Saved cert to " + this.tsname);
569                             break;
570                         } catch (Exception e) {
571                             if (this.tsname.equals(USERTS))
572                                 throw new CertificateException(e);
573                             this.tsname = USERTS;
574                         }
575                     }
576                 }
577         }
578     }
579 
580     /**
581      * @see javax.net.ssl.X509TrustManager#checkServerTrusted(java.security.cert.X509Certificate[],
582      *      java.lang.String)
583      */
584     public void checkServerTrusted(X509Certificate[] chain, String authType)
585             throws CertificateException {
586         if ((this.mode & SERVER) != 0) {
587             if (null == chain || chain.length == 0)
588                 throw new IllegalArgumentException(
589                         "Certificate chain is null or empty");
590 
591             int i;
592             TrustManager[] mgrs = this.tmf.getTrustManagers();
593             for (i = 0; i < mgrs.length; i++) {
594                 if (mgrs[i] instanceof X509TrustManager) {
595                     X509TrustManager m = (X509TrustManager) (mgrs[i]);
596                     try {
597                         m.checkServerTrusted(chain, authType);
598                         return;
599                     } catch (Exception e) {
600                         // try next
601                     }
602                 }
603             }
604 
605             // If we get here, the certificate could not be verified.
606             // Ask the user what to do.
607             CertDialog(chain[0]);
608         }
609         // In dummy mode: Nothing to do.
610     }
611 
612     /**
613      * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers()
614      */
615     public X509Certificate[] getAcceptedIssuers() {
616         System.out.println("getAcceptedIssuers");
617         return new X509Certificate[0];
618     }
619 }