From: Mathieu Baudier Date: Mon, 22 Jun 2020 09:54:17 +0000 (+0200) Subject: Implement 389 DS's PBKDF2_SHA256 password scheme. X-Git-Tag: argeo-commons-2.1.89~121 X-Git-Url: https://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=926f90e3c10006130db5dc382ec698e666b80e5f Implement 389 DS's PBKDF2_SHA256 password scheme. --- diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java index 6f353f155..85a447082 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java @@ -166,11 +166,21 @@ public class AggregatingUserAdmin implements UserAdmin { return tokens; List res = new ArrayList(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); diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java index 939b03852..01db8be98 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java @@ -21,6 +21,8 @@ public class AuthenticatingUser implements User { private final Dictionary 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; + } + } diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DigestUtils.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DigestUtils.java index 51d18349b..511c2fede 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DigestUtils.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DigestUtils.java @@ -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; diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapUserAdmin.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapUserAdmin.java index 58f6eb1fa..22c178ef4 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapUserAdmin.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapUserAdmin.java @@ -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 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 doGetRoles(Filter f) { + ArrayList res = new ArrayList(); try { String searchFilter = f != null ? f.toString() : "(|(" + objectClass + "=" + getUserObjectClass() + ")(" + objectClass + "=" @@ -140,7 +151,6 @@ public class LdapUserAdmin extends AbstractUserDirectory { LdapName searchBase = getBaseDn(); NamingEnumeration results = getLdapContext().search(searchBase, searchFilter, searchControls); - ArrayList res = new ArrayList(); 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); } diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java index 392b17428..f18c148f8 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java @@ -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> 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-")) { diff --git a/org.argeo.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java index 331e9acdd..5a0834d30 100644 --- a/org.argeo.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java +++ b/org.argeo.jcr/src/org/argeo/jackrabbit/fs/DavexFsProvider.java @@ -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;