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