View Javadoc
1   /*
2    * DavMail POP/IMAP/SMTP/CalDav/LDAP Exchange Gateway
3    * Copyright (C) 2011  Mickael Guessant
4    *
5    * This program is free software; you can redistribute it and/or
6    * modify it under the terms of the GNU General Public License
7    * as published by the Free Software Foundation; either version 2
8    * of the License, or (at your option) any later version.
9    *
10   * This program is distributed in the hope that it will be useful,
11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   * GNU General Public License for more details.
14   *
15   * You should have received a copy of the GNU General Public License
16   * along with this program; if not, write to the Free Software
17   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
18   */
19  package davmail.http;
20  
21  import davmail.Settings;
22  import davmail.ui.SelectCertificateDialog;
23  import org.apache.log4j.Logger;
24  
25  import javax.net.ssl.X509KeyManager;
26  import java.awt.*;
27  import java.io.BufferedReader;
28  import java.io.IOException;
29  import java.io.InputStreamReader;
30  import java.net.Socket;
31  import java.security.Principal;
32  import java.security.PrivateKey;
33  import java.security.cert.X509Certificate;
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.List;
37  
38  /**
39   * Special X509 Key Manager that handles cases where more than one private key
40   * is sufficient to establish the HTTPs connection by asking the user to
41   * select one.
42   */
43  public class DavMailX509KeyManager implements X509KeyManager {
44  
45      protected static final Logger LOGGER = Logger.getLogger(DavMailX509KeyManager.class);
46  
47      // Wrap an existing key manager to handle most of the interface as a pass through
48      private final X509KeyManager keyManager;
49  
50      // Remember selected alias so we don't continually bug the user
51      private String cachedAlias;
52  
53      /**
54       * Build the specialized key manager wrapping the default one
55       *
56       * @param keyManager original key manager
57       */
58      public DavMailX509KeyManager(X509KeyManager keyManager) {
59          this.keyManager = keyManager;
60      }
61  
62      /**
63       * Get the client aliases, simply pass this through to wrapped key manager
64       */
65      public String[] getClientAliases(String string, Principal[] principals) {
66          return keyManager.getClientAliases(string, principals);
67      }
68  
69      /**
70       * Select a client alias. Some servers are misconfigured and claim to accept
71       * any client certificate during the SSL handshake, however OWA only authenticates
72       * using a single certificate.
73       * <p/>
74       * This method allows the user to select the right client certificate
75       */
76      public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
77          LOGGER.debug("Find client certificates issued by: " + Arrays.asList(issuers));
78          // Build a list of all aliases
79          ArrayList<String> aliases = new ArrayList<>();
80          for (String keyTypeValue : keyType) {
81              String[] keyAliases = keyManager.getClientAliases(keyTypeValue, issuers);
82  
83              if (keyAliases != null) {
84                  aliases.addAll(Arrays.asList(keyAliases));
85              }
86          }
87  
88          // If there are more than one show a dialog and return the selected alias
89          if (aliases.size() > 1) {
90  
91              //If there's a saved pattern try to match it
92              if (cachedAlias != null) {
93                  for (String alias : aliases) {
94                      if (cachedAlias.equals(stripAlias(alias))) {
95                          LOGGER.debug(alias + " matched cached alias: " + cachedAlias);
96                          return alias;
97                      }
98                  }
99  
100                 // pattern didn't match, clear the pattern and ask user to select an alias
101                 cachedAlias = null;
102             }
103 
104             String[] aliasesArray = aliases.toArray(new String[0]);
105             String[] descriptionsArray = new String[aliasesArray.length];
106             int i = 0;
107             for (String alias : aliasesArray) {
108                 X509Certificate certificate = getCertificateChain(alias)[0];
109                 String subject = certificate.getSubjectX500Principal().getName();
110                 if (subject.contains("=")) {
111                     subject = subject.substring(subject.indexOf("=")+1);
112                 }
113                 if (subject.contains(",")) {
114                     subject = subject.substring(0, subject.indexOf(","));
115                 }
116                 try {
117                     for (List<?> subjectAltName:certificate.getSubjectAlternativeNames()) {
118                         if (subjectAltName.get(1) instanceof String) {
119                             subject = " " + subjectAltName.get(1);
120                         }
121                     }
122                 } catch (Exception e) {
123                     // ignore
124                 }
125                 String issuer = certificate.getIssuerX500Principal().getName();
126                 if (issuer.contains("=")) {
127                     issuer = issuer.substring(issuer.indexOf("=")+1);
128                 }
129                 if (issuer.contains(",")) {
130                     issuer = issuer.substring(0, issuer.indexOf(","));
131                 }
132                 descriptionsArray[i++] = subject + " [" + issuer + "]";
133             }
134             String selectedAlias;
135             if (Settings.getBooleanProperty("davmail.server") || GraphicsEnvironment.isHeadless()) {
136                 // headless or server mode
137                 selectedAlias = chooseClientAlias(aliasesArray, descriptionsArray);
138             } else {
139                 SelectCertificateDialog selectCertificateDialog = new SelectCertificateDialog(aliasesArray, descriptionsArray);
140 
141                 selectedAlias = selectCertificateDialog.getSelectedAlias();
142                 LOGGER.debug("User selected Key Alias: " + selectedAlias);
143             }
144 
145             cachedAlias = stripAlias(selectedAlias);
146             LOGGER.debug("Stored Key Alias Pattern: " + cachedAlias);
147 
148             return selectedAlias;
149 
150             // exactly one, simply return that and don't bother the user
151         } else if (aliases.size() == 1) {
152             LOGGER.debug("One Private Key found, returning that");
153             return aliases.get(0);
154 
155             // none, return null
156         } else {
157             LOGGER.debug("No Private Keys found");
158             return null;
159         }
160     }
161 
162     private String chooseClientAlias(String[] aliasesArray, String[] descriptionsArray) {
163         System.out.println("Choose client alias:");
164         int i = 1;
165         for (String aliasDescription:descriptionsArray) {
166             System.out.println(i+++": "+aliasDescription);
167         }
168         BufferedReader inReader = new BufferedReader(new InputStreamReader(System.in));
169         int chosenIndex = 0;
170         while (chosenIndex == 0 || chosenIndex > descriptionsArray.length) {
171             try {
172                 System.out.print("Alias: ");
173                 chosenIndex = Integer.parseInt(inReader.readLine());
174             } catch (NumberFormatException | IOException e) {
175                 System.out.println("Invalid");
176             }
177         }
178 
179         return aliasesArray[chosenIndex - 1];
180     }
181 
182     /**
183      * PKCS11 aliases are in the format: dd.0, dd is incremented
184      * every time the SSL connection is re-negotiated
185      *
186      * @param alias original alias
187      * @return alias without prefix
188      */
189     protected String stripAlias(String alias) {
190         String value = alias;
191         if (value != null && value.length() > 1) {
192             char firstChar = value.charAt(0);
193             int dotIndex = value.indexOf('.'); 
194             if (firstChar >= '0' && firstChar <= '9' && dotIndex >= 0) {
195                 value = value.substring(dotIndex+1);
196             }
197         }
198         return value;
199     }
200 
201     /**
202      * Passthrough to wrapped keymanager
203      */
204     public String[] getServerAliases(String string, Principal[] prncpls) {
205         return keyManager.getServerAliases(string, prncpls);
206     }
207 
208     /**
209      * Passthrough to wrapped keymanager
210      */
211     public String chooseServerAlias(String string, Principal[] prncpls, Socket socket) {
212         return keyManager.chooseServerAlias(string, prncpls, socket);
213     }
214 
215     /**
216      * Passthrough to wrapped keymanager
217      */
218     public X509Certificate[] getCertificateChain(String string) {
219         X509Certificate[] certificates = keyManager.getCertificateChain(string);
220         for (X509Certificate certificate: certificates) {
221             LOGGER.debug("Certificate chain: " + certificate.getSubjectX500Principal());
222         }
223         return certificates;
224     }
225 
226     /**
227      * Passthrough to wrapped keymanager
228      */
229     public PrivateKey getPrivateKey(String string) {
230         return keyManager.getPrivateKey(string);
231     }
232 }