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