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