Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
* Copyright 2016-2024 Ping Identity Corporation
* All Rights Reserved.
*/
/*
* Copyright 2016-2024 Ping Identity Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright (C) 2016-2024 Ping Identity Corporation
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License (GPLv2 only)
* or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, see .
*/
package com.unboundid.ldap.sdk.transformations;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import com.unboundid.ldap.matchingrules.BooleanMatchingRule;
import com.unboundid.ldap.matchingrules.CaseIgnoreStringMatchingRule;
import com.unboundid.ldap.matchingrules.DistinguishedNameMatchingRule;
import com.unboundid.ldap.matchingrules.GeneralizedTimeMatchingRule;
import com.unboundid.ldap.matchingrules.IntegerMatchingRule;
import com.unboundid.ldap.matchingrules.MatchingRule;
import com.unboundid.ldap.matchingrules.NumericStringMatchingRule;
import com.unboundid.ldap.matchingrules.OctetStringMatchingRule;
import com.unboundid.ldap.matchingrules.TelephoneNumberMatchingRule;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.Modification;
import com.unboundid.ldap.sdk.RDN;
import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
import com.unboundid.ldap.sdk.schema.Schema;
import com.unboundid.ldif.LDIFAddChangeRecord;
import com.unboundid.ldif.LDIFChangeRecord;
import com.unboundid.ldif.LDIFDeleteChangeRecord;
import com.unboundid.ldif.LDIFModifyChangeRecord;
import com.unboundid.ldif.LDIFModifyDNChangeRecord;
import com.unboundid.util.Debug;
import com.unboundid.util.NotNull;
import com.unboundid.util.Nullable;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.ThreadLocalRandom;
import com.unboundid.util.ThreadSafety;
import com.unboundid.util.ThreadSafetyLevel;
import com.unboundid.util.json.JSONArray;
import com.unboundid.util.json.JSONBoolean;
import com.unboundid.util.json.JSONNumber;
import com.unboundid.util.json.JSONObject;
import com.unboundid.util.json.JSONString;
import com.unboundid.util.json.JSONValue;
/**
* This class provides an implementation of an entry and change record
* transformation that may be used to scramble the values of a specified set of
* attributes in a way that attempts to obscure the original values but that
* preserves the syntax for the values. When possible the scrambling will be
* performed in a repeatable manner, so that a given input value will
* consistently yield the same scrambled representation.
*/
@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
public final class ScrambleAttributeTransformation
implements EntryTransformation, LDIFChangeRecordTransformation
{
/**
* The characters in the set of ASCII numeric digits.
*/
@NotNull private static final char[] ASCII_DIGITS =
"0123456789".toCharArray();
/**
* The set of ASCII symbols, which are printable ASCII characters that are not
* letters or digits.
*/
@NotNull private static final char[] ASCII_SYMBOLS =
" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".toCharArray();
/**
* The characters in the set of lowercase ASCII letters.
*/
@NotNull private static final char[] LOWERCASE_ASCII_LETTERS =
"abcdefghijklmnopqrstuvwxyz".toCharArray();
/**
* The characters in the set of uppercase ASCII letters.
*/
@NotNull private static final char[] UPPERCASE_ASCII_LETTERS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
/**
* The number of milliseconds in a day.
*/
private static final long MILLIS_PER_DAY =
1000L * // 1000 milliseconds per second
60L * // 60 seconds per minute
60L * // 60 minutes per hour
24L; // 24 hours per day
// Indicates whether to scramble attribute values in entry DNs.
private final boolean scrambleEntryDNs;
// The seed to use for the random number generator.
private final long randomSeed;
// The time this transformation was created.
private final long createTime;
// The schema to use when processing.
@Nullable private final Schema schema;
// The names of the attributes to scramble.
@NotNull private final Map attributes;
// The names of the JSON fields to scramble.
@NotNull private final Set jsonFields;
// A thread-local collection of reusable random number generators.
@NotNull private final ThreadLocal randoms;
/**
* Creates a new scramble attribute transformation that will scramble the
* values of the specified attributes. A default standard schema will be
* used, entry DNs will not be scrambled, and if any of the target attributes
* have values that are JSON objects, the values of all of those objects'
* fields will be scrambled.
*
* @param attributes The names or OIDs of the attributes to scramble.
*/
public ScrambleAttributeTransformation(@NotNull final String... attributes)
{
this(null, null, attributes);
}
/**
* Creates a new scramble attribute transformation that will scramble the
* values of the specified attributes. A default standard schema will be
* used, entry DNs will not be scrambled, and if any of the target attributes
* have values that are JSON objects, the values of all of those objects'
* fields will be scrambled.
*
* @param attributes The names or OIDs of the attributes to scramble.
*/
public ScrambleAttributeTransformation(
@NotNull final Collection attributes)
{
this(null, null, false, attributes, null);
}
/**
* Creates a new scramble attribute transformation that will scramble the
* values of a specified set of attributes. Entry DNs will not be scrambled,
* and if any of the target attributes have values that are JSON objects, the
* values of all of those objects' fields will be scrambled.
*
* @param schema The schema to use when processing. This may be
* {@code null} if a default standard schema should be
* used. The schema will be used to identify alternate
* names that may be used to reference the attributes, and
* to determine the expected syntax for more accurate
* scrambling.
* @param randomSeed The seed to use for the random number generator when
* scrambling each value. It may be {@code null} if the
* random seed should be automatically selected.
* @param attributes The names or OIDs of the attributes to scramble.
*/
public ScrambleAttributeTransformation(@Nullable final Schema schema,
@Nullable final Long randomSeed,
@NotNull final String... attributes)
{
this(schema, randomSeed, false, StaticUtils.toList(attributes), null);
}
/**
* Creates a new scramble attribute transformation that will scramble the
* values of a specified set of attributes.
*
* @param schema The schema to use when processing. This may be
* {@code null} if a default standard schema should
* be used. The schema will be used to identify
* alternate names that may be used to reference the
* attributes, and to determine the expected syntax
* for more accurate scrambling.
* @param randomSeed The seed to use for the random number generator
* when scrambling each value. It may be
* {@code null} if the random seed should be
* automatically selected.
* @param scrambleEntryDNs Indicates whether to scramble any appropriate
* attributes contained in entry DNs and the values
* of attributes with a DN syntax.
* @param attributes The names or OIDs of the attributes to scramble.
* @param jsonFields The names of the JSON fields whose values should
* be scrambled. If any field names are specified,
* then any JSON objects to be scrambled will only
* have those fields scrambled (with field names
* treated in a case-insensitive manner) and all
* other fields will be preserved without
* scrambling. If this is {@code null} or empty,
* then scrambling will be applied for all values in
* all fields.
*/
public ScrambleAttributeTransformation(@Nullable final Schema schema,
@Nullable final Long randomSeed,
final boolean scrambleEntryDNs,
@NotNull final Collection attributes,
@Nullable final Collection jsonFields)
{
createTime = System.currentTimeMillis();
randoms = new ThreadLocal<>();
this.scrambleEntryDNs = scrambleEntryDNs;
// If a random seed was provided, then use it. Otherwise, select one.
if (randomSeed == null)
{
this.randomSeed = ThreadLocalRandom.get().nextLong();
}
else
{
this.randomSeed = randomSeed;
}
// If a schema was provided, then use it. Otherwise, use the default
// standard schema.
Schema s = schema;
if (s == null)
{
try
{
s = Schema.getDefaultStandardSchema();
}
catch (final Exception e)
{
// This should never happen.
Debug.debugException(e);
}
}
this.schema = s;
// Iterate through the set of provided attribute names. Identify all of the
// alternate names (including the OID) that may be used to reference the
// attribute, and identify the associated matching rule.
final HashMap m =
new HashMap<>(StaticUtils.computeMapCapacity(10));
for (final String a : attributes)
{
final String baseName = StaticUtils.toLowerCase(Attribute.getBaseName(a));
AttributeTypeDefinition at = null;
if (schema != null)
{
at = schema.getAttributeType(baseName);
}
if (at == null)
{
m.put(baseName, CaseIgnoreStringMatchingRule.getInstance());
}
else
{
final MatchingRule mr =
MatchingRule.selectEqualityMatchingRule(baseName, schema);
m.put(StaticUtils.toLowerCase(at.getOID()), mr);
for (final String attrName : at.getNames())
{
m.put(StaticUtils.toLowerCase(attrName), mr);
}
}
}
this.attributes = Collections.unmodifiableMap(m);
// See if any JSON fields were specified. If so, then process them.
if (jsonFields == null)
{
this.jsonFields = Collections.emptySet();
}
else
{
final HashSet fieldNames =
new HashSet<>(StaticUtils.computeMapCapacity(jsonFields.size()));
for (final String fieldName : jsonFields)
{
fieldNames.add(StaticUtils.toLowerCase(fieldName));
}
this.jsonFields = Collections.unmodifiableSet(fieldNames);
}
}
/**
* {@inheritDoc}
*/
@Override()
@Nullable()
public Entry transformEntry(@NotNull final Entry e)
{
if (e == null)
{
return null;
}
final String dn;
if (scrambleEntryDNs)
{
dn = scrambleDN(e.getDN());
}
else
{
dn = e.getDN();
}
final Collection originalAttributes = e.getAttributes();
final ArrayList scrambledAttributes =
new ArrayList<>(originalAttributes.size());
for (final Attribute a : originalAttributes)
{
scrambledAttributes.add(scrambleAttribute(a));
}
return new Entry(dn, schema, scrambledAttributes);
}
/**
* {@inheritDoc}
*/
@Override()
@Nullable()
public LDIFChangeRecord transformChangeRecord(
@NotNull final LDIFChangeRecord r)
{
if (r == null)
{
return null;
}
// If it's an add change record, then just use the same processing as for an
// entry.
if (r instanceof LDIFAddChangeRecord)
{
final LDIFAddChangeRecord addRecord = (LDIFAddChangeRecord) r;
return new LDIFAddChangeRecord(transformEntry(addRecord.getEntryToAdd()),
addRecord.getControls());
}
// If it's a delete change record, then see if we need to scramble the DN.
if (r instanceof LDIFDeleteChangeRecord)
{
if (scrambleEntryDNs)
{
return new LDIFDeleteChangeRecord(scrambleDN(r.getDN()),
r.getControls());
}
else
{
return r;
}
}
// If it's a modify change record, then scramble all of the appropriate
// modification values.
if (r instanceof LDIFModifyChangeRecord)
{
final LDIFModifyChangeRecord modifyRecord = (LDIFModifyChangeRecord) r;
final Modification[] originalMods = modifyRecord.getModifications();
final Modification[] newMods = new Modification[originalMods.length];
for (int i=0; i < originalMods.length; i++)
{
// If the modification doesn't have any values, then just use the
// original modification.
final Modification m = originalMods[i];
if (! m.hasValue())
{
newMods[i] = m;
continue;
}
// See if the modification targets an attribute that we should scramble.
// If not, then just use the original modification.
final String attrName = StaticUtils.toLowerCase(
Attribute.getBaseName(m.getAttributeName()));
if (! attributes.containsKey(attrName))
{
newMods[i] = m;
continue;
}
// Scramble the values just like we do for an attribute.
final Attribute scrambledAttribute =
scrambleAttribute(m.getAttribute());
newMods[i] = new Modification(m.getModificationType(),
m.getAttributeName(), scrambledAttribute.getRawValues());
}
if (scrambleEntryDNs)
{
return new LDIFModifyChangeRecord(scrambleDN(modifyRecord.getDN()),
newMods, modifyRecord.getControls());
}
else
{
return new LDIFModifyChangeRecord(modifyRecord.getDN(), newMods,
modifyRecord.getControls());
}
}
// If it's a modify DN change record, then see if we need to scramble any
// of the components.
if (r instanceof LDIFModifyDNChangeRecord)
{
if (scrambleEntryDNs)
{
final LDIFModifyDNChangeRecord modDNRecord =
(LDIFModifyDNChangeRecord) r;
return new LDIFModifyDNChangeRecord(scrambleDN(modDNRecord.getDN()),
scrambleDN(modDNRecord.getNewRDN()), modDNRecord.deleteOldRDN(),
scrambleDN(modDNRecord.getNewSuperiorDN()),
modDNRecord.getControls());
}
else
{
return r;
}
}
// This should never happen.
return r;
}
/**
* Creates a scrambled copy of the provided DN. If the DN contains any
* components with attributes to be scrambled, then the values of those
* attributes will be scrambled appropriately. If the DN does not contain
* any components with attributes to be scrambled, then no changes will be
* made.
*
* @param dn The DN to be scrambled.
*
* @return A scrambled copy of the provided DN, or the original DN if no
* scrambling is required or the provided string cannot be parsed as
* a valid DN.
*/
@Nullable()
public String scrambleDN(@Nullable() final String dn)
{
if (dn == null)
{
return null;
}
try
{
return scrambleDN(new DN(dn)).toString();
}
catch (final Exception e)
{
Debug.debugException(e);
return dn;
}
}
/**
* Creates a scrambled copy of the provided DN. If the DN contains any
* components with attributes to be scrambled, then the values of those
* attributes will be scrambled appropriately. If the DN does not contain
* any components with attributes to be scrambled, then no changes will be
* made.
*
* @param dn The DN to be scrambled.
*
* @return A scrambled copy of the provided DN, or the original DN if no
* scrambling is required.
*/
@Nullable()
public DN scrambleDN(@Nullable final DN dn)
{
if ((dn == null) || dn.isNullDN())
{
return dn;
}
boolean changeApplied = false;
final RDN[] originalRDNs = dn.getRDNs();
final RDN[] scrambledRDNs = new RDN[originalRDNs.length];
for (int i=0; i < originalRDNs.length; i++)
{
scrambledRDNs[i] = scrambleRDN(originalRDNs[i]);
if (scrambledRDNs[i] != originalRDNs[i])
{
changeApplied = true;
}
}
if (changeApplied)
{
return new DN(scrambledRDNs);
}
else
{
return dn;
}
}
/**
* Creates a scrambled copy of the provided RDN. If the RDN contains any
* attributes to be scrambled, then the values of those attributes will be
* scrambled appropriately. If the RDN does not contain any attributes to be
* scrambled, then no changes will be made.
*
* @param rdn The RDN to be scrambled. It must not be {@code null}.
*
* @return A scrambled copy of the provided RDN, or the original RDN if no
* scrambling is required.
*/
@NotNull()
public RDN scrambleRDN(@NotNull final RDN rdn)
{
boolean changeRequired = false;
final String[] names = rdn.getAttributeNames();
for (final String s : names)
{
final String lowerBaseName =
StaticUtils.toLowerCase(Attribute.getBaseName(s));
if (attributes.containsKey(lowerBaseName))
{
changeRequired = true;
break;
}
}
if (! changeRequired)
{
return rdn;
}
final Attribute[] originalAttrs = rdn.getAttributes();
final byte[][] scrambledValues = new byte[originalAttrs.length][];
for (int i=0; i < originalAttrs.length; i++)
{
scrambledValues[i] =
scrambleAttribute(originalAttrs[i]).getValueByteArray();
}
return new RDN(names, scrambledValues, schema);
}
/**
* Creates a copy of the provided attribute with its values scrambled if
* appropriate.
*
* @param a The attribute to scramble.
*
* @return A copy of the provided attribute with its values scrambled, or
* the original attribute if no scrambling should be performed.
*/
@Nullable()
public Attribute scrambleAttribute(@NotNull final Attribute a)
{
if ((a == null) || (a.size() == 0))
{
return a;
}
final String baseName = StaticUtils.toLowerCase(a.getBaseName());
final MatchingRule matchingRule = attributes.get(baseName);
if (matchingRule == null)
{
return a;
}
if (matchingRule instanceof BooleanMatchingRule)
{
// In the case of a boolean value, we won't try to create reproducible
// results. We will just pick boolean values at random.
if (a.size() == 1)
{
return new Attribute(a.getName(), schema,
ThreadLocalRandom.get().nextBoolean() ? "TRUE" : "FALSE");
}
else
{
// This is highly unusual, but since there are only two possible valid
// boolean values, we will return an attribute with both values,
// regardless of how many values the provided attribute actually had.
return new Attribute(a.getName(), schema, "TRUE", "FALSE");
}
}
else if (matchingRule instanceof DistinguishedNameMatchingRule)
{
final String[] originalValues = a.getValues();
final String[] scrambledValues = new String[originalValues.length];
for (int i=0; i < originalValues.length; i++)
{
try
{
scrambledValues[i] = scrambleDN(new DN(originalValues[i])).toString();
}
catch (final Exception e)
{
Debug.debugException(e);
scrambledValues[i] = scrambleString(originalValues[i]);
}
}
return new Attribute(a.getName(), schema, scrambledValues);
}
else if (matchingRule instanceof GeneralizedTimeMatchingRule)
{
final String[] originalValues = a.getValues();
final String[] scrambledValues = new String[originalValues.length];
for (int i=0; i < originalValues.length; i++)
{
scrambledValues[i] = scrambleGeneralizedTime(originalValues[i]);
}
return new Attribute(a.getName(), schema, scrambledValues);
}
else if ((matchingRule instanceof IntegerMatchingRule) ||
(matchingRule instanceof NumericStringMatchingRule) ||
(matchingRule instanceof TelephoneNumberMatchingRule))
{
final String[] originalValues = a.getValues();
final String[] scrambledValues = new String[originalValues.length];
for (int i=0; i < originalValues.length; i++)
{
scrambledValues[i] = scrambleNumericValue(originalValues[i]);
}
return new Attribute(a.getName(), schema, scrambledValues);
}
else if (matchingRule instanceof OctetStringMatchingRule)
{
// If the target attribute is userPassword, then treat it like an encoded
// password.
final byte[][] originalValues = a.getValueByteArrays();
final byte[][] scrambledValues = new byte[originalValues.length][];
for (int i=0; i < originalValues.length; i++)
{
if (baseName.equals("userpassword") || baseName.equals("2.5.4.35"))
{
scrambledValues[i] = StaticUtils.getBytes(scrambleEncodedPassword(
StaticUtils.toUTF8String(originalValues[i])));
}
else
{
scrambledValues[i] = scrambleBinaryValue(originalValues[i]);
}
}
return new Attribute(a.getName(), schema, scrambledValues);
}
else
{
final String[] originalValues = a.getValues();
final String[] scrambledValues = new String[originalValues.length];
for (int i=0; i < originalValues.length; i++)
{
if (baseName.equals("userpassword") || baseName.equals("2.5.4.35") ||
baseName.equals("authpassword") ||
baseName.equals("1.3.6.1.4.1.4203.1.3.4"))
{
scrambledValues[i] = scrambleEncodedPassword(originalValues[i]);
}
else if (originalValues[i].startsWith("{") &&
originalValues[i].endsWith("}"))
{
scrambledValues[i] = scrambleJSONObject(originalValues[i]);
}
else
{
scrambledValues[i] = scrambleString(originalValues[i]);
}
}
return new Attribute(a.getName(), schema, scrambledValues);
}
}
/**
* Scrambles the provided generalized time value. If the provided value can
* be parsed as a valid generalized time, then the resulting value will be a
* generalized time in the same format but with the timestamp randomized. The
* randomly-selected time will adhere to the following constraints:
*
*
* The range for the timestamp will be twice the size of the current time
* and the original timestamp. If the original timestamp is within one
* day of the current time, then the original range will be expanded by
* an additional one day.
*
*
* If the original timestamp is in the future, then the scrambled
* timestamp will also be in the future. Otherwise, it will be in the
* past.
*
*
*
* @param s The value to scramble.
*
* @return The scrambled value.
*/
@Nullable()
public String scrambleGeneralizedTime(@Nullable final String s)
{
if (s == null)
{
return null;
}
// See if we can parse the value as a generalized time. If not, then just
// apply generic scrambling.
final long decodedTime;
final Random random = getRandom(s);
try
{
decodedTime = StaticUtils.decodeGeneralizedTime(s).getTime();
}
catch (final Exception e)
{
Debug.debugException(e);
return scrambleString(s);
}
// We want to choose a timestamp at random, but we still want to pick
// something that is reasonably close to the provided value. To start
// with, see how far away the timestamp is from the time this attribute
// scrambler was created. If it's less than one day, then add one day to
// it. Then, double the resulting value.
long timeSpan = Math.abs(createTime - decodedTime);
if (timeSpan < MILLIS_PER_DAY)
{
timeSpan += MILLIS_PER_DAY;
}
timeSpan *= 2;
// Generate a random value between zero and the computed time span.
final long randomLong = (random.nextLong() & 0x7FFF_FFFF_FFFF_FFFFL);
final long randomOffset = randomLong % timeSpan;
// If the provided timestamp is in the future, then add the randomly-chosen
// offset to the time that this attribute scrambler was created. Otherwise,
// subtract it from the time that this attribute scrambler was created.
final long randomTime;
if (decodedTime > createTime)
{
randomTime = createTime + randomOffset;
}
else
{
randomTime = createTime - randomOffset;
}
// Create a generalized time representation of the provided value.
final String generalizedTime =
StaticUtils.encodeGeneralizedTime(randomTime);
// We want to preserve the original precision and time zone specifier for
// the timestamp, so just take as much of the generalized time value as we
// need to do that.
boolean stillInGeneralizedTime = true;
final StringBuilder scrambledValue = new StringBuilder(s.length());
for (int i=0; i < s.length(); i++)
{
final char originalCharacter = s.charAt(i);
if (stillInGeneralizedTime)
{
if ((i < generalizedTime.length()) &&
(originalCharacter >= '0') && (originalCharacter <= '9'))
{
final char generalizedTimeCharacter = generalizedTime.charAt(i);
if ((generalizedTimeCharacter >= '0') &&
(generalizedTimeCharacter <= '9'))
{
scrambledValue.append(generalizedTimeCharacter);
}
else
{
scrambledValue.append(originalCharacter);
if (generalizedTimeCharacter != '.')
{
stillInGeneralizedTime = false;
}
}
}
else
{
scrambledValue.append(originalCharacter);
if (originalCharacter != '.')
{
stillInGeneralizedTime = false;
}
}
}
else
{
scrambledValue.append(originalCharacter);
}
}
return scrambledValue.toString();
}
/**
* Scrambles the provided value, which is expected to be largely numeric.
* Only digits will be scrambled, with all other characters left intact.
* The first digit will be required to be nonzero unless it is also the last
* character of the string.
*
* @param s The value to scramble.
*
* @return The scrambled value.
*/
@Nullable()
public String scrambleNumericValue(@Nullable final String s)
{
if (s == null)
{
return null;
}
// Scramble all digits in the value, leaving all non-digits intact.
int firstDigitPos = -1;
boolean multipleDigits = false;
final char[] chars = s.toCharArray();
final Random random = getRandom(s);
final StringBuilder scrambledValue = new StringBuilder(s.length());
for (int i=0; i < chars.length; i++)
{
final char c = chars[i];
if ((c >= '0') && (c <= '9'))
{
scrambledValue.append(random.nextInt(10));
if (firstDigitPos < 0)
{
firstDigitPos = i;
}
else
{
multipleDigits = true;
}
}
else
{
scrambledValue.append(c);
}
}
// If there weren't any digits, then just scramble the value as an ordinary
// string.
if (firstDigitPos < 0)
{
return scrambleString(s);
}
// If there were multiple digits, then ensure that the first digit is
// nonzero.
if (multipleDigits && (scrambledValue.charAt(firstDigitPos) == '0'))
{
scrambledValue.setCharAt(firstDigitPos,
(char) (random.nextInt(9) + (int) '1'));
}
return scrambledValue.toString();
}
/**
* Scrambles the provided value, which may contain non-ASCII characters. The
* scrambling will be performed as follows:
*
*
* Each lowercase ASCII letter will be replaced with a randomly-selected
* lowercase ASCII letter.
*
*
* Each uppercase ASCII letter will be replaced with a randomly-selected
* uppercase ASCII letter.
*
*
* Each ASCII digit will be replaced with a randomly-selected ASCII digit.
*
*
* Each ASCII symbol (all printable ASCII characters not included in one
* of the above categories) will be replaced with a randomly-selected
* ASCII symbol.
*
*
* Each ASCII control character will be replaced with a randomly-selected
* printable ASCII character.
*
*
* Each non-ASCII byte will be replaced with a randomly-selected non-ASCII
* byte.
*
*
*
* @param value The value to scramble.
*
* @return The scrambled value.
*/
@Nullable()
public byte[] scrambleBinaryValue(@Nullable final byte[] value)
{
if (value == null)
{
return null;
}
final Random random = getRandom(value);
final byte[] scrambledValue = new byte[value.length];
for (int i=0; i < value.length; i++)
{
final byte b = value[i];
if ((b >= 'a') && (b <= 'z'))
{
scrambledValue[i] =
(byte) randomCharacter(LOWERCASE_ASCII_LETTERS, random);
}
else if ((b >= 'A') && (b <= 'Z'))
{
scrambledValue[i] =
(byte) randomCharacter(UPPERCASE_ASCII_LETTERS, random);
}
else if ((b >= '0') && (b <= '9'))
{
scrambledValue[i] = (byte) randomCharacter(ASCII_DIGITS, random);
}
else if ((b >= ' ') && (b <= '~'))
{
scrambledValue[i] = (byte) randomCharacter(ASCII_SYMBOLS, random);
}
else if ((b & 0x80) == 0x00)
{
// We don't want to include any control characters in the resulting
// value, so we will replace this control character with a printable
// ASCII character. ASCII control characters are 0x00-0x1F and 0x7F.
// So the printable ASCII characters are 0x20-0x7E, which is a
// continuous span of 95 characters starting at 0x20.
scrambledValue[i] = (byte) (random.nextInt(95) + 0x20);
}
else
{
// It's a non-ASCII byte, so pick a non-ASCII byte at random.
scrambledValue[i] = (byte) ((random.nextInt() & 0xFF) | 0x80);
}
}
return scrambledValue;
}
/**
* Scrambles the provided encoded password value. It is expected that it will
* either start with a storage scheme name in curly braces (e.g.,
* "{SSHA256}XrgyNdl3fid7KYdhd/Ju47KJQ5PYZqlUlyzxQ28f/QXUnNd9fupj9g==") or
* that it will use the authentication password syntax as described in RFC
* 3112 in which the scheme name is separated from the rest of the password by
* a dollar sign (e.g.,
* "SHA256$QGbHtDCi1i4=$8/X7XRGaFCovC5mn7ATPDYlkVoocDD06Zy3lbD4AoO4="). In
* either case, the scheme name will be left unchanged but the remainder of
* the value will be scrambled.
*
* @param s The encoded password to scramble.
*
* @return The scrambled value.
*/
@Nullable()
public String scrambleEncodedPassword(@Nullable final String s)
{
if (s == null)
{
return null;
}
// Check to see if the value starts with a scheme name in curly braces and
// has something after the closing curly brace. If so, then preserve the
// scheme and scramble the rest of the value.
final int closeBracePos = s.indexOf('}');
if (s.startsWith("{") && (closeBracePos > 0) &&
(closeBracePos < (s.length() - 1)))
{
return s.substring(0, (closeBracePos+1)) +
scrambleString(s.substring(closeBracePos+1));
}
// Check to see if the value has at least two dollar signs and that they are
// not the first or last characters of the string. If so, then the scheme
// should appear before the first dollar sign. Preserve that and scramble
// the rest of the value.
final int firstDollarPos = s.indexOf('$');
if (firstDollarPos > 0)
{
final int secondDollarPos = s.indexOf('$', (firstDollarPos+1));
if (secondDollarPos > 0)
{
return s.substring(0, (firstDollarPos+1)) +
scrambleString(s.substring(firstDollarPos+1));
}
}
// It isn't an encoding format that we recognize, so we'll just scramble it
// like a generic string.
return scrambleString(s);
}
/**
* Scrambles the provided JSON object value. If the provided value can be
* parsed as a valid JSON object, then the resulting value will be a JSON
* object with all field names preserved and some or all of the field values
* scrambled. If this {@code AttributeScrambler} was created with a set of
* JSON fields, then only the values of those fields will be scrambled;
* otherwise, all field values will be scrambled.
*
* @param s The time value to scramble.
*
* @return The scrambled value.
*/
@Nullable()
public String scrambleJSONObject(@Nullable final String s)
{
if (s == null)
{
return null;
}
// Try to parse the value as a JSON object. If this fails, then just
// scramble it as a generic string.
final JSONObject o;
try
{
o = new JSONObject(s);
}
catch (final Exception e)
{
Debug.debugException(e);
return scrambleString(s);
}
final boolean scrambleAllFields = jsonFields.isEmpty();
final Map originalFields = o.getFields();
final LinkedHashMap scrambledFields = new LinkedHashMap<>(
StaticUtils.computeMapCapacity(originalFields.size()));
for (final Map.Entry e : originalFields.entrySet())
{
final JSONValue scrambledValue;
final String fieldName = e.getKey();
final JSONValue originalValue = e.getValue();
if (scrambleAllFields ||
jsonFields.contains(StaticUtils.toLowerCase(fieldName)))
{
scrambledValue = scrambleJSONValue(originalValue, true);
}
else if (originalValue instanceof JSONArray)
{
scrambledValue = scrambleObjectsInArray((JSONArray) originalValue);
}
else if (originalValue instanceof JSONObject)
{
scrambledValue = scrambleJSONValue(originalValue, false);
}
else
{
scrambledValue = originalValue;
}
scrambledFields.put(fieldName, scrambledValue);
}
return new JSONObject(scrambledFields).toString();
}
/**
* Scrambles the provided JSON value.
*
* @param v The JSON value to be scrambled.
* @param scrambleAllFields Indicates whether all fields of any JSON object
* should be scrambled.
*
* @return The scrambled JSON value.
*/
@NotNull()
private JSONValue scrambleJSONValue(@NotNull final JSONValue v,
final boolean scrambleAllFields)
{
if (v instanceof JSONArray)
{
final JSONArray a = (JSONArray) v;
final List originalValues = a.getValues();
final ArrayList scrambledValues =
new ArrayList<>(originalValues.size());
for (final JSONValue arrayValue : originalValues)
{
scrambledValues.add(scrambleJSONValue(arrayValue, true));
}
return new JSONArray(scrambledValues);
}
else if (v instanceof JSONBoolean)
{
return new JSONBoolean(ThreadLocalRandom.get().nextBoolean());
}
else if (v instanceof JSONNumber)
{
try
{
return new JSONNumber(scrambleNumericValue(v.toString()));
}
catch (final Exception e)
{
// This should never happen.
Debug.debugException(e);
return v;
}
}
else if (v instanceof JSONObject)
{
final JSONObject o = (JSONObject) v;
final Map originalFields = o.getFields();
final LinkedHashMap scrambledFields =
new LinkedHashMap<>(StaticUtils.computeMapCapacity(
originalFields.size()));
for (final Map.Entry e : originalFields.entrySet())
{
final JSONValue scrambledValue;
final String fieldName = e.getKey();
final JSONValue originalValue = e.getValue();
if (scrambleAllFields ||
jsonFields.contains(StaticUtils.toLowerCase(fieldName)))
{
scrambledValue = scrambleJSONValue(originalValue, scrambleAllFields);
}
else if (originalValue instanceof JSONArray)
{
scrambledValue = scrambleObjectsInArray((JSONArray) originalValue);
}
else if (originalValue instanceof JSONObject)
{
scrambledValue = scrambleJSONValue(originalValue, false);
}
else
{
scrambledValue = originalValue;
}
scrambledFields.put(fieldName, scrambledValue);
}
return new JSONObject(scrambledFields);
}
else if (v instanceof JSONString)
{
final JSONString s = (JSONString) v;
return new JSONString(scrambleString(s.stringValue()));
}
else
{
// We should only get here for JSON null values, and we can't scramble
// those.
return v;
}
}
/**
* Creates a new JSON array that will have all the same elements as the
* provided array except that any values in the array that are JSON objects
* (including objects contained in nested arrays) will have any appropriate
* scrambling performed.
*
* @param a The JSON array for which to scramble any values.
*
* @return The array with any appropriate scrambling performed.
*/
@NotNull()
private JSONArray scrambleObjectsInArray(@NotNull final JSONArray a)
{
final List originalValues = a.getValues();
final ArrayList scrambledValues =
new ArrayList<>(originalValues.size());
for (final JSONValue arrayValue : originalValues)
{
if (arrayValue instanceof JSONArray)
{
scrambledValues.add(scrambleObjectsInArray((JSONArray) arrayValue));
}
else if (arrayValue instanceof JSONObject)
{
scrambledValues.add(scrambleJSONValue(arrayValue, false));
}
else
{
scrambledValues.add(arrayValue);
}
}
return new JSONArray(scrambledValues);
}
/**
* Scrambles the provided string. The scrambling will be performed as
* follows:
*
*
* Each lowercase ASCII letter will be replaced with a randomly-selected
* lowercase ASCII letter.
*
*
* Each uppercase ASCII letter will be replaced with a randomly-selected
* uppercase ASCII letter.
*
*
* Each ASCII digit will be replaced with a randomly-selected ASCII digit.
*
*
* All other characters will remain unchanged.
*
*
*
* @param s The value to scramble.
*
* @return The scrambled value.
*/
@Nullable()
public String scrambleString(@Nullable final String s)
{
if (s == null)
{
return null;
}
final Random random = getRandom(s);
final StringBuilder scrambledString = new StringBuilder(s.length());
for (final char c : s.toCharArray())
{
if ((c >= 'a') && (c <= 'z'))
{
scrambledString.append(
randomCharacter(LOWERCASE_ASCII_LETTERS, random));
}
else if ((c >= 'A') && (c <= 'Z'))
{
scrambledString.append(
randomCharacter(UPPERCASE_ASCII_LETTERS, random));
}
else if ((c >= '0') && (c <= '9'))
{
scrambledString.append(randomCharacter(ASCII_DIGITS, random));
}
else
{
scrambledString.append(c);
}
}
return scrambledString.toString();
}
/**
* Retrieves a randomly-selected character from the provided character set.
*
* @param set The array containing the possible characters to select.
* @param r The random number generator to use to select the character.
*
* @return A randomly-selected character from the provided character set.
*/
private static char randomCharacter(@NotNull final char[] set,
@NotNull final Random r)
{
return set[r.nextInt(set.length)];
}
/**
* Retrieves a random number generator to use in the course of generating a
* value. It will be reset with the random seed so that it should yield
* repeatable output for the same input.
*
* @param value The value that will be scrambled. It will contribute to the
* random seed that is ultimately used for the random number
* generator.
*
* @return A random number generator to use in the course of generating a
* value.
*/
@NotNull()
private Random getRandom(@NotNull final String value)
{
Random r = randoms.get();
if (r == null)
{
r = new Random(randomSeed + value.hashCode());
randoms.set(r);
}
else
{
r.setSeed(randomSeed + value.hashCode());
}
return r;
}
/**
* Retrieves a random number generator to use in the course of generating a
* value. It will be reset with the random seed so that it should yield
* repeatable output for the same input.
*
* @param value The value that will be scrambled. It will contribute to the
* random seed that is ultimately used for the random number
* generator.
*
* @return A random number generator to use in the course of generating a
* value.
*/
@NotNull()
private Random getRandom(@NotNull final byte[] value)
{
Random r = randoms.get();
if (r == null)
{
r = new Random(randomSeed + Arrays.hashCode(value));
randoms.set(r);
}
else
{
r.setSeed(randomSeed + Arrays.hashCode(value));
}
return r;
}
/**
* {@inheritDoc}
*/
@Override()
@Nullable()
public Entry translate(@NotNull final Entry original,
final long firstLineNumber)
{
return transformEntry(original);
}
/**
* {@inheritDoc}
*/
@Override()
@Nullable()
public LDIFChangeRecord translate(@NotNull final LDIFChangeRecord original,
final long firstLineNumber)
{
return transformChangeRecord(original);
}
/**
* {@inheritDoc}
*/
@Override()
@Nullable()
public Entry translateEntryToWrite(@NotNull final Entry original)
{
return transformEntry(original);
}
/**
* {@inheritDoc}
*/
@Override()
@Nullable()
public LDIFChangeRecord translateChangeRecordToWrite(
@NotNull final LDIFChangeRecord original)
{
return transformChangeRecord(original);
}
}