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