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 }