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