Implement 389 DS's PBKDF2_SHA256 password scheme.
authorMathieu Baudier <mbaudier@argeo.org>
Mon, 22 Jun 2020 09:54:17 +0000 (11:54 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Mon, 22 Jun 2020 09:54:17 +0000 (11:54 +0200)
org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java
org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java
org.argeo.enterprise/src/org/argeo/osgi/useradmin/DigestUtils.java
org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapUserAdmin.java
org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java
org.argeo.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java

index 6f353f155c5c620e9c0e24de4e260fb24165c797..85a44708204312646d30b7db4f235599231957a3 100644 (file)
@@ -166,11 +166,21 @@ public class AggregatingUserAdmin implements UserAdmin {
                        return tokens;
                List<UserAdmin> res = new ArrayList<UserAdmin>(1);
                for (LdapName baseDn : businessRoles.keySet()) {
+                       AbstractUserDirectory ud = businessRoles.get(baseDn);
                        if (name.startsWith(baseDn)) {
-                               AbstractUserDirectory ud = businessRoles.get(baseDn);
                                if (!ud.isDisabled())
                                        res.add(ud);
                        }
+//                     Object principal = ud.getProperties().get(Context.SECURITY_PRINCIPAL);
+//                     if (principal != null) {
+//                             try {
+//                                     LdapName principalLdapName = new LdapName(principal.toString());
+//                                     if (principalLdapName.equals(name))
+//                                             res.add(ud);
+//                             } catch (InvalidNameException e) {
+//                                     // silent
+//                             }
+//                     }
                }
                if (res.size() == 0)
                        throw new UserDirectoryException("Cannot find user admin for " + name);
index 939b03852247cfd0973900b9745c7f93bdc3aeb9..01db8be9895b9f3548728f2b6d5c580f684424e4 100644 (file)
@@ -21,6 +21,8 @@ public class AuthenticatingUser implements User {
        private final Dictionary<String, Object> credentials;
 
        public AuthenticatingUser(LdapName name) {
+               if (name == null)
+                       throw new NullPointerException("Provided name cannot be null.");
                this.name = name.toString();
                this.credentials = new Hashtable<>();
        }
@@ -31,6 +33,8 @@ public class AuthenticatingUser implements User {
        }
 
        public AuthenticatingUser(String name, char[] password) {
+               if (name == null)
+                       throw new NullPointerException("Provided name cannot be null.");
                this.name = name;
                credentials = new Hashtable<>();
                credentials.put(SHARED_STATE_NAME, name);
@@ -65,4 +69,14 @@ public class AuthenticatingUser implements User {
                throw new UnsupportedOperationException();
        }
 
+       @Override
+       public int hashCode() {
+               return name.hashCode();
+       }
+
+       @Override
+       public String toString() {
+               return "Authenticating user " + name;
+       }
+
 }
index 51d18349b2a662a762871e9464c5e24370a01c77..511c2fede5e747e8e7e3adef942a09063793c579 100644 (file)
@@ -1,13 +1,21 @@
 package org.argeo.osgi.useradmin;
 
+import java.math.BigInteger;
 import java.nio.ByteBuffer;
 import java.nio.CharBuffer;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
+import java.security.spec.KeySpec;
 import java.util.Arrays;
 
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+
 /** Utilities around digests, mostly those related to passwords. */
 class DigestUtils {
+       final static String PASSWORD_SCHEME_SHA = "SHA";
+       final static String PASSWORD_SCHEME_PBKDF2_SHA256 = "PBKDF2_SHA256";
+
        static byte[] sha1(byte[] bytes) {
                try {
                        MessageDigest digest = MessageDigest.getInstance("SHA1");
@@ -19,6 +27,40 @@ class DigestUtils {
                }
        }
 
+       static byte[] toPasswordScheme(String passwordScheme, char[] password, byte[] salt, Integer iterations,
+                       Integer keyLength) {
+               try {
+                       if (PASSWORD_SCHEME_SHA.equals(passwordScheme)) {
+                               MessageDigest digest = MessageDigest.getInstance("SHA1");
+                               byte[] bytes = charsToBytes(password);
+                               digest.update(bytes);
+                               return digest.digest();
+                       } else if (PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) {
+                               KeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength);
+
+                               SecretKeyFactory f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
+                               final int ITERATION_LENGTH = 4;
+                               byte[] key = f.generateSecret(spec).getEncoded();
+                               byte[] result = new byte[ITERATION_LENGTH + salt.length + key.length];
+                               byte iterationsArr[] = new BigInteger(iterations.toString()).toByteArray();
+                               if (iterationsArr.length < ITERATION_LENGTH) {
+                                       Arrays.fill(result, 0, ITERATION_LENGTH - iterationsArr.length, (byte) 0);
+                                       System.arraycopy(iterationsArr, 0, result, ITERATION_LENGTH - iterationsArr.length,
+                                                       iterationsArr.length);
+                               } else {
+                                       System.arraycopy(iterationsArr, 0, result, 0, ITERATION_LENGTH);
+                               }
+                               System.arraycopy(salt, 0, result, ITERATION_LENGTH, salt.length);
+                               System.arraycopy(key, 0, result, ITERATION_LENGTH + salt.length, key.length);
+                               return result;
+                       } else {
+                               throw new UnsupportedOperationException("Unkown password scheme " + passwordScheme);
+                       }
+               } catch (Exception e) {
+                       throw new UserDirectoryException("Cannot digest", e);
+               }
+       }
+
        static char[] bytesToChars(Object obj) {
                if (obj instanceof char[])
                        return (char[]) obj;
index 58f6eb1face2b1c92a76fad634abeeb9db918adb..22c178ef473916f6597ab471d8f69dd7dbb36b7a 100644 (file)
@@ -34,6 +34,9 @@ import org.osgi.service.useradmin.User;
 public class LdapUserAdmin extends AbstractUserDirectory {
        private InitialLdapContext initialLdapContext = null;
 
+//     private LdapName adminUserDn = null;
+//     private LdifUser adminUser = null;
+
        public LdapUserAdmin(Dictionary<String, ?> properties) {
                super(null, properties);
                try {
@@ -54,11 +57,15 @@ public class LdapUserAdmin extends AbstractUserDirectory {
                        Object principal = properties.get(Context.SECURITY_PRINCIPAL);
                        if (principal != null) {
                                initialLdapContext.addToEnvironment(Context.SECURITY_PRINCIPAL, principal.toString());
+//                             adminUserDn = new LdapName(principal.toString());
+//                             BasicAttributes adminUserAttrs = new BasicAttributes();
+//                             adminUser = new LdifUser(this, adminUserDn, adminUserAttrs);
                                Object creds = properties.get(Context.SECURITY_CREDENTIALS);
                                if (creds != null) {
                                        initialLdapContext.addToEnvironment(Context.SECURITY_CREDENTIALS, creds.toString());
-
+//                                     adminUserAttrs.put(LdapAttrs.userPassword.name(), adminUser.hash(creds.toString().toCharArray()));
                                }
+//                             adminUserAttrs.put(LdapAttrs.memberOf.name(), "cn=admin,ou=roles,ou=node");
                        }
                } catch (Exception e) {
                        throw new UserDirectoryException("Cannot connect to LDAP", e);
@@ -122,6 +129,9 @@ public class LdapUserAdmin extends AbstractUserDirectory {
                                throw new UserDirectoryException("Unsupported LDAP type for " + name);
                        return res;
                } catch (NameNotFoundException e) {
+//                     if (adminUserDn != null && adminUserDn.equals(name)) {
+//                             return adminUser;
+//                     }
                        throw e;
                } catch (NamingException e) {
                        return null;
@@ -130,6 +140,7 @@ public class LdapUserAdmin extends AbstractUserDirectory {
 
        @Override
        protected List<DirectoryUser> doGetRoles(Filter f) {
+               ArrayList<DirectoryUser> res = new ArrayList<DirectoryUser>();
                try {
                        String searchFilter = f != null ? f.toString()
                                        : "(|(" + objectClass + "=" + getUserObjectClass() + ")(" + objectClass + "="
@@ -140,7 +151,6 @@ public class LdapUserAdmin extends AbstractUserDirectory {
                        LdapName searchBase = getBaseDn();
                        NamingEnumeration<SearchResult> results = getLdapContext().search(searchBase, searchFilter, searchControls);
 
-                       ArrayList<DirectoryUser> res = new ArrayList<DirectoryUser>();
                        results: while (results.hasMoreElements()) {
                                SearchResult searchResult = results.next();
                                Attributes attrs = searchResult.getAttributes();
@@ -160,6 +170,8 @@ public class LdapUserAdmin extends AbstractUserDirectory {
                                res.add(role);
                        }
                        return res;
+//             } catch (NameNotFoundException e) {
+//                     return res;
                } catch (Exception e) {
                        throw new UserDirectoryException("Cannot get roles for filter " + f, e);
                }
index 392b17428b00b748be25de7f2c8b04000438c120..f18c148f849c3a0e856396fdc4de098044d74a3f 100644 (file)
@@ -1,5 +1,8 @@
 package org.argeo.osgi.useradmin;
 
+import static java.nio.charset.StandardCharsets.US_ASCII;
+
+import java.math.BigInteger;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -95,49 +98,12 @@ class LdifUser implements DirectoryUser {
                        }
 
                        // Regular password
-                       byte[] hashedPassword = hash(password);
-                       if (hasCredential(LdapAttrs.userPassword.name(), hashedPassword))
+//                     byte[] hashedPassword = hash(password, DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256);
+                       if (hasCredential(LdapAttrs.userPassword.name(), DigestUtils.charsToBytes(password)))
                                return true;
-                       // if (hasCredential(LdapAttrs.authPassword.name(), pwd))
-                       // return true;
                        return false;
                }
 
-               // authPassword (RFC 3112 https://tools.ietf.org/html/rfc3112)
-               // if (key.startsWith(ClientToken.X_CLIENT_TOKEN)) {
-               // return ClientToken.checkAttribute(getAttributes(), key, value);
-               // } else if (key.startsWith(OnceToken.X_ONCE_TOKEN)) {
-               // return OnceToken.checkAttribute(getAttributes(), key, value);
-               // }
-               // StringTokenizer st = new StringTokenizer((String) storedValue, "$ ");
-               // // TODO make it more robust, deal with bad formatting
-               // String authScheme = st.nextToken();
-               // String authInfo = st.nextToken();
-               // String authValue = st.nextToken();
-               // if (authScheme.equals(UriToken.X_URI_TOKEN)) {
-               // UriToken token = new UriToken((String)storedValue);
-               // try {
-               // URI uri = new URI(authInfo);
-               // Map<String, List<String>> query = NamingUtils.queryToMap(uri);
-               // String expiryTimestamp = NamingUtils.getQueryValue(query,
-               // LdapAttrs.modifyTimestamp.name());
-               // if (expiryTimestamp != null) {
-               // Instant expiryOdt = NamingUtils.ldapDateToInstant(expiryTimestamp);
-               // if (expiryOdt.isBefore(Instant.now()))
-               // return false;
-               // } else {
-               // throw new UnsupportedOperationException("An expiry timestamp "
-               // + LdapAttrs.modifyTimestamp.name() + " must be set in the URI query");
-               // }
-               // byte[] hash = Base64.getDecoder().decode(authValue);
-               // byte[] hashedInput = DigestUtils.sha1((authInfo +
-               // value).getBytes(StandardCharsets.US_ASCII));
-               // return Arrays.equals(hash, hashedInput);
-               // } catch (URISyntaxException e) {
-               // throw new UserDirectoryException("Badly formatted " + authInfo, e);
-               // }
-               // }
-
                Object storedValue = getCredentials().get(key);
                if (storedValue == null || value == null)
                        return false;
@@ -145,45 +111,60 @@ class LdifUser implements DirectoryUser {
                        return false;
                if (storedValue instanceof String && value instanceof String)
                        return storedValue.equals(value);
-               if (storedValue instanceof byte[] && value instanceof byte[])
-                       return Arrays.equals((byte[]) storedValue, (byte[]) value);
+               if (storedValue instanceof byte[] && value instanceof byte[]) {
+                       String storedBase64 = new String((byte[]) storedValue, US_ASCII);
+                       String passwordScheme = null;
+                       if (storedBase64.charAt(0) == '{') {
+                               int index = storedBase64.indexOf('}');
+                               if (index > 0) {
+                                       passwordScheme = storedBase64.substring(1, index);
+                                       byte[] storedValueBytes = Base64.getDecoder().decode(storedBase64.substring(index + 1));
+                                       char[] passwordValue = DigestUtils.bytesToChars((byte[]) value);
+                                       byte[] valueBytes;
+                                       if (DigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) {
+                                               valueBytes = DigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null,
+                                                               null);
+                                       } else if (DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) {
+                                               // see https://www.thesubtlety.com/post/a-389-ds-pbkdf2-password-checker/
+                                               byte[] iterationsArr = Arrays.copyOfRange(storedValueBytes, 0, 4);
+                                               BigInteger iterations = new BigInteger(iterationsArr);
+                                               byte[] salt = Arrays.copyOfRange(storedValueBytes, iterationsArr.length,
+                                                               iterationsArr.length + 64);
+                                               byte[] keyArr = Arrays.copyOfRange(storedValueBytes, iterationsArr.length + salt.length,
+                                                               storedValueBytes.length);
+                                               int keyLengthBits = keyArr.length * 8;
+                                               valueBytes = DigestUtils.toPasswordScheme(passwordScheme, passwordValue, salt,
+                                                               iterations.intValue(), keyLengthBits);
+                                       } else {
+                                               throw new UnsupportedOperationException("Unknown password scheme " + passwordScheme);
+                                       }
+                                       return Arrays.equals(storedValueBytes, valueBytes);
+                               }
+                       }
+               }
+//             if (storedValue instanceof byte[] && value instanceof byte[]) {
+//                     return Arrays.equals((byte[]) storedValue, (byte[]) value);
+//             }
                return false;
        }
 
-       /** Hash and clear the password */
-       private byte[] hash(char[] password) {
+       /** Hash the password */
+       byte[] sha1hash(char[] password) {
                byte[] hashedPassword = ("{SHA}"
                                + Base64.getEncoder().encodeToString(DigestUtils.sha1(DigestUtils.charsToBytes(password))))
                                                .getBytes(StandardCharsets.UTF_8);
-               // Arrays.fill(password, '\u0000');
                return hashedPassword;
        }
 
-       // private byte[] toBytes(char[] chars) {
-       // CharBuffer charBuffer = CharBuffer.wrap(chars);
-       // ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(charBuffer);
-       // byte[] bytes = Arrays.copyOfRange(byteBuffer.array(), byteBuffer.position(),
-       // byteBuffer.limit());
-       // // Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data
-       // Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data
-       // return bytes;
-       // }
-       //
-       // private char[] toChars(Object obj) {
-       // if (obj instanceof char[])
-       // return (char[]) obj;
-       // if (!(obj instanceof byte[]))
-       // throw new IllegalArgumentException(obj.getClass() + " is not a byte array");
-       // ByteBuffer fromBuffer = ByteBuffer.wrap((byte[]) obj);
-       // CharBuffer toBuffer = StandardCharsets.UTF_8.decode(fromBuffer);
-       // char[] res = Arrays.copyOfRange(toBuffer.array(), toBuffer.position(),
-       // toBuffer.limit());
-       // Arrays.fill(fromBuffer.array(), (byte) 0); // clear sensitive data
-       // Arrays.fill((byte[]) obj, (byte) 0); // clear sensitive data
-       // Arrays.fill(toBuffer.array(), '\u0000'); // clear sensitive data
-       // return res;
-       // }
-       //
+//     byte[] hash(char[] password, String passwordScheme) {
+//             if (passwordScheme == null)
+//                     passwordScheme = DigestUtils.PASSWORD_SCHEME_SHA;
+//             byte[] hashedPassword = ("{" + passwordScheme + "}"
+//                             + Base64.getEncoder().encodeToString(DigestUtils.toPasswordScheme(passwordScheme, password)))
+//                                             .getBytes(US_ASCII);
+//             return hashedPassword;
+//     }
+
        @Override
        public LdapName getDn() {
                return dn;
@@ -348,7 +329,7 @@ class LdifUser implements DirectoryUser {
                        if (key == null) {
                                // TODO persist to other sources (like PKCS12)
                                char[] password = DigestUtils.bytesToChars(value);
-                               byte[] hashedPassword = hash(password);
+                               byte[] hashedPassword = sha1hash(password);
                                return put(LdapAttrs.userPassword.name(), hashedPassword);
                        }
                        if (key.startsWith("X-")) {
index 331e9acdd948062ebb6644b4b70e26424d1f1be5..5a0834d3062ec5500d37ee0d1702919db1294c91 100644 (file)
@@ -15,7 +15,6 @@ import javax.jcr.Repository;
 import javax.jcr.RepositoryFactory;
 import javax.jcr.Session;
 
-import org.apache.jackrabbit.core.security.authentication.LocalAuthContext;
 import org.argeo.jackrabbit.client.ClientDavexRepositoryFactory;
 import org.argeo.jcr.ArgeoJcrException;
 import org.argeo.jcr.fs.JcrFileSystem;