001/*
002 * Copyright 2016-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2016-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2016-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.sdk.transformations;
037
038
039
040import java.util.ArrayList;
041import java.util.Arrays;
042import java.util.Collection;
043import java.util.Collections;
044import java.util.LinkedHashMap;
045import java.util.HashMap;
046import java.util.HashSet;
047import java.util.List;
048import java.util.Map;
049import java.util.Random;
050import java.util.Set;
051
052import com.unboundid.ldap.matchingrules.BooleanMatchingRule;
053import com.unboundid.ldap.matchingrules.CaseIgnoreStringMatchingRule;
054import com.unboundid.ldap.matchingrules.DistinguishedNameMatchingRule;
055import com.unboundid.ldap.matchingrules.GeneralizedTimeMatchingRule;
056import com.unboundid.ldap.matchingrules.IntegerMatchingRule;
057import com.unboundid.ldap.matchingrules.MatchingRule;
058import com.unboundid.ldap.matchingrules.NumericStringMatchingRule;
059import com.unboundid.ldap.matchingrules.OctetStringMatchingRule;
060import com.unboundid.ldap.matchingrules.TelephoneNumberMatchingRule;
061import com.unboundid.ldap.sdk.Attribute;
062import com.unboundid.ldap.sdk.DN;
063import com.unboundid.ldap.sdk.Entry;
064import com.unboundid.ldap.sdk.Modification;
065import com.unboundid.ldap.sdk.RDN;
066import com.unboundid.ldap.sdk.schema.AttributeTypeDefinition;
067import com.unboundid.ldap.sdk.schema.Schema;
068import com.unboundid.ldif.LDIFAddChangeRecord;
069import com.unboundid.ldif.LDIFChangeRecord;
070import com.unboundid.ldif.LDIFDeleteChangeRecord;
071import com.unboundid.ldif.LDIFModifyChangeRecord;
072import com.unboundid.ldif.LDIFModifyDNChangeRecord;
073import com.unboundid.util.Debug;
074import com.unboundid.util.NotNull;
075import com.unboundid.util.Nullable;
076import com.unboundid.util.StaticUtils;
077import com.unboundid.util.ThreadLocalRandom;
078import com.unboundid.util.ThreadSafety;
079import com.unboundid.util.ThreadSafetyLevel;
080import com.unboundid.util.json.JSONArray;
081import com.unboundid.util.json.JSONBoolean;
082import com.unboundid.util.json.JSONNumber;
083import com.unboundid.util.json.JSONObject;
084import com.unboundid.util.json.JSONString;
085import com.unboundid.util.json.JSONValue;
086
087
088
089/**
090 * This class provides an implementation of an entry and change record
091 * transformation that may be used to scramble the values of a specified set of
092 * attributes in a way that attempts to obscure the original values but that
093 * preserves the syntax for the values.  When possible the scrambling will be
094 * performed in a repeatable manner, so that a given input value will
095 * consistently yield the same scrambled representation.
096 */
097@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
098public final class ScrambleAttributeTransformation
099       implements EntryTransformation, LDIFChangeRecordTransformation
100{
101  /**
102   * The characters in the set of ASCII numeric digits.
103   */
104  @NotNull private static final char[] ASCII_DIGITS =
105       "0123456789".toCharArray();
106
107
108
109  /**
110   * The set of ASCII symbols, which are printable ASCII characters that are not
111   * letters or digits.
112   */
113  @NotNull private static final char[] ASCII_SYMBOLS =
114       " !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~".toCharArray();
115
116
117
118  /**
119   * The characters in the set of lowercase ASCII letters.
120   */
121  @NotNull private static final char[] LOWERCASE_ASCII_LETTERS =
122       "abcdefghijklmnopqrstuvwxyz".toCharArray();
123
124
125
126  /**
127   * The characters in the set of uppercase ASCII letters.
128   */
129  @NotNull private static final char[] UPPERCASE_ASCII_LETTERS =
130       "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
131
132
133
134  /**
135   * The number of milliseconds in a day.
136   */
137  private static final long MILLIS_PER_DAY =
138       1000L * // 1000 milliseconds per second
139       60L *   // 60 seconds per minute
140       60L *   // 60 minutes per hour
141       24L;    // 24 hours per day
142
143
144
145  // Indicates whether to scramble attribute values in entry DNs.
146  private final boolean scrambleEntryDNs;
147
148  // The seed to use for the random number generator.
149  private final long randomSeed;
150
151  // The time this transformation was created.
152  private final long createTime;
153
154  // The schema to use when processing.
155  @Nullable private final Schema schema;
156
157  // The names of the attributes to scramble.
158  @NotNull private final Map<String,MatchingRule> attributes;
159
160  // The names of the JSON fields to scramble.
161  @NotNull private final Set<String> jsonFields;
162
163  // A thread-local collection of reusable random number generators.
164  @NotNull private final ThreadLocal<Random> randoms;
165
166
167
168  /**
169   * Creates a new scramble attribute transformation that will scramble the
170   * values of the specified attributes.  A default standard schema will be
171   * used, entry DNs will not be scrambled, and if any of the target attributes
172   * have values that are JSON objects, the values of all of those objects'
173   * fields will be scrambled.
174   *
175   * @param  attributes  The names or OIDs of the attributes to scramble.
176   */
177  public ScrambleAttributeTransformation(@NotNull final String... attributes)
178  {
179    this(null, null, attributes);
180  }
181
182
183
184  /**
185   * Creates a new scramble attribute transformation that will scramble the
186   * values of the specified attributes.  A default standard schema will be
187   * used, entry DNs will not be scrambled, and if any of the target attributes
188   * have values that are JSON objects, the values of all of those objects'
189   * fields will be scrambled.
190   *
191   * @param  attributes  The names or OIDs of the attributes to scramble.
192   */
193  public ScrambleAttributeTransformation(
194              @NotNull final Collection<String> attributes)
195  {
196    this(null, null, false, attributes, null);
197  }
198
199
200
201  /**
202   * Creates a new scramble attribute transformation that will scramble the
203   * values of a specified set of attributes.  Entry DNs will not be scrambled,
204   * and if any of the target attributes have values that are JSON objects, the
205   * values of all of those objects' fields will be scrambled.
206   *
207   * @param  schema      The schema to use when processing.  This may be
208   *                     {@code null} if a default standard schema should be
209   *                     used.  The schema will be used to identify alternate
210   *                     names that may be used to reference the attributes, and
211   *                     to determine the expected syntax for more accurate
212   *                     scrambling.
213   * @param  randomSeed  The seed to use for the random number generator when
214   *                     scrambling each value.  It may be {@code null} if the
215   *                     random seed should be automatically selected.
216   * @param  attributes  The names or OIDs of the attributes to scramble.
217   */
218  public ScrambleAttributeTransformation(@Nullable final Schema schema,
219                                         @Nullable final Long randomSeed,
220                                         @NotNull final String... attributes)
221  {
222    this(schema, randomSeed, false, StaticUtils.toList(attributes), null);
223  }
224
225
226
227  /**
228   * Creates a new scramble attribute transformation that will scramble the
229   * values of a specified set of attributes.
230   *
231   * @param  schema            The schema to use when processing.  This may be
232   *                           {@code null} if a default standard schema should
233   *                           be used.  The schema will be used to identify
234   *                           alternate names that may be used to reference the
235   *                           attributes, and to determine the expected syntax
236   *                           for more accurate scrambling.
237   * @param  randomSeed        The seed to use for the random number generator
238   *                           when scrambling each value.  It may be
239   *                           {@code null} if the random seed should be
240   *                           automatically selected.
241   * @param  scrambleEntryDNs  Indicates whether to scramble any appropriate
242   *                           attributes contained in entry DNs and the values
243   *                           of attributes with a DN syntax.
244   * @param  attributes        The names or OIDs of the attributes to scramble.
245   * @param  jsonFields        The names of the JSON fields whose values should
246   *                           be scrambled.  If any field names are specified,
247   *                           then any JSON objects to be scrambled will only
248   *                           have those fields scrambled (with field names
249   *                           treated in a case-insensitive manner) and all
250   *                           other fields will be preserved without
251   *                           scrambling.  If this is {@code null} or empty,
252   *                           then scrambling will be applied for all values in
253   *                           all fields.
254   */
255  public ScrambleAttributeTransformation(@Nullable final Schema schema,
256              @Nullable final Long randomSeed,
257              final boolean scrambleEntryDNs,
258              @NotNull final Collection<String> attributes,
259              @Nullable final Collection<String> jsonFields)
260  {
261    createTime = System.currentTimeMillis();
262    randoms = new ThreadLocal<>();
263
264    this.scrambleEntryDNs = scrambleEntryDNs;
265
266
267    // If a random seed was provided, then use it.  Otherwise, select one.
268    if (randomSeed == null)
269    {
270      this.randomSeed = ThreadLocalRandom.get().nextLong();
271    }
272    else
273    {
274      this.randomSeed = randomSeed;
275    }
276
277
278    // If a schema was provided, then use it.  Otherwise, use the default
279    // standard schema.
280    Schema s = schema;
281    if (s == null)
282    {
283      try
284      {
285        s = Schema.getDefaultStandardSchema();
286      }
287      catch (final Exception e)
288      {
289        // This should never happen.
290        Debug.debugException(e);
291      }
292    }
293    this.schema = s;
294
295
296    // Iterate through the set of provided attribute names.  Identify all of the
297    // alternate names (including the OID) that may be used to reference the
298    // attribute, and identify the associated matching rule.
299    final HashMap<String,MatchingRule> m =
300         new HashMap<>(StaticUtils.computeMapCapacity(10));
301    for (final String a : attributes)
302    {
303      final String baseName = StaticUtils.toLowerCase(Attribute.getBaseName(a));
304
305      AttributeTypeDefinition at = null;
306      if (schema != null)
307      {
308        at = schema.getAttributeType(baseName);
309      }
310
311      if (at == null)
312      {
313        m.put(baseName, CaseIgnoreStringMatchingRule.getInstance());
314      }
315      else
316      {
317        final MatchingRule mr =
318             MatchingRule.selectEqualityMatchingRule(baseName, schema);
319        m.put(StaticUtils.toLowerCase(at.getOID()), mr);
320        for (final String attrName : at.getNames())
321        {
322          m.put(StaticUtils.toLowerCase(attrName), mr);
323        }
324      }
325    }
326    this.attributes = Collections.unmodifiableMap(m);
327
328
329    // See if any JSON fields were specified.  If so, then process them.
330    if (jsonFields == null)
331    {
332      this.jsonFields = Collections.emptySet();
333    }
334    else
335    {
336      final HashSet<String> fieldNames =
337           new HashSet<>(StaticUtils.computeMapCapacity(jsonFields.size()));
338      for (final String fieldName : jsonFields)
339      {
340        fieldNames.add(StaticUtils.toLowerCase(fieldName));
341      }
342      this.jsonFields = Collections.unmodifiableSet(fieldNames);
343    }
344  }
345
346
347
348  /**
349   * {@inheritDoc}
350   */
351  @Override()
352  @Nullable()
353  public Entry transformEntry(@NotNull final Entry e)
354  {
355    if (e == null)
356    {
357      return null;
358    }
359
360    final String dn;
361    if (scrambleEntryDNs)
362    {
363      dn = scrambleDN(e.getDN());
364    }
365    else
366    {
367      dn = e.getDN();
368    }
369
370    final Collection<Attribute> originalAttributes = e.getAttributes();
371    final ArrayList<Attribute> scrambledAttributes =
372         new ArrayList<>(originalAttributes.size());
373
374    for (final Attribute a : originalAttributes)
375    {
376      scrambledAttributes.add(scrambleAttribute(a));
377    }
378
379    return new Entry(dn, schema, scrambledAttributes);
380  }
381
382
383
384  /**
385   * {@inheritDoc}
386   */
387  @Override()
388  @Nullable()
389  public LDIFChangeRecord transformChangeRecord(
390                               @NotNull final LDIFChangeRecord r)
391  {
392    if (r == null)
393    {
394      return null;
395    }
396
397
398    // If it's an add change record, then just use the same processing as for an
399    // entry.
400    if (r instanceof LDIFAddChangeRecord)
401    {
402      final LDIFAddChangeRecord addRecord = (LDIFAddChangeRecord) r;
403      return new LDIFAddChangeRecord(transformEntry(addRecord.getEntryToAdd()),
404           addRecord.getControls());
405    }
406
407
408    // If it's a delete change record, then see if we need to scramble the DN.
409    if (r instanceof LDIFDeleteChangeRecord)
410    {
411      if (scrambleEntryDNs)
412      {
413        return new LDIFDeleteChangeRecord(scrambleDN(r.getDN()),
414             r.getControls());
415      }
416      else
417      {
418        return r;
419      }
420    }
421
422
423    // If it's a modify change record, then scramble all of the appropriate
424    // modification values.
425    if (r instanceof LDIFModifyChangeRecord)
426    {
427      final LDIFModifyChangeRecord modifyRecord = (LDIFModifyChangeRecord) r;
428
429      final Modification[] originalMods = modifyRecord.getModifications();
430      final Modification[] newMods = new Modification[originalMods.length];
431
432      for (int i=0; i < originalMods.length; i++)
433      {
434        // If the modification doesn't have any values, then just use the
435        // original modification.
436        final Modification m = originalMods[i];
437        if (! m.hasValue())
438        {
439          newMods[i] = m;
440          continue;
441        }
442
443
444        // See if the modification targets an attribute that we should scramble.
445        // If not, then just use the original modification.
446        final String attrName = StaticUtils.toLowerCase(
447             Attribute.getBaseName(m.getAttributeName()));
448        if (! attributes.containsKey(attrName))
449        {
450          newMods[i] = m;
451          continue;
452        }
453
454
455        // Scramble the values just like we do for an attribute.
456        final Attribute scrambledAttribute =
457             scrambleAttribute(m.getAttribute());
458        newMods[i] = new Modification(m.getModificationType(),
459             m.getAttributeName(), scrambledAttribute.getRawValues());
460      }
461
462      if (scrambleEntryDNs)
463      {
464        return new LDIFModifyChangeRecord(scrambleDN(modifyRecord.getDN()),
465             newMods, modifyRecord.getControls());
466      }
467      else
468      {
469        return new LDIFModifyChangeRecord(modifyRecord.getDN(), newMods,
470             modifyRecord.getControls());
471      }
472    }
473
474
475    // If it's a modify DN change record, then see if we need to scramble any
476    // of the components.
477    if (r instanceof LDIFModifyDNChangeRecord)
478    {
479      if (scrambleEntryDNs)
480      {
481        final LDIFModifyDNChangeRecord modDNRecord =
482             (LDIFModifyDNChangeRecord) r;
483        return new LDIFModifyDNChangeRecord(scrambleDN(modDNRecord.getDN()),
484             scrambleDN(modDNRecord.getNewRDN()), modDNRecord.deleteOldRDN(),
485             scrambleDN(modDNRecord.getNewSuperiorDN()),
486             modDNRecord.getControls());
487      }
488      else
489      {
490        return r;
491      }
492    }
493
494
495    // This should never happen.
496    return r;
497  }
498
499
500
501  /**
502   * Creates a scrambled copy of the provided DN.  If the DN contains any
503   * components with attributes to be scrambled, then the values of those
504   * attributes will be scrambled appropriately.  If the DN does not contain
505   * any components with attributes to be scrambled, then no changes will be
506   * made.
507   *
508   * @param  dn  The DN to be scrambled.
509   *
510   * @return  A scrambled copy of the provided DN, or the original DN if no
511   *          scrambling is required or the provided string cannot be parsed as
512   *          a valid DN.
513   */
514  @Nullable()
515  public String scrambleDN(@Nullable() final String dn)
516  {
517    if (dn == null)
518    {
519      return null;
520    }
521
522    try
523    {
524      return scrambleDN(new DN(dn)).toString();
525    }
526    catch (final Exception e)
527    {
528      Debug.debugException(e);
529      return dn;
530    }
531  }
532
533
534
535  /**
536   * Creates a scrambled copy of the provided DN.  If the DN contains any
537   * components with attributes to be scrambled, then the values of those
538   * attributes will be scrambled appropriately.  If the DN does not contain
539   * any components with attributes to be scrambled, then no changes will be
540   * made.
541   *
542   * @param  dn  The DN to be scrambled.
543   *
544   * @return  A scrambled copy of the provided DN, or the original DN if no
545   *          scrambling is required.
546   */
547  @Nullable()
548  public DN scrambleDN(@Nullable final DN dn)
549  {
550    if ((dn == null) || dn.isNullDN())
551    {
552      return dn;
553    }
554
555    boolean changeApplied = false;
556    final RDN[] originalRDNs = dn.getRDNs();
557    final RDN[] scrambledRDNs = new RDN[originalRDNs.length];
558    for (int i=0; i < originalRDNs.length; i++)
559    {
560      scrambledRDNs[i] = scrambleRDN(originalRDNs[i]);
561      if (scrambledRDNs[i] != originalRDNs[i])
562      {
563        changeApplied = true;
564      }
565    }
566
567    if (changeApplied)
568    {
569      return new DN(scrambledRDNs);
570    }
571    else
572    {
573      return dn;
574    }
575  }
576
577
578
579  /**
580   * Creates a scrambled copy of the provided RDN.  If the RDN contains any
581   * attributes to be scrambled, then the values of those attributes will be
582   * scrambled appropriately.  If the RDN does not contain any attributes to be
583   * scrambled, then no changes will be made.
584   *
585   * @param  rdn  The RDN to be scrambled.  It must not be {@code null}.
586   *
587   * @return  A scrambled copy of the provided RDN, or the original RDN if no
588   *          scrambling is required.
589   */
590  @NotNull()
591  public RDN scrambleRDN(@NotNull final RDN rdn)
592  {
593    boolean changeRequired = false;
594    final String[] names = rdn.getAttributeNames();
595    for (final String s : names)
596    {
597      final String lowerBaseName =
598           StaticUtils.toLowerCase(Attribute.getBaseName(s));
599      if (attributes.containsKey(lowerBaseName))
600      {
601        changeRequired = true;
602        break;
603      }
604    }
605
606    if (! changeRequired)
607    {
608      return rdn;
609    }
610
611    final Attribute[] originalAttrs = rdn.getAttributes();
612    final byte[][] scrambledValues = new byte[originalAttrs.length][];
613    for (int i=0; i < originalAttrs.length; i++)
614    {
615      scrambledValues[i] =
616           scrambleAttribute(originalAttrs[i]).getValueByteArray();
617    }
618
619    return new RDN(names, scrambledValues, schema);
620  }
621
622
623
624  /**
625   * Creates a copy of the provided attribute with its values scrambled if
626   * appropriate.
627   *
628   * @param  a  The attribute to scramble.
629   *
630   * @return  A copy of the provided attribute with its values scrambled, or
631   *          the original attribute if no scrambling should be performed.
632   */
633  @Nullable()
634  public Attribute scrambleAttribute(@NotNull final Attribute a)
635  {
636    if ((a == null) || (a.size() == 0))
637    {
638      return a;
639    }
640
641    final String baseName = StaticUtils.toLowerCase(a.getBaseName());
642    final MatchingRule matchingRule = attributes.get(baseName);
643    if (matchingRule == null)
644    {
645      return a;
646    }
647
648    if (matchingRule instanceof BooleanMatchingRule)
649    {
650      // In the case of a boolean value, we won't try to create reproducible
651      // results.  We will just  pick boolean values at random.
652      if (a.size() == 1)
653      {
654        return new Attribute(a.getName(), schema,
655             ThreadLocalRandom.get().nextBoolean() ? "TRUE" : "FALSE");
656      }
657      else
658      {
659        // This is highly unusual, but since there are only two possible valid
660        // boolean values, we will return an attribute with both values,
661        // regardless of how many values the provided attribute actually had.
662        return new Attribute(a.getName(), schema, "TRUE", "FALSE");
663      }
664    }
665    else if (matchingRule instanceof DistinguishedNameMatchingRule)
666    {
667      final String[] originalValues = a.getValues();
668      final String[] scrambledValues = new String[originalValues.length];
669      for (int i=0; i < originalValues.length; i++)
670      {
671        try
672        {
673          scrambledValues[i] = scrambleDN(new DN(originalValues[i])).toString();
674        }
675        catch (final Exception e)
676        {
677          Debug.debugException(e);
678          scrambledValues[i] = scrambleString(originalValues[i]);
679        }
680      }
681
682      return new Attribute(a.getName(), schema, scrambledValues);
683    }
684    else if (matchingRule instanceof GeneralizedTimeMatchingRule)
685    {
686      final String[] originalValues = a.getValues();
687      final String[] scrambledValues = new String[originalValues.length];
688      for (int i=0; i < originalValues.length; i++)
689      {
690        scrambledValues[i] = scrambleGeneralizedTime(originalValues[i]);
691      }
692
693      return new Attribute(a.getName(), schema, scrambledValues);
694    }
695    else if ((matchingRule instanceof IntegerMatchingRule) ||
696             (matchingRule instanceof NumericStringMatchingRule) ||
697             (matchingRule instanceof TelephoneNumberMatchingRule))
698    {
699      final String[] originalValues = a.getValues();
700      final String[] scrambledValues = new String[originalValues.length];
701      for (int i=0; i < originalValues.length; i++)
702      {
703        scrambledValues[i] = scrambleNumericValue(originalValues[i]);
704      }
705
706      return new Attribute(a.getName(), schema, scrambledValues);
707    }
708    else if (matchingRule instanceof OctetStringMatchingRule)
709    {
710      // If the target attribute is userPassword, then treat it like an encoded
711      // password.
712      final byte[][] originalValues = a.getValueByteArrays();
713      final byte[][] scrambledValues = new byte[originalValues.length][];
714      for (int i=0; i < originalValues.length; i++)
715      {
716        if (baseName.equals("userpassword") || baseName.equals("2.5.4.35"))
717        {
718          scrambledValues[i] = StaticUtils.getBytes(scrambleEncodedPassword(
719               StaticUtils.toUTF8String(originalValues[i])));
720        }
721        else
722        {
723          scrambledValues[i] = scrambleBinaryValue(originalValues[i]);
724        }
725      }
726
727      return new Attribute(a.getName(), schema, scrambledValues);
728    }
729    else
730    {
731      final String[] originalValues = a.getValues();
732      final String[] scrambledValues = new String[originalValues.length];
733      for (int i=0; i < originalValues.length; i++)
734      {
735        if (baseName.equals("userpassword") || baseName.equals("2.5.4.35") ||
736            baseName.equals("authpassword") ||
737            baseName.equals("1.3.6.1.4.1.4203.1.3.4"))
738        {
739          scrambledValues[i] = scrambleEncodedPassword(originalValues[i]);
740        }
741        else if (originalValues[i].startsWith("{") &&
742                 originalValues[i].endsWith("}"))
743        {
744          scrambledValues[i] = scrambleJSONObject(originalValues[i]);
745        }
746        else
747        {
748          scrambledValues[i] = scrambleString(originalValues[i]);
749        }
750      }
751
752      return new Attribute(a.getName(), schema, scrambledValues);
753    }
754  }
755
756
757
758  /**
759   * Scrambles the provided generalized time value.  If the provided value can
760   * be parsed as a valid generalized time, then the resulting value will be a
761   * generalized time in the same format but with the timestamp randomized.  The
762   * randomly-selected time will adhere to the following constraints:
763   * <UL>
764   *   <LI>
765   *     The range for the timestamp will be twice the size of the current time
766   *     and the original timestamp.  If the original timestamp is within one
767   *     day of the current time, then the original range will be expanded by
768   *     an additional one day.
769   *   </LI>
770   *   <LI>
771   *     If the original timestamp is in the future, then the scrambled
772   *     timestamp will also be in the future. Otherwise, it will be in the
773   *     past.
774   *   </LI>
775   * </UL>
776   *
777   * @param  s  The value to scramble.
778   *
779   * @return  The scrambled value.
780   */
781  @Nullable()
782  public String scrambleGeneralizedTime(@Nullable final String s)
783  {
784    if (s == null)
785    {
786      return null;
787    }
788
789
790    // See if we can parse the value as a generalized time.  If not, then just
791    // apply generic scrambling.
792    final long decodedTime;
793    final Random random = getRandom(s);
794    try
795    {
796      decodedTime = StaticUtils.decodeGeneralizedTime(s).getTime();
797    }
798    catch (final Exception e)
799    {
800      Debug.debugException(e);
801      return scrambleString(s);
802    }
803
804
805    // We want to choose a timestamp at random, but we still want to pick
806    // something that is reasonably close to the provided value.  To start
807    // with, see how far away the timestamp is from the time this attribute
808    // scrambler was created.  If it's less than one day, then add one day to
809    // it.  Then, double the resulting value.
810    long timeSpan = Math.abs(createTime - decodedTime);
811    if (timeSpan < MILLIS_PER_DAY)
812    {
813      timeSpan += MILLIS_PER_DAY;
814    }
815
816    timeSpan *= 2;
817
818
819    // Generate a random value between zero and the computed time span.
820    final long randomLong = (random.nextLong() & 0x7FFF_FFFF_FFFF_FFFFL);
821    final long randomOffset = randomLong % timeSpan;
822
823
824    // If the provided timestamp is in the future, then add the randomly-chosen
825    // offset to the time that this attribute scrambler was created.  Otherwise,
826    // subtract it from the time that this attribute scrambler was created.
827    final long randomTime;
828    if (decodedTime > createTime)
829    {
830      randomTime = createTime + randomOffset;
831    }
832    else
833    {
834      randomTime = createTime - randomOffset;
835    }
836
837
838    // Create a generalized time representation of the provided value.
839    final String generalizedTime =
840         StaticUtils.encodeGeneralizedTime(randomTime);
841
842
843    // We want to preserve the original precision and time zone specifier for
844    // the timestamp, so just take as much of the generalized time value as we
845    // need to do that.
846    boolean stillInGeneralizedTime = true;
847    final StringBuilder scrambledValue = new StringBuilder(s.length());
848    for (int i=0; i < s.length(); i++)
849    {
850      final char originalCharacter = s.charAt(i);
851      if (stillInGeneralizedTime)
852      {
853        if ((i < generalizedTime.length()) &&
854            (originalCharacter >= '0') && (originalCharacter <= '9'))
855        {
856          final char generalizedTimeCharacter = generalizedTime.charAt(i);
857          if ((generalizedTimeCharacter >= '0') &&
858              (generalizedTimeCharacter <= '9'))
859          {
860            scrambledValue.append(generalizedTimeCharacter);
861          }
862          else
863          {
864            scrambledValue.append(originalCharacter);
865            if (generalizedTimeCharacter != '.')
866            {
867              stillInGeneralizedTime = false;
868            }
869          }
870        }
871        else
872        {
873          scrambledValue.append(originalCharacter);
874          if (originalCharacter != '.')
875          {
876            stillInGeneralizedTime = false;
877          }
878        }
879      }
880      else
881      {
882        scrambledValue.append(originalCharacter);
883      }
884    }
885
886    return scrambledValue.toString();
887  }
888
889
890
891  /**
892   * Scrambles the provided value, which is expected to be largely numeric.
893   * Only digits will be scrambled, with all other characters left intact.
894   * The first digit will be required to be nonzero unless it is also the last
895   * character of the string.
896   *
897   * @param  s  The value to scramble.
898   *
899   * @return  The scrambled value.
900   */
901  @Nullable()
902  public String scrambleNumericValue(@Nullable final String s)
903  {
904    if (s == null)
905    {
906      return null;
907    }
908
909
910    // Scramble all digits in the value, leaving all non-digits intact.
911    int firstDigitPos = -1;
912    boolean multipleDigits = false;
913    final char[] chars = s.toCharArray();
914    final Random random = getRandom(s);
915    final StringBuilder scrambledValue = new StringBuilder(s.length());
916    for (int i=0; i < chars.length; i++)
917    {
918      final char c = chars[i];
919      if ((c >= '0') && (c <= '9'))
920      {
921        scrambledValue.append(random.nextInt(10));
922        if (firstDigitPos < 0)
923        {
924          firstDigitPos = i;
925        }
926        else
927        {
928          multipleDigits = true;
929        }
930      }
931      else
932      {
933        scrambledValue.append(c);
934      }
935    }
936
937
938    // If there weren't any digits, then just scramble the value as an ordinary
939    // string.
940    if (firstDigitPos < 0)
941    {
942      return scrambleString(s);
943    }
944
945
946    // If there were multiple digits, then ensure that the first digit is
947    // nonzero.
948    if (multipleDigits && (scrambledValue.charAt(firstDigitPos) == '0'))
949    {
950      scrambledValue.setCharAt(firstDigitPos,
951           (char) (random.nextInt(9) + (int) '1'));
952    }
953
954
955    return scrambledValue.toString();
956  }
957
958
959
960  /**
961   * Scrambles the provided value, which may contain non-ASCII characters.  The
962   * scrambling will be performed as follows:
963   * <UL>
964   *   <LI>
965   *     Each lowercase ASCII letter will be replaced with a randomly-selected
966   *     lowercase ASCII letter.
967   *   </LI>
968   *   <LI>
969   *     Each uppercase ASCII letter will be replaced with a randomly-selected
970   *     uppercase ASCII letter.
971   *   </LI>
972   *   <LI>
973   *     Each ASCII digit will be replaced with a randomly-selected ASCII digit.
974   *   </LI>
975   *   <LI>
976   *     Each ASCII symbol (all printable ASCII characters not included in one
977   *     of the above categories) will be replaced with a randomly-selected
978   *     ASCII symbol.
979   *   </LI>
980   *   <LI>
981   *   Each ASCII control character will be replaced with a randomly-selected
982   *   printable ASCII character.
983   *   </LI>
984   *   <LI>
985   *     Each non-ASCII byte will be replaced with a randomly-selected non-ASCII
986   *     byte.
987   *   </LI>
988   * </UL>
989   *
990   * @param  value  The value to scramble.
991   *
992   * @return  The scrambled value.
993   */
994  @Nullable()
995  public byte[] scrambleBinaryValue(@Nullable final byte[] value)
996  {
997    if (value == null)
998    {
999      return null;
1000    }
1001
1002
1003    final Random random = getRandom(value);
1004    final byte[] scrambledValue = new byte[value.length];
1005    for (int i=0; i < value.length; i++)
1006    {
1007      final byte b = value[i];
1008      if ((b >= 'a') && (b <= 'z'))
1009      {
1010        scrambledValue[i] =
1011             (byte) randomCharacter(LOWERCASE_ASCII_LETTERS, random);
1012      }
1013      else if ((b >= 'A') && (b <= 'Z'))
1014      {
1015        scrambledValue[i] =
1016             (byte) randomCharacter(UPPERCASE_ASCII_LETTERS, random);
1017      }
1018      else if ((b >= '0') && (b <= '9'))
1019      {
1020        scrambledValue[i] = (byte) randomCharacter(ASCII_DIGITS, random);
1021      }
1022      else if ((b >= ' ') && (b <= '~'))
1023      {
1024        scrambledValue[i] = (byte) randomCharacter(ASCII_SYMBOLS, random);
1025      }
1026      else if ((b & 0x80) == 0x00)
1027      {
1028        // We don't want to include any control characters in the resulting
1029        // value, so we will replace this control character with a printable
1030        // ASCII character.  ASCII control characters are 0x00-0x1F and 0x7F.
1031        // So the printable ASCII characters are 0x20-0x7E, which is a
1032        // continuous span of 95 characters starting at 0x20.
1033        scrambledValue[i] = (byte) (random.nextInt(95) + 0x20);
1034      }
1035      else
1036      {
1037        // It's a non-ASCII byte, so pick a non-ASCII byte at random.
1038        scrambledValue[i] = (byte) ((random.nextInt() & 0xFF) | 0x80);
1039      }
1040    }
1041
1042    return scrambledValue;
1043  }
1044
1045
1046
1047  /**
1048   * Scrambles the provided encoded password value.  It is expected that it will
1049   * either start with a storage scheme name in curly braces (e.g.,
1050   * "{SSHA256}XrgyNdl3fid7KYdhd/Ju47KJQ5PYZqlUlyzxQ28f/QXUnNd9fupj9g==") or
1051   * that it will use the authentication password syntax as described in RFC
1052   * 3112 in which the scheme name is separated from the rest of the password by
1053   * a dollar sign (e.g.,
1054   * "SHA256$QGbHtDCi1i4=$8/X7XRGaFCovC5mn7ATPDYlkVoocDD06Zy3lbD4AoO4=").  In
1055   * either case, the scheme name will be left unchanged but the remainder of
1056   * the value will be scrambled.
1057   *
1058   * @param  s  The encoded password to scramble.
1059   *
1060   * @return  The scrambled value.
1061   */
1062  @Nullable()
1063  public String scrambleEncodedPassword(@Nullable final String s)
1064  {
1065    if (s == null)
1066    {
1067      return null;
1068    }
1069
1070
1071    // Check to see if the value starts with a scheme name in curly braces and
1072    // has something after the closing curly brace.  If so, then preserve the
1073    // scheme and scramble the rest of the value.
1074    final int closeBracePos = s.indexOf('}');
1075    if (s.startsWith("{") && (closeBracePos > 0) &&
1076        (closeBracePos < (s.length() - 1)))
1077    {
1078      return s.substring(0, (closeBracePos+1)) +
1079           scrambleString(s.substring(closeBracePos+1));
1080    }
1081
1082
1083    // Check to see if the value has at least two dollar signs and that they are
1084    // not the first or last characters of the string.  If so, then the scheme
1085    // should appear before the first dollar sign.  Preserve that and scramble
1086    // the rest of the value.
1087    final int firstDollarPos = s.indexOf('$');
1088    if (firstDollarPos > 0)
1089    {
1090      final int secondDollarPos = s.indexOf('$', (firstDollarPos+1));
1091      if (secondDollarPos > 0)
1092      {
1093        return s.substring(0, (firstDollarPos+1)) +
1094             scrambleString(s.substring(firstDollarPos+1));
1095      }
1096    }
1097
1098
1099    // It isn't an encoding format that we recognize, so we'll just scramble it
1100    // like a generic string.
1101    return scrambleString(s);
1102  }
1103
1104
1105
1106  /**
1107   * Scrambles the provided JSON object value.  If the provided value can be
1108   * parsed as a valid JSON object, then the resulting value will be a JSON
1109   * object with all field names preserved and some or all of the field values
1110   * scrambled.  If this {@code AttributeScrambler} was created with a set of
1111   * JSON fields, then only the values of those fields will be scrambled;
1112   * otherwise, all field values will be scrambled.
1113   *
1114   * @param  s  The time value to scramble.
1115   *
1116   * @return  The scrambled value.
1117   */
1118  @Nullable()
1119  public String scrambleJSONObject(@Nullable final String s)
1120  {
1121    if (s == null)
1122    {
1123      return null;
1124    }
1125
1126
1127    // Try to parse the value as a JSON object.  If this fails, then just
1128    // scramble it as a generic string.
1129    final JSONObject o;
1130    try
1131    {
1132      o = new JSONObject(s);
1133    }
1134    catch (final Exception e)
1135    {
1136      Debug.debugException(e);
1137      return scrambleString(s);
1138    }
1139
1140
1141    final boolean scrambleAllFields = jsonFields.isEmpty();
1142    final Map<String,JSONValue> originalFields = o.getFields();
1143    final LinkedHashMap<String,JSONValue> scrambledFields = new LinkedHashMap<>(
1144         StaticUtils.computeMapCapacity(originalFields.size()));
1145    for (final Map.Entry<String,JSONValue> e : originalFields.entrySet())
1146    {
1147      final JSONValue scrambledValue;
1148      final String fieldName = e.getKey();
1149      final JSONValue originalValue = e.getValue();
1150      if (scrambleAllFields ||
1151          jsonFields.contains(StaticUtils.toLowerCase(fieldName)))
1152      {
1153        scrambledValue = scrambleJSONValue(originalValue, true);
1154      }
1155      else if (originalValue instanceof JSONArray)
1156      {
1157        scrambledValue = scrambleObjectsInArray((JSONArray) originalValue);
1158      }
1159      else if (originalValue instanceof JSONObject)
1160      {
1161        scrambledValue = scrambleJSONValue(originalValue, false);
1162      }
1163      else
1164      {
1165        scrambledValue = originalValue;
1166      }
1167
1168      scrambledFields.put(fieldName, scrambledValue);
1169    }
1170
1171    return new JSONObject(scrambledFields).toString();
1172  }
1173
1174
1175
1176  /**
1177   * Scrambles the provided JSON value.
1178   *
1179   * @param  v                  The JSON value to be scrambled.
1180   * @param  scrambleAllFields  Indicates whether all fields of any JSON object
1181   *                            should be scrambled.
1182   *
1183   * @return  The scrambled JSON value.
1184   */
1185  @NotNull()
1186  private JSONValue scrambleJSONValue(@NotNull final JSONValue v,
1187                                      final boolean scrambleAllFields)
1188  {
1189    if (v instanceof JSONArray)
1190    {
1191      final JSONArray a = (JSONArray) v;
1192      final List<JSONValue> originalValues = a.getValues();
1193      final ArrayList<JSONValue> scrambledValues =
1194           new ArrayList<>(originalValues.size());
1195      for (final JSONValue arrayValue : originalValues)
1196      {
1197        scrambledValues.add(scrambleJSONValue(arrayValue, true));
1198      }
1199      return new JSONArray(scrambledValues);
1200    }
1201    else if (v instanceof JSONBoolean)
1202    {
1203      return new JSONBoolean(ThreadLocalRandom.get().nextBoolean());
1204    }
1205    else if (v instanceof JSONNumber)
1206    {
1207      try
1208      {
1209        return new JSONNumber(scrambleNumericValue(v.toString()));
1210      }
1211      catch (final Exception e)
1212      {
1213        // This should never happen.
1214        Debug.debugException(e);
1215        return v;
1216      }
1217    }
1218    else if (v instanceof JSONObject)
1219    {
1220      final JSONObject o = (JSONObject) v;
1221      final Map<String,JSONValue> originalFields = o.getFields();
1222      final LinkedHashMap<String,JSONValue> scrambledFields =
1223           new LinkedHashMap<>(StaticUtils.computeMapCapacity(
1224                originalFields.size()));
1225      for (final Map.Entry<String,JSONValue> e : originalFields.entrySet())
1226      {
1227        final JSONValue scrambledValue;
1228        final String fieldName = e.getKey();
1229        final JSONValue originalValue = e.getValue();
1230        if (scrambleAllFields ||
1231            jsonFields.contains(StaticUtils.toLowerCase(fieldName)))
1232        {
1233          scrambledValue = scrambleJSONValue(originalValue, scrambleAllFields);
1234        }
1235        else if (originalValue instanceof JSONArray)
1236        {
1237          scrambledValue = scrambleObjectsInArray((JSONArray) originalValue);
1238        }
1239        else if (originalValue instanceof JSONObject)
1240        {
1241          scrambledValue = scrambleJSONValue(originalValue, false);
1242        }
1243        else
1244        {
1245          scrambledValue = originalValue;
1246        }
1247
1248        scrambledFields.put(fieldName, scrambledValue);
1249      }
1250
1251      return new JSONObject(scrambledFields);
1252    }
1253    else if (v instanceof JSONString)
1254    {
1255      final JSONString s = (JSONString) v;
1256      return new JSONString(scrambleString(s.stringValue()));
1257    }
1258    else
1259    {
1260      // We should only get here for JSON null values, and we can't scramble
1261      // those.
1262      return v;
1263    }
1264  }
1265
1266
1267
1268  /**
1269   * Creates a new JSON array that will have all the same elements as the
1270   * provided array except that any values in the array that are JSON objects
1271   * (including objects contained in nested arrays) will have any appropriate
1272   * scrambling performed.
1273   *
1274   * @param  a  The JSON array for which to scramble any values.
1275   *
1276   * @return  The array with any appropriate scrambling performed.
1277   */
1278  @NotNull()
1279  private JSONArray scrambleObjectsInArray(@NotNull final JSONArray a)
1280  {
1281    final List<JSONValue> originalValues = a.getValues();
1282    final ArrayList<JSONValue> scrambledValues =
1283         new ArrayList<>(originalValues.size());
1284
1285    for (final JSONValue arrayValue : originalValues)
1286    {
1287      if (arrayValue instanceof JSONArray)
1288      {
1289        scrambledValues.add(scrambleObjectsInArray((JSONArray) arrayValue));
1290      }
1291      else if (arrayValue instanceof JSONObject)
1292      {
1293        scrambledValues.add(scrambleJSONValue(arrayValue, false));
1294      }
1295      else
1296      {
1297        scrambledValues.add(arrayValue);
1298      }
1299    }
1300
1301    return new JSONArray(scrambledValues);
1302  }
1303
1304
1305
1306  /**
1307   * Scrambles the provided string.  The scrambling will be performed as
1308   * follows:
1309   * <UL>
1310   *   <LI>
1311   *     Each lowercase ASCII letter will be replaced with a randomly-selected
1312   *     lowercase ASCII letter.
1313   *   </LI>
1314   *   <LI>
1315   *     Each uppercase ASCII letter will be replaced with a randomly-selected
1316   *     uppercase ASCII letter.
1317   *   </LI>
1318   *   <LI>
1319   *     Each ASCII digit will be replaced with a randomly-selected ASCII digit.
1320   *   </LI>
1321   *   <LI>
1322   *     All other characters will remain unchanged.
1323   *   <LI>
1324   * </UL>
1325   *
1326   * @param  s  The value to scramble.
1327   *
1328   * @return  The scrambled value.
1329   */
1330  @Nullable()
1331  public String scrambleString(@Nullable final String s)
1332  {
1333    if (s == null)
1334    {
1335      return null;
1336    }
1337
1338
1339    final Random random = getRandom(s);
1340    final StringBuilder scrambledString = new StringBuilder(s.length());
1341    for (final char c : s.toCharArray())
1342    {
1343      if ((c >= 'a') && (c <= 'z'))
1344      {
1345        scrambledString.append(
1346             randomCharacter(LOWERCASE_ASCII_LETTERS, random));
1347      }
1348      else if ((c >= 'A') && (c <= 'Z'))
1349      {
1350        scrambledString.append(
1351             randomCharacter(UPPERCASE_ASCII_LETTERS, random));
1352      }
1353      else if ((c >= '0') && (c <= '9'))
1354      {
1355        scrambledString.append(randomCharacter(ASCII_DIGITS, random));
1356      }
1357      else
1358      {
1359        scrambledString.append(c);
1360      }
1361    }
1362
1363    return scrambledString.toString();
1364  }
1365
1366
1367
1368  /**
1369   * Retrieves a randomly-selected character from the provided character set.
1370   *
1371   * @param  set  The array containing the possible characters to select.
1372   * @param  r    The random number generator to use to select the character.
1373   *
1374   * @return  A randomly-selected character from the provided character set.
1375   */
1376  private static char randomCharacter(@NotNull final char[] set,
1377                                      @NotNull final Random r)
1378  {
1379    return set[r.nextInt(set.length)];
1380  }
1381
1382
1383
1384  /**
1385   * Retrieves a random number generator to use in the course of generating a
1386   * value.  It will be reset with the random seed so that it should yield
1387   * repeatable output for the same input.
1388   *
1389   * @param  value  The value that will be scrambled.  It will contribute to the
1390   *                random seed that is ultimately used for the random number
1391   *                generator.
1392   *
1393   * @return  A random number generator to use in the course of generating a
1394   *          value.
1395   */
1396  @NotNull()
1397  private Random getRandom(@NotNull final String value)
1398  {
1399    Random r = randoms.get();
1400    if (r == null)
1401    {
1402      r = new Random(randomSeed + value.hashCode());
1403      randoms.set(r);
1404    }
1405    else
1406    {
1407      r.setSeed(randomSeed + value.hashCode());
1408    }
1409
1410    return r;
1411  }
1412
1413
1414
1415  /**
1416   * Retrieves a random number generator to use in the course of generating a
1417   * value.  It will be reset with the random seed so that it should yield
1418   * repeatable output for the same input.
1419   *
1420   * @param  value  The value that will be scrambled.  It will contribute to the
1421   *                random seed that is ultimately used for the random number
1422   *                generator.
1423     *
1424   * @return  A random number generator to use in the course of generating a
1425   *          value.
1426   */
1427  @NotNull()
1428  private Random getRandom(@NotNull final byte[] value)
1429  {
1430    Random r = randoms.get();
1431    if (r == null)
1432    {
1433      r = new Random(randomSeed + Arrays.hashCode(value));
1434      randoms.set(r);
1435    }
1436    else
1437    {
1438      r.setSeed(randomSeed + Arrays.hashCode(value));
1439    }
1440
1441    return r;
1442  }
1443
1444
1445
1446  /**
1447   * {@inheritDoc}
1448   */
1449  @Override()
1450  @Nullable()
1451  public Entry translate(@NotNull final Entry original,
1452                         final long firstLineNumber)
1453  {
1454    return transformEntry(original);
1455  }
1456
1457
1458
1459  /**
1460   * {@inheritDoc}
1461   */
1462  @Override()
1463  @Nullable()
1464  public LDIFChangeRecord translate(@NotNull final LDIFChangeRecord original,
1465                                    final long firstLineNumber)
1466  {
1467    return transformChangeRecord(original);
1468  }
1469
1470
1471
1472  /**
1473   * {@inheritDoc}
1474   */
1475  @Override()
1476  @Nullable()
1477  public Entry translateEntryToWrite(@NotNull final Entry original)
1478  {
1479    return transformEntry(original);
1480  }
1481
1482
1483
1484  /**
1485   * {@inheritDoc}
1486   */
1487  @Override()
1488  @Nullable()
1489  public LDIFChangeRecord translateChangeRecordToWrite(
1490                               @NotNull final LDIFChangeRecord original)
1491  {
1492    return transformChangeRecord(original);
1493  }
1494}