Skip to content

Commit 22749e3

Browse files
authored
Use readFully() to read bytes from CipherInputStream (#30640)
Changes how data is read from CipherInputStream in KeyStoreWrapper. Instead of using `read()` and checking that the bytes read are what we expect, use `readFully()` which will read exactly the number of bytes while keep reading until the end of the stream or throw an `EOFException` if not all bytes can be read. This approach keeps the simplicity of using CipherInputStream while working as expected with both JCE and BCFIPS Security Providers. See also: #28515
1 parent 4207078 commit 22749e3

File tree

2 files changed

+164
-18
lines changed

2 files changed

+164
-18
lines changed

server/src/main/java/org/elasticsearch/common/settings/KeyStoreWrapper.java

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.io.ByteArrayOutputStream;
3232
import java.io.DataInputStream;
3333
import java.io.DataOutputStream;
34+
import java.io.EOFException;
3435
import java.io.IOException;
3536
import java.io.InputStream;
3637
import java.nio.ByteBuffer;
@@ -318,38 +319,39 @@ public void decrypt(char[] password) throws GeneralSecurityException, IOExceptio
318319
DataInputStream input = new DataInputStream(bytesStream)) {
319320
int saltLen = input.readInt();
320321
salt = new byte[saltLen];
321-
if (input.read(salt) != saltLen) {
322-
throw new SecurityException("Keystore has been corrupted or tampered with");
323-
}
322+
input.readFully(salt);
324323
int ivLen = input.readInt();
325324
iv = new byte[ivLen];
326-
if (input.read(iv) != ivLen) {
327-
throw new SecurityException("Keystore has been corrupted or tampered with");
328-
}
325+
input.readFully(iv);
329326
int encryptedLen = input.readInt();
330327
encryptedBytes = new byte[encryptedLen];
331-
if (input.read(encryptedBytes) != encryptedLen) {
328+
input.readFully(encryptedBytes);
329+
if (input.read() != -1) {
332330
throw new SecurityException("Keystore has been corrupted or tampered with");
333331
}
332+
} catch (EOFException e) {
333+
throw new SecurityException("Keystore has been corrupted or tampered with", e);
334334
}
335335

336336
Cipher cipher = createCipher(Cipher.DECRYPT_MODE, password, salt, iv);
337337
try (ByteArrayInputStream bytesStream = new ByteArrayInputStream(encryptedBytes);
338338
CipherInputStream cipherStream = new CipherInputStream(bytesStream, cipher);
339339
DataInputStream input = new DataInputStream(cipherStream)) {
340-
341340
entries.set(new HashMap<>());
342341
int numEntries = input.readInt();
343342
while (numEntries-- > 0) {
344343
String setting = input.readUTF();
345344
EntryType entryType = EntryType.valueOf(input.readUTF());
346345
int entrySize = input.readInt();
347346
byte[] entryBytes = new byte[entrySize];
348-
if (input.read(entryBytes) != entrySize) {
349-
throw new SecurityException("Keystore has been corrupted or tampered with");
350-
}
347+
input.readFully(entryBytes);
351348
entries.get().put(setting, new Entry(entryType, entryBytes));
352349
}
350+
if (input.read() != -1) {
351+
throw new SecurityException("Keystore has been corrupted or tampered with");
352+
}
353+
} catch (EOFException e) {
354+
throw new SecurityException("Keystore has been corrupted or tampered with", e);
353355
}
354356
}
355357

@@ -361,7 +363,6 @@ private byte[] encrypt(char[] password, byte[] salt, byte[] iv) throws GeneralSe
361363
Cipher cipher = createCipher(Cipher.ENCRYPT_MODE, password, salt, iv);
362364
try (CipherOutputStream cipherStream = new CipherOutputStream(bytes, cipher);
363365
DataOutputStream output = new DataOutputStream(cipherStream)) {
364-
365366
output.writeInt(entries.get().size());
366367
for (Map.Entry<String, Entry> mapEntry : entries.get().entrySet()) {
367368
output.writeUTF(mapEntry.getKey());
@@ -371,7 +372,6 @@ private byte[] encrypt(char[] password, byte[] salt, byte[] iv) throws GeneralSe
371372
output.write(entry.bytes);
372373
}
373374
}
374-
375375
return bytes.toByteArray();
376376
}
377377

server/src/test/java/org/elasticsearch/common/settings/KeyStoreWrapperTests.java

Lines changed: 151 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,30 +19,33 @@
1919

2020
package org.elasticsearch.common.settings;
2121

