23
23
import javax .crypto .SecretKeyFactory ;
24
24
import javax .crypto .spec .PBEKeySpec ;
25
25
import javax .security .auth .DestroyFailedException ;
26
+ import java .io .ByteArrayInputStream ;
27
+ import java .io .ByteArrayOutputStream ;
26
28
import java .io .Closeable ;
27
- import java .io .DataInputStream ;
28
- import java .io .DataOutputStream ;
29
29
import java .io .IOException ;
30
30
import java .io .InputStream ;
31
+ import java .nio .CharBuffer ;
32
+ import java .nio .charset .CharsetEncoder ;
33
+ import java .nio .charset .StandardCharsets ;
31
34
import java .nio .file .Files ;
32
35
import java .nio .file .Path ;
36
+ import java .nio .file .StandardCopyOption ;
33
37
import java .nio .file .attribute .PosixFileAttributeView ;
34
38
import java .nio .file .attribute .PosixFilePermissions ;
35
39
import java .security .GeneralSecurityException ;
39
43
import java .util .Arrays ;
40
44
import java .util .Enumeration ;
41
45
import java .util .HashSet ;
46
+ import java .util .Locale ;
42
47
import java .util .Set ;
43
48
49
+ import org .apache .lucene .codecs .CodecUtil ;
50
+ import org .apache .lucene .store .BufferedChecksumIndexInput ;
51
+ import org .apache .lucene .store .ChecksumIndexInput ;
52
+ import org .apache .lucene .store .IOContext ;
53
+ import org .apache .lucene .store .IndexInput ;
54
+ import org .apache .lucene .store .IndexOutput ;
55
+ import org .apache .lucene .store .NIOFSDirectory ;
44
56
import org .apache .lucene .util .SetOnce ;
45
57
46
58
/**
47
59
* A wrapper around a Java KeyStore which provides supplements the keystore with extra metadata.
48
60
*
49
- * Loading a keystore has 2 phases. First, call {@link #loadMetadata(Path)}. Then call
50
- * {@link #loadKeystore(char[])} with the keystore password, or an empty char array if
51
- * {@link #hasPassword()} is {@code false}.
61
+ * Loading a keystore has 2 phases. First, call {@link #load(Path)}. Then call
62
+ * {@link #decrypt(char[])} with the keystore password, or an empty char array if
63
+ * {@link #hasPassword()} is {@code false}. Loading and decrypting should happen
64
+ * in a single thread. Once decrypted, keys may be read with the wrapper in
65
+ * multiple threads.
52
66
*/
53
67
public class KeyStoreWrapper implements Closeable {
54
68
69
+ /** The name of the keystore file to read and write. */
70
+ private static final String KEYSTORE_FILENAME = "elasticsearch.keystore" ;
71
+
55
72
/** The version of the metadata written before the keystore data. */
56
73
private static final int FORMAT_VERSION = 1 ;
57
74
58
75
/** The keystore type for a newly created keystore. */
59
76
private static final String NEW_KEYSTORE_TYPE = "PKCS12" ;
60
77
61
78
/** The algorithm used to store password for a newly created keystore. */
62
- private static final String NEW_KEYSTORE_SECRET_KEY_ALGO = "PBEWithHmacSHA256AndAES_128" ;
79
+ private static final String NEW_KEYSTORE_SECRET_KEY_ALGO = "PBE" ;//"PBEWithHmacSHA256AndAES_128";
80
+
81
+ /** An encoder to check whether string values are ascii. */
82
+ private static final CharsetEncoder ASCII_ENCODER = StandardCharsets .US_ASCII .newEncoder ();
63
83
64
84
/** True iff the keystore has a password needed to read. */
65
85
private final boolean hasPassword ;
@@ -70,32 +90,32 @@ public class KeyStoreWrapper implements Closeable {
70
90
/** A factory necessary for constructing instances of secrets in a {@link KeyStore}. */
71
91
private final SecretKeyFactory secretFactory ;
72
92
73
- /** A stream of the actual keystore data . */
74
- private final InputStream input ;
93
+ /** The raw bytes of the encrypted keystore. */
94
+ private final byte [] keystoreBytes ;
75
95
76
- /** The loaded keystore. See {@link #loadKeystore (char[])}. */
96
+ /** The loaded keystore. See {@link #decrypt (char[])}. */
77
97
private final SetOnce <KeyStore > keystore = new SetOnce <>();
78
98
79
- /** The password for the keystore. See {@link #loadKeystore (char[])}. */
99
+ /** The password for the keystore. See {@link #decrypt (char[])}. */
80
100
private final SetOnce <KeyStore .PasswordProtection > keystorePassword = new SetOnce <>();
81
101
82
102
/** The setting names contained in the loaded keystore. */
83
103
private final Set <String > settingNames = new HashSet <>();
84
104
85
- private KeyStoreWrapper (boolean hasPassword , String type , String secretKeyAlgo , InputStream input ) {
105
+ private KeyStoreWrapper (boolean hasPassword , String type , String secretKeyAlgo , byte [] keystoreBytes ) {
86
106
this .hasPassword = hasPassword ;
87
107
this .type = type ;
88
108
try {
89
109
secretFactory = SecretKeyFactory .getInstance (secretKeyAlgo );
90
110
} catch (NoSuchAlgorithmException e ) {
91
111
throw new RuntimeException (e );
92
112
}
93
- this .input = input ;
113
+ this .keystoreBytes = keystoreBytes ;
94
114
}
95
115
96
116
/** Returns a path representing the ES keystore in the given config dir. */
97
117
static Path keystorePath (Path configDir ) {
98
- return configDir .resolve ("elasticsearch.keystore" );
118
+ return configDir .resolve (KEYSTORE_FILENAME );
99
119
}
100
120
101
121
/** Constructs a new keystore with the given password. */
@@ -111,43 +131,61 @@ static KeyStoreWrapper create(char[] password) throws Exception {
111
131
/**
112
132
* Loads information about the Elasticsearch keystore from the provided config directory.
113
133
*
114
- * {@link #loadKeystore (char[])} must be called before reading or writing any entries.
134
+ * {@link #decrypt (char[])} must be called before reading or writing any entries.
115
135
* Returns {@code null} if no keystore exists.
116
136
*/
117
- public static KeyStoreWrapper loadMetadata (Path configDir ) throws IOException {
137
+ public static KeyStoreWrapper load (Path configDir ) throws IOException {
118
138
Path keystoreFile = keystorePath (configDir );
119
139
if (Files .exists (keystoreFile ) == false ) {
120
140
return null ;
121
141
}
122
- DataInputStream inputStream = new DataInputStream (Files .newInputStream (keystoreFile ));
123
- int format = inputStream .readInt ();
124
- if (format != FORMAT_VERSION ) {
125
- throw new IllegalStateException ("Unknown keystore metadata format [" + format + "]" );
142
+
143
+ NIOFSDirectory directory = new NIOFSDirectory (configDir );
144
+ try (IndexInput indexInput = directory .openInput (KEYSTORE_FILENAME , IOContext .READONCE )) {
145
+ ChecksumIndexInput input = new BufferedChecksumIndexInput (indexInput );
146
+ CodecUtil .checkHeader (input , KEYSTORE_FILENAME , FORMAT_VERSION , FORMAT_VERSION );
147
+ byte hasPasswordByte = input .readByte ();
148
+ boolean hasPassword = hasPasswordByte == 1 ;
149
+ if (hasPassword == false && hasPasswordByte != 0 ) {
150
+ throw new IllegalStateException ("hasPassword boolean is corrupt: "
151
+ + String .format (Locale .ROOT , "%02x" , hasPasswordByte ));
152
+ }
153
+ String type = input .readString ();
154
+ String secretKeyAlgo = input .readString ();
155
+ byte [] keystoreBytes = new byte [input .readInt ()];
156
+ input .readBytes (keystoreBytes , 0 , keystoreBytes .length );
157
+ CodecUtil .checkFooter (input );
158
+ return new KeyStoreWrapper (hasPassword , type , secretKeyAlgo , keystoreBytes );
126
159
}
127
- boolean hasPassword = inputStream .readBoolean ();
128
- String type = inputStream .readUTF ();
129
- String secretKeyAlgo = inputStream .readUTF ();
130
- return new KeyStoreWrapper (hasPassword , type , secretKeyAlgo , inputStream );
131
160
}
132
161
133
- /** Returns true iff {@link #loadKeystore (char[])} has been called. */
162
+ /** Returns true iff {@link #decrypt (char[])} has been called. */
134
163
public boolean isLoaded () {
135
164
return keystore .get () != null ;
136
165
}
137
166
138
- /** Return true iff calling {@link #loadKeystore (char[])} requires a non-empty password. */
167
+ /** Return true iff calling {@link #decrypt (char[])} requires a non-empty password. */
139
168
public boolean hasPassword () {
140
169
return hasPassword ;
141
170
}
142
171
143
- /** Loads the keystore this metadata wraps. This may only be called once. */
144
- public void loadKeystore (char [] password ) throws GeneralSecurityException , IOException {
145
- this .keystore .set (KeyStore .getInstance (type ));
146
- try (InputStream in = input ) {
172
+ /**
173
+ * Decrypts the underlying java keystore.
174
+ *
175
+ * This may only be called once. The provided password will be zeroed out.
176
+ */
177
+ public void decrypt (char [] password ) throws GeneralSecurityException , IOException {
178
+ if (keystore .get () != null ) {
179
+ throw new IllegalStateException ("Keystore has already been decrypted" );
180
+ }
181
+ keystore .set (KeyStore .getInstance (type ));
182
+ try (InputStream in = new ByteArrayInputStream (keystoreBytes )) {
147
183
keystore .get ().load (in , password );
184
+ } finally {
185
+ Arrays .fill (keystoreBytes , (byte )0 );
148
186
}
149
187
150
- this . keystorePassword .set (new KeyStore .PasswordProtection (password ));
188
+ keystorePassword .set (new KeyStore .PasswordProtection (password ));
151
189
Arrays .fill (password , '\0' );
152
190
153
191
// convert keystore aliases enum into a set for easy lookup
@@ -159,15 +197,27 @@ public void loadKeystore(char[] password) throws GeneralSecurityException, IOExc
159
197
160
198
/** Write the keystore to the given config directory. */
161
199
void save (Path configDir ) throws Exception {
162
- Path keystoreFile = keystorePath (configDir );
163
- try (DataOutputStream outputStream = new DataOutputStream (Files .newOutputStream (keystoreFile ))) {
164
- outputStream .writeInt (FORMAT_VERSION );
165
- char [] password = this .keystorePassword .get ().getPassword ();
166
- outputStream .writeBoolean (password .length != 0 );
167
- outputStream .writeUTF (type );
168
- outputStream .writeUTF (secretFactory .getAlgorithm ());
169
- keystore .get ().store (outputStream , password );
200
+ char [] password = this .keystorePassword .get ().getPassword ();
201
+
202
+ NIOFSDirectory directory = new NIOFSDirectory (configDir );
203
+ // write to tmp file first, then overwrite
204
+ String tmpFile = KEYSTORE_FILENAME + ".tmp" ;
205
+ try (IndexOutput output = directory .createOutput (tmpFile , IOContext .DEFAULT )) {
206
+ CodecUtil .writeHeader (output , KEYSTORE_FILENAME , FORMAT_VERSION );
207
+ output .writeByte (password .length == 0 ? (byte )0 : (byte )1 );
208
+ output .writeString (type );
209
+ output .writeString (secretFactory .getAlgorithm ());
210
+
211
+ ByteArrayOutputStream keystoreBytesStream = new ByteArrayOutputStream ();
212
+ keystore .get ().store (keystoreBytesStream , password );
213
+ byte [] keystoreBytes = keystoreBytesStream .toByteArray ();
214
+ output .writeInt (keystoreBytes .length );
215
+ output .writeBytes (keystoreBytes , keystoreBytes .length );
216
+ CodecUtil .writeFooter (output );
170
217
}
218
+
219
+ Path keystoreFile = keystorePath (configDir );
220
+ Files .move (configDir .resolve (tmpFile ), keystoreFile , StandardCopyOption .REPLACE_EXISTING );
171
221
PosixFileAttributeView attrs = Files .getFileAttributeView (keystoreFile , PosixFileAttributeView .class );
172
222
if (attrs != null ) {
173
223
// don't rely on umask: ensure the keystore has minimal permissions
@@ -194,8 +244,15 @@ SecureString getStringSetting(String setting) throws GeneralSecurityException {
194
244
return value ;
195
245
}
196
246
197
- /** Set a string setting. */
247
+ /**
248
+ * Set a string setting.
249
+ *
250
+ * @throws IllegalArgumentException if the value is not ASCII
251
+ */
198
252
void setStringSetting (String setting , char [] value ) throws GeneralSecurityException {
253
+ if (ASCII_ENCODER .canEncode (CharBuffer .wrap (value )) == false ) {
254
+ throw new IllegalArgumentException ("Value must be ascii" );
255
+ }
199
256
SecretKey secretKey = secretFactory .generateSecret (new PBEKeySpec (value ));
200
257
keystore .get ().setEntry (setting , new KeyStore .SecretKeyEntry (secretKey ), keystorePassword .get ());
201
258
settingNames .add (setting );
0 commit comments