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