28
28
import java .security .KeyStore ;
29
29
import java .security .KeyStoreException ;
30
30
import java .security .NoSuchAlgorithmException ;
31
+ import java .security .PrivateKey ;
32
+ import java .security .cert .Certificate ;
31
33
import java .security .cert .CertificateException ;
34
+ import java .util .Collections ;
32
35
import java .util .List ;
33
36
import java .util .concurrent .TimeUnit ;
34
37
import java .util .stream .Collectors ;
35
38
36
39
import static java .lang .System .Logger .Level .DEBUG ;
37
40
import static java .lang .System .Logger .Level .ERROR ;
38
41
import static java .lang .System .Logger .Level .INFO ;
42
+ import static java .lang .System .Logger .Level .WARNING ;
39
43
40
44
/**
41
45
* Java adapter to call the keytool command.
@@ -63,18 +67,34 @@ public class KeyTool {
63
67
}
64
68
65
69
private final File keyStore ;
70
+ private final String keyStoreType ;
66
71
private char [] password ;
67
72
68
73
/**
69
74
* Creates a new instance of KeyTool managing the keystore file.
70
75
* The file may not exist yet.
76
+ * The type is detected automatically from the file extension.
71
77
*
72
78
* @param keyStore the file representing the keystore
73
79
* @param password keystore and key password, must have at least 6 characters
74
80
*/
75
81
public KeyTool (File keyStore , char [] password ) {
82
+ this (keyStore , guessKeyStoreType (keyStore ), password );
83
+ }
84
+
85
+
86
+ /**
87
+ * Creates a new instance of KeyTool managing the keystore file.
88
+ * The file may not exist yet.
89
+ *
90
+ * @param keyStore the file representing the keystore
91
+ * @param keyStoreType the type of the keystore, e.g. "PKCS12", "JKS"
92
+ * @param password keystore and key password, must have at least 6 characters
93
+ */
94
+ public KeyTool (File keyStore , String keyStoreType , char [] password ) {
76
95
this .keyStore = keyStore ;
77
96
this .password = password ;
97
+ this .keyStoreType = keyStoreType ;
78
98
}
79
99
80
100
@@ -113,7 +133,7 @@ public void generateKeyPair(String alias, String dn, String keyAlgorithm, int ce
113
133
"-keyalg" , keyAlgorithm ,
114
134
"-validity" , Integer .toString (certValidity ),
115
135
"-keystore" , keyStore .getAbsolutePath (),
116
- "-storetype" , "JKS"
136
+ "-storetype" , keyStoreType
117
137
);
118
138
if (keyStore .getParentFile ().mkdirs ()) {
119
139
// The directory must exist, keytool will not create it
@@ -196,6 +216,7 @@ public void exportCertificate(String alias, final File outputFile) throws IOExce
196
216
197
217
/**
198
218
* Changes the key store password and remembers it.
219
+ * Changes also passwords of all keys in the key store which use the same password.
199
220
*
200
221
* @param newPassword the new key store password
201
222
* @throws IOException
@@ -209,29 +230,61 @@ public void changeKeyStorePassword(char[] newPassword) throws IOException {
209
230
"-keystore" , this .keyStore .getAbsolutePath ()
210
231
);
211
232
execute (command , password , newPassword , newPassword , newPassword );
233
+ final char [] oldPassword = password ;
212
234
this .password = newPassword ;
235
+ if ("PKCS12" .equals (this .keyStoreType )) {
236
+ // PKCS12 key store type changes passwords of keys together with the key store password
237
+ // JKS and JCEKS key store types require changing passwords of keys separately
238
+ return ;
239
+ }
240
+ KeyStore ks = loadKeyStore ();
241
+ final List <String > aliases ;
242
+ try {
243
+ aliases = Collections .list (ks .aliases ());
244
+ } catch (KeyStoreException e ) {
245
+ throw new IOException ("Could not list aliases in keystore: " + keyStore , e );
246
+ }
247
+ for (String alias : aliases ) {
248
+ try {
249
+ if (ks .isKeyEntry (alias )) {
250
+ changeKeyPassword (ks , alias , oldPassword , newPassword );
251
+ }
252
+ } catch (IOException | KeyStoreException e ) {
253
+ LOG .log (WARNING , "Could not change key password for alias: {0}, it may use different password." , alias );
254
+ }
255
+ }
256
+ try (FileOutputStream output = new FileOutputStream (keyStore )) {
257
+ ks .store (output , password );
258
+ } catch (GeneralSecurityException e ) {
259
+ throw new IOException (
260
+ "Keystore password successfuly changed, however failed changing key passwords: " + keyStore , e );
261
+ }
213
262
}
214
263
215
264
216
265
/**
217
266
* Changes the key password
267
+ * <p>
268
+ * WARNING: This is not required for the PKCS12 key store type, as it changes passwords of keys
269
+ * together with the key store password.
218
270
*
219
271
* @param alias the alias of the key whose password should be changed
220
272
* @param oldPassword the current key entry password
221
273
* @param newPassword the new key entry password
222
274
* @throws IOException
223
275
*/
224
276
public void changeKeyPassword (String alias , char [] oldPassword , char [] newPassword ) throws IOException {
225
- List <String > command = List .of (
226
- KEYTOOL ,
227
- "-J-Duser.language=en" ,
228
- "-noprompt" ,
229
- "-keypasswd" ,
230
- "-alias" , alias ,
231
- "-keystore" , this .keyStore .getAbsolutePath ()
232
- );
233
-
234
- execute (command , password , newPassword , newPassword );
277
+ try {
278
+ KeyStore sourceStore = loadKeyStore ();
279
+ Certificate [] chain = sourceStore .getCertificateChain (alias );
280
+ PrivateKey key = (PrivateKey ) sourceStore .getKey (alias , oldPassword );
281
+ sourceStore .setKeyEntry (alias , key , newPassword , chain );
282
+ try (FileOutputStream output = new FileOutputStream (keyStore )) {
283
+ sourceStore .store (output , password );
284
+ }
285
+ } catch (GeneralSecurityException e ) {
286
+ throw new IOException ("Could not change key password for alias: " + alias , e );
287
+ }
235
288
}
236
289
237
290
@@ -240,6 +293,20 @@ private void execute(final List<String> command, char[]... stdinLines) throws IO
240
293
}
241
294
242
295
296
+ /**
297
+ * Creates an empty key store file with the specified password.
298
+ * The type is detected from the file extension.
299
+ *
300
+ * @param file
301
+ * @param password
302
+ * @return KeyTool suitable to manage the newly created key store
303
+ * @throws IOException
304
+ */
305
+ public static KeyTool createEmptyKeyStore (File file , char [] password ) throws IOException {
306
+ return createEmptyKeyStore (file , guessKeyStoreType (file ), password );
307
+ }
308
+
309
+
243
310
/**
244
311
* Creates an empty key store file with the specified type and password.
245
312
*
@@ -268,6 +335,41 @@ public static KeyTool createEmptyKeyStore(File file, String keyStoreType, char[]
268
335
}
269
336
270
337
338
+ private static String guessKeyStoreType (File keyStore ) {
339
+ String filename = keyStore .getName ();
340
+ int lastDot = filename .lastIndexOf ('.' );
341
+ if (lastDot < 0 ) {
342
+ throw new IllegalArgumentException (
343
+ "Key store file name must have an extension to guess the key store type: " + keyStore );
344
+ }
345
+ String suffix = filename .substring (lastDot + 1 ).toUpperCase ();
346
+ switch (suffix ) {
347
+ case "JKS" :
348
+ return "JKS" ;
349
+ case "P12" :
350
+ case "PFX" :
351
+ return "PKCS12" ;
352
+ case "JCEKS" :
353
+ return "JCEKS" ;
354
+ default :
355
+ LOG .log (WARNING , "Unknown key store type for file {0}, using its suffix as a keystore type." , keyStore );
356
+ return suffix ;
357
+ }
358
+ }
359
+
360
+
361
+ private static void changeKeyPassword (KeyStore keyStore , String alias , char [] oldPassword , char [] newPassword )
362
+ throws IOException {
363
+ try {
364
+ Certificate [] chain = keyStore .getCertificateChain (alias );
365
+ PrivateKey key = (PrivateKey ) keyStore .getKey (alias , oldPassword );
366
+ keyStore .setKeyEntry (alias , key , newPassword , chain );
367
+ } catch (GeneralSecurityException e ) {
368
+ throw new IOException ("Could not change key password for alias: " + alias , e );
369
+ }
370
+ }
371
+
372
+
271
373
private static void execute (final File keyStore , final List <String > command , final char []... stdinLines ) throws IOException {
272
374
LOG .log (INFO , () -> "Executing command: " + command .stream ().collect (Collectors .joining (" " )));
273
375
final ProcessBuilder builder = new ProcessBuilder (command ).directory (keyStore .getParentFile ());
0 commit comments