Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 8ae9090

Browse files
committedNov 25, 2020
Packet-based enc/dec cipher streams (#49896)
This adds a new bare snapshot repository project which contains the classes implementing encryption (and decryption) input stream decorators that support mark and reset. Relates #48221 , #46170
1 parent a24bc6e commit 8ae9090

17 files changed

+4669
-0
lines changed
 

Diff for: ‎x-pack/plugin/repository-encrypted/build.gradle

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
evaluationDependsOn(xpackModule('core'))
2+
3+
apply plugin: 'elasticsearch.esplugin'
4+
esplugin {
5+
name 'repository-encrypted'
6+
description 'Elasticsearch Expanded Pack Plugin - client-side encrypted repositories.'
7+
classname 'org.elasticsearch.repositories.encrypted.EncryptedRepositoryPlugin'
8+
extendedPlugins = ['x-pack-core']
9+
}
10+
11+
integTest.enabled = false

Diff for: ‎x-pack/plugin/repository-encrypted/src/main/java/org/elasticsearch/repositories/encrypted/BufferOnMarkInputStream.java

+546
Large diffs are not rendered by default.

Diff for: ‎x-pack/plugin/repository-encrypted/src/main/java/org/elasticsearch/repositories/encrypted/ChainingInputStream.java

+375
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.repositories.encrypted;
7+
8+
import java.io.IOException;
9+
import java.io.InputStream;
10+
import java.util.Objects;
11+
12+
/**
13+
* A {@code CountingInputStream} wraps another input stream and counts the number of bytes
14+
* that have been read or skipped.
15+
* <p>
16+
* This input stream does no buffering on its own and only supports {@code mark} and
17+
* {@code reset} if the underlying wrapped stream supports it.
18+
* <p>
19+
* If the stream supports {@code mark} and {@code reset} the byte count is also reset to the
20+
* value that it had on the last {@code mark} call, thereby not counting the same bytes twice.
21+
* <p>
22+
* If the {@code closeSource} constructor argument is {@code true}, closing this
23+
* stream will also close the wrapped input stream. Apart from closing the wrapped
24+
* stream in this case, the {@code close} method does nothing else.
25+
*/
26+
public final class CountingInputStream extends InputStream {
27+
28+
private final InputStream source;
29+
private final boolean closeSource;
30+
long count; // package-protected for tests
31+
long mark; // package-protected for tests
32+
boolean closed; // package-protected for tests
33+
34+
/**
35+
* Wraps another input stream, counting the number of bytes read.
36+
*
37+
* @param source the input stream to be wrapped
38+
* @param closeSource {@code true} if closing this stream will also close the wrapped stream
39+
*/
40+
public CountingInputStream(InputStream source, boolean closeSource) {
41+
this.source = Objects.requireNonNull(source);
42+
this.closeSource = closeSource;
43+
this.count = 0L;
44+
this.mark = -1L;
45+
this.closed = false;
46+
}
47+
48+
/** Returns the number of bytes read. */
49+
public long getCount() {
50+
return count;
51+
}
52+
53+
@Override
54+
public int read() throws IOException {
55+
int result = source.read();
56+
if (result != -1) {
57+
count++;
58+
}
59+
return result;
60+
}
61+
62+
@Override
63+
public int read(byte[] b, int off, int len) throws IOException {
64+
int result = source.read(b, off, len);
65+
if (result != -1) {
66+
count += result;
67+
}
68+
return result;
69+
}
70+
71+
@Override
72+
public long skip(long n) throws IOException {
73+
long result = source.skip(n);
74+
count += result;
75+
return result;
76+
}
77+
78+
@Override
79+
public int available() throws IOException {
80+
return source.available();
81+
}
82+
83+
@Override
84+
public boolean markSupported() {
85+
return source.markSupported();
86+
}
87+
88+
@Override
89+
public synchronized void mark(int readlimit) {
90+
source.mark(readlimit);
91+
mark = count;
92+
}
93+
94+
@Override
95+
public synchronized void reset() throws IOException {
96+
if (false == source.markSupported()) {
97+
throw new IOException("Mark not supported");
98+
}
99+
if (mark == -1L) {
100+
throw new IOException("Mark not set");
101+
}
102+
count = mark;
103+
source.reset();
104+
}
105+
106+
@Override
107+
public void close() throws IOException {
108+
if (false == closed) {
109+
closed = true;
110+
if (closeSource) {
111+
source.close();
112+
}
113+
}
114+
}
115+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
package org.elasticsearch.repositories.encrypted;
7+
8+
import javax.crypto.BadPaddingException;
9+
import javax.crypto.Cipher;
10+
import javax.crypto.IllegalBlockSizeException;
11+
import javax.crypto.NoSuchPaddingException;
12+
import javax.crypto.SecretKey;
13+
import javax.crypto.ShortBufferException;
14+
import javax.crypto.spec.GCMParameterSpec;
15+
import java.io.ByteArrayInputStream;
16+
import java.io.IOException;
17+
import java.io.InputStream;
18+
import java.nio.ByteBuffer;
19+
import java.nio.ByteOrder;
20+
import java.security.InvalidAlgorithmParameterException;
21+
import java.security.InvalidKeyException;
22+
import java.security.NoSuchAlgorithmException;
23+
import java.util.Objects;
24+
25+
import static org.elasticsearch.repositories.encrypted.EncryptedRepository.GCM_IV_LENGTH_IN_BYTES;
26+
import static org.elasticsearch.repositories.encrypted.EncryptedRepository.GCM_TAG_LENGTH_IN_BYTES;
27+
28+
/**
29+
* A {@code DecryptionPacketsInputStream} wraps an encrypted input stream and decrypts
30+
* its contents. This is designed (and tested) to decrypt only the encryption format that
31+
* {@link EncryptionPacketsInputStream} generates. No decrypted bytes are returned before
32+
* they are authenticated.
33+
* <p>
34+
* The same parameters, namely {@code secretKey}, {@code nonce} and {@code packetLength},
35+
* that have been used during encryption must also be used for decryption, otherwise
36+
* decryption will fail.
37+
* <p>
38+
* This implementation buffers the encrypted packet in memory. The maximum packet size it can
39+
* accommodate is {@link EncryptedRepository#MAX_PACKET_LENGTH_IN_BYTES}.
40+
* <p>
41+
* This implementation does not support {@code mark} and {@code reset}.
42+
* <p>
43+
* The {@code close} call will close the decryption input stream and any subsequent {@code read},
44+
* {@code skip}, {@code available} and {@code reset} calls will throw {@code IOException}s.
45+
* <p>
46+
* This is NOT thread-safe, multiple threads sharing a single instance must synchronize access.
47+
*
48+
* @see EncryptionPacketsInputStream
49+
*/
50+
public final class DecryptionPacketsInputStream extends ChainingInputStream {
51+
52+
private final InputStream source;
53+
private final SecretKey secretKey;
54+
private final int nonce;
55+
private final int packetLength;
56+
private final byte[] packetBuffer;
57+
58+
private boolean hasNext;
59+
private long counter;
60+
61+
/**
62+
* Computes and returns the length of the plaintext given the {@code ciphertextLength} and the {@code packetLength}
63+
* used during encryption.
64+
* Each ciphertext packet is prepended by the Initilization Vector and has the Authentication Tag appended.
65+
* Decryption is 1:1, and the ciphertext is not padded, but stripping away the IV and the AT amounts to a shorter
66+
* plaintext compared to the ciphertext.
67+
*
68+
* @see EncryptionPacketsInputStream#getEncryptionLength(long, int)
69+
*/
70+
public static long getDecryptionLength(long ciphertextLength, int packetLength) {
71+
long encryptedPacketLength = packetLength + GCM_TAG_LENGTH_IN_BYTES + GCM_IV_LENGTH_IN_BYTES;
72+
long completePackets = ciphertextLength / encryptedPacketLength;
73+
long decryptedSize = completePackets * packetLength;
74+
if (ciphertextLength % encryptedPacketLength != 0) {
75+
decryptedSize += (ciphertextLength % encryptedPacketLength) - GCM_IV_LENGTH_IN_BYTES - GCM_TAG_LENGTH_IN_BYTES;
76+
}
77+
return decryptedSize;
78+
}
79+
80+
public DecryptionPacketsInputStream(InputStream source, SecretKey secretKey, int nonce, int packetLength) {
81+
this.source = Objects.requireNonNull(source);
82+
this.secretKey = Objects.requireNonNull(secretKey);
83+
this.nonce = nonce;
84+
if (packetLength <= 0 || packetLength >= EncryptedRepository.MAX_PACKET_LENGTH_IN_BYTES) {
85+
throw new IllegalArgumentException("Invalid packet length [" + packetLength + "]");
86+
}
87+
this.packetLength = packetLength;
88+
this.packetBuffer = new byte[packetLength + GCM_TAG_LENGTH_IN_BYTES];
89+
this.hasNext = true;
90+
this.counter = EncryptedRepository.PACKET_START_COUNTER;
91+
}
92+
93+
@Override
94+
InputStream nextComponent(InputStream currentComponentIn) throws IOException {
95+
if (currentComponentIn != null && currentComponentIn.read() != -1) {
96+
throw new IllegalStateException("Stream for previous packet has not been fully processed");
97+
}
98+
if (false == hasNext) {
99+
return null;
100+
}
101+
PrefixInputStream packetInputStream = new PrefixInputStream(source,
102+
packetLength + GCM_IV_LENGTH_IN_BYTES + GCM_TAG_LENGTH_IN_BYTES,
103+
false);
104+
int currentPacketLength = decrypt(packetInputStream);
105+
// only the last packet is shorter, so this must be the last packet
106+
if (currentPacketLength != packetLength) {
107+
hasNext = false;
108+
}
109+
return new ByteArrayInputStream(packetBuffer, 0, currentPacketLength);
110+
}
111+
112+
@Override
113+
public boolean markSupported() {
114+
return false;
115+
}
116+
117+
@Override
118+
public void mark(int readlimit) {
119+
}
120+
121+
@Override
122+
public void reset() throws IOException {
123+
throw new IOException("Mark/reset not supported");
124+
}
125+
126+
private int decrypt(PrefixInputStream packetInputStream) throws IOException {
127+
// read only the IV prefix into the packet buffer
128+
int ivLength = packetInputStream.readNBytes(packetBuffer, 0, GCM_IV_LENGTH_IN_BYTES);
129+
if (ivLength != GCM_IV_LENGTH_IN_BYTES) {
130+
throw new IOException("Packet heading IV error. Unexpected length [" + ivLength + "].");
131+
}
132+
// extract the nonce and the counter from the packet IV
133+
ByteBuffer ivBuffer = ByteBuffer.wrap(packetBuffer, 0, GCM_IV_LENGTH_IN_BYTES).order(ByteOrder.LITTLE_ENDIAN);
134+
int packetIvNonce = ivBuffer.getInt(0);
135+
long packetIvCounter = ivBuffer.getLong(Integer.BYTES);
136+
if (packetIvNonce != nonce) {
137+
throw new IOException("Packet nonce mismatch. Expecting [" + nonce + "], but got [" + packetIvNonce + "].");
138+
}
139+
if (packetIvCounter != counter) {
140+
throw new IOException("Packet counter mismatch. Expecting [" + counter + "], but got [" + packetIvCounter + "].");
141+
}
142+
// counter increment for the subsequent packet
143+
counter++;
144+
// counter wrap around
145+
if (counter == EncryptedRepository.PACKET_START_COUNTER) {
146+
throw new IOException("Maximum packet count limit exceeded");
147+
}
148+
// cipher used to decrypt only the current packetInputStream
149+
Cipher packetCipher = getPacketDecryptionCipher(packetBuffer);
150+
// read the rest of the packet, reusing the packetBuffer
151+
int packetLength = packetInputStream.readNBytes(packetBuffer, 0, packetBuffer.length);
152+
if (packetLength < GCM_TAG_LENGTH_IN_BYTES) {
153+
throw new IOException("Encrypted packet is too short");
154+
}
155+
try {
156+
// in-place decryption of the whole packet and return decrypted length
157+
return packetCipher.doFinal(packetBuffer, 0, packetLength, packetBuffer);
158+
} catch (ShortBufferException | IllegalBlockSizeException | BadPaddingException e) {
159+
throw new IOException("Exception during packet decryption", e);
160+
}
161+
}
162+
163+
private Cipher getPacketDecryptionCipher(byte[] packet) throws IOException {
164+
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH_IN_BYTES * Byte.SIZE, packet, 0, GCM_IV_LENGTH_IN_BYTES);
165+
try {
166+
Cipher packetCipher = Cipher.getInstance(EncryptedRepository.GCM_ENCRYPTION_SCHEME);
167+
packetCipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec);
168+
return packetCipher;
169+
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
170+
throw new IOException("Exception during packet cipher initialisation", e);
171+
}
172+
}
173+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.repositories.encrypted;
8+
9+
public class EncryptedRepository {
10+
static final int GCM_TAG_LENGTH_IN_BYTES = 16;
11+
static final int GCM_IV_LENGTH_IN_BYTES = 12;
12+
static final int AES_BLOCK_SIZE_IN_BYTES = 128;
13+
static final String GCM_ENCRYPTION_SCHEME = "AES/GCM/NoPadding";
14+
static final long PACKET_START_COUNTER = Long.MIN_VALUE;
15+
static final int MAX_PACKET_LENGTH_IN_BYTES = 1 << 30;
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.repositories.encrypted;
8+
9+
import org.elasticsearch.common.settings.Setting;
10+
import org.elasticsearch.common.settings.Settings;
11+
import org.elasticsearch.plugins.Plugin;
12+
import org.elasticsearch.plugins.ReloadablePlugin;
13+
import org.elasticsearch.plugins.RepositoryPlugin;
14+
15+
import java.util.List;
16+
17+
public final class EncryptedRepositoryPlugin extends Plugin implements RepositoryPlugin, ReloadablePlugin {
18+
19+
public EncryptedRepositoryPlugin(final Settings settings) {
20+
}
21+
22+
@Override
23+
public List<Setting<?>> getSettings() {
24+
return List.of();
25+
}
26+
27+
@Override
28+
public void reload(Settings settings) {
29+
// Secure settings should be readable inside this method.
30+
}
31+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.repositories.encrypted;
8+
9+
import javax.crypto.Cipher;
10+
import javax.crypto.CipherInputStream;
11+
import javax.crypto.NoSuchPaddingException;
12+
import javax.crypto.SecretKey;
13+
import javax.crypto.spec.GCMParameterSpec;
14+
import java.io.ByteArrayInputStream;
15+
import java.io.IOException;
16+
import java.io.InputStream;
17+
import java.io.SequenceInputStream;
18+
import java.nio.ByteBuffer;
19+
import java.nio.ByteOrder;
20+
import java.security.InvalidAlgorithmParameterException;
21+
import java.security.InvalidKeyException;
22+
import java.security.NoSuchAlgorithmException;
23+
import java.util.Objects;
24+
25+
/**
26+
* An {@code EncryptionPacketsInputStream} wraps another input stream and encrypts its contents.
27+
* The method of encryption is AES/GCM/NoPadding, which is a type of authenticated encryption.
28+
* The encryption works packet wise, i.e. the stream is segmented into fixed-size byte packets
29+
* which are separately encrypted using a unique {@link Cipher}. As an exception, only the last
30+
* packet will have a different size, possibly zero. Note that the encrypted packets are
31+
* larger compared to the plaintext packets, because they contain a 16 byte length trailing
32+
* authentication tag. The resulting encrypted and authenticated packets are assembled back into
33+
* the resulting stream.
34+
* <p>
35+
* The packets are encrypted using the same {@link SecretKey} but using a different initialization
36+
* vector. The IV of each packet is 12 bytes wide and is comprised of a 4-byte integer {@code nonce},
37+
* the same for every packet in the stream, and a monotonically increasing 8-byte integer counter.
38+
* The caller must assure that the same {@code nonce} is not reused for other encrypted streams
39+
* using the same {@code secretKey}. The counter from the IV identifies the position of the packet
40+
* in the encrypted stream, so that packets cannot be reordered without breaking the decryption.
41+
* When assembling the encrypted stream, the IV is prepended to the corresponding packet's ciphertext.
42+
* <p>
43+
* The packet length is preferably a large multiple (typically 128) of the AES block size (128 bytes),
44+
* but any positive integer value smaller than {@link EncryptedRepository#MAX_PACKET_LENGTH_IN_BYTES}
45+
* is valid. A larger packet length incurs smaller relative size overhead because the 12 byte wide IV
46+
* and the 16 byte wide authentication tag are constant no matter the packet length. A larger packet
47+
* length also exposes more opportunities for the JIT compilation of the AES encryption loop. But
48+
* {@code mark} will buffer up to packet length bytes, and, more importantly, <b>decryption</b> might
49+
* need to allocate a memory buffer the size of the packet in order to assure that no un-authenticated
50+
* decrypted ciphertext is returned. The decryption procedure is the primary factor that limits the
51+
* packet length.
52+
* <p>
53+
* This input stream supports the {@code mark} and {@code reset} operations, but only if the wrapped
54+
* stream supports them as well. A {@code mark} call will trigger the memory buffering of the current
55+
* packet and will also trigger a {@code mark} call on the wrapped input stream on the next
56+
* packet boundary. Upon a {@code reset} call, the buffered packet will be replayed and new packets
57+
* will be generated starting from the marked packet boundary on the wrapped stream.
58+
* <p>
59+
* The {@code close} call will close the encryption input stream and any subsequent {@code read},
60+
* {@code skip}, {@code available} and {@code reset} calls will throw {@code IOException}s.
61+
* <p>
62+
* This is NOT thread-safe, multiple threads sharing a single instance must synchronize access.
63+
*
64+
* @see DecryptionPacketsInputStream
65+
*/
66+
public final class EncryptionPacketsInputStream extends ChainingInputStream {
67+
68+
private final SecretKey secretKey;
69+
private final int packetLength;
70+
private final ByteBuffer packetIv;
71+
private final int encryptedPacketLength;
72+
73+
final InputStream source; // package-protected for tests
74+
long counter; // package-protected for tests
75+
Long markCounter; // package-protected for tests
76+
int markSourceOnNextPacket; // package-protected for tests
77+
78+
/**
79+
* Computes and returns the length of the ciphertext given the {@code plaintextLength} and the {@code packetLength}
80+
* used during encryption.
81+
* The plaintext is segmented into packets of equal {@code packetLength} length, with the exception of the last
82+
* packet which is shorter and can have a length of {@code 0}. Encryption is packet-wise and is 1:1, with no padding.
83+
* But each encrypted packet is prepended by the Initilization Vector and appended the Authentication Tag, including
84+
* the last packet, so when pieced together will amount to a longer resulting ciphertext.
85+
*
86+
* @see DecryptionPacketsInputStream#getDecryptionLength(long, int)
87+
*/
88+
public static long getEncryptionLength(long plaintextLength, int packetLength) {
89+
return plaintextLength + (plaintextLength / packetLength + 1) * (EncryptedRepository.GCM_TAG_LENGTH_IN_BYTES
90+
+ EncryptedRepository.GCM_IV_LENGTH_IN_BYTES);
91+
}
92+
93+
public EncryptionPacketsInputStream(InputStream source, SecretKey secretKey, int nonce, int packetLength) {
94+
this.source = Objects.requireNonNull(source);
95+
this.secretKey = Objects.requireNonNull(secretKey);
96+
if (packetLength <= 0 || packetLength >= EncryptedRepository.MAX_PACKET_LENGTH_IN_BYTES) {
97+
throw new IllegalArgumentException("Invalid packet length [" + packetLength + "]");
98+
}
99+
this.packetLength = packetLength;
100+
this.packetIv = ByteBuffer.allocate(EncryptedRepository.GCM_IV_LENGTH_IN_BYTES).order(ByteOrder.LITTLE_ENDIAN);
101+
// nonce takes the first 4 bytes of the IV
102+
this.packetIv.putInt(0, nonce);
103+
this.encryptedPacketLength =
104+
packetLength + EncryptedRepository.GCM_IV_LENGTH_IN_BYTES + EncryptedRepository.GCM_TAG_LENGTH_IN_BYTES;
105+
this.counter = EncryptedRepository.PACKET_START_COUNTER;
106+
this.markCounter = null;
107+
this.markSourceOnNextPacket = -1;
108+
}
109+
110+
@Override
111+
InputStream nextComponent(InputStream currentComponentIn) throws IOException {
112+
// the last packet input stream is the only one shorter than encryptedPacketLength
113+
if (currentComponentIn != null && ((CountingInputStream) currentComponentIn).getCount() < encryptedPacketLength) {
114+
// there are no more packets
115+
return null;
116+
}
117+
// If the enclosing stream has a mark set,
118+
// then apply it to the source input stream when we reach a packet boundary
119+
if (markSourceOnNextPacket != -1) {
120+
source.mark(markSourceOnNextPacket);
121+
markSourceOnNextPacket = -1;
122+
}
123+
// create the new packet
124+
InputStream encryptionInputStream = new PrefixInputStream(source, packetLength, false);
125+
// the counter takes up the last 8 bytes of the packet IV (12 byte wide)
126+
// the first 4 bytes are used by the nonce (which is the same for every packet IV)
127+
packetIv.putLong(Integer.BYTES, counter++);
128+
// counter wrap around
129+
if (counter == EncryptedRepository.PACKET_START_COUNTER) {
130+
throw new IOException("Maximum packet count limit exceeded");
131+
}
132+
Cipher packetCipher = getPacketEncryptionCipher(secretKey, packetIv.array());
133+
encryptionInputStream = new CipherInputStream(encryptionInputStream, packetCipher);
134+
encryptionInputStream = new SequenceInputStream(new ByteArrayInputStream(packetIv.array()), encryptionInputStream);
135+
encryptionInputStream = new BufferOnMarkInputStream(encryptionInputStream, encryptedPacketLength);
136+
return new CountingInputStream(encryptionInputStream, false);
137+
}
138+
139+
@Override
140+
public boolean markSupported() {
141+
return source.markSupported();
142+
}
143+
144+
@Override
145+
public void mark(int readlimit) {
146+
if (markSupported()) {
147+
if (readlimit <= 0) {
148+
throw new IllegalArgumentException("Mark readlimit must be a positive integer");
149+
}
150+
// handles the packet-wise part of the marking operation
151+
super.mark(encryptedPacketLength);
152+
// saves the counter used to generate packet IVs
153+
markCounter = counter;
154+
// stores the flag used to mark the source input stream at packet boundary
155+
markSourceOnNextPacket = readlimit;
156+
}
157+
}
158+
159+
@Override
160+
public void reset() throws IOException {
161+
if (false == markSupported()) {
162+
throw new IOException("Mark/reset not supported");
163+
}
164+
if (markCounter == null) {
165+
throw new IOException("Mark no set");
166+
}
167+
super.reset();
168+
counter = markCounter;
169+
if (markSourceOnNextPacket == -1) {
170+
source.reset();
171+
}
172+
}
173+
174+
private static Cipher getPacketEncryptionCipher(SecretKey secretKey, byte[] packetIv) throws IOException {
175+
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(EncryptedRepository.GCM_TAG_LENGTH_IN_BYTES * Byte.SIZE, packetIv);
176+
try {
177+
Cipher packetCipher = Cipher.getInstance(EncryptedRepository.GCM_ENCRYPTION_SCHEME);
178+
packetCipher.init(Cipher.ENCRYPT_MODE, secretKey, gcmParameterSpec);
179+
return packetCipher;
180+
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
181+
throw new IOException(e);
182+
}
183+
}
184+
185+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.repositories.encrypted;
8+
9+
import java.io.IOException;
10+
import java.io.InputStream;
11+
import java.util.Objects;
12+
13+
/**
14+
* A {@code PrefixInputStream} wraps another input stream and exposes
15+
* only the first bytes of it. Reading from the wrapping
16+
* {@code PrefixInputStream} consumes the underlying stream. The stream
17+
* is exhausted when {@code prefixLength} bytes have been read, or the underlying
18+
* stream is exhausted before that.
19+
* <p>
20+
* Only if the {@code closeSource} constructor argument is {@code true}, the
21+
* closing of this stream will also close the underlying input stream.
22+
* Any subsequent {@code read}, {@code skip} and {@code available} calls
23+
* will throw {@code IOException}s.
24+
*/
25+
public final class PrefixInputStream extends InputStream {
26+
27+
/**
28+
* The underlying stream of which only a prefix is returned
29+
*/
30+
private final InputStream source;
31+
/**
32+
* The length in bytes of the prefix.
33+
* This is the maximum number of bytes that can be read from this stream,
34+
* but fewer bytes can be read if the wrapped source stream itself contains fewer bytes
35+
*/
36+
private final int prefixLength;
37+
/**
38+
* The current count of bytes read from this stream.
39+
* This starts of as {@code 0} and is always smaller or equal to {@code prefixLength}.
40+
*/
41+
private int count;
42+
/**
43+
* whether closing this stream must also close the underlying stream
44+
*/
45+
private boolean closeSource;
46+
/**
47+
* flag signalling if this stream has been closed
48+
*/
49+
private boolean closed;
50+
51+
public PrefixInputStream(InputStream source, int prefixLength, boolean closeSource) {
52+
if (prefixLength < 0) {
53+
throw new IllegalArgumentException("The prefixLength constructor argument must be a positive integer");
54+
}
55+
this.source = source;
56+
this.prefixLength = prefixLength;
57+
this.count = 0;
58+
this.closeSource = closeSource;
59+
this.closed = false;
60+
}
61+
62+
@Override
63+
public int read() throws IOException {
64+
ensureOpen();
65+
if (remainingPrefixByteCount() <= 0) {
66+
return -1;
67+
}
68+
int byteVal = source.read();
69+
if (byteVal == -1) {
70+
return -1;
71+
}
72+
count++;
73+
return byteVal;
74+
}
75+
76+
@Override
77+
public int read(byte[] b, int off, int len) throws IOException {
78+
ensureOpen();
79+
Objects.checkFromIndexSize(off, len, b.length);
80+
if (len == 0) {
81+
return 0;
82+
}
83+
if (remainingPrefixByteCount() <= 0) {
84+
return -1;
85+
}
86+
int readSize = Math.min(len, remainingPrefixByteCount());
87+
int bytesRead = source.read(b, off, readSize);
88+
if (bytesRead == -1) {
89+
return -1;
90+
}
91+
count += bytesRead;
92+
return bytesRead;
93+
}
94+
95+
@Override
96+
public long skip(long n) throws IOException {
97+
ensureOpen();
98+
if (n <= 0 || remainingPrefixByteCount() <= 0) {
99+
return 0;
100+
}
101+
long bytesToSkip = Math.min(n, remainingPrefixByteCount());
102+
assert bytesToSkip > 0;
103+
long bytesSkipped = source.skip(bytesToSkip);
104+
count += bytesSkipped;
105+
return bytesSkipped;
106+
}
107+
108+
@Override
109+
public int available() throws IOException {
110+
ensureOpen();
111+
return Math.min(remainingPrefixByteCount(), source.available());
112+
}
113+
114+
@Override
115+
public boolean markSupported() {
116+
return false;
117+
}
118+
119+
@Override
120+
public void mark(int readlimit) {
121+
// mark and reset are not supported
122+
}
123+
124+
@Override
125+
public void reset() throws IOException {
126+
throw new IOException("mark/reset not supported");
127+
}
128+
129+
@Override
130+
public void close() throws IOException {
131+
if (closed) {
132+
return;
133+
}
134+
closed = true;
135+
if (closeSource) {
136+
source.close();
137+
}
138+
}
139+
140+
private int remainingPrefixByteCount() {
141+
return prefixLength - count;
142+
}
143+
144+
private void ensureOpen() throws IOException {
145+
if (closed) {
146+
throw new IOException("Stream has been closed");
147+
}
148+
}
149+
150+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
grant {
8+
};

Diff for: ‎x-pack/plugin/repository-encrypted/src/test/java/org/elasticsearch/repositories/encrypted/BufferOnMarkInputStreamTests.java

+862
Large diffs are not rendered by default.

Diff for: ‎x-pack/plugin/repository-encrypted/src/test/java/org/elasticsearch/repositories/encrypted/ChainingInputStreamTests.java

+1,079
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.repositories.encrypted;
8+
9+
import org.elasticsearch.common.Randomness;
10+
import org.elasticsearch.test.ESTestCase;
11+
import org.hamcrest.Matchers;
12+
import org.junit.BeforeClass;
13+
import org.mockito.invocation.InvocationOnMock;
14+
import org.mockito.stubbing.Answer;
15+
16+
import java.io.ByteArrayInputStream;
17+
import java.io.InputStream;
18+
import java.util.concurrent.atomic.AtomicBoolean;
19+
20+
import static org.mockito.Mockito.doAnswer;
21+
import static org.mockito.Mockito.mock;
22+
import static org.mockito.Mockito.when;
23+
24+
public class CountingInputStreamTests extends ESTestCase {
25+
26+
private static byte[] testArray;
27+
28+
@BeforeClass
29+
static void createTestArray() throws Exception {
30+
testArray = new byte[32];
31+
for (int i = 0; i < testArray.length; i++) {
32+
testArray[i] = (byte) i;
33+
}
34+
}
35+
36+
public void testWrappedMarkAndClose() throws Exception {
37+
AtomicBoolean isClosed = new AtomicBoolean(false);
38+
InputStream mockIn = mock(InputStream.class);
39+
doAnswer(new Answer<Void>() {
40+
public Void answer(InvocationOnMock invocation) {
41+
isClosed.set(true);
42+
return null;
43+
}
44+
}).when(mockIn).close();
45+
new CountingInputStream(mockIn, true).close();
46+
assertThat(isClosed.get(), Matchers.is(true));
47+
isClosed.set(false);
48+
new CountingInputStream(mockIn, false).close();
49+
assertThat(isClosed.get(), Matchers.is(false));
50+
when(mockIn.markSupported()).thenAnswer(invocationOnMock -> {
51+
return false;
52+
});
53+
assertThat(new CountingInputStream(mockIn, randomBoolean()).markSupported(), Matchers.is(false));
54+
when(mockIn.markSupported()).thenAnswer(invocationOnMock -> {
55+
return true;
56+
});
57+
assertThat(new CountingInputStream(mockIn, randomBoolean()).markSupported(), Matchers.is(true));
58+
}
59+
60+
public void testSimpleCountForRead() throws Exception {
61+
CountingInputStream test = new CountingInputStream(new ByteArrayInputStream(testArray), randomBoolean());
62+
assertThat(test.getCount(), Matchers.is(0L));
63+
int readLen = Randomness.get().nextInt(testArray.length);
64+
test.readNBytes(readLen);
65+
assertThat(test.getCount(), Matchers.is((long)readLen));
66+
readLen = testArray.length - readLen;
67+
test.readNBytes(readLen);
68+
assertThat(test.getCount(), Matchers.is((long)testArray.length));
69+
test.close();
70+
assertThat(test.getCount(), Matchers.is((long)testArray.length));
71+
}
72+
73+
public void testSimpleCountForSkip() throws Exception {
74+
CountingInputStream test = new CountingInputStream(new ByteArrayInputStream(testArray), randomBoolean());
75+
assertThat(test.getCount(), Matchers.is(0L));
76+
int skipLen = Randomness.get().nextInt(testArray.length);
77+
test.skip(skipLen);
78+
assertThat(test.getCount(), Matchers.is((long)skipLen));
79+
skipLen = testArray.length - skipLen;
80+
test.readNBytes(skipLen);
81+
assertThat(test.getCount(), Matchers.is((long)testArray.length));
82+
test.close();
83+
assertThat(test.getCount(), Matchers.is((long)testArray.length));
84+
}
85+
86+
public void testCountingForMarkAndReset() throws Exception {
87+
CountingInputStream test = new CountingInputStream(new ByteArrayInputStream(testArray), randomBoolean());
88+
assertThat(test.getCount(), Matchers.is(0L));
89+
assertThat(test.markSupported(), Matchers.is(true));
90+
int offset1 = Randomness.get().nextInt(testArray.length - 1);
91+
if (randomBoolean()) {
92+
test.skip(offset1);
93+
} else {
94+
test.read(new byte[offset1]);
95+
}
96+
assertThat(test.getCount(), Matchers.is((long)offset1));
97+
test.mark(testArray.length);
98+
int offset2 = 1 + Randomness.get().nextInt(testArray.length - offset1 - 1);
99+
if (randomBoolean()) {
100+
test.skip(offset2);
101+
} else {
102+
test.read(new byte[offset2]);
103+
}
104+
assertThat(test.getCount(), Matchers.is((long)offset1 + offset2));
105+
test.reset();
106+
assertThat(test.getCount(), Matchers.is((long)offset1));
107+
int offset3 = Randomness.get().nextInt(offset2);
108+
if (randomBoolean()) {
109+
test.skip(offset3);
110+
} else {
111+
test.read(new byte[offset3]);
112+
}
113+
assertThat(test.getCount(), Matchers.is((long)offset1 + offset3));
114+
test.reset();
115+
assertThat(test.getCount(), Matchers.is((long)offset1));
116+
test.readAllBytes();
117+
assertThat(test.getCount(), Matchers.is((long)testArray.length));
118+
test.close();
119+
assertThat(test.getCount(), Matchers.is((long)testArray.length));
120+
}
121+
122+
public void testCountingForMarkAfterReset() throws Exception {
123+
CountingInputStream test = new CountingInputStream(new ByteArrayInputStream(testArray), randomBoolean());
124+
assertThat(test.getCount(), Matchers.is(0L));
125+
assertThat(test.markSupported(), Matchers.is(true));
126+
int offset1 = Randomness.get().nextInt(testArray.length - 1);
127+
if (randomBoolean()) {
128+
test.skip(offset1);
129+
} else {
130+
test.read(new byte[offset1]);
131+
}
132+
assertThat(test.getCount(), Matchers.is((long)offset1));
133+
test.mark(testArray.length);
134+
int offset2 = 1 + Randomness.get().nextInt(testArray.length - offset1 - 1);
135+
if (randomBoolean()) {
136+
test.skip(offset2);
137+
} else {
138+
test.read(new byte[offset2]);
139+
}
140+
assertThat(test.getCount(), Matchers.is((long)offset1 + offset2));
141+
test.reset();
142+
assertThat(test.getCount(), Matchers.is((long)offset1));
143+
int offset3 = Randomness.get().nextInt(offset2);
144+
if (randomBoolean()) {
145+
test.skip(offset3);
146+
} else {
147+
test.read(new byte[offset3]);
148+
}
149+
test.mark(testArray.length);
150+
assertThat(test.getCount(), Matchers.is((long)offset1 + offset3));
151+
int offset4 = Randomness.get().nextInt(testArray.length - offset1 - offset3);
152+
if (randomBoolean()) {
153+
test.skip(offset4);
154+
} else {
155+
test.read(new byte[offset4]);
156+
}
157+
assertThat(test.getCount(), Matchers.is((long)offset1 + offset3 + offset4));
158+
test.reset();
159+
assertThat(test.getCount(), Matchers.is((long)offset1 + offset3));
160+
test.readAllBytes();
161+
assertThat(test.getCount(), Matchers.is((long)testArray.length));
162+
test.close();
163+
assertThat(test.getCount(), Matchers.is((long)testArray.length));
164+
}
165+
166+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.repositories.encrypted;
8+
9+
import org.elasticsearch.common.Randomness;
10+
import org.elasticsearch.test.ESTestCase;
11+
import org.hamcrest.Matchers;
12+
13+
import javax.crypto.KeyGenerator;
14+
import javax.crypto.SecretKey;
15+
import java.io.ByteArrayInputStream;
16+
import java.io.FilterInputStream;
17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.security.SecureRandom;
20+
import java.util.Arrays;
21+
22+
public class DecryptionPacketsInputStreamTests extends ESTestCase {
23+
24+
public void testSuccessEncryptAndDecryptSmallPacketLength() throws Exception {
25+
int len = 8 + Randomness.get().nextInt(8);
26+
byte[] plainBytes = new byte[len];
27+
Randomness.get().nextBytes(plainBytes);
28+
SecretKey secretKey = generateSecretKey();
29+
int nonce = Randomness.get().nextInt();
30+
for (int packetLen : Arrays.asList(1, 2, 3, 4)) {
31+
testEncryptAndDecryptSuccess(plainBytes, secretKey, nonce, packetLen);
32+
}
33+
}
34+
35+
public void testSuccessEncryptAndDecryptLargePacketLength() throws Exception {
36+
int len = 256 + Randomness.get().nextInt(256);
37+
byte[] plainBytes = new byte[len];
38+
Randomness.get().nextBytes(plainBytes);
39+
SecretKey secretKey = generateSecretKey();
40+
int nonce = Randomness.get().nextInt();
41+
for (int packetLen : Arrays.asList(len - 1, len - 2, len - 3, len - 4)) {
42+
testEncryptAndDecryptSuccess(plainBytes, secretKey, nonce, packetLen);
43+
}
44+
}
45+
46+
public void testSuccessEncryptAndDecryptTypicalPacketLength() throws Exception {
47+
int len = 1024 + Randomness.get().nextInt(512);
48+
byte[] plainBytes = new byte[len];
49+
Randomness.get().nextBytes(plainBytes);
50+
SecretKey secretKey = generateSecretKey();
51+
int nonce = Randomness.get().nextInt();
52+
for (int packetLen : Arrays.asList(128, 256, 512)) {
53+
testEncryptAndDecryptSuccess(plainBytes, secretKey, nonce, packetLen);
54+
}
55+
}
56+
57+
public void testFailureEncryptAndDecryptWrongNonce() throws Exception {
58+
int len = 256 + Randomness.get().nextInt(256);
59+
// 2-3 packets
60+
int packetLen = 1 + Randomness.get().nextInt(len / 2);
61+
byte[] plainBytes = new byte[len];
62+
Randomness.get().nextBytes(plainBytes);
63+
SecretKey secretKey = generateSecretKey();
64+
int encryptNonce = Randomness.get().nextInt();
65+
int decryptNonce = Randomness.get().nextInt();
66+
while (decryptNonce == encryptNonce) {
67+
decryptNonce = Randomness.get().nextInt();
68+
}
69+
byte[] encryptedBytes;
70+
try (InputStream in = new EncryptionPacketsInputStream(new ByteArrayInputStream(plainBytes, 0, len), secretKey, encryptNonce,
71+
packetLen)) {
72+
encryptedBytes = in.readAllBytes();
73+
}
74+
try (InputStream in = new DecryptionPacketsInputStream(new ByteArrayInputStream(encryptedBytes), secretKey, decryptNonce,
75+
packetLen)) {
76+
IOException e = expectThrows(IOException.class, () -> {
77+
in.readAllBytes();
78+
});
79+
assertThat(e.getMessage(), Matchers.startsWith("Packet nonce mismatch."));
80+
}
81+
}
82+
83+
public void testFailureEncryptAndDecryptWrongKey() throws Exception {
84+
int len = 256 + Randomness.get().nextInt(256);
85+
// 2-3 packets
86+
int packetLen = 1 + Randomness.get().nextInt(len / 2);
87+
byte[] plainBytes = new byte[len];
88+
Randomness.get().nextBytes(plainBytes);
89+
SecretKey encryptSecretKey = generateSecretKey();
90+
SecretKey decryptSecretKey = generateSecretKey();
91+
int nonce = Randomness.get().nextInt();
92+
byte[] encryptedBytes;
93+
try (InputStream in = new EncryptionPacketsInputStream(new ByteArrayInputStream(plainBytes, 0, len), encryptSecretKey, nonce,
94+
packetLen)) {
95+
encryptedBytes = in.readAllBytes();
96+
}
97+
try (InputStream in = new DecryptionPacketsInputStream(new ByteArrayInputStream(encryptedBytes), decryptSecretKey, nonce,
98+
packetLen)) {
99+
IOException e = expectThrows(IOException.class, () -> {
100+
in.readAllBytes();
101+
});
102+
assertThat(e.getMessage(), Matchers.is("Exception during packet decryption"));
103+
}
104+
}
105+
106+
public void testFailureEncryptAndDecryptAlteredCiphertext() throws Exception {
107+
int len = 8 + Randomness.get().nextInt(8);
108+
// one packet
109+
int packetLen = len + Randomness.get().nextInt(8);
110+
byte[] plainBytes = new byte[len];
111+
Randomness.get().nextBytes(plainBytes);
112+
SecretKey secretKey = generateSecretKey();
113+
int nonce = Randomness.get().nextInt();
114+
byte[] encryptedBytes;
115+
try (InputStream in = new EncryptionPacketsInputStream(new ByteArrayInputStream(plainBytes, 0, len), secretKey, nonce,
116+
packetLen)) {
117+
encryptedBytes = in.readAllBytes();
118+
}
119+
for (int i = EncryptedRepository.GCM_IV_LENGTH_IN_BYTES; i < EncryptedRepository.GCM_IV_LENGTH_IN_BYTES + len +
120+
EncryptedRepository.GCM_TAG_LENGTH_IN_BYTES; i++) {
121+
for (int j = 0; j < 8; j++) {
122+
// flip bit
123+
encryptedBytes[i] ^= (1 << j);
124+
// fail decryption
125+
try (InputStream in = new DecryptionPacketsInputStream(new ByteArrayInputStream(encryptedBytes), secretKey, nonce,
126+
packetLen)) {
127+
IOException e = expectThrows(IOException.class, () -> {
128+
in.readAllBytes();
129+
});
130+
assertThat(e.getMessage(), Matchers.is("Exception during packet decryption"));
131+
}
132+
// flip bit back
133+
encryptedBytes[i] ^= (1 << j);
134+
}
135+
}
136+
}
137+
138+
public void testFailureEncryptAndDecryptAlteredCiphertextIV() throws Exception {
139+
int len = 8 + Randomness.get().nextInt(8);
140+
int packetLen = 4 + Randomness.get().nextInt(4);
141+
byte[] plainBytes = new byte[len];
142+
Randomness.get().nextBytes(plainBytes);
143+
SecretKey secretKey = generateSecretKey();
144+
int nonce = Randomness.get().nextInt();
145+
byte[] encryptedBytes;
146+
try (InputStream in = new EncryptionPacketsInputStream(new ByteArrayInputStream(plainBytes, 0, len), secretKey, nonce,
147+
packetLen)) {
148+
encryptedBytes = in.readAllBytes();
149+
}
150+
assertThat(encryptedBytes.length, Matchers.is((int) EncryptionPacketsInputStream.getEncryptionLength(len, packetLen)));
151+
int encryptedPacketLen = EncryptedRepository.GCM_IV_LENGTH_IN_BYTES + packetLen + EncryptedRepository.GCM_TAG_LENGTH_IN_BYTES;
152+
for (int i = 0; i < encryptedBytes.length; i += encryptedPacketLen) {
153+
for (int j = 0; j < EncryptedRepository.GCM_IV_LENGTH_IN_BYTES; j++) {
154+
for (int k = 0; k < 8; k++) {
155+
// flip bit
156+
encryptedBytes[i + j] ^= (1 << k);
157+
try (InputStream in = new DecryptionPacketsInputStream(new ByteArrayInputStream(encryptedBytes), secretKey, nonce,
158+
packetLen)) {
159+
IOException e = expectThrows(IOException.class, () -> {
160+
in.readAllBytes();
161+
});
162+
if (j < Integer.BYTES) {
163+
assertThat(e.getMessage(), Matchers.startsWith("Packet nonce mismatch"));
164+
} else {
165+
assertThat(e.getMessage(), Matchers.startsWith("Packet counter mismatch"));
166+
}
167+
}
168+
// flip bit back
169+
encryptedBytes[i + j] ^= (1 << k);
170+
}
171+
}
172+
}
173+
}
174+
175+
private void testEncryptAndDecryptSuccess(byte[] plainBytes, SecretKey secretKey, int nonce, int packetLen) throws Exception {
176+
for (int len = 0; len <= plainBytes.length; len++) {
177+
byte[] encryptedBytes;
178+
try (InputStream in = new EncryptionPacketsInputStream(new ByteArrayInputStream(plainBytes, 0, len), secretKey, nonce,
179+
packetLen)) {
180+
encryptedBytes = in.readAllBytes();
181+
}
182+
assertThat((long) encryptedBytes.length, Matchers.is(EncryptionPacketsInputStream.getEncryptionLength(len, packetLen)));
183+
byte[] decryptedBytes;
184+
try (InputStream in = new DecryptionPacketsInputStream(new ReadLessFilterInputStream(new ByteArrayInputStream(encryptedBytes)),
185+
secretKey, nonce, packetLen)) {
186+
decryptedBytes = in.readAllBytes();
187+
}
188+
assertThat(decryptedBytes.length, Matchers.is(len));
189+
assertThat((long) decryptedBytes.length, Matchers.is(DecryptionPacketsInputStream.getDecryptionLength(encryptedBytes.length,
190+
packetLen)));
191+
for (int i = 0; i < len; i++) {
192+
assertThat(decryptedBytes[i], Matchers.is(plainBytes[i]));
193+
}
194+
}
195+
}
196+
197+
// input stream that reads less bytes than asked to, testing that packet-wide reads don't rely on `read` calls for memory buffers which
198+
// always return the same number of bytes they are asked to
199+
private static class ReadLessFilterInputStream extends FilterInputStream {
200+
201+
protected ReadLessFilterInputStream(InputStream in) {
202+
super(in);
203+
}
204+
205+
@Override
206+
public int read(byte[] b, int off, int len) throws IOException {
207+
if (len == 0) {
208+
return 0;
209+
}
210+
return super.read(b, off, randomIntBetween(1, len));
211+
}
212+
}
213+
214+
private SecretKey generateSecretKey() throws Exception {
215+
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
216+
keyGen.init(256, new SecureRandom());
217+
return keyGen.generateKey();
218+
}
219+
}

Diff for: ‎x-pack/plugin/repository-encrypted/src/test/java/org/elasticsearch/repositories/encrypted/EncryptionPacketsInputStreamTests.java

+486
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.repositories.encrypted;
8+
9+
import org.elasticsearch.common.Randomness;
10+
import org.elasticsearch.common.collect.Tuple;
11+
import org.elasticsearch.test.ESTestCase;
12+
import org.hamcrest.Matchers;
13+
import org.mockito.invocation.InvocationOnMock;
14+
import org.mockito.stubbing.Answer;
15+
16+
import java.io.EOFException;
17+
import java.io.IOException;
18+
import java.io.InputStream;
19+
import java.util.concurrent.atomic.AtomicBoolean;
20+
import java.util.concurrent.atomic.AtomicInteger;
21+
22+
import static org.mockito.Mockito.doAnswer;
23+
import static org.mockito.Mockito.mock;
24+
import static org.mockito.Mockito.when;
25+
26+
public class PrefixInputStreamTests extends ESTestCase {
27+
28+
public void testZeroLength() throws Exception {
29+
Tuple<AtomicInteger, InputStream> mockTuple = getMockBoundedInputStream(0);
30+
PrefixInputStream test = new PrefixInputStream(mockTuple.v2(), 1 + Randomness.get().nextInt(32), randomBoolean());
31+
assertThat(test.available(), Matchers.is(0));
32+
assertThat(test.read(), Matchers.is(-1));
33+
assertThat(test.skip(1 + Randomness.get().nextInt(32)), Matchers.is(0L));
34+
}
35+
36+
public void testClose() throws Exception {
37+
int boundedLength = 1 + Randomness.get().nextInt(256);
38+
Tuple<AtomicInteger, InputStream> mockTuple = getMockBoundedInputStream(boundedLength);
39+
int prefixLength = Randomness.get().nextInt(boundedLength);
40+
PrefixInputStream test = new PrefixInputStream(mockTuple.v2(), prefixLength, randomBoolean());
41+
test.close();
42+
int byteCountBefore = mockTuple.v1().get();
43+
IOException e = expectThrows(IOException.class, () -> {
44+
test.read();
45+
});
46+
assertThat(e.getMessage(), Matchers.is("Stream has been closed"));
47+
e = expectThrows(IOException.class, () -> {
48+
byte[] b = new byte[1 + Randomness.get().nextInt(32)];
49+
test.read(b, 0, 1 + Randomness.get().nextInt(b.length));
50+
});
51+
assertThat(e.getMessage(), Matchers.is("Stream has been closed"));
52+
e = expectThrows(IOException.class, () -> {
53+
test.skip(1 + Randomness.get().nextInt(32));
54+
});
55+
assertThat(e.getMessage(), Matchers.is("Stream has been closed"));
56+
e = expectThrows(IOException.class, () -> {
57+
test.available();
58+
});
59+
assertThat(e.getMessage(), Matchers.is("Stream has been closed"));
60+
int byteCountAfter = mockTuple.v1().get();
61+
assertThat(byteCountBefore - byteCountAfter, Matchers.is(0));
62+
// test closeSource parameter
63+
AtomicBoolean isClosed = new AtomicBoolean(false);
64+
InputStream mockIn = mock(InputStream.class);
65+
doAnswer(new Answer<Void>() {
66+
public Void answer(InvocationOnMock invocation) {
67+
isClosed.set(true);
68+
return null;
69+
}
70+
}).when(mockIn).close();
71+
new PrefixInputStream(mockIn, 1 + Randomness.get().nextInt(32), true).close();
72+
assertThat(isClosed.get(), Matchers.is(true));
73+
isClosed.set(false);
74+
new PrefixInputStream(mockIn, 1 + Randomness.get().nextInt(32), false).close();
75+
assertThat(isClosed.get(), Matchers.is(false));
76+
}
77+
78+
public void testAvailable() throws Exception {
79+
AtomicInteger available = new AtomicInteger(0);
80+
int boundedLength = 1 + Randomness.get().nextInt(256);
81+
InputStream mockIn = mock(InputStream.class);
82+
when(mockIn.available()).thenAnswer(invocationOnMock -> {
83+
return available.get();
84+
});
85+
PrefixInputStream test = new PrefixInputStream(mockIn, boundedLength, randomBoolean());
86+
assertThat(test.available(), Matchers.is(0));
87+
available.set(Randomness.get().nextInt(boundedLength));
88+
assertThat(test.available(), Matchers.is(available.get()));
89+
available.set(boundedLength + 1 + Randomness.get().nextInt(boundedLength));
90+
assertThat(test.available(), Matchers.is(boundedLength));
91+
}
92+
93+
public void testReadPrefixLength() throws Exception {
94+
int boundedLength = 1 + Randomness.get().nextInt(256);
95+
Tuple<AtomicInteger, InputStream> mockTuple = getMockBoundedInputStream(boundedLength);
96+
int prefixLength = Randomness.get().nextInt(boundedLength);
97+
PrefixInputStream test = new PrefixInputStream(mockTuple.v2(), prefixLength, randomBoolean());
98+
int byteCountBefore = mockTuple.v1().get();
99+
byte[] b = test.readAllBytes();
100+
int byteCountAfter = mockTuple.v1().get();
101+
assertThat(b.length, Matchers.is(prefixLength));
102+
assertThat(byteCountBefore - byteCountAfter, Matchers.is(prefixLength));
103+
assertThat(test.read(), Matchers.is(-1));
104+
assertThat(test.available(), Matchers.is(0));
105+
assertThat(mockTuple.v2().read(), Matchers.not(-1));
106+
}
107+
108+
public void testSkipPrefixLength() throws Exception {
109+
int boundedLength = 1 + Randomness.get().nextInt(256);
110+
Tuple<AtomicInteger, InputStream> mockTuple = getMockBoundedInputStream(boundedLength);
111+
int prefixLength = Randomness.get().nextInt(boundedLength);
112+
PrefixInputStream test = new PrefixInputStream(mockTuple.v2(), prefixLength, randomBoolean());
113+
int byteCountBefore = mockTuple.v1().get();
114+
skipNBytes(test, prefixLength);
115+
int byteCountAfter = mockTuple.v1().get();
116+
assertThat(byteCountBefore - byteCountAfter, Matchers.is(prefixLength));
117+
assertThat(test.read(), Matchers.is(-1));
118+
assertThat(test.available(), Matchers.is(0));
119+
assertThat(mockTuple.v2().read(), Matchers.not(-1));
120+
}
121+
122+
public void testReadShorterWrapped() throws Exception {
123+
int boundedLength = 1 + Randomness.get().nextInt(256);
124+
Tuple<AtomicInteger, InputStream> mockTuple = getMockBoundedInputStream(boundedLength);
125+
int prefixLength = boundedLength;
126+
if (randomBoolean()) {
127+
prefixLength += 1 + Randomness.get().nextInt(boundedLength);
128+
}
129+
PrefixInputStream test = new PrefixInputStream(mockTuple.v2(), prefixLength, randomBoolean());
130+
int byteCountBefore = mockTuple.v1().get();
131+
byte[] b = test.readAllBytes();
132+
int byteCountAfter = mockTuple.v1().get();
133+
assertThat(b.length, Matchers.is(boundedLength));
134+
assertThat(byteCountBefore - byteCountAfter, Matchers.is(boundedLength));
135+
assertThat(test.read(), Matchers.is(-1));
136+
assertThat(test.available(), Matchers.is(0));
137+
assertThat(mockTuple.v2().read(), Matchers.is(-1));
138+
assertThat(mockTuple.v2().available(), Matchers.is(0));
139+
}
140+
141+
public void testSkipShorterWrapped() throws Exception {
142+
int boundedLength = 1 + Randomness.get().nextInt(256);
143+
Tuple<AtomicInteger, InputStream> mockTuple = getMockBoundedInputStream(boundedLength);
144+
final int prefixLength;
145+
if (randomBoolean()) {
146+
prefixLength = boundedLength + 1 + Randomness.get().nextInt(boundedLength);
147+
} else {
148+
prefixLength = boundedLength;
149+
}
150+
PrefixInputStream test = new PrefixInputStream(mockTuple.v2(), prefixLength, randomBoolean());
151+
int byteCountBefore = mockTuple.v1().get();
152+
if (prefixLength == boundedLength) {
153+
skipNBytes(test, prefixLength);
154+
} else {
155+
expectThrows(EOFException.class, () -> {
156+
skipNBytes(test, prefixLength);
157+
});
158+
}
159+
int byteCountAfter = mockTuple.v1().get();
160+
assertThat(byteCountBefore - byteCountAfter, Matchers.is(boundedLength));
161+
assertThat(test.read(), Matchers.is(-1));
162+
assertThat(test.available(), Matchers.is(0));
163+
assertThat(mockTuple.v2().read(), Matchers.is(-1));
164+
assertThat(mockTuple.v2().available(), Matchers.is(0));
165+
}
166+
167+
private Tuple<AtomicInteger, InputStream> getMockBoundedInputStream(int bound) throws IOException {
168+
InputStream mockSource = mock(InputStream.class);
169+
AtomicInteger bytesRemaining = new AtomicInteger(bound);
170+
when(mockSource.read(org.mockito.Matchers.<byte[]>any(), org.mockito.Matchers.anyInt(), org.mockito.Matchers.anyInt())).
171+
thenAnswer(invocationOnMock -> {
172+
final byte[] b = (byte[]) invocationOnMock.getArguments()[0];
173+
final int off = (int) invocationOnMock.getArguments()[1];
174+
final int len = (int) invocationOnMock.getArguments()[2];
175+
if (len == 0) {
176+
return 0;
177+
} else {
178+
if (bytesRemaining.get() <= 0) {
179+
return -1;
180+
}
181+
int bytesCount = 1 + Randomness.get().nextInt(Math.min(len, bytesRemaining.get()));
182+
bytesRemaining.addAndGet(-bytesCount);
183+
return bytesCount;
184+
}
185+
});
186+
when(mockSource.read()).thenAnswer(invocationOnMock -> {
187+
if (bytesRemaining.get() <= 0) {
188+
return -1;
189+
}
190+
bytesRemaining.decrementAndGet();
191+
return Randomness.get().nextInt(256);
192+
});
193+
when(mockSource.skip(org.mockito.Matchers.anyLong())).thenAnswer(invocationOnMock -> {
194+
final long n = (long) invocationOnMock.getArguments()[0];
195+
if (n <= 0 || bytesRemaining.get() <= 0) {
196+
return 0;
197+
}
198+
int bytesSkipped = 1 + Randomness.get().nextInt(Math.min(bytesRemaining.get(), Math.toIntExact(n)));
199+
bytesRemaining.addAndGet(-bytesSkipped);
200+
return bytesSkipped;
201+
});
202+
when(mockSource.available()).thenAnswer(invocationOnMock -> {
203+
if (bytesRemaining.get() <= 0) {
204+
return 0;
205+
}
206+
return 1 + Randomness.get().nextInt(bytesRemaining.get());
207+
});
208+
when(mockSource.markSupported()).thenReturn(false);
209+
return new Tuple<>(bytesRemaining, mockSource);
210+
}
211+
212+
private static void skipNBytes(InputStream in, long n) throws IOException {
213+
if (n > 0) {
214+
long ns = in.skip(n);
215+
if (ns >= 0 && ns < n) { // skipped too few bytes
216+
// adjust number to skip
217+
n -= ns;
218+
// read until requested number skipped or EOS reached
219+
while (n > 0 && in.read() != -1) {
220+
n--;
221+
}
222+
// if not enough skipped, then EOFE
223+
if (n != 0) {
224+
throw new EOFException();
225+
}
226+
} else if (ns != n) { // skipped negative or too many bytes
227+
throw new IOException("Unable to skip exactly");
228+
}
229+
}
230+
}
231+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Integration tests for repository-encrypted
2+
#
3+
"Plugin repository-encrypted is loaded":
4+
- skip:
5+
reason: "contains is a newly added assertion"
6+
features: contains
7+
- do:
8+
cluster.state: {}
9+
10+
# Get master node id
11+
- set: { master_node: master }
12+
13+
- do:
14+
nodes.info: {}
15+
16+
- contains: { nodes.$master.plugins: { name: repository-encrypted } }

0 commit comments

Comments
 (0)
Please sign in to comment.