Skip to content

Commit 21bc87a

Browse files
authored
Use readFully() to read bytes from CipherInputStream (#28515)
Changes how data is read from CipherInputStream 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
1 parent d6a91ea commit 21bc87a

File tree

2 files changed

+166
-18
lines changed

2 files changed

+166
-18
lines changed

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

+13-13
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;
@@ -317,38 +318,39 @@ public void decrypt(char[] password) throws GeneralSecurityException, IOExceptio
317318
DataInputStream input = new DataInputStream(bytesStream)) {
318319
int saltLen = input.readInt();
319320
salt = new byte[saltLen];
320-
if (input.read(salt) != saltLen) {
321-
throw new SecurityException("Keystore has been corrupted or tampered with");
322-
}
321+
input.readFully(salt);
323322
int ivLen = input.readInt();
324323
iv = new byte[ivLen];
325-
if (input.read(iv) != ivLen) {
326-
throw new SecurityException("Keystore has been corrupted or tampered with");
327-
}
324+
input.readFully(iv);
328325
int encryptedLen = input.readInt();
329326
encryptedBytes = new byte[encryptedLen];
330-
if (input.read(encryptedBytes) != encryptedLen) {
327+
input.readFully(encryptedBytes);
328+
if (input.read() != -1) {
331329
throw new SecurityException("Keystore has been corrupted or tampered with");
332330
}
331+
} catch (EOFException e) {
332+
throw new SecurityException("Keystore has been corrupted or tampered with", e);
333333
}
334334

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

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

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

+153-5
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,41 @@
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.junit.After;
4952
import org.junit.Before;
5053

54+
import static org.hamcrest.Matchers.containsString;
5155
import static org.hamcrest.Matchers.equalTo;
56+
import static org.hamcrest.Matchers.instanceOf;
5257

5358
public class KeyStoreWrapperTests extends ESTestCase {
5459

@@ -104,6 +109,149 @@ public void testUpgradeNoop() throws Exception {
104109
assertEquals(seed.toString(), keystore.getString(KeyStoreWrapper.SEED_SETTING.getKey()).toString());
105110
}
106111

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

0 commit comments

Comments
 (0)