View Javadoc
1   /* 
2    * Copyright (C) 2016 Hobrasoft s.r.o.
3    *
4    * This program is free software: you can redistribute it and/or modify
5    * it under the terms of the GNU Affero General Public License as published by
6    * the Free Software Foundation, either version 3 of the License, or
7    * (at your option) any later version.
8    *
9    * This program is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12   * GNU Affero General Public License for more details.
13   *
14   * You should have received a copy of the GNU Affero General Public License
15   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16   */
17  package cz.hobrasoft.pdfmu.operation.signature;
18  
19  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_ALIASES;
20  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_ALIAS_EXCEPTION;
21  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_ALIAS_KEY_EXCEPTION;
22  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_ALIAS_MISSING;
23  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_ALIAS_NOT_KEY;
24  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_CERTIFICATE_CHAIN;
25  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_EMPTY;
26  import static cz.hobrasoft.pdfmu.error.ErrorType.SIGNATURE_ADD_KEYSTORE_PRIVATE_KEY;
27  import cz.hobrasoft.pdfmu.operation.OperationException;
28  import cz.hobrasoft.pdfmu.operation.args.ArgsConfiguration;
29  import cz.hobrasoft.pdfmu.operation.args.PasswordArgs;
30  import java.nio.charset.StandardCharsets;
31  import java.security.KeyStore;
32  import java.security.KeyStoreException;
33  import java.security.NoSuchAlgorithmException;
34  import java.security.PrivateKey;
35  import java.security.UnrecoverableKeyException;
36  import java.security.cert.Certificate;
37  import java.util.AbstractMap.SimpleEntry;
38  import java.util.Enumeration;
39  import java.util.logging.Logger;
40  import net.sourceforge.argparse4j.inf.Argument;
41  import net.sourceforge.argparse4j.inf.ArgumentParser;
42  import net.sourceforge.argparse4j.inf.Namespace;
43  
44  /**
45   *
46   * @author <a href="mailto:filip.bartek@hobrasoft.cz">Filip Bartek</a>
47   */
48  class KeyParameters implements ArgsConfiguration {
49  
50      public String alias = null;
51      public char[] password = null;
52  
53      // TODO: Replace with Console
54      private static final Logger logger = Logger.getLogger(KeyParameters.class.getName());
55  
56      public PasswordArgs passwordArgs = new PasswordArgs("key password");
57  
58      private Argument keyAliasArgument;
59  
60      @Override
61      public void addArguments(ArgumentParser parser) {
62          keyAliasArgument = parser.addArgument("--key-alias")
63                  .help("key keystore entry alias (default: <first entry in the keystore>)")
64                  .type(String.class);
65  
66          passwordArgs.passwordArgument = parser.addArgument("--key-password")
67                  .help("key password (default: <empty>)");
68          passwordArgs.environmentVariableArgument = parser.addArgument("--key-password-envvar")
69                  .help("key password environment variable")
70                  .setDefault("PDFMU_KEYPASS");
71          passwordArgs.finalizeArguments();
72      }
73  
74      @Override
75      public void setFromNamespace(Namespace namespace) {
76          assert keyAliasArgument != null;
77          alias = namespace.getString(keyAliasArgument.getDest());
78  
79          // Set password
80          assert passwordArgs != null;
81          passwordArgs.setFromNamespace(namespace);
82          password = passwordArgs.getPasswordCharArray();
83          // TODO?: Use keystore password by default
84      }
85  
86      /**
87       * Encodes the alias using ISO-8859-1 and replaces it with the longest
88       * prefix of itself that is an actual alias in the keystore. The shortening
89       * is a necessary magic necessary for strings with some non-ASCII
90       * characters.
91       */
92      private void fixAliasWcs(KeyStore ks) throws OperationException {
93          assert "Windows-MY".equals(ks.getType());
94          if (alias != null) {
95              logger.info(String.format("WCS alias correction will be applied. Original alias: %s", alias));
96              alias = new String(alias.getBytes(), StandardCharsets.ISO_8859_1);
97  
98              Enumeration<String> aliases = null;
99              try {
100                 aliases = ks.aliases();
101             } catch (KeyStoreException ex) {
102                 throw new OperationException(SIGNATURE_ADD_KEYSTORE_ALIASES, ex);
103             }
104             String aliasBest = null;
105             while (aliases.hasMoreElements()) {
106                 String aliasCandidate = aliases.nextElement();
107                 if (alias.startsWith(aliasCandidate)
108                         && (aliasBest == null
109                         || aliasCandidate.length() > aliasBest.length())) {
110                     aliasBest = aliasCandidate;
111                 }
112             }
113             if (aliasBest != null) {
114                 alias = aliasBest;
115             }
116         }
117     }
118 
119     public void fixAlias(KeyStore ks) throws OperationException {
120         if (alias == null) {
121             // Get the first alias in the keystore
122             logger.info("Keystore entry alias not set. Using the first entry in the keystore.");
123             Enumeration<String> aliases;
124             try {
125                 aliases = ks.aliases();
126             } catch (KeyStoreException ex) {
127                 throw new OperationException(SIGNATURE_ADD_KEYSTORE_ALIASES, ex);
128             }
129             if (!aliases.hasMoreElements()) {
130                 throw new OperationException(SIGNATURE_ADD_KEYSTORE_EMPTY);
131             }
132             alias = aliases.nextElement();
133             assert alias != null;
134         } else if ("Windows-MY".equals(ks.getType())) {
135             fixAliasWcs(ks);
136         }
137         logger.info(String.format("Keystore entry alias: %s", alias));
138 
139         // Make sure the entry `alias` is present in the keystore
140         try {
141             if (!ks.containsAlias(alias)) {
142                 throw new OperationException(SIGNATURE_ADD_KEYSTORE_ALIAS_MISSING,
143                         new SimpleEntry<String, Object>("alias", alias));
144             }
145         } catch (KeyStoreException ex) {
146             throw new OperationException(SIGNATURE_ADD_KEYSTORE_ALIAS_EXCEPTION, ex,
147                     new SimpleEntry<String, Object>("alias", alias));
148         }
149 
150         // Make sure `alias` is a key entry
151         try {
152             if (!ks.isKeyEntry(alias)) {
153                 throw new OperationException(SIGNATURE_ADD_KEYSTORE_ALIAS_NOT_KEY,
154                         new SimpleEntry<String, Object>("alias", alias));
155             }
156         } catch (KeyStoreException ex) {
157             throw new OperationException(SIGNATURE_ADD_KEYSTORE_ALIAS_KEY_EXCEPTION, ex,
158                     new SimpleEntry<String, Object>("alias", alias));
159         }
160     }
161 
162     public void fixPassword(KeyStore ks, String ksPassword) {
163         switch (ks.getType()) {
164             case "Windows-MY":
165                 if (password != null) {
166                     logger.info("The keystore Windows-MY does not accept key password.");
167                     password = null;
168                 }
169                 break;
170             case "pkcs12":
171                 if (ksPassword != null) {
172                     if (password != null && password != ksPassword.toCharArray()) {
173                         logger.warning("In PKCS12 keystores, key password should not differ from the keystore password.");
174                     }
175                     if (password == null) {
176                         logger.info("Key password not set. Using the keystore password.");
177                         password = ksPassword.toCharArray();
178                     }
179                 } else if (password == null) {
180                     logger.info("Key password not set. Using an empty password.");
181                     password = "".toCharArray();
182                 }
183                 break;
184             default:
185                 // Set key password to empty string if not set from command line
186                 if (password == null) {
187                     logger.info("Key password not set. Using an empty password.");
188                     password = "".toCharArray();
189                 }
190         }
191     }
192 
193     public void fix(KeyStore ks, String ksPassword) throws OperationException {
194         fixAlias(ks);
195         fixPassword(ks, ksPassword);
196     }
197 
198     private int countAliasOccurrences(KeyStore ks, String alias) throws KeyStoreException {
199         if (!ks.containsAlias(alias)) {
200             return 0;
201         }
202         Enumeration<String> aliases = ks.aliases();
203         int count = 0;
204         while (aliases.hasMoreElements()) {
205             if (alias.equals(aliases.nextElement())) {
206                 ++count;
207             }
208         }
209         assert (count > 0);
210         return count;
211     }
212 
213     private boolean isAliasDuplicit(KeyStore ks, String alias) throws KeyStoreException {
214         return countAliasOccurrences(ks, alias) > 1;
215     }
216 
217     public PrivateKey getPrivateKey(KeyStore ks) throws OperationException {
218         // Get private key from keystore
219         PrivateKey pk;
220         try {
221             if (isAliasDuplicit(ks, alias)) {
222                 logger.warning(String.format("The key alias \"%1$s\" occurs multiple times in the keystore.", alias));
223             }
224 
225             pk = (PrivateKey) ks.getKey(alias, password);
226         } catch (KeyStoreException ex) {
227             throw new OperationException(SIGNATURE_ADD_KEYSTORE_PRIVATE_KEY, ex,
228                     new SimpleEntry<String, Object>("alias", alias));
229         } catch (NoSuchAlgorithmException ex) {
230             throw new OperationException(SIGNATURE_ADD_KEYSTORE_PRIVATE_KEY, ex,
231                     new SimpleEntry<String, Object>("alias", alias));
232         } catch (UnrecoverableKeyException ex) {
233             // Incorrect key password? Incorrect keystore type?
234             throw new OperationException(SIGNATURE_ADD_KEYSTORE_PRIVATE_KEY, ex,
235                     new SimpleEntry<String, Object>("alias", alias));
236         }
237         if (pk == null) {
238             // Incorrect alias?
239             throw new OperationException(SIGNATURE_ADD_KEYSTORE_PRIVATE_KEY,
240                     new SimpleEntry<String, Object>("alias", alias));
241         }
242         return pk;
243     }
244 
245     public Certificate[] getCertificateChain(KeyStore ks) throws OperationException {
246         Certificate[] chain;
247         try {
248             chain = ks.getCertificateChain(alias);
249         } catch (KeyStoreException ex) {
250             throw new OperationException(SIGNATURE_ADD_KEYSTORE_CERTIFICATE_CHAIN, ex,
251                     new SimpleEntry<String, Object>("alias", alias));
252         }
253         return chain;
254     }
255 
256 }