Skip to content

Commit d19a8ba

Browse files
Configure masking algorithm default (#4336)
Signed-off-by: Terry Quigley <terry.quigley@sas.com> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent f065575 commit d19a8ba

File tree

7 files changed

+226
-55
lines changed

7 files changed

+226
-55
lines changed

src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
import java.util.stream.Stream;
5959

6060
import com.google.common.collect.Lists;
61+
import org.apache.commons.lang3.StringUtils;
6162
import org.apache.logging.log4j.LogManager;
6263
import org.apache.logging.log4j.Logger;
6364
import org.apache.lucene.search.QueryCachingPolicy;
@@ -430,6 +431,19 @@ public List<Path> run() {
430431
}
431432
}
432433

434+
try {
435+
String maskingAlgorithmDefault = settings.get(ConfigConstants.SECURITY_MASKED_FIELDS_ALGORITHM_DEFAULT);
436+
if (StringUtils.isNotEmpty(maskingAlgorithmDefault)) {
437+
MessageDigest.getInstance(maskingAlgorithmDefault);
438+
}
439+
} catch (Exception ex) {
440+
throw new OpenSearchSecurityException(
441+
"JVM does not support algorithm for {}",
442+
ex,
443+
ConfigConstants.SECURITY_MASKED_FIELDS_ALGORITHM_DEFAULT
444+
);
445+
}
446+
433447
if (!client && !settings.getAsBoolean(ConfigConstants.SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES, false)) {
434448
// check for demo certificates
435449
final List<String> files = AccessController.doPrivileged(new PrivilegedAction<List<String>>() {
@@ -1383,6 +1397,9 @@ public List<Setting<?>> getSettings() {
13831397
settings.add(
13841398
Setting.boolSetting(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ENABLE_TRANSPORT, true, Property.NodeScope, Property.Filtered)
13851399
);
1400+
settings.add(
1401+
Setting.simpleString(ConfigConstants.SECURITY_MASKED_FIELDS_ALGORITHM_DEFAULT, Property.NodeScope, Property.Filtered)
1402+
);
13861403
final List<String> disabledCategories = new ArrayList<String>(2);
13871404
disabledCategories.add("AUTHENTICATED");
13881405
disabledCategories.add("GRANTED_PRIVILEGES");

src/main/java/org/opensearch/security/configuration/DlsFlsFilterLeafReader.java

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ class DlsFlsFilterLeafReader extends SequentialStoredFieldsLeafReader {
106106
private final ShardId shardId;
107107
private final boolean maskFields;
108108
private final Salt salt;
109+
private final String maskingAlgorithmDefault;
109110

110111
private DlsGetEvaluator dge = null;
111112

@@ -130,7 +131,8 @@ class DlsFlsFilterLeafReader extends SequentialStoredFieldsLeafReader {
130131
this.clusterService = clusterService;
131132
this.auditlog = auditlog;
132133
this.salt = salt;
133-
this.maskedFieldsMap = MaskedFieldsMap.extractMaskedFields(maskFields, maskedFields, salt);
134+
this.maskingAlgorithmDefault = clusterService.getSettings().get(ConfigConstants.SECURITY_MASKED_FIELDS_ALGORITHM_DEFAULT);
135+
this.maskedFieldsMap = MaskedFieldsMap.extractMaskedFields(maskFields, maskedFields, salt, maskingAlgorithmDefault);
134136

135137
this.shardId = shardId;
136138
flsEnabled = includesExcludes != null && !includesExcludes.isEmpty();
@@ -292,11 +294,16 @@ private MaskedFieldsMap(Map<WildcardMatcher, MaskedField> maskedFieldsMap) {
292294
this.maskedFieldsMap = maskedFieldsMap;
293295
}
294296

295-
public static MaskedFieldsMap extractMaskedFields(boolean maskFields, Set<String> maskedFields, final Salt salt) {
297+
public static MaskedFieldsMap extractMaskedFields(
298+
boolean maskFields,
299+
Set<String> maskedFields,
300+
final Salt salt,
301+
String algorithmDefault
302+
) {
296303
if (maskFields) {
297304
return new MaskedFieldsMap(
298305
maskedFields.stream()
299-
.map(mf -> new MaskedField(mf, salt))
306+
.map(mf -> new MaskedField(mf, salt, algorithmDefault))
300307
.collect(ImmutableMap.toImmutableMap(mf -> WildcardMatcher.from(mf.getName()), Function.identity()))
301308
);
302309
} else {
@@ -1210,7 +1217,7 @@ private MaskedFieldsMap getRuntimeMaskedFieldInfo() {
12101217
if (maskedEval != null) {
12111218
final Set<String> mf = maskedFieldsMap.get(maskedEval);
12121219
if (mf != null && !mf.isEmpty()) {
1213-
return MaskedFieldsMap.extractMaskedFields(true, mf, salt);
1220+
return MaskedFieldsMap.extractMaskedFields(true, mf, salt, maskingAlgorithmDefault);
12141221
}
12151222

12161223
}

src/main/java/org/opensearch/security/configuration/MaskedField.java

Lines changed: 26 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import java.util.Objects;
2121

2222
import com.google.common.base.Splitter;
23+
import org.apache.commons.lang3.StringUtils;
2324
import org.apache.lucene.util.BytesRef;
2425
import org.bouncycastle.util.encoders.Hex;
2526

@@ -31,9 +32,11 @@ public class MaskedField {
3132
private String algo = null;
3233
private List<RegexReplacement> regexReplacements;
3334
private final byte[] defaultSalt;
35+
private final String defaultAlgorithm;
3436

35-
public MaskedField(final String value, final Salt salt) {
37+
public MaskedField(final String value, final Salt salt, final String defaultAlgorithm) {
3638
this.defaultSalt = salt.getSalt16();
39+
this.defaultAlgorithm = defaultAlgorithm;
3740
final List<String> tokens = Splitter.on("::").splitToList(Objects.requireNonNull(value));
3841
final int tokenCount = tokens.size();
3942
if (tokenCount == 1) {
@@ -57,31 +60,31 @@ public final void isValid() throws Exception {
5760
}
5861

5962
public byte[] mask(byte[] value) {
60-
if (isDefault()) {
61-
return blake2bHash(value);
63+
if (algo != null) {
64+
return customHash(value, algo);
65+
} else if (regexReplacements != null) {
66+
String cur = new String(value, StandardCharsets.UTF_8);
67+
for (RegexReplacement rr : regexReplacements) {
68+
cur = cur.replaceAll(rr.getRegex(), rr.getReplacement());
69+
}
70+
return cur.getBytes(StandardCharsets.UTF_8);
71+
} else if (StringUtils.isNotEmpty(defaultAlgorithm)) {
72+
return customHash(value, defaultAlgorithm);
6273
} else {
63-
return customHash(value);
74+
return blake2bHash(value);
6475
}
6576
}
6677

6778
public String mask(String value) {
68-
if (isDefault()) {
69-
return blake2bHash(value);
70-
} else {
71-
return customHash(value);
72-
}
79+
return new String(mask(value.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
7380
}
7481

7582
public BytesRef mask(BytesRef value) {
7683
if (value == null) {
7784
return null;
7885
}
79-
80-
if (isDefault()) {
81-
return blake2bHash(value);
82-
} else {
83-
return customHash(value);
84-
}
86+
final BytesRef copy = BytesRef.deepCopyOf(value);
87+
return new BytesRef(mask(copy.bytes));
8588
}
8689

8790
public String getName() {
@@ -126,6 +129,8 @@ public String toString() {
126129
+ regexReplacements
127130
+ ", defaultSalt="
128131
+ Arrays.toString(defaultSalt)
132+
+ ", defaultAlgorithm="
133+
+ defaultAlgorithm
129134
+ ", isDefault()="
130135
+ isDefault()
131136
+ "]";
@@ -135,35 +140,15 @@ private boolean isDefault() {
135140
return regexReplacements == null && algo == null;
136141
}
137142

138-
private byte[] customHash(byte[] in) {
139-
if (algo != null) {
140-
try {
141-
MessageDigest digest = MessageDigest.getInstance(algo);
142-
return Hex.encode(digest.digest(in));
143-
} catch (NoSuchAlgorithmException e) {
144-
throw new IllegalArgumentException(e);
145-
}
146-
} else if (regexReplacements != null) {
147-
String cur = new String(in, StandardCharsets.UTF_8);
148-
for (RegexReplacement rr : regexReplacements) {
149-
cur = cur.replaceAll(rr.getRegex(), rr.getReplacement());
150-
}
151-
return cur.getBytes(StandardCharsets.UTF_8);
152-
153-
} else {
154-
throw new IllegalArgumentException();
143+
private static byte[] customHash(byte[] in, final String algorithm) {
144+
try {
145+
MessageDigest digest = MessageDigest.getInstance(algorithm);
146+
return Hex.encode(digest.digest(in));
147+
} catch (NoSuchAlgorithmException e) {
148+
throw new IllegalArgumentException(e);
155149
}
156150
}
157151

158-
private BytesRef customHash(BytesRef in) {
159-
final BytesRef copy = BytesRef.deepCopyOf(in);
160-
return new BytesRef(customHash(copy.bytes));
161-
}
162-
163-
private String customHash(String in) {
164-
return new String(customHash(in.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
165-
}
166-
167152
private byte[] blake2bHash(byte[] in) {
168153
// Salt is passed incorrectly but order of parameters is retained at present to ensure full backwards compatibility
169154
// Tracking with https://github.com/opensearch-project/security/issues/4274
@@ -174,15 +159,6 @@ private byte[] blake2bHash(byte[] in) {
174159
return Hex.encode(out);
175160
}
176161

177-
private BytesRef blake2bHash(BytesRef in) {
178-
final BytesRef copy = BytesRef.deepCopyOf(in);
179-
return new BytesRef(blake2bHash(copy.bytes));
180-
}
181-
182-
private String blake2bHash(String in) {
183-
return new String(blake2bHash(in.getBytes(StandardCharsets.UTF_8)), StandardCharsets.UTF_8);
184-
}
185-
186162
private static class RegexReplacement {
187163
private final String regex;
188164
private final String replacement;

src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType;
3737
import org.opensearch.security.dlic.rest.validation.ValidationResult;
3838
import org.opensearch.security.securityconf.impl.CType;
39+
import org.opensearch.security.support.ConfigConstants;
3940
import org.opensearch.threadpool.ThreadPool;
4041

4142
import static org.opensearch.security.dlic.rest.api.RequestHandler.methodNotImplementedHandler;
@@ -91,7 +92,11 @@ private ValidationResult<JsonNode> validateMaskedFields(final JsonNode content)
9192

9293
private Pair<String, String> validateMaskedFieldSyntax(final JsonNode maskedFieldNode) {
9394
try {
94-
new MaskedField(maskedFieldNode.asText(), SALT).isValid();
95+
new MaskedField(
96+
maskedFieldNode.asText(),
97+
SALT,
98+
validationContext.settings().get(ConfigConstants.SECURITY_MASKED_FIELDS_ALGORITHM_DEFAULT)
99+
).isValid();
95100
} catch (Exception e) {
96101
return Pair.of(maskedFieldNode.asText(), e.getMessage());
97102
}

src/main/java/org/opensearch/security/support/ConfigConstants.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,7 @@ public enum RolesMappingResolution {
327327
public static final Boolean SECURITY_SYSTEM_INDICES_PERMISSIONS_DEFAULT = false;
328328
public static final String SECURITY_SYSTEM_INDICES_KEY = "plugins.security.system_indices.indices";
329329
public static final List<String> SECURITY_SYSTEM_INDICES_DEFAULT = Collections.emptyList();
330+
public static final String SECURITY_MASKED_FIELDS_ALGORITHM_DEFAULT = "plugins.security.masked_fields.algorithm.default";
330331

331332
public static final String TENANCY_PRIVATE_TENANT_NAME = "private";
332333
public static final String TENANCY_GLOBAL_TENANT_NAME = "global";

src/test/java/org/opensearch/security/dlic/dlsfls/CustomFieldMaskedTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import org.opensearch.action.index.IndexRequest;
1919
import org.opensearch.action.support.WriteRequest.RefreshPolicy;
2020
import org.opensearch.client.Client;
21+
import org.opensearch.common.settings.Settings;
2122
import org.opensearch.common.xcontent.XContentType;
23+
import org.opensearch.security.support.ConfigConstants;
2224
import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse;
2325

2426
public class CustomFieldMaskedTest extends AbstractDlsFlsTest {
@@ -272,4 +274,49 @@ public void testCustomMaskedGet() throws Exception {
272274
Assert.assertTrue(res.getBody().contains("***.100.1.XXX"));
273275
Assert.assertTrue(res.getBody().contains("123.123.1.XXX"));
274276
}
277+
278+
@Test
279+
public void testCustomMaskedGetWithClusterDefaultSHA3() throws Exception {
280+
281+
final Settings settings = Settings.builder().put(ConfigConstants.SECURITY_MASKED_FIELDS_ALGORITHM_DEFAULT, "SHA3-224").build();
282+
setup(settings);
283+
284+
HttpResponse res;
285+
286+
Assert.assertEquals(
287+
HttpStatus.SC_OK,
288+
(res = rh.executeGetRequest("/deals/_doc/0?pretty", encodeBasicHeader("admin", "admin"))).getStatusCode()
289+
);
290+
Assert.assertTrue(res.getBody().contains("\"found\" : true"));
291+
Assert.assertTrue(res.getBody().contains("cust1"));
292+
Assert.assertFalse(res.getBody().contains("cust2"));
293+
Assert.assertTrue(res.getBody().contains("100.100.1.1"));
294+
Assert.assertFalse(res.getBody().contains("100.100.2.2"));
295+
Assert.assertFalse(
296+
res.getBody()
297+
.contains(
298+
"8976994d0491e35f74fcac67ede9c83334a6ad34dae07c176df32f10225f93c5077ddd302c02ddd618b2406b1e4dfe50a727cbc880cfe264c552decf2d224ffc"
299+
)
300+
);
301+
Assert.assertFalse(res.getBody().contains("***"));
302+
Assert.assertFalse(res.getBody().contains("XXX"));
303+
304+
Assert.assertEquals(
305+
HttpStatus.SC_OK,
306+
(res = rh.executeGetRequest("/deals/_doc/0?pretty", encodeBasicHeader("user_masked_custom", "password"))).getStatusCode()
307+
);
308+
Assert.assertTrue(res.getBody().contains("\"found\" : true"));
309+
Assert.assertFalse(res.getBody().contains("cust1"));
310+
Assert.assertFalse(res.getBody().contains("cust2"));
311+
Assert.assertFalse(res.getBody().contains("100.100.1.1"));
312+
Assert.assertFalse(res.getBody().contains("100.100.2.2"));
313+
Assert.assertTrue(
314+
res.getBody()
315+
.contains(
316+
"8976994d0491e35f74fcac67ede9c83334a6ad34dae07c176df32f10225f93c5077ddd302c02ddd618b2406b1e4dfe50a727cbc880cfe264c552decf2d224ffc"
317+
)
318+
);
319+
Assert.assertTrue(res.getBody().contains("***.100.1.XXX"));
320+
Assert.assertTrue(res.getBody().contains("123.123.1.XXX"));
321+
}
275322
}

0 commit comments

Comments
 (0)