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