Uploaded image for project: 'Directory Studio'
  1. Directory Studio
  2. DIRSTUDIO-648

Studio should support the Password Modify Extended Operation according to RFC 3062

    XMLWordPrintableJSON

    Details

    • Type: New Feature
    • Status: Closed
    • Priority: Major
    • Resolution: Fixed
    • Affects Version/s: 1.5.3
    • Fix Version/s: 2.0.0-M15
    • Component/s: None
    • Labels:
      None
    • Environment:
      Apache Directory Studio 1.5.3 against OpenLDAP 2.4.x with slapo-ppolicy enabled; using hashed passwords; on Ubuntu Linux

      Description

      In my environment I use Directory Studio 1.5.3 to connect to an OpenLDAP 2.4.x-Server with the ppolicy overlay enabled.
      The policy overlay is used to set individual policies to user accounts, i.e. maximum password age etc, and it is also configured to check for minimum password quality ( pwdCheckQuality is set to 2 in some policies ).
      When I edit a user account's (hashed) userPassword in Directory Studio, a "19 - Password is too simple" is returned in every case, because the server cannot know the real password (because it is hashed) it rejects the new one - this behaviour is expected with hashed passwords, of course.
      But that's one of the points RFC 3062 was made up for - it passes the cleartext password (via a TLS secured channel in our case) to the server and let's the server hash the password. Sadly, Directory Studio can not / does not support this operation, so currently, I have to do administrative password modifications in two steps:
      1) set pwdReset to TRUE to allow password modification even if the minimum password age is not reached
      2) use ldappasswd (on the linux shell) to set the new password
      This is of unnecessary complexity I think, as the Extended Operation is quite easy to implement with JNDI, I give an example using a little bit of Java and Groovy:

      I use the Bouncycastle Crypto Lib for ASN.1 encoding, but since it has some nasty features/bugs, I had to build a special version of the DERTaggedObject (basically a copy&paste version with some changes I don't recall in detail right now), of course other ASN.1 libs might not need this special behaviour:


      package org.bouncycastle.asn1;

      import java.io.ByteArrayOutputStream;
      import java.io.IOException;
      import java.io.OutputStream;

      /**

      • DER TaggedObject - in ASN.1 nottation this is any object proceeded by a [n]
      • where n is some number - these are assume to follow the construction rules
      • (as with sequences).
        */
        public class DERLongTaggedObject extends DERTaggedObject {

      @SuppressWarnings("unused")
      private final static org.apache.commons.logging.Log logger = org.apache.commons.logging.LogFactory
      .getLog(DERLongTaggedObject.class);

      protected int hiBits = 0;

      /**

      • @param tagNo
      • the tag number for this object.
      • @param obj
      • the tagged object.
        */
        public DERLongTaggedObject(int hiBits, int tagNo, DEREncodable obj) { super(tagNo, obj); this.hiBits = hiBits; }

      /**

      • @param explicit
      • true if an explicitly tagged object.
      • @param tagNo
      • the tag number for this object.
      • @param obj
      • the tagged object.
        */
        public DERLongTaggedObject(boolean explicit, int hiBits, int tagNo, DEREncodable obj) { super(explicit, tagNo, obj); this.hiBits = hiBits; }

      /**

      • create an implicitly tagged object that contains a zero length sequence.
        */
        public DERLongTaggedObject(int hiBits, int tagNo) { this(false, hiBits, tagNo, new DERSequence()); }

      void encode(DEROutputStream out) throws IOException {
      // logger.debug("going to write with tag = "tagNo", hiBits = "+hiBits);
      if (!empty) {
      ByteArrayOutputStream bOut = new ByteArrayOutputStream();
      DEROutputStream dOut = new DEROutputStream(bOut);

      dOut.writeObject(obj);
      dOut.close();

      byte[] bytes = bOut.toByteArray();

      if (tagNo < 31)

      { encodeTaggedShort(out, bytes); }

      else

      { encodeTaggedLong(out, bytes); }

      } else {
      if (tagNo < 31)

      { encodeEmptyTaggedShort(out); }

      else

      { encodeEmptyTaggedLong(out); }

      }
      }

      private void encodeEmptyTaggedLong(DEROutputStream out) throws IOException

      { out.write(CONSTRUCTED | TAGGED | 31); writeTagNoLong(out); out.write(0); // length }

      private void encodeEmptyTaggedShort(DEROutputStream out) throws IOException

      { out.writeEncoded(CONSTRUCTED | TAGGED | tagNo, new byte[0]); }

      private void encodeTaggedLong(DEROutputStream out, byte[] encodedObject)
      throws IOException {
      if (explicit)

      { // out.writeEncoded(CONSTRUCTED | TAGGED | tagNo, encodedObject); out.write(CONSTRUCTED | TAGGED | 31); // a tag in long format // follows writeTagNoLong(out); writeLength(out, encodedObject.length); out.write(encodedObject); }

      else {
      //
      // need to mark constructed types...
      //
      if ((hiBits & CONSTRUCTED ) != 0 || (encodedObject[0] & CONSTRUCTED) != 0)

      { out.write(CONSTRUCTED | TAGGED | 31); }

      else

      { out.write(TAGGED | 31); }

      writeTagNoLong(out);
      out.write(encodedObject, 1, encodedObject.length - 1);
      }

      }

      private void writeTagNoLong(DEROutputStream out) throws IOException {
      long tagNoL = tagNo;
      boolean writeZero = false;
      for (int offset = 28; offset >= 0; offset -= 7) {
      long sevenbits = (tagNoL >>> offset) & 0x7F;
      if (sevenbits == 0 && !writeZero)

      { // leading block is empty, go to next 7 bits continue; }

      // from now on, zero value blocks have to be written.
      writeZero = true;
      if (offset > 0)

      { // set highest bit, because more blocks follow sevenbits |= 0x80; }

      out.write((int) sevenbits);
      }
      }

      private void encodeTaggedShort(DEROutputStream out, byte[] encodedObject)
      throws IOException {
      if (explicit)

      { out.writeEncoded(CONSTRUCTED | TAGGED | tagNo, encodedObject); }

      else {
      //
      // need to mark constructed types...
      //
      if ((hiBits & CONSTRUCTED ) != 0 || (encodedObject[0] & CONSTRUCTED) != 0 )

      { encodedObject[0] = (byte) (CONSTRUCTED | TAGGED | tagNo); }

      else

      { encodedObject[0] = (byte) (TAGGED | tagNo); }

      out.write(encodedObject);
      }
      }

      private void writeLength(OutputStream out, int length) throws IOException {
      if (length > 127) {
      int size = 1;
      int val = length;

      while ((val >>>= 8) != 0)

      { size++; }

      out.write((byte) (size | 0x80));

      for (int i = (size - 1) * 8; i >= 0; i -= 8)

      { out.write((byte) (length >> i)); }

      } else

      { out.write((byte) length); }

      }
      }

      Now the groovy code:


      class PasswordModifyResponse implements ExtendedResponse {

      byte[] encodedValue

      String id

      String genPasswd

      public PasswordModifyResponse(String id, byte[] encodedValue)

      { this.id = id this.encodedValue = encodedValue performDecoding() }

      private performDecoding() {
      def ev = getEncodedValue()
      if ( ev.length == 0 )

      { return }

      def asn1in = new ASN1InputStream(ev)
      ASN1Sequence seq = asn1in.readObject()
      println "[1] seq: ${seq.class} ${seq}"
      for ( int i = 0 ; i < seq.size() ; i++ ) {
      def obj = seq.getObjectAt
      println "[2] obj: ${obj.class} ${obj} [${obj.tagNo}]"
      if ( obj.tagNo == 0 )

      { genPasswd = new String(obj.object.octets,'UTF-8') }

      }
      }

      @Override
      public byte[] getEncodedValue()

      { return encodedValue; }

      @Override
      public String getID()

      { return id; }

      @Override
      public String toString()

      { return super.toString() + 'ID:'+getID()+';encodedValue:'+getEncodedValue()+';genPasswd:'+genPasswd }

      }


      class PasswordModifyRequest implements ExtendedRequest {

      static final OID = '1.3.6.1.4.1.4203.1.11.1'

      String userIdentity
      String oldPasswd
      String newPasswd

      public PasswordModifyRequest(String userIdentity, String oldPasswd,
      String newPasswd)

      { this.userIdentity = userIdentity; this.oldPasswd = oldPasswd; this.newPasswd = newPasswd; }

      /*

      • passwdModifyOID OBJECT IDENTIFIER ::= 1.3.6.1.4.1.4203.1.11.1

      PasswdModifyRequestValue ::= SEQUENCE

      { userIdentity [0] OCTET STRING OPTIONAL oldPasswd [1] OCTET STRING OPTIONAL newPasswd [2] OCTET STRING OPTIONAL }

      PasswdModifyResponseValue ::= SEQUENCE

      { genPasswd [0] OCTET STRING OPTIONAL }

      */

      @Override
      public ExtendedResponse createExtendedResponse(String id, byte[] berValue,
      int offset, int length) throws NamingException {
      byte[] input = new byte[length]
      if ( berValue != null )

      { System.arraycopy(berValue, offset, input, 0, length) }


      return new PasswordModifyResponse(id, input);
      }

      @Override
      public byte[] getEncodedValue() {
      ASN1EncodableVector v = new ASN1EncodableVector()

      if ( userIdentity )

      { v.add(new DERLongTaggedObject(false, 0, 0, new DEROctetString(userIdentity.getBytes('UTF-8')))) }

      if ( oldPasswd )

      { v.add(new DERLongTaggedObject(false, 0, 1, new DEROctetString(oldPasswd.getBytes('UTF-8')))) }

      if ( newPasswd )

      { v.add(new DERLongTaggedObject(false, 0, 2, new DEROctetString(newPasswd.getBytes('UTF-8')))) }

      BERSequence sequence = new BERSequence(v)
      def encoded = sequence.getEncoded(BERSequence.BER)
      println "encoded: ${encoded}"
      File f = new File('/tmp/asn1.content')
      f.delete()
      f << encoded
      return encoded
      }

      @Override
      public String getID()

      { return OID; }

      }

      Usage will then be as follows (groovy pseudocode as well):

      LdapContext ctx = ...
      PasswordModifyRequest req = new PasswordModifyRequest(userDn, userPass, newPassword)
      PasswordModifyResponse resp = ctx.extendedOperation(req)
      println "resp: ${resp}"

      Hope this helps!

        Attachments

        1. Screenshot_2020-03-29_19-46-24.png
          79 kB
          Stefan Seelmann
        2. Screenshot_2020-03-29_19-48-57.png
          30 kB
          Stefan Seelmann

          Issue Links

            Activity

              People

              • Assignee:
                seelmann Stefan Seelmann
                Reporter:
                laenny Carsten Tolkmit
              • Votes:
                0 Vote for this issue
                Watchers:
                2 Start watching this issue

                Dates

                • Created:
                  Updated:
                  Resolved: