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