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