001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2011-2012 profiq s.r.o. 025 * Portions Copyright 2011-2015 ForgeRock AS. 026 */ 027package org.opends.server.plugins; 028 029import java.io.UnsupportedEncodingException; 030import java.security.InvalidKeyException; 031import java.security.MessageDigest; 032import java.security.NoSuchAlgorithmException; 033import java.security.NoSuchProviderException; 034import java.util.*; 035 036import javax.crypto.*; 037import javax.crypto.spec.SecretKeySpec; 038 039import org.forgerock.i18n.LocalizableMessage; 040import org.forgerock.i18n.slf4j.LocalizedLogger; 041import org.forgerock.opendj.ldap.ByteString; 042import org.forgerock.opendj.ldap.ModificationType; 043import org.opends.server.admin.server.ConfigurationChangeListener; 044import org.opends.server.admin.std.meta.PluginCfgDefn; 045import org.opends.server.admin.std.meta.SambaPasswordPluginCfgDefn.PwdSyncPolicy; 046import org.opends.server.admin.std.server.SambaPasswordPluginCfg; 047import org.opends.server.api.plugin.DirectoryServerPlugin; 048import org.opends.server.api.plugin.PluginResult; 049import org.opends.server.api.plugin.PluginType; 050import org.forgerock.opendj.config.server.ConfigChangeResult; 051import org.forgerock.opendj.config.server.ConfigException; 052import org.opends.server.controls.LDAPAssertionRequestControl; 053import org.opends.server.core.DirectoryServer; 054import org.opends.server.core.ModifyOperation; 055import org.opends.server.extensions.PasswordModifyExtendedOperation; 056import org.opends.server.protocols.internal.InternalClientConnection; 057import org.opends.server.protocols.ldap.LDAPFilter; 058import org.opends.server.types.*; 059import org.forgerock.opendj.ldap.ResultCode; 060import org.opends.server.types.operation.PostOperationExtendedOperation; 061import org.opends.server.types.operation.PreOperationModifyOperation; 062 063import static org.opends.messages.PluginMessages.*; 064import static org.opends.server.util.StaticUtils.*; 065 066/** 067 * The Samba password synchronization plugin implementation class. 068 * <p> 069 * This plugin synchronizes the userPassword attribute with the Samba password 070 * attribute(s) for all entries containing the specified Samba object class. 071 * <p> 072 * It handles clear-text userPassword modify operations and password modify 073 * extended operations. It does not cover the case of using pre-encoded 074 * password. 075 */ 076public final class SambaPasswordPlugin extends 077 DirectoryServerPlugin<SambaPasswordPluginCfg> implements 078 ConfigurationChangeListener<SambaPasswordPluginCfg> 079{ 080 081 /** 082 * The implementation of this algorithm has been derived from BouncyCastle.org 083 * whose license can be found at http://www.bouncycastle.org/licence.html: 084 * <p> 085 * Copyright (c) 2000 - 2011 The Legion Of The Bouncy Castle 086 * (http://www.bouncycastle.org) 087 * <p> 088 * Permission is hereby granted, free of charge, to any person obtaining a 089 * copy of this software and associated documentation files (the "Software"), 090 * to deal in the Software without restriction, including without limitation 091 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 092 * and/or sell copies of the Software, and to permit persons to whom the 093 * Software is furnished to do so, subject to the following conditions: 094 * <p> 095 * The above copyright notice and this permission notice shall be included in 096 * all copies or substantial portions of the Software. 097 * <p> 098 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 099 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 100 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 101 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 102 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 103 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 104 * DEALINGS IN THE SOFTWARE. 105 */ 106 static final class MD4MessageDigest extends MessageDigest 107 { 108 /** Class is package private for testing. */ 109 private final byte[] xBuf = new byte[4]; 110 private int xBufOff; 111 private long byteCount; 112 113 private static final int DIGEST_LENGTH = 16; 114 /** IV's. */ 115 private int H1, H2, H3, H4; 116 private final int[] X = new int[16]; 117 private int xOff; 118 119 /** Round 1 left rotates. */ 120 private static final int S11 = 3; 121 private static final int S12 = 7; 122 private static final int S13 = 11; 123 private static final int S14 = 19; 124 125 /** Round 2 left rotates. */ 126 private static final int S21 = 3; 127 private static final int S22 = 5; 128 private static final int S23 = 9; 129 private static final int S24 = 13; 130 131 /** Round 3 left rotates. */ 132 private static final int S31 = 3; 133 private static final int S32 = 9; 134 private static final int S33 = 11; 135 private static final int S34 = 15; 136 137 138 139 /** 140 * Creates a new MD4 message digest algorithm. 141 */ 142 MD4MessageDigest() 143 { 144 super("MD4"); 145 engineReset(); 146 } 147 148 149 150 /** {@inheritDoc} */ 151 @Override 152 public byte[] engineDigest() 153 { 154 final byte[] digestBytes = new byte[DIGEST_LENGTH]; 155 finish(); 156 unpackWord(H1, digestBytes, 0); 157 unpackWord(H2, digestBytes, 4); 158 unpackWord(H3, digestBytes, 8); 159 unpackWord(H4, digestBytes, 12); 160 engineReset(); 161 return digestBytes; 162 } 163 164 165 166 /** {@inheritDoc} */ 167 @Override 168 public void engineReset() 169 { 170 byteCount = 0; 171 xBufOff = 0; 172 for (int i = 0; i < xBuf.length; i++) 173 { 174 xBuf[i] = 0; 175 } 176 177 H1 = 0x67452301; 178 H2 = 0xefcdab89; 179 H3 = 0x98badcfe; 180 H4 = 0x10325476; 181 xOff = 0; 182 for (int i = 0; i != X.length; i++) 183 { 184 X[i] = 0; 185 } 186 } 187 188 189 190 /** {@inheritDoc} */ 191 @Override 192 public void engineUpdate(final byte input) 193 { 194 xBuf[xBufOff++] = input; 195 if (xBufOff == xBuf.length) 196 { 197 processWord(xBuf, 0); 198 xBufOff = 0; 199 } 200 byteCount++; 201 } 202 203 204 205 /** {@inheritDoc} */ 206 @Override 207 public void engineUpdate(final byte[] in, int inOff, int len) 208 { 209 // 210 // fill the current word 211 // 212 while (xBufOff != 0 && len > 0) 213 { 214 update(in[inOff]); 215 216 inOff++; 217 len--; 218 } 219 220 // 221 // process whole words. 222 // 223 while (len > xBuf.length) 224 { 225 processWord(in, inOff); 226 227 inOff += xBuf.length; 228 len -= xBuf.length; 229 byteCount += xBuf.length; 230 } 231 232 // 233 // load in the remainder. 234 // 235 while (len > 0) 236 { 237 update(in[inOff]); 238 239 inOff++; 240 len--; 241 } 242 } 243 244 245 246 /** 247 * F, G, H and I are the basic MD4 functions. 248 */ 249 private int F(final int u, final int v, final int w) 250 { 251 return (u & v) | (~u & w); 252 } 253 254 255 256 private void finish() 257 { 258 final long bitLength = byteCount << 3; 259 260 // 261 // add the pad bytes. 262 // 263 engineUpdate((byte) 128); 264 while (xBufOff != 0) 265 { 266 engineUpdate((byte) 0); 267 } 268 processLength(bitLength); 269 processBlock(); 270 } 271 272 273 274 private int G(final int u, final int v, final int w) 275 { 276 return (u & v) | (u & w) | (v & w); 277 } 278 279 280 281 private int H(final int u, final int v, final int w) 282 { 283 return u ^ v ^ w; 284 } 285 286 287 288 private void processBlock() 289 { 290 int a = H1; 291 int b = H2; 292 int c = H3; 293 int d = H4; 294 295 // 296 // Round 1 - F cycle, 16 times. 297 // 298 a = rotateLeft(a + F(b, c, d) + X[0], S11); 299 d = rotateLeft(d + F(a, b, c) + X[1], S12); 300 c = rotateLeft(c + F(d, a, b) + X[2], S13); 301 b = rotateLeft(b + F(c, d, a) + X[3], S14); 302 a = rotateLeft(a + F(b, c, d) + X[4], S11); 303 d = rotateLeft(d + F(a, b, c) + X[5], S12); 304 c = rotateLeft(c + F(d, a, b) + X[6], S13); 305 b = rotateLeft(b + F(c, d, a) + X[7], S14); 306 a = rotateLeft(a + F(b, c, d) + X[8], S11); 307 d = rotateLeft(d + F(a, b, c) + X[9], S12); 308 c = rotateLeft(c + F(d, a, b) + X[10], S13); 309 b = rotateLeft(b + F(c, d, a) + X[11], S14); 310 a = rotateLeft(a + F(b, c, d) + X[12], S11); 311 d = rotateLeft(d + F(a, b, c) + X[13], S12); 312 c = rotateLeft(c + F(d, a, b) + X[14], S13); 313 b = rotateLeft(b + F(c, d, a) + X[15], S14); 314 315 // 316 // Round 2 - G cycle, 16 times. 317 // 318 a = rotateLeft(a + G(b, c, d) + X[0] + 0x5a827999, S21); 319 d = rotateLeft(d + G(a, b, c) + X[4] + 0x5a827999, S22); 320 c = rotateLeft(c + G(d, a, b) + X[8] + 0x5a827999, S23); 321 b = rotateLeft(b + G(c, d, a) + X[12] + 0x5a827999, S24); 322 a = rotateLeft(a + G(b, c, d) + X[1] + 0x5a827999, S21); 323 d = rotateLeft(d + G(a, b, c) + X[5] + 0x5a827999, S22); 324 c = rotateLeft(c + G(d, a, b) + X[9] + 0x5a827999, S23); 325 b = rotateLeft(b + G(c, d, a) + X[13] + 0x5a827999, S24); 326 a = rotateLeft(a + G(b, c, d) + X[2] + 0x5a827999, S21); 327 d = rotateLeft(d + G(a, b, c) + X[6] + 0x5a827999, S22); 328 c = rotateLeft(c + G(d, a, b) + X[10] + 0x5a827999, S23); 329 b = rotateLeft(b + G(c, d, a) + X[14] + 0x5a827999, S24); 330 a = rotateLeft(a + G(b, c, d) + X[3] + 0x5a827999, S21); 331 d = rotateLeft(d + G(a, b, c) + X[7] + 0x5a827999, S22); 332 c = rotateLeft(c + G(d, a, b) + X[11] + 0x5a827999, S23); 333 b = rotateLeft(b + G(c, d, a) + X[15] + 0x5a827999, S24); 334 335 // 336 // Round 3 - H cycle, 16 times. 337 // 338 a = rotateLeft(a + H(b, c, d) + X[0] + 0x6ed9eba1, S31); 339 d = rotateLeft(d + H(a, b, c) + X[8] + 0x6ed9eba1, S32); 340 c = rotateLeft(c + H(d, a, b) + X[4] + 0x6ed9eba1, S33); 341 b = rotateLeft(b + H(c, d, a) + X[12] + 0x6ed9eba1, S34); 342 a = rotateLeft(a + H(b, c, d) + X[2] + 0x6ed9eba1, S31); 343 d = rotateLeft(d + H(a, b, c) + X[10] + 0x6ed9eba1, S32); 344 c = rotateLeft(c + H(d, a, b) + X[6] + 0x6ed9eba1, S33); 345 b = rotateLeft(b + H(c, d, a) + X[14] + 0x6ed9eba1, S34); 346 a = rotateLeft(a + H(b, c, d) + X[1] + 0x6ed9eba1, S31); 347 d = rotateLeft(d + H(a, b, c) + X[9] + 0x6ed9eba1, S32); 348 c = rotateLeft(c + H(d, a, b) + X[5] + 0x6ed9eba1, S33); 349 b = rotateLeft(b + H(c, d, a) + X[13] + 0x6ed9eba1, S34); 350 a = rotateLeft(a + H(b, c, d) + X[3] + 0x6ed9eba1, S31); 351 d = rotateLeft(d + H(a, b, c) + X[11] + 0x6ed9eba1, S32); 352 c = rotateLeft(c + H(d, a, b) + X[7] + 0x6ed9eba1, S33); 353 b = rotateLeft(b + H(c, d, a) + X[15] + 0x6ed9eba1, S34); 354 355 H1 += a; 356 H2 += b; 357 H3 += c; 358 H4 += d; 359 360 // 361 // reset the offset and clean out the word buffer. 362 // 363 xOff = 0; 364 for (int i = 0; i != X.length; i++) 365 { 366 X[i] = 0; 367 } 368 } 369 370 371 372 private void processLength(final long bitLength) 373 { 374 if (xOff > 14) 375 { 376 processBlock(); 377 } 378 379 X[14] = (int) (bitLength & 0xffffffff); 380 X[15] = (int) (bitLength >>> 32); 381 } 382 383 384 385 private void processWord(final byte[] in, final int inOff) 386 { 387 X[xOff++] = (in[inOff] & 0xff) | ((in[inOff + 1] & 0xff) << 8) 388 | ((in[inOff + 2] & 0xff) << 16) | ((in[inOff + 3] & 0xff) << 24); 389 390 if (xOff == 16) 391 { 392 processBlock(); 393 } 394 } 395 396 397 398 /* 399 * rotate int x left n bits. 400 */ 401 private int rotateLeft(final int x, final int n) 402 { 403 return (x << n) | (x >>> 32 - n); 404 } 405 406 407 408 private void unpackWord(final int word, final byte[] out, final int outOff) 409 { 410 out[outOff] = (byte) word; 411 out[outOff + 1] = (byte) (word >>> 8); 412 out[outOff + 2] = (byte) (word >>> 16); 413 out[outOff + 3] = (byte) (word >>> 24); 414 } 415 } 416 417 418 419 /** 420 * Plugin configuration object. 421 */ 422 private SambaPasswordPluginCfg config; 423 424 /** The name of the Samba LanMan password attribute. */ 425 private static final String SAMBA_LM_PASSWORD_ATTRIBUTE_NAME = 426 "sambaLMPassword"; 427 428 /** The name of the Samba NT password attribute. */ 429 private static final String SAMBA_NT_PASSWORD_ATTRIBUTE_NAME = 430 "sambaNTPassword"; 431 432 /** The name of the Samba account object class. */ 433 private static final String SAMBA_SAM_ACCOUNT_OC_NAME = "sambaSAMAccount"; 434 435 /** The name of the Samba last password change attribute. */ 436 private static final String SAMBA_PWD_LAST_SET_NAME = "sambaPwdLastSet"; 437 438 /** Debug tracer object to log debugging information. */ 439 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 440 441 /** Password Modify Extended Operation OID. */ 442 private static final String PWMOD_EXTOP_OID = "1.3.6.1.4.1.4203.1.11.1"; 443 444 /** Magic string to be used as salt. */ 445 private static final String MAGIC_STR = "KGS!@#$%"; 446 447 /** Default timestamp provider implementation. */ 448 private static final TimeStampProvider DEFAULT_TIMESTAMP_PROVIDER = 449 new TimeStampProvider() 450 { 451 @Override 452 public long getCurrentTime() 453 { 454 return System.currentTimeMillis() / 1000L; 455 } 456 }; 457 458 /** Use the default implementation of the timestamp provider... by default. */ 459 private TimeStampProvider timeStampProvider = DEFAULT_TIMESTAMP_PROVIDER; 460 461 462 /** 463 * Add the parity to the 56-bit key converting it to 64-bit key. 464 * 465 * @param key56 466 * 56-bit key. 467 * @return 64-bit key. 468 */ 469 private static byte[] addParity(final byte[] key56) 470 { 471 final byte[] key64 = new byte[8]; 472 final int[] key7 = new int[7]; 473 final int[] key8 = new int[8]; 474 475 for (int i = 0; i < 7; i++) 476 { 477 key7[i] = key56[i] & 0xFF; 478 } 479 480 key8[0] = key7[0]; 481 key8[1] = ((key7[0] << 7) & 0xFF) | (key7[1] >> 1); 482 key8[2] = ((key7[1] << 6) & 0xFF) | (key7[2] >> 2); 483 key8[3] = ((key7[2] << 5) & 0xFF) | (key7[3] >> 3); 484 key8[4] = ((key7[3] << 4) & 0xFF) | (key7[4] >> 4); 485 key8[5] = ((key7[4] << 3) & 0xFF) | (key7[5] >> 5); 486 key8[6] = ((key7[5] << 2) & 0xFF) | (key7[6] >> 6); 487 key8[7] = key7[6] << 1; 488 489 for (int i = 0; i < 8; i++) 490 { 491 key64[i] = (byte) setOddParity(key8[i]); 492 } 493 494 return key64; 495 496 } 497 498 499 500 /** 501 * Create a LanMan hash from a clear-text password. 502 * 503 * @param password 504 * Clear-text password. 505 * @return Hex string version of the hash based on the clear-text password. 506 * @throws UnsupportedEncodingException 507 * if the <code>US-ASCII</code> coding is not available. 508 * @throws NoSuchAlgorithmException 509 * if the algorithm does not exist for the used provider. 510 * @throws InvalidKeyException 511 * if the key is inappropriate to initialize the cipher. 512 * @throws NoSuchPaddingException 513 * if the padding scheme is not available. 514 * @throws IllegalBlockSizeException 515 * if this encryption algorithm is unable to process the input data 516 * provided 517 * @throws BadPaddingException 518 * if this cipher is in decryption mode, and (un)padding has been 519 * requested, but the decrypted data is not bounded by the 520 * appropriate padding bytes 521 */ 522 private static String lmHash(final String password) 523 throws UnsupportedEncodingException, NoSuchAlgorithmException, 524 InvalidKeyException, NoSuchPaddingException, IllegalBlockSizeException, 525 BadPaddingException 526 { 527 // Password has to be OEM encoded and in upper case 528 final byte[] oemPass = password.toUpperCase().getBytes("US-ASCII"); 529 530 // It shouldn't be longer then 14 bytes 531 int length = 14; 532 if (oemPass.length < length) 533 { 534 length = oemPass.length; 535 } 536 537 // The password should be divided into two 7-byte keys 538 final byte[] key1 = new byte[7]; 539 final byte[] key2 = new byte[7]; 540 if (length <= 7) 541 { 542 System.arraycopy(oemPass, 0, key1, 0, length); 543 } 544 else 545 { 546 System.arraycopy(oemPass, 0, key1, 0, 7); 547 System.arraycopy(oemPass, 7, key2, 0, length - 7); 548 } 549 550 // We create two DES keys using key1 and key2 to on the magic string 551 final SecretKey lowKey = new SecretKeySpec(addParity(key1), "DES"); 552 final SecretKey highKey = new SecretKeySpec(addParity(key2), "DES"); 553 final Cipher des = Cipher.getInstance("DES/ECB/NoPadding"); 554 des.init(Cipher.ENCRYPT_MODE, lowKey); 555 final byte[] lowHash = des.doFinal(MAGIC_STR.getBytes()); 556 des.init(Cipher.ENCRYPT_MODE, highKey); 557 final byte[] highHash = des.doFinal(MAGIC_STR.getBytes()); 558 559 // We finally merge hashes and return them to the client 560 561 final byte[] lmHash = new byte[16]; 562 System.arraycopy(lowHash, 0, lmHash, 0, 8); 563 System.arraycopy(highHash, 0, lmHash, 8, 8); 564 return toLowerCase(bytesToHexNoSpace(lmHash)); 565 } 566 567 568 569 /** 570 * Creates a NTLM hash from a clear-text password. 571 * 572 * @param password 573 * Clear text password. 574 * @return Returns a NTLM hash. 575 * @throws NoSuchProviderException 576 * if the BouncyCastle provider does not load 577 * @throws NoSuchAlgorithmException 578 * if the MD4 algorithm is not found 579 * @throws UnsupportedEncodingException 580 * if the encoding <code>UnicodeLittleUnmarked</code> is not 581 * supported. 582 */ 583 private static String ntHash(final String password) 584 throws NoSuchProviderException, UnsupportedEncodingException, 585 NoSuchAlgorithmException 586 { 587 final byte[] unicodePassword = password.getBytes("UnicodeLittleUnmarked"); 588 final MessageDigest md4 = new MD4MessageDigest(); 589 return toLowerCase(bytesToHexNoSpace(md4.digest(unicodePassword))); 590 } 591 592 593 594 /** 595 * Set the parity bit for an integer. 596 * 597 * @param integer 598 * to add the parity bit for. 599 * @return integer with the parity bit set. 600 */ 601 private static int setOddParity(final int parity) 602 { 603 final boolean hasEvenBits = (parity >>> 7 ^ parity >>> 6 604 ^ parity >>> 5 ^ parity >>> 4 605 ^ parity >>> 3 ^ parity >>> 2 606 ^ ((parity >>> 1) & 0x01)) == 0; 607 if (hasEvenBits) 608 { 609 return parity | 0x01; 610 } 611 else 612 { 613 return parity & 0xFE; 614 } 615 } 616 617 618 619 /** 620 * Default constructor. 621 */ 622 public SambaPasswordPlugin() 623 { 624 super(); 625 } 626 627 628 629 /** {@inheritDoc} */ 630 @Override 631 public ConfigChangeResult applyConfigurationChange( 632 final SambaPasswordPluginCfg newConfig) 633 { 634 635 // No validation required and no restart required. 636 config = newConfig; 637 638 return new ConfigChangeResult(); 639 } 640 641 642 643 /** {@inheritDoc} */ 644 @Override 645 public PluginResult.PostOperation doPostOperation( 646 final PostOperationExtendedOperation extendedOperation) 647 { 648 /* 649 * If the operation is not Password Modify Extended Operation then skip this 650 * operation. 651 */ 652 if (!extendedOperation.getRequestOID().equals(PWMOD_EXTOP_OID)) 653 { 654 return PluginResult.PostOperation.continueOperationProcessing(); 655 } 656 657 /* 658 * If the operation has not been successful then ignore the operation. 659 */ 660 if (extendedOperation.getResultCode() != ResultCode.SUCCESS) 661 { 662 return PluginResult.PostOperation.continueOperationProcessing(); 663 } 664 665 /* 666 * Verify if the operation has been initiated by what was defined as Samba 667 * administrative user. If so, we will skip this operation to avoid double 668 * synchronization of Samba attributes. 669 */ 670 final DN authDN = extendedOperation.getAuthorizationDN(); 671 final DN sambaAdminDN = config.getSambaAdministratorDN(); 672 if (sambaAdminDN != null 673 && !sambaAdminDN.isRootDN() 674 && authDN.equals(sambaAdminDN)) 675 { 676 if (logger.isTraceEnabled()) 677 { 678 logger.trace("This operation will be skipped because" 679 + " it was performed by Samba admin user: " + sambaAdminDN); 680 } 681 return PluginResult.PostOperation.continueOperationProcessing(); 682 } 683 684 // Get the name of the entry and clear passwords from the operation 685 // attachments. 686 final DN dn = (DN) extendedOperation 687 .getAttachment(PasswordModifyExtendedOperation.AUTHZ_DN_ATTACHMENT); 688 if (dn == null) 689 { 690 // The attachment is missing which should never happen. 691 if (logger.isTraceEnabled()) 692 { 693 logger.trace("SambaPasswordPlugin: missing DN attachment"); 694 } 695 return PluginResult.PostOperation.continueOperationProcessing(); 696 } 697 698 final String password = extendedOperation.getAttachment( 699 PasswordModifyExtendedOperation.CLEAR_PWD_ATTACHMENT).toString(); 700 if (password == null) 701 { 702 if (logger.isTraceEnabled()) 703 { 704 logger.trace("SambaPasswordPlugin: skipping syncing " 705 + "pre-encoded password"); 706 } 707 return PluginResult.PostOperation.continueOperationProcessing(); 708 } 709 710 @SuppressWarnings("unchecked") 711 final List<ByteString> encPasswords = (List<ByteString>) extendedOperation 712 .getAttachment(PasswordModifyExtendedOperation.ENCODED_PWD_ATTACHMENT); 713 714 try 715 { 716 // Before proceeding make sure this entry has samba object class. 717 final Entry entry = DirectoryServer.getEntry(dn); 718 if (!isSynchronizable(entry)) 719 { 720 if (logger.isTraceEnabled()) 721 { 722 logger.trace("The entry is not Samba object."); 723 } 724 return PluginResult.PostOperation.continueOperationProcessing(); 725 } 726 727 /* 728 * Make an internal connection to process the password modification. It 729 * will not trigger this plugin again with the pre-operation modify since 730 * the password passed would be encoded hence the pre operation part would 731 * skip it. 732 */ 733 final InternalClientConnection connection = InternalClientConnection 734 .getRootConnection(); 735 736 final List<Modification> modifications = getModifications(password); 737 738 // Use an assertion control to avoid race conditions since extended 739 // operation post-ops are done outside of the write lock. 740 List<Control> controls = null; 741 if (!encPasswords.isEmpty()) 742 { 743 final AttributeType pwdAttribute = (AttributeType) extendedOperation 744 .getAttachment( 745 PasswordModifyExtendedOperation.PWD_ATTRIBUTE_ATTACHMENT); 746 final LDAPFilter filter = RawFilter.createEqualityFilter( 747 pwdAttribute.getNameOrOID(), encPasswords.get(0)); 748 final Control assertionControl = new LDAPAssertionRequestControl(true, 749 filter); 750 controls = Collections.singletonList(assertionControl); 751 } 752 753 final ModifyOperation modifyOperation = connection.processModify(dn, 754 modifications, controls); 755 756 if (logger.isTraceEnabled()) 757 { 758 logger.trace("rc=%s", modifyOperation.getResultCode()); 759 } 760 } 761 catch (final DirectoryException e) 762 { 763 /* 764 * This exception occurs if there is a problem while retrieving the entry. 765 * This should never happen as we are processing the post-operation which 766 * succeeded so the entry has to exist if we have reached this point. 767 */ 768 logger.traceException(e); 769 } 770 771 return PluginResult.PostOperation.continueOperationProcessing(); 772 } 773 774 775 776 /** {@inheritDoc} */ 777 @Override 778 public PluginResult.PreOperation doPreOperation( 779 final PreOperationModifyOperation modifyOperation) 780 { 781 /* 782 * If the passwords are changed in clear text they will be available with 783 * the getNewPasswords() method. If they are encoded the method would return 784 * null. The list of passwords should not be modified. 785 */ 786 final List<ByteString> passwords = modifyOperation.getNewPasswords(); 787 788 /* 789 * If the password list is not empty, we can be sure the current operation 790 * is the one that applies to our case: - it's a modify operation on 791 * userPassword attribute - it's replaces or adds new userPassword attribute 792 * value. If it doesn't then we skip this modify operation. 793 */ 794 if (passwords == null) 795 { 796 return PluginResult.PreOperation.continueOperationProcessing(); 797 } 798 799 // Skip synchronization operations. 800 if (modifyOperation.isSynchronizationOperation()) 801 { 802 if (logger.isTraceEnabled()) 803 { 804 logger.trace("Synchronization operation. Skipping."); 805 } 806 return PluginResult.PreOperation.continueOperationProcessing(); 807 } 808 809 /* 810 * Verify if the operation has been initiated by the Samba administrative 811 * user. If so, we will skip this operation to avoid double synchronization 812 * of Samba attributes. 813 */ 814 final DN authDN = modifyOperation.getAuthorizationDN(); 815 final DN sambaAdminDN = config.getSambaAdministratorDN(); 816 if (sambaAdminDN != null 817 && !sambaAdminDN.isRootDN() 818 && authDN.equals(sambaAdminDN)) 819 { 820 if (logger.isTraceEnabled()) 821 { 822 logger.trace("This operation will be skipped because" 823 + " it was performed by Samba admin user: " + sambaAdminDN); 824 } 825 return PluginResult.PreOperation.continueOperationProcessing(); 826 } 827 828 /* 829 * Before proceeding with the modification, we have to make sure this entry 830 * is indeed a Samba object. 831 */ 832 if (!isSynchronizable(modifyOperation.getCurrentEntry())) 833 { 834 if (logger.isTraceEnabled()) 835 { 836 logger.trace("Skipping '%s' because it does not have Samba object class.", modifyOperation.getEntryDN()); 837 } 838 return PluginResult.PreOperation.continueOperationProcessing(); 839 } 840 841 /* 842 * Proceed with processing: add a new modification to the current modify 843 * operation, so they could be executed at the same time. 844 */ 845 processModification(modifyOperation, passwords); 846 847 // Continue plugin processing. 848 return PluginResult.PreOperation.continueOperationProcessing(); 849 } 850 851 852 853 /** {@inheritDoc} */ 854 @Override 855 public void initializePlugin(final Set<PluginType> pluginTypes, 856 final SambaPasswordPluginCfg configuration) throws ConfigException, 857 InitializationException 858 { 859 860 // Verify config parameters. 861 final LinkedList<LocalizableMessage> messages = new LinkedList<>(); 862 if (!isConfigurationAcceptable(configuration, messages)) 863 { 864 for (final LocalizableMessage m : messages) 865 { 866 logger.error(m); 867 } 868 throw new ConfigException(messages.poll()); 869 } 870 871 // Register the configuration change listener. 872 configuration.addSambaPasswordChangeListener(this); 873 874 // Save the configuration. 875 this.config = configuration; 876 } 877 878 879 880 /** 881 * Verifies if the plugin configuration is acceptable. 882 * 883 * @param configuration 884 * The plugin configuration. 885 * @param unacceptableReasons 886 * Reasons why the configuration is not acceptable. 887 * @return Returns <code>true</code> for the correct configuration and 888 * <code>false</code> for the incorrect one. 889 */ 890 public boolean isConfigurationAcceptable( 891 final SambaPasswordPluginCfg configuration, 892 final List<LocalizableMessage> unacceptableReasons) 893 { 894 return isConfigurationChangeAcceptable(configuration, unacceptableReasons); 895 } 896 897 898 899 /** {@inheritDoc} */ 900 @Override 901 public boolean isConfigurationChangeAcceptable( 902 final SambaPasswordPluginCfg newConfig, final List<LocalizableMessage> messages) 903 { 904 /* 905 * The plugin implements only postoperationmodify and postoperationextended 906 * plugin types. The rest should be rejected. 907 */ 908 909 final SortedSet<PluginCfgDefn.PluginType> pluginTypes = newConfig 910 .getPluginType(); 911 for (final PluginCfgDefn.PluginType t : pluginTypes) 912 { 913 switch (t) 914 { 915 case PREOPERATIONMODIFY: 916 case POSTOPERATIONEXTENDED: 917 break; 918 default: 919 messages.add(ERR_PLUGIN_SAMBA_SYNC_INVALID_PLUGIN_TYPE.get(t)); 920 return false; 921 } 922 } 923 924 return true; 925 } 926 927 928 929 /** 930 * Creates the modifications to modify Samba password attributes. It uses 931 * clear-text password and encodes it with the appropriate algorithm for it's 932 * respective type (NTLM or LanMan); then it wraps it in the modifications to 933 * be added to the modify operation. 934 * 935 * @param password 936 * New password which is to be encoded for Samba. 937 * @return Returns a list of modifications, or null if a problem occurs. 938 */ 939 private List<Modification> getModifications(final String password) 940 { 941 ArrayList<Modification> modifications = new ArrayList<>(); 942 try 943 { 944 if (config.getPwdSyncPolicy().contains(PwdSyncPolicy.SYNC_NT_PASSWORD)) 945 { 946 final Attribute attribute = Attributes.create( 947 SAMBA_NT_PASSWORD_ATTRIBUTE_NAME, ntHash(password)); 948 modifications.add(new Modification(ModificationType.REPLACE, attribute)); 949 } 950 951 if (config.getPwdSyncPolicy().contains(PwdSyncPolicy.SYNC_LM_PASSWORD)) 952 { 953 final Attribute attribute = Attributes.create( 954 SAMBA_LM_PASSWORD_ATTRIBUTE_NAME, lmHash(password)); 955 modifications.add(new Modification(ModificationType.REPLACE, attribute)); 956 } 957 final Attribute pwdLastSet = Attributes.create( 958 SAMBA_PWD_LAST_SET_NAME, 959 String.valueOf(timeStampProvider.getCurrentTime())); 960 modifications.add(new Modification(ModificationType.REPLACE, pwdLastSet)); 961 } 962 catch (final Exception e) 963 { 964 logger.info(ERR_PLUGIN_SAMBA_SYNC_ENCODING, e.getMessage(), e); 965 modifications = null; 966 } 967 968 return modifications; 969 } 970 971 972 973 /** 974 * Verify if the target entry contains pre-defined Samba object class. 975 * 976 * @param entry 977 * The entry being modified. 978 * @return Returns true if the entry has Samba object class, otherwise returns 979 * false. 980 */ 981 private boolean isSynchronizable(final Entry entry) 982 { 983 final Schema schema = DirectoryServer.getSchema(); 984 final ObjectClass sambaOc = schema 985 .getObjectClass(toLowerCase(SAMBA_SAM_ACCOUNT_OC_NAME)); 986 return sambaOc != null && entry.hasObjectClass(sambaOc); 987 } 988 989 990 991 /** 992 * Adds modifications for the configured Samba password attributes to the 993 * current modify operation. 994 * 995 * @param modifyOperation 996 * Current modify operation which will be modified to add Samba 997 * password attribute changes. 998 * @param passwords 999 * List of userPassword clear-text attribute values to be hashed for 1000 * Samba 1001 */ 1002 private void processModification( 1003 final PreOperationModifyOperation modifyOperation, 1004 final List<ByteString> passwords) 1005 { 1006 // Get the last password (in case there is more then one). 1007 final String password = passwords.get(passwords.size() - 1).toString(); 1008 try 1009 { 1010 // Generate the necessary modifications. 1011 for (final Modification modification : getModifications(password)) 1012 { 1013 modifyOperation.addModification(modification); 1014 } 1015 } 1016 catch (final DirectoryException e) 1017 { 1018 logger.info(ERR_PLUGIN_SAMBA_SYNC_MODIFICATION_PROCESSING, e.getMessage(), e); 1019 } 1020 } 1021 1022 /** 1023 * Timestamp provider interface. Intended primarily for testing purposes. 1024 */ 1025 static interface TimeStampProvider 1026 { 1027 /** 1028 * Generates a custom time stamp. 1029 * 1030 * @return A timestamp in UNIX format. 1031 */ 1032 long getCurrentTime(); 1033 } 1034 1035 /** 1036 * Use custom timestamp provider. Intended primarily for testing purposes. 1037 * 1038 * @param timeStampProvider Provider object that implements the 1039 * TimeStampProvider interface. 1040 */ 1041 void setTimeStampProvider(TimeStampProvider timeStampProvider) 1042 { 1043 this.timeStampProvider = (timeStampProvider == null) 1044 ? DEFAULT_TIMESTAMP_PROVIDER : timeStampProvider; 1045 } 1046}