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

                 Key: DIRSTUDIO-648
                 URL: https://issues.apache.org/jira/browse/DIRSTUDIO-648
             Project: Directory Studio
          Issue Type: New Feature
    Affects Versions: 1.5.3
         Environment: Apache Directory Studio 1.5.3 against OpenLDAP 2.4.x with 
slapo-ppolicy enabled; using hashed passwords; on Ubuntu Linux
            Reporter: Carsten Tolkmit
            Priority: Minor


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(i)
                        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!


-- 
This message is automatically generated by JIRA.
-
If you think it was sent incorrectly contact one of the administrators: 
https://issues.apache.org/jira/secure/Administrators.jspa
-
For more information on JIRA, see: http://www.atlassian.com/software/jira

        

Reply via email to