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