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