Skip to content

Commit 97a9860

Browse files
committed
Move CharArrays to core lib
This change cleans up some methods in the CharArrays class from x-pack, which includes the unification of char[] to utf8 and utf8 to char[] conversions that intentionally do not use strings. There was previously an implementation in x-pack and in the reloading of secure settings. The method from the reloading of secure settings was adopted as it handled more scenarios related to the backing byte and char buffers that were used to perform the conversions. The cleaned up class is moved into libs/core to allow it to be used by requests that will be migrated to the high level rest client. Relates elastic#32332
1 parent cd0de16 commit 97a9860

File tree

18 files changed

+257
-178
lines changed

18 files changed

+257
-178
lines changed
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.common;
21+
22+
import java.nio.ByteBuffer;
23+
import java.nio.CharBuffer;
24+
import java.nio.charset.StandardCharsets;
25+
import java.util.Arrays;
26+
import java.util.Objects;
27+
28+
/**
29+
* Helper class similar to Arrays to handle conversions for Char arrays
30+
*/
31+
public final class CharArrays {
32+
33+
private CharArrays() {}
34+
35+
/**
36+
* Decodes the provided byte[] to a UTF-8 char[]. This is done while avoiding
37+
* conversions to String. The provided byte[] is not modified by this method, so
38+
* the caller needs to take care of clearing the value if it is sensitive.
39+
*/
40+
public static char[] utf8BytesToChars(byte[] utf8Bytes) {
41+
final ByteBuffer byteBuffer = ByteBuffer.wrap(utf8Bytes);
42+
final CharBuffer charBuffer = StandardCharsets.UTF_8.decode(byteBuffer);
43+
final char[] chars;
44+
if (charBuffer.hasArray()) {
45+
// there is no guarantee that the char buffers backing array is the right size
46+
// so we need to make a copy
47+
chars = Arrays.copyOfRange(charBuffer.array(), charBuffer.position(), charBuffer.limit());
48+
Arrays.fill(charBuffer.array(), (char) 0); // clear sensitive data
49+
} else {
50+
final int length = charBuffer.limit() - charBuffer.position();
51+
chars = new char[length];
52+
charBuffer.get(chars);
53+
// if the buffer is not read only we can reset and fill with 0's
54+
if (charBuffer.isReadOnly() == false) {
55+
charBuffer.clear(); // reset
56+
for (int i = 0; i < charBuffer.limit(); i++) {
57+
charBuffer.put((char) 0);
58+
}
59+
}
60+
}
61+
return chars;
62+
}
63+
64+
/**
65+
* Encodes the provided char[] to a UTF-8 byte[]. This is done while avoiding
66+
* conversions to String. The provided char[] is not modified by this method, so
67+
* the caller needs to take care of clearing the value if it is sensitive.
68+
*/
69+
public static byte[] toUtf8Bytes(char[] chars) {
70+
final CharBuffer charBuffer = CharBuffer.wrap(chars);
71+
final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
72+
final byte[] bytes;
73+
if (byteBuffer.hasArray()) {
74+
// there is no guarantee that the byte buffers backing array is the right size
75+
// so we need to make a copy
76+
bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
77+
Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
78+
} else {
79+
final int length = byteBuffer.limit() - byteBuffer.position();
80+
bytes = new byte[length];
81+
byteBuffer.get(bytes);
82+
// if the buffer is not read only we can reset and fill with 0's
83+
if (byteBuffer.isReadOnly() == false) {
84+
byteBuffer.clear(); // reset
85+
for (int i = 0; i < byteBuffer.limit(); i++) {
86+
byteBuffer.put((byte) 0);
87+
}
88+
}
89+
}
90+
return bytes;
91+
}
92+
93+
/**
94+
* Tests if a char[] contains a sequence of characters that match the prefix. This is like
95+
* {@link String#startsWith(String)} but does not require conversion of the char[] to a string.
96+
*/
97+
public static boolean charsBeginsWith(String prefix, char[] chars) {
98+
if (chars == null || prefix == null) {
99+
return false;
100+
}
101+
102+
if (prefix.length() > chars.length) {
103+
return false;
104+
}
105+
106+
for (int i = 0; i < prefix.length(); i++) {
107+
if (chars[i] != prefix.charAt(i)) {
108+
return false;
109+
}
110+
}
111+
112+
return true;
113+
}
114+
115+
/**
116+
* Constant time equality check of char arrays to avoid potential timing attacks.
117+
*/
118+
public static boolean constantTimeEquals(char[] a, char[] b) {
119+
Objects.requireNonNull(a, "char arrays must not be null for constantTimeEquals");
120+
Objects.requireNonNull(b, "char arrays must not be null for constantTimeEquals");
121+
if (a.length != b.length) {
122+
return false;
123+
}
124+
125+
int equals = 0;
126+
for (int i = 0; i < a.length; i++) {
127+
equals |= a[i] ^ b[i];
128+
}
129+
130+
return equals == 0;
131+
}
132+
133+
/**
134+
* Constant time equality check of strings to avoid potential timing attacks.
135+
*/
136+
public static boolean constantTimeEquals(String a, String b) {
137+
Objects.requireNonNull(a, "strings must not be null for constantTimeEquals");
138+
Objects.requireNonNull(b, "strings must not be null for constantTimeEquals");
139+
if (a.length() != b.length()) {
140+
return false;
141+
}
142+
143+
int equals = 0;
144+
for (int i = 0; i < a.length(); i++) {
145+
equals |= a.charAt(i) ^ b.charAt(i);
146+
}
147+
148+
return equals == 0;
149+
}
150+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.common;
21+
22+
import org.elasticsearch.test.ESTestCase;
23+
24+
import java.nio.charset.StandardCharsets;
25+
26+
public class CharArraysTests extends ESTestCase {
27+
28+
public void testCharsToBytes() {
29+
final String originalValue = randomUnicodeOfCodepointLengthBetween(0, 32);
30+
final byte[] expectedBytes = originalValue.getBytes(StandardCharsets.UTF_8);
31+
final char[] valueChars = originalValue.toCharArray();
32+
33+
final byte[] convertedBytes = CharArrays.toUtf8Bytes(valueChars);
34+
assertArrayEquals(expectedBytes, convertedBytes);
35+
}
36+
37+
public void testBytesToUtf8Chars() {
38+
final String originalValue = randomUnicodeOfCodepointLengthBetween(0, 32);
39+
final byte[] bytes = originalValue.getBytes(StandardCharsets.UTF_8);
40+
final char[] expectedChars = originalValue.toCharArray();
41+
42+
final char[] convertedChars = CharArrays.utf8BytesToChars(bytes);
43+
assertArrayEquals(expectedChars, convertedChars);
44+
}
45+
46+
public void testCharsBeginsWith() {
47+
assertFalse(CharArrays.charsBeginsWith(randomAlphaOfLength(4), null));
48+
assertFalse(CharArrays.charsBeginsWith(null, null));
49+
assertFalse(CharArrays.charsBeginsWith(null, randomAlphaOfLength(4).toCharArray()));
50+
assertFalse(CharArrays.charsBeginsWith(randomAlphaOfLength(2), randomAlphaOfLengthBetween(3, 8).toCharArray()));
51+
52+
final String prefix = randomAlphaOfLengthBetween(2, 4);
53+
assertTrue(CharArrays.charsBeginsWith(prefix, prefix.toCharArray()));
54+
final char[] prefixedValue = prefix.concat(randomAlphaOfLengthBetween(1, 12)).toCharArray();
55+
assertTrue(CharArrays.charsBeginsWith(prefix, prefixedValue));
56+
57+
final String modifiedPrefix = randomBoolean() ? prefix.substring(1) : prefix.substring(0, prefix.length() - 1);
58+
final char[] nonMatchingValue = modifiedPrefix.concat(randomAlphaOfLengthBetween(0, 12)).toCharArray();
59+
assertFalse(CharArrays.charsBeginsWith(prefix, nonMatchingValue));
60+
assertTrue(CharArrays.charsBeginsWith(modifiedPrefix, nonMatchingValue));
61+
}
62+
63+
public void testConstantTimeEquals() {
64+
final String value = randomAlphaOfLengthBetween(0, 32);
65+
assertTrue(CharArrays.constantTimeEquals(value, value));
66+
assertTrue(CharArrays.constantTimeEquals(value.toCharArray(), value.toCharArray()));
67+
68+
final String other = randomAlphaOfLengthBetween(1, 32);
69+
assertFalse(CharArrays.constantTimeEquals(value, other));
70+
assertFalse(CharArrays.constantTimeEquals(value.toCharArray(), other.toCharArray()));
71+
}
72+
}

server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/NodesReloadSecureSettingsRequest.java

Lines changed: 3 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,12 @@
2222

2323
import org.elasticsearch.action.ActionRequestValidationException;
2424
import org.elasticsearch.action.support.nodes.BaseNodesRequest;
25+
import org.elasticsearch.common.CharArrays;
2526
import org.elasticsearch.common.io.stream.StreamInput;
2627
import org.elasticsearch.common.io.stream.StreamOutput;
2728
import org.elasticsearch.common.settings.SecureString;
2829

2930
import java.io.IOException;
30-
import java.nio.ByteBuffer;
31-
import java.nio.CharBuffer;
32-
import java.nio.charset.StandardCharsets;
3331
import java.util.Arrays;
3432

3533
import static org.elasticsearch.action.ValidateActions.addValidationError;
@@ -83,7 +81,7 @@ public void readFrom(StreamInput in) throws IOException {
8381
super.readFrom(in);
8482
final byte[] passwordBytes = in.readByteArray();
8583
try {
86-
this.secureSettingsPassword = new SecureString(utf8BytesToChars(passwordBytes));
84+
this.secureSettingsPassword = new SecureString(CharArrays.utf8BytesToChars(passwordBytes));
8785
} finally {
8886
Arrays.fill(passwordBytes, (byte) 0);
8987
}
@@ -92,69 +90,11 @@ public void readFrom(StreamInput in) throws IOException {
9290
@Override
9391
public void writeTo(StreamOutput out) throws IOException {
9492
super.writeTo(out);
95-
final byte[] passwordBytes = charsToUtf8Bytes(this.secureSettingsPassword.getChars());
93+
final byte[] passwordBytes = CharArrays.toUtf8Bytes(this.secureSettingsPassword.getChars());
9694
try {
9795
out.writeByteArray(passwordBytes);
9896
} finally {
9997
Arrays.fill(passwordBytes, (byte) 0);
10098
}
10199
}
102-
103-
/**
104-
* Encodes the provided char[] to a UTF-8 byte[]. This is done while avoiding
105-
* conversions to String. The provided char[] is not modified by this method, so
106-
* the caller needs to take care of clearing the value if it is sensitive.
107-
*/
108-
private static byte[] charsToUtf8Bytes(char[] chars) {
109-
final CharBuffer charBuffer = CharBuffer.wrap(chars);
110-
final ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
111-
final byte[] bytes;
112-
if (byteBuffer.hasArray()) {
113-
// there is no guarantee that the byte buffers backing array is the right size
114-
// so we need to make a copy
115-
bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit());
116-
Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
117-
} else {
118-
final int length = byteBuffer.limit() - byteBuffer.position();
119-
bytes = new byte[length];
120-
byteBuffer.get(bytes);
121-
// if the buffer is not read only we can reset and fill with 0's
122-
if (byteBuffer.isReadOnly() == false) {
123-
byteBuffer.clear(); // reset
124-
for (int i = 0; i < byteBuffer.limit(); i++) {
125-
byteBuffer.put((byte) 0);
126-
}
127-
}
128-
}
129-
return bytes;
130-
}
131-
132-
/**
133-
* Decodes the provided byte[] to a UTF-8 char[]. This is done while avoiding
134-
* conversions to String. The provided byte[] is not modified by this method, so
135-
* the caller needs to take care of clearing the value if it is sensitive.
136-
*/
137-
public static char[] utf8BytesToChars(byte[] utf8Bytes) {
138-
final ByteBuffer byteBuffer = ByteBuffer.wrap(utf8Bytes);
139-
final CharBuffer charBuffer = StandardCharsets.UTF_8.decode(byteBuffer);
140-
final char[] chars;
141-
if (charBuffer.hasArray()) {
142-
// there is no guarantee that the char buffers backing array is the right size
143-
// so we need to make a copy
144-
chars = Arrays.copyOfRange(charBuffer.array(), charBuffer.position(), charBuffer.limit());
145-
Arrays.fill(charBuffer.array(), (char) 0); // clear sensitive data
146-
} else {
147-
final int length = charBuffer.limit() - charBuffer.position();
148-
chars = new char[length];
149-
charBuffer.get(chars);
150-
// if the buffer is not read only we can reset and fill with 0's
151-
if (charBuffer.isReadOnly() == false) {
152-
charBuffer.clear(); // reset
153-
for (int i = 0; i < charBuffer.limit(); i++) {
154-
charBuffer.put((char) 0);
155-
}
156-
}
157-
}
158-
return chars;
159-
}
160100
}

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import org.elasticsearch.common.io.stream.StreamInput;
1616
import org.elasticsearch.common.io.stream.StreamOutput;
1717
import org.elasticsearch.common.settings.SecureString;
18-
import org.elasticsearch.xpack.core.security.authc.support.CharArrays;
18+
import org.elasticsearch.common.CharArrays;
1919

2020
import java.io.IOException;
2121
import java.util.Arrays;

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/ChangePasswordRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import org.elasticsearch.common.bytes.BytesReference;
1313
import org.elasticsearch.common.io.stream.StreamInput;
1414
import org.elasticsearch.common.io.stream.StreamOutput;
15-
import org.elasticsearch.xpack.core.security.authc.support.CharArrays;
15+
import org.elasticsearch.common.CharArrays;
1616

1717
import java.io.IOException;
1818

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import org.elasticsearch.common.io.stream.StreamInput;
1515
import org.elasticsearch.common.io.stream.StreamOutput;
1616
import org.elasticsearch.common.settings.Settings;
17-
import org.elasticsearch.xpack.core.security.authc.support.CharArrays;
17+
import org.elasticsearch.common.CharArrays;
1818
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
1919
import org.elasticsearch.xpack.core.security.support.Validation;
2020

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/support/BCrypt.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1515
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1616

17+
import org.elasticsearch.common.CharArrays;
1718
import org.elasticsearch.common.settings.SecureString;
1819

1920
import java.security.SecureRandom;
@@ -54,7 +55,7 @@
5455
* String stronger_salt = BCrypt.gensalt(12)<br>
5556
* </code>
5657
* <p>
57-
* The amount of work increases exponentially (2**log_rounds), so
58+
* The amount of work increases exponentially (2**log_rounds), so
5859
* each increment is twice as much work. The default log_rounds is
5960
* 10, and the valid range is 4 to 30.
6061
*
@@ -689,7 +690,11 @@ public static String hashpw(SecureString password, String salt) {
689690

690691
// the next lines are the SecureString replacement for the above commented-out section
691692
if (minor >= 'a') {
692-
try (SecureString secureString = new SecureString(CharArrays.concat(password.getChars(), "\000".toCharArray()))) {
693+
final char[] suffix = "\000".toCharArray();
694+
final char[] result = new char[password.length() + suffix.length];
695+
System.arraycopy(password.getChars(), 0, result, 0, password.length());
696+
System.arraycopy(suffix, 0, result, password.length(), suffix.length);
697+
try (SecureString secureString = new SecureString(result)) {
693698
passwordb = CharArrays.toUtf8Bytes(secureString.getChars());
694699
}
695700
} else {

0 commit comments

Comments
 (0)