[ https://issues.apache.org/jira/browse/DIRSTUDIO-648?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=17072192#comment-17072192 ]
Emmanuel Lécharny commented on DIRSTUDIO-648: --------------------------------------------- Thanks Stefan ! > 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 > Assignee: Stefan Seelmann > Priority: Major > Fix For: 2.0.0-M15 > > Attachments: Screenshot_2020-03-29_19-46-24.png, > Screenshot_2020-03-29_19-48-57.png > > > 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 was sent by Atlassian Jira (v8.3.4#803005) --------------------------------------------------------------------- To unsubscribe, e-mail: dev-unsubscr...@directory.apache.org For additional commands, e-mail: dev-h...@directory.apache.org