22+
import javax.crypto.Cipher;
23+
import javax.crypto.CipherOutputStream;
2224
import javax.crypto.SecretKey;
2325
import javax.crypto.SecretKeyFactory;
26+
import javax.crypto.spec.GCMParameterSpec;
2427
import javax.crypto.spec.PBEKeySpec;
28+
import javax.crypto.spec.SecretKeySpec;
2529
import java.io.ByteArrayOutputStream;
30+
import java.io.DataOutputStream;
31+
import java.io.EOFException;
2632
import java.io.IOException;
2733
import java.io.InputStream;
28-
import java.nio.CharBuffer;
29-
import java.nio.charset.CharsetEncoder;
3034
import java.nio.charset.StandardCharsets;
3135
import java.nio.file.FileSystem;
3236
import java.nio.file.Path;
3337
import java.security.KeyStore;
38+
import java.security.SecureRandom;
3439
import java.util.ArrayList;
3540
import java.util.Base64;
3641
import java.util.List;
37-
import java.util.Map;
38-
import java.util.stream.Collectors;
3942

4043
import org.apache.lucene.codecs.CodecUtil;
4144
import org.apache.lucene.store.IOContext;
4245
import org.apache.lucene.store.IndexOutput;
4346
import org.apache.lucene.store.SimpleFSDirectory;
47+
import org.elasticsearch.common.Randomness;
4448
import org.elasticsearch.core.internal.io.IOUtils;
45-
import org.elasticsearch.bootstrap.BootstrapSettings;
4649
import org.elasticsearch.env.Environment;
4750
import org.elasticsearch.test.ESTestCase;
4851
import org.hamcrest.Matchers;
@@ -121,6 +124,149 @@ public void testUpgradeNoop() throws Exception {
121124
assertEquals(seed.toString(), keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey()).toString());
122125
}
123126

