From: Mathieu Baudier Date: Tue, 14 Nov 2017 15:13:59 +0000 (+0100) Subject: Full LDAP bind support X-Git-Tag: argeo-commons-2.1.70~24 X-Git-Url: https://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=a5459b7f0a4ce0463b950efd5c776368fe169256 Full LDAP bind support --- diff --git a/demo/argeo_node_rap.properties b/demo/argeo_node_rap.properties index d7149b966..a9f8f01d7 100644 --- a/demo/argeo_node_rap.properties +++ b/demo/argeo_node_rap.properties @@ -5,7 +5,6 @@ org.eclipse.equinox.metatype,\ org.eclipse.equinox.cm,\ org.eclipse.rap.rwt.osgi - argeo.osgi.start.3.node=\ org.argeo.cms @@ -23,9 +22,7 @@ java.security.policy=file:../../all.policy argeo.node.repo.type=h2 -#argeo.node.useradmin.uris=ldap://uid=admin,ou=system:secret@localhost:10389/dc=example,dc=com -#argeo.node.useradmin.uris=ldap://uid=admin,ou=system:secret@localhost:10389\ -#/dc=example,dc=com?userBase=ou=users&groupBase=ou=groups +#argeo.node.useradmin.uris=ldap://localhost:10389/dc=example,dc=com #argeo.node.useradmin.uris="\ #ldap://uid=admin,ou=system:secret\ @@ -35,10 +32,6 @@ argeo.node.repo.type=h2 #&userObjectClass=inetOrgPerson \ #dc=example,dc=org.ldif" -#argeo.node.useradmin.uris="dc=example,dc=com.ldif dc=example,dc=org.ldif" - -#sun.security.krb5.debug=true - # HTTP org.osgi.service.http.port=7070 #org.eclipse.equinox.http.jetty.log.stderr.threshold=info diff --git a/org.argeo.cms/src/org/argeo/cms/auth/AuthenticatingUser.java b/org.argeo.cms/src/org/argeo/cms/auth/AuthenticatingUser.java deleted file mode 100644 index 9df88a064..000000000 --- a/org.argeo.cms/src/org/argeo/cms/auth/AuthenticatingUser.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.argeo.cms.auth; - -import java.nio.ByteBuffer; -import java.nio.CharBuffer; -import java.nio.charset.Charset; -import java.util.Arrays; -import java.util.Dictionary; -import java.util.Hashtable; - -import javax.naming.ldap.LdapName; - -import org.osgi.service.useradmin.User; - -/** - * A special user type used during authentication in order to provide the - * credentials required for scoping the user admin. - */ -class AuthenticatingUser implements User { - private final String name; - private final Dictionary credentials; - - public AuthenticatingUser(LdapName name) { - this.name = name.toString(); - this.credentials = new Hashtable<>(); - } - - public AuthenticatingUser(String name, Dictionary credentials) { - this.name = name; - this.credentials = credentials; - } - - public AuthenticatingUser(String name, char[] password) { - this.name = name; - credentials = new Hashtable<>(); - credentials.put(CmsAuthUtils.SHARED_STATE_NAME, name); - byte[] pwd = charsToBytes(password); - credentials.put(CmsAuthUtils.SHARED_STATE_PWD, pwd); - } - - @Override - public String getName() { - return name; - } - - @Override - public int getType() { - return User.USER; - } - - @SuppressWarnings("rawtypes") - @Override - public Dictionary getProperties() { - throw new UnsupportedOperationException(); - } - - @SuppressWarnings("rawtypes") - @Override - public Dictionary getCredentials() { - return credentials; - } - - @Override - public boolean hasCredential(String key, Object value) { - throw new UnsupportedOperationException(); - } - - - static byte[] charsToBytes(char[] chars) { - CharBuffer charBuffer = CharBuffer.wrap(chars); - ByteBuffer byteBuffer = Charset.forName("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; - } - - static char[] bytesToChars(byte[] bytes) { - ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); - CharBuffer charBuffer = Charset.forName("UTF-8").decode(byteBuffer); - char[] chars = Arrays.copyOfRange(charBuffer.array(), charBuffer.position(), charBuffer.limit()); - Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data - Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data - return chars; - } - - -} diff --git a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java index 65ccbd6ab..d50535eae 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/CmsAuthUtils.java @@ -23,26 +23,22 @@ import org.argeo.node.NodeConstants; import org.argeo.node.security.AnonymousPrincipal; import org.argeo.node.security.DataAdminPrincipal; import org.argeo.node.security.NodeSecurityUtils; +import org.argeo.osgi.useradmin.AuthenticatingUser; import org.osgi.service.http.HttpContext; import org.osgi.service.useradmin.Authorization; class CmsAuthUtils { - /** Shared HTTP request */ - final static String SHARED_STATE_HTTP_REQUEST = "org.argeo.cms.auth.http.request"; - /** From org.osgi.service.http.HttpContext */ - // final static String SHARED_STATE_AUTHORIZATION = - // "org.osgi.service.useradmin.authorization"; - /** From com.sun.security.auth.module.*LoginModule */ - final static String SHARED_STATE_NAME = "javax.security.auth.login.name"; - /** From com.sun.security.auth.module.*LoginModule */ - final static String SHARED_STATE_PWD = "javax.security.auth.login.password"; + // Standard + final static String SHARED_STATE_NAME = AuthenticatingUser.SHARED_STATE_NAME; + final static String SHARED_STATE_PWD = AuthenticatingUser.SHARED_STATE_PWD; + final static String HEADER_AUTHORIZATION = "Authorization"; + final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; + // Argeo specific + final static String SHARED_STATE_HTTP_REQUEST = "org.argeo.cms.auth.http.request"; final static String SHARED_STATE_SPNEGO_TOKEN = "org.argeo.cms.auth.spnegoToken"; final static String SHARED_STATE_SPNEGO_OUT_TOKEN = "org.argeo.cms.auth.spnegoOutToken"; - final static String HEADER_AUTHORIZATION = "Authorization"; - final static String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; - static void addAuthorization(Subject subject, Authorization authorization, Locale locale, HttpServletRequest request) { assert subject != null; diff --git a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java index 269f509ed..1d91a21ca 100644 --- a/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java +++ b/org.argeo.cms/src/org/argeo/cms/auth/UserAdminLoginModule.java @@ -27,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.argeo.cms.CmsException; import org.argeo.naming.LdapAttrs; +import org.argeo.osgi.useradmin.AuthenticatingUser; import org.argeo.osgi.useradmin.IpaUtils; import org.osgi.framework.BundleContext; import org.osgi.framework.FrameworkUtil; @@ -41,14 +42,16 @@ public class UserAdminLoginModule implements LoginModule { private CallbackHandler callbackHandler; private Map sharedState = null; - private List indexedUserProperties = Arrays.asList( - new String[] { LdapAttrs.DN, LdapAttrs.mail.name(), LdapAttrs.uid.name(), LdapAttrs.authPassword.name() }); + private List indexedUserProperties = Arrays + .asList(new String[] { LdapAttrs.mail.name(), LdapAttrs.uid.name(), LdapAttrs.authPassword.name() }); // private state private BundleContext bc; private User authenticatedUser = null; private Locale locale; + private Authorization bindAuthorization = null; + @SuppressWarnings("unchecked") @Override public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, @@ -105,14 +108,27 @@ public class UserAdminLoginModule implements LoginModule { throw new CredentialNotFoundException("No credentials provided"); } - // User user = userAdmin.getUser(null, username); User user = searchForUser(userAdmin, username); if (user == null) return true;// expect Kerberos - // throw new FailedLoginException("Invalid credentials"); - if (!user.hasCredential(null, password)) + + // try bind first + try { + AuthenticatingUser authenticatingUser = new AuthenticatingUser(user.getName(), password); + bindAuthorization = userAdmin.getAuthorization(authenticatingUser); + // TODO check tokens as well + if (bindAuthorization != null) + return true; + } catch (Exception e) { + // silent + if(log.isTraceEnabled()) + log.trace("Bind failed", e); + } + + // works only if a connection password is provided + if (!user.hasCredential(null, password)) { return false; - // throw new FailedLoginException("Invalid credentials"); + } authenticatedUser = user; return true; } @@ -123,7 +139,9 @@ public class UserAdminLoginModule implements LoginModule { Authorization authorization; if (callbackHandler == null) {// anonymous authorization = userAdmin.getAuthorization(null); - } else { + } else if (bindAuthorization != null) {// bind + authorization = bindAuthorization; + } else {// Kerberos User authenticatingUser; Set kerberosPrincipals = subject.getPrincipals(KerberosPrincipal.class); if (kerberosPrincipals.isEmpty()) { @@ -184,23 +202,26 @@ public class UserAdminLoginModule implements LoginModule { Set collectedUsers = new HashSet<>(); // try dn User user = null; - try { - user = (User) userAdmin.getRole(providedUsername); - if (user != null) - collectedUsers.add(user); - } catch (Exception e) { - // silent - } // try all indexes for (String attr : indexedUserProperties) { user = userAdmin.getUser(attr, providedUsername); if (user != null) collectedUsers.add(user); } - if (collectedUsers.size() == 1) - return collectedUsers.iterator().next(); - else if (collectedUsers.size() > 1) + if (collectedUsers.size() == 1) { + user = collectedUsers.iterator().next(); + return user; + } else if (collectedUsers.size() > 1) { log.warn(collectedUsers.size() + " users for provided username" + providedUsername); + } + // try DN as a last resort + try { + user = (User) userAdmin.getRole(providedUsername); + if (user != null) + return user; + } catch (Exception e) { + // silent + } return null; } catch (Exception e) { if (log.isTraceEnabled()) diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java index 957a1c953..d6135f8e8 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AbstractUserDirectory.java @@ -34,7 +34,6 @@ import javax.transaction.TransactionManager; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.argeo.naming.LdapAttrs; -import org.argeo.naming.LdapObjs; import org.osgi.framework.Filter; import org.osgi.framework.FrameworkUtil; import org.osgi.framework.InvalidSyntaxException; @@ -297,6 +296,8 @@ public abstract class AbstractUserDirectory implements UserAdmin, UserDirectory AbstractUserDirectory scopedUserAdmin = scope(user); try { DirectoryUser directoryUser = (DirectoryUser) scopedUserAdmin.getRole(user.getName()); + if (directoryUser == null) + throw new UserDirectoryException("No scoped user found for " + user); LdifAuthorization authorization = new LdifAuthorization(directoryUser, scopedUserAdmin.getAllRoles(directoryUser)); return authorization; 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 93ecdca47..2b2ca0c51 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AggregatingUserAdmin.java @@ -118,7 +118,8 @@ public class AggregatingUserAdmin implements UserAdmin { private UserAdmin findUserAdmin(String name) { try { - return findUserAdmin(new LdapName(name)); + UserAdmin userAdmin = findUserAdmin(new LdapName(name)); + return userAdmin; } catch (InvalidNameException e) { throw new UserDirectoryException("Badly formatted name " + name, e); } diff --git a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java new file mode 100644 index 000000000..2689d34fc --- /dev/null +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/AuthenticatingUser.java @@ -0,0 +1,90 @@ +package org.argeo.osgi.useradmin; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Dictionary; +import java.util.Hashtable; + +import javax.naming.ldap.LdapName; + +import org.osgi.service.useradmin.User; + +/** + * A special user type used during authentication in order to provide the + * credentials required for scoping the user admin. + */ +public class AuthenticatingUser implements User { + /** From com.sun.security.auth.module.*LoginModule */ + public final static String SHARED_STATE_NAME = "javax.security.auth.login.name"; + /** From com.sun.security.auth.module.*LoginModule */ + public final static String SHARED_STATE_PWD = "javax.security.auth.login.password"; + + private final String name; + private final Dictionary credentials; + + public AuthenticatingUser(LdapName name) { + this.name = name.toString(); + this.credentials = new Hashtable<>(); + } + + public AuthenticatingUser(String name, Dictionary credentials) { + this.name = name; + this.credentials = credentials; + } + + public AuthenticatingUser(String name, char[] password) { + this.name = name; + credentials = new Hashtable<>(); + credentials.put(SHARED_STATE_NAME, name); + byte[] pwd = charsToBytes(password); + credentials.put(SHARED_STATE_PWD, pwd); + } + + @Override + public String getName() { + return name; + } + + @Override + public int getType() { + return User.USER; + } + + @SuppressWarnings("rawtypes") + @Override + public Dictionary getProperties() { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("rawtypes") + @Override + public Dictionary getCredentials() { + return credentials; + } + + @Override + public boolean hasCredential(String key, Object value) { + throw new UnsupportedOperationException(); + } + + static byte[] charsToBytes(char[] chars) { + CharBuffer charBuffer = CharBuffer.wrap(chars); + ByteBuffer byteBuffer = Charset.forName("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; + } + + static char[] bytesToChars(byte[] bytes) { + ByteBuffer byteBuffer = ByteBuffer.wrap(bytes); + CharBuffer charBuffer = Charset.forName("UTF-8").decode(byteBuffer); + char[] chars = Arrays.copyOfRange(charBuffer.array(), charBuffer.position(), charBuffer.limit()); + Arrays.fill(charBuffer.array(), '\u0000'); // clear sensitive data + Arrays.fill(byteBuffer.array(), (byte) 0); // clear sensitive data + return chars; + } + +} 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 d8f8ce9d5..f6a237bcc 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DigestUtils.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/DigestUtils.java @@ -1,7 +1,12 @@ package org.argeo.osgi.useradmin; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; +import java.util.Arrays; +/** Utilities around digests, mostly those related to passwords. */ class DigestUtils { static byte[] sha1(byte[] bytes) { try { @@ -14,6 +19,29 @@ class DigestUtils { } } + static char[] bytesToChars(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; + } + + static byte[] charsToBytes(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 DigestUtils() { } 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 cf97ebea3..a9e32fae5 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapUserAdmin.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdapUserAdmin.java @@ -82,16 +82,19 @@ public class LdapUserAdmin extends AbstractUserDirectory { @Override protected AbstractUserDirectory scope(User user) { Dictionary credentials = user.getCredentials(); - // FIXME use arrays String username = (String) credentials.get(SHARED_STATE_USERNAME); if (username == null) username = user.getName(); - // byte[] pwd = (byte[]) credentials.get(SHARED_STATE_PASSWORD); - // char[] password = DigestUtils.bytesToChars(pwd); Dictionary properties = cloneProperties(); properties.put(Context.SECURITY_PRINCIPAL, username.toString()); - // properties.put(Context.SECURITY_CREDENTIALS, password); - properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); + Object pwdCred = credentials.get(SHARED_STATE_PASSWORD); + byte[] pwd = (byte[]) pwdCred; + if (pwd != null) { + char[] password = DigestUtils.bytesToChars(pwd); + properties.put(Context.SECURITY_CREDENTIALS, new String(password)); + } else { + properties.put(Context.SECURITY_AUTHENTICATION, "GSSAPI"); + } return new LdapUserAdmin(properties); } 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 b25ebfc22..392b17428 100644 --- a/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java +++ b/org.argeo.enterprise/src/org/argeo/osgi/useradmin/LdifUser.java @@ -1,7 +1,5 @@ package org.argeo.osgi.useradmin; -import java.nio.ByteBuffer; -import java.nio.CharBuffer; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -76,7 +74,7 @@ class LdifUser implements DirectoryUser { // TODO check other sources (like PKCS12) // String pwd = new String((char[]) value); // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112) - char[] password = toChars(value); + char[] password = DigestUtils.bytesToChars(value); AuthPassword authPassword = AuthPassword.matchAuthValue(getAttributes(), password); if (authPassword != null) { if (authPassword.getAuthScheme().equals(SharedSecret.X_SHARED_SECRET)) { @@ -154,35 +152,38 @@ class LdifUser implements DirectoryUser { /** Hash and clear the password */ private byte[] hash(char[] password) { - byte[] hashedPassword = ("{SHA}" + Base64.getEncoder().encodeToString(DigestUtils.sha1(toBytes(password)))) - .getBytes(StandardCharsets.UTF_8); + 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; - } - + // 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; + // } + // @Override public LdapName getDn() { return dn; @@ -346,7 +347,7 @@ class LdifUser implements DirectoryUser { public Object put(String key, Object value) { if (key == null) { // TODO persist to other sources (like PKCS12) - char[] password = toChars(value); + char[] password = DigestUtils.bytesToChars(value); byte[] hashedPassword = hash(password); return put(LdapAttrs.userPassword.name(), hashedPassword); }