127+
public void testFailWhenCannotConsumeSecretStream() throws Exception {
128+
Path configDir = env.configFile();
129+
SimpleFSDirectory directory = new SimpleFSDirectory(configDir);
130+
try (IndexOutput indexOutput = directory.createOutput("elasticsearch.keystore", IOContext.DEFAULT)) {
131+
CodecUtil.writeHeader(indexOutput, "elasticsearch.keystore", 3);
132+
indexOutput.writeByte((byte) 0); // No password
133+
SecureRandom random = Randomness.createSecure();
134+
byte[] salt = new byte[64];
135+
random.nextBytes(salt);
136+
byte[] iv = new byte[12];
137+
random.nextBytes(iv);
138+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
139+
CipherOutputStream cipherStream = getCipherStream(bytes, salt, iv);
140+
DataOutputStream output = new DataOutputStream(cipherStream);
141+
// Indicate that the secret string is longer than it is so readFully() fails
142+
possiblyAlterSecretString(output, -4);
143+
cipherStream.close();
144+
final byte[] encryptedBytes = bytes.toByteArray();
145+
possiblyAlterEncryptedBytes(indexOutput, salt, iv, encryptedBytes, 0);
146+
CodecUtil.writeFooter(indexOutput);
147+
}
148+
149+
KeyStoreWrapper keystore = KeyStoreWrapper.load(configDir);
150+
SecurityException e = expectThrows(SecurityException.class, () -> keystore.decrypt(new char[0]));
151+
assertThat(e.getMessage(), containsString("Keystore has been corrupted or tampered with"));
152+
assertThat(e.getCause(), instanceOf(EOFException.class));
153+
}
154+
155+
public void testFailWhenCannotConsumeEncryptedBytesStream() throws Exception {
156+
Path configDir = env.configFile();
157+
SimpleFSDirectory directory = new SimpleFSDirectory(configDir);
158+
try (IndexOutput indexOutput = directory.createOutput("elasticsearch.keystore", IOContext.DEFAULT)) {
159+
CodecUtil.writeHeader(indexOutput, "elasticsearch.keystore", 3);
160+
indexOutput.writeByte((byte) 0); // No password
161+
SecureRandom random = Randomness.createSecure();
162+
byte[] salt = new byte[64];
163+
random.nextBytes(salt);
164+
byte[] iv = new byte[12];
165+
random.nextBytes(iv);
166+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
167+
CipherOutputStream cipherStream = getCipherStream(bytes, salt, iv);
168+
DataOutputStream output = new DataOutputStream(cipherStream);
169+
170+
possiblyAlterSecretString(output, 0);
171+
cipherStream.close();
172+
final byte[] encryptedBytes = bytes.toByteArray();
173+
// Indicate that the encryptedBytes is larger than it is so readFully() fails
174+
possiblyAlterEncryptedBytes(indexOutput, salt, iv, encryptedBytes, -12);
175+
CodecUtil.writeFooter(indexOutput);
176+
}
177+
178+
KeyStoreWrapper keystore = KeyStoreWrapper.load(configDir);
179+
SecurityException e = expectThrows(SecurityException.class, () -> keystore.decrypt(new char[0]));
180+
assertThat(e.getMessage(), containsString("Keystore has been corrupted or tampered with"));
181+
assertThat(e.getCause(), instanceOf(EOFException.class));
182+
}
183+
184+
public void testFailWhenSecretStreamNotConsumed() throws Exception {
185+
Path configDir = env.configFile();
186+
SimpleFSDirectory directory = new SimpleFSDirectory(configDir);
187+
try (IndexOutput indexOutput = directory.createOutput("elasticsearch.keystore", IOContext.DEFAULT)) {
188+
CodecUtil.writeHeader(indexOutput, "elasticsearch.keystore", 3);
189+
indexOutput.writeByte((byte) 0); // No password
190+
SecureRandom random = Randomness.createSecure();
191+
byte[] salt = new byte[64];
192+
random.nextBytes(salt);
193+
byte[] iv = new byte[12];
194+
random.nextBytes(iv);
195+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
196+
CipherOutputStream cipherStream = getCipherStream(bytes, salt, iv);
197+
DataOutputStream output = new DataOutputStream(cipherStream);
198+
// So that readFully during decryption will not consume the entire stream
199+
possiblyAlterSecretString(output, 4);
200+
cipherStream.close();
201+
final byte[] encryptedBytes = bytes.toByteArray();
202+
possiblyAlterEncryptedBytes(indexOutput, salt, iv, encryptedBytes, 0);
203+
CodecUtil.writeFooter(indexOutput);
204+
}
205+
206+
KeyStoreWrapper keystore = KeyStoreWrapper.load(configDir);
207+
SecurityException e = expectThrows(SecurityException.class, () -> keystore.decrypt(new char[0]));
208+
assertThat(e.getMessage(), containsString("Keystore has been corrupted or tampered with"));
209+
}
210+
211+
public void testFailWhenEncryptedBytesStreamIsNotConsumed() throws Exception {
212+
Path configDir = env.configFile();
213+
SimpleFSDirectory directory = new SimpleFSDirectory(configDir);
214+
try (IndexOutput indexOutput = directory.createOutput("elasticsearch.keystore", IOContext.DEFAULT)) {
215+
CodecUtil.writeHeader(indexOutput, "elasticsearch.keystore", 3);
216+
indexOutput.writeByte((byte) 0); // No password
217+
SecureRandom random = Randomness.createSecure();
218+
byte[] salt = new byte[64];
219+
random.nextBytes(salt);
220+
byte[] iv = new byte[12];
221+
random.nextBytes(iv);
222+
ByteArrayOutputStream bytes = new ByteArrayOutputStream();
223+
CipherOutputStream cipherStream = getCipherStream(bytes, salt, iv);
224+
DataOutputStream output = new DataOutputStream(cipherStream);
225+
possiblyAlterSecretString(output, 0);
226+
cipherStream.close();
227+
final byte[] encryptedBytes = bytes.toByteArray();
228+
possiblyAlterEncryptedBytes(indexOutput, salt, iv, encryptedBytes, randomIntBetween(2, encryptedBytes.length));
229+
CodecUtil.writeFooter(indexOutput);
230+
}
231+
232+
KeyStoreWrapper keystore = KeyStoreWrapper.load(configDir);
233+
SecurityException e = expectThrows(SecurityException.class, () -> keystore.decrypt(new char[0]));
234+
assertThat(e.getMessage(), containsString("Keystore has been corrupted or tampered with"));
235+
}
236+
237+
private CipherOutputStream getCipherStream(ByteArrayOutputStream bytes, byte[] salt, byte[] iv) throws Exception {
238+
PBEKeySpec keySpec = new PBEKeySpec(new char[0], salt, 10000, 128);
239+
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA512");
240+
SecretKey secretKey = keyFactory.generateSecret(keySpec);
241+
SecretKeySpec secret = new SecretKeySpec(secretKey.getEncoded(), "AES");
242+
GCMParameterSpec spec = new GCMParameterSpec(128, iv);
243+
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
244+
cipher.init(Cipher.ENCRYPT_MODE, secret, spec);
245+
cipher.updateAAD(salt);
246+
return new CipherOutputStream(bytes, cipher);
247+
}
248+
249+
private void possiblyAlterSecretString(DataOutputStream output, int truncLength) throws Exception {
250+
byte[] secret_value = "super_secret_value".getBytes(StandardCharsets.UTF_8);
251+
output.writeInt(1); // One entry
252+
output.writeUTF("string_setting");
253+
output.writeUTF("STRING");
254+
output.writeInt(secret_value.length - truncLength);
255+
output.write(secret_value);
256+
}
257+
258+
private void possiblyAlterEncryptedBytes(IndexOutput indexOutput, byte[] salt, byte[] iv, byte[] encryptedBytes, int
259+
truncEncryptedDataLength)
260+
throws Exception {
261+
indexOutput.writeInt(4 + salt.length + 4 + iv.length + 4 + encryptedBytes.length);
262+
indexOutput.writeInt(salt.length);
263+
indexOutput.writeBytes(salt, salt.length);
264+
indexOutput.writeInt(iv.length);
265+
indexOutput.writeBytes(iv, iv.length);
266+
indexOutput.writeInt(encryptedBytes.length - truncEncryptedDataLength);
267+
indexOutput.writeBytes(encryptedBytes, encryptedBytes.length);
268+
}
269+
124270
public void testUpgradeAddsSeed() throws Exception {
125271
KeyStoreWrapper keystore = KeyStoreWrapper.create();
126272
keystore.remove(KeyStoreWrapper.SEED_SETTING.getKey());

0 commit comments

Comments
 (0)