1 package org
.argeo
.osgi
.useradmin
;
3 import static java
.nio
.charset
.StandardCharsets
.US_ASCII
;
5 import java
.math
.BigInteger
;
6 import java
.nio
.charset
.StandardCharsets
;
7 import java
.util
.ArrayList
;
8 import java
.util
.Arrays
;
9 import java
.util
.Base64
;
10 import java
.util
.Collections
;
11 import java
.util
.Dictionary
;
12 import java
.util
.Enumeration
;
13 import java
.util
.Iterator
;
14 import java
.util
.List
;
15 import java
.util
.StringJoiner
;
17 import javax
.naming
.NamingEnumeration
;
18 import javax
.naming
.NamingException
;
19 import javax
.naming
.directory
.Attribute
;
20 import javax
.naming
.directory
.Attributes
;
21 import javax
.naming
.directory
.BasicAttribute
;
22 import javax
.naming
.ldap
.LdapName
;
24 import org
.argeo
.util
.directory
.DirectoryDigestUtils
;
25 import org
.argeo
.util
.directory
.Person
;
26 import org
.argeo
.util
.directory
.ldap
.AbstractLdapDirectory
;
27 import org
.argeo
.util
.directory
.ldap
.AbstractLdapEntry
;
28 import org
.argeo
.util
.directory
.ldap
.AuthPassword
;
29 import org
.argeo
.util
.naming
.LdapAttrs
;
30 import org
.argeo
.util
.naming
.LdapObjs
;
31 import org
.argeo
.util
.naming
.SharedSecret
;
33 /** Directory user implementation */
34 abstract class LdifUser
extends AbstractLdapEntry
implements DirectoryUser
{
35 private final AttributeDictionary properties
;
36 private final AttributeDictionary credentials
;
38 LdifUser(AbstractLdapDirectory userAdmin
, LdapName dn
, Attributes attributes
) {
39 super(userAdmin
, dn
, attributes
);
40 properties
= new AttributeDictionary(false);
41 credentials
= new AttributeDictionary(true);
45 public String
getName() {
46 return getDn().toString();
50 public int getType() {
55 public Dictionary
<String
, Object
> getProperties() {
60 public Dictionary
<String
, Object
> getCredentials() {
65 public boolean hasCredential(String key
, Object value
) {
67 // TODO check other sources (like PKCS12)
68 // String pwd = new String((char[]) value);
69 // authPassword (RFC 312 https://tools.ietf.org/html/rfc3112)
70 char[] password
= DirectoryDigestUtils
.bytesToChars(value
);
72 if (getDirectory().getForcedPassword() != null
73 && getDirectory().getForcedPassword().equals(new String(password
)))
76 AuthPassword authPassword
= AuthPassword
.matchAuthValue(getAttributes(), password
);
77 if (authPassword
!= null) {
78 if (authPassword
.getAuthScheme().equals(SharedSecret
.X_SHARED_SECRET
)) {
79 SharedSecret onceToken
= new SharedSecret(authPassword
);
80 if (onceToken
.isExpired()) {
81 // AuthPassword.remove(getAttributes(), onceToken);
84 // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken);
87 // TODO delete expired tokens?
90 throw new UnsupportedOperationException(
91 "Unsupported authPassword scheme " + authPassword
.getAuthScheme());
96 // byte[] hashedPassword = hash(password, DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256);
97 if (hasCredential(LdapAttrs
.userPassword
.name(), DirectoryDigestUtils
.charsToBytes(password
)))
102 Object storedValue
= getCredentials().get(key
);
103 if (storedValue
== null || value
== null)
105 if (!(value
instanceof String
|| value
instanceof byte[]))
107 if (storedValue
instanceof String
&& value
instanceof String
)
108 return storedValue
.equals(value
);
109 if (storedValue
instanceof byte[] && value
instanceof byte[]) {
110 String storedBase64
= new String((byte[]) storedValue
, US_ASCII
);
111 String passwordScheme
= null;
112 if (storedBase64
.charAt(0) == '{') {
113 int index
= storedBase64
.indexOf('}');
115 passwordScheme
= storedBase64
.substring(1, index
);
116 String storedValueBase64
= storedBase64
.substring(index
+ 1);
117 byte[] storedValueBytes
= Base64
.getDecoder().decode(storedValueBase64
);
118 char[] passwordValue
= DirectoryDigestUtils
.bytesToChars((byte[]) value
);
120 if (DirectoryDigestUtils
.PASSWORD_SCHEME_SHA
.equals(passwordScheme
)) {
121 valueBytes
= DirectoryDigestUtils
.toPasswordScheme(passwordScheme
, passwordValue
, null, null,
123 } else if (DirectoryDigestUtils
.PASSWORD_SCHEME_PBKDF2_SHA256
.equals(passwordScheme
)) {
124 // see https://www.thesubtlety.com/post/a-389-ds-pbkdf2-password-checker/
125 byte[] iterationsArr
= Arrays
.copyOfRange(storedValueBytes
, 0, 4);
126 BigInteger iterations
= new BigInteger(iterationsArr
);
127 byte[] salt
= Arrays
.copyOfRange(storedValueBytes
, iterationsArr
.length
,
128 iterationsArr
.length
+ 64);
129 byte[] keyArr
= Arrays
.copyOfRange(storedValueBytes
, iterationsArr
.length
+ salt
.length
,
130 storedValueBytes
.length
);
131 int keyLengthBits
= keyArr
.length
* 8;
132 valueBytes
= DirectoryDigestUtils
.toPasswordScheme(passwordScheme
, passwordValue
, salt
,
133 iterations
.intValue(), keyLengthBits
);
135 throw new UnsupportedOperationException("Unknown password scheme " + passwordScheme
);
137 return Arrays
.equals(storedValueBytes
, valueBytes
);
141 // if (storedValue instanceof byte[] && value instanceof byte[]) {
142 // return Arrays.equals((byte[]) storedValue, (byte[]) value);
147 /** Hash the password */
148 byte[] sha1hash(char[] password
) {
149 byte[] hashedPassword
= ("{SHA}" + Base64
.getEncoder()
150 .encodeToString(DirectoryDigestUtils
.sha1(DirectoryDigestUtils
.charsToBytes(password
))))
151 .getBytes(StandardCharsets
.UTF_8
);
152 return hashedPassword
;
155 // byte[] hash(char[] password, String passwordScheme) {
156 // if (passwordScheme == null)
157 // passwordScheme = DigestUtils.PASSWORD_SCHEME_SHA;
158 // byte[] hashedPassword = ("{" + passwordScheme + "}"
159 // + Base64.getEncoder().encodeToString(DigestUtils.toPasswordScheme(passwordScheme, password)))
160 // .getBytes(US_ASCII);
161 // return hashedPassword;
164 protected DirectoryUserAdmin
getUserAdmin() {
165 return (DirectoryUserAdmin
) getDirectory();
168 private class AttributeDictionary
extends Dictionary
<String
, Object
> {
169 private final List
<String
> effectiveKeys
= new ArrayList
<String
>();
170 private final List
<String
> attrFilter
;
171 private final Boolean includeFilter
;
173 public AttributeDictionary(Boolean credentials
) {
174 this.attrFilter
= getDirectory().getCredentialAttributeIds();
175 this.includeFilter
= credentials
;
177 NamingEnumeration
<String
> ids
= getAttributes().getIDs();
178 while (ids
.hasMore()) {
179 String id
= ids
.next();
180 if (credentials
&& attrFilter
.contains(id
))
181 effectiveKeys
.add(id
);
182 else if (!credentials
&& !attrFilter
.contains(id
))
183 effectiveKeys
.add(id
);
185 } catch (NamingException e
) {
186 throw new IllegalStateException("Cannot initialise attribute dictionary", e
);
189 effectiveKeys
.add(LdapAttrs
.objectClasses
.name());
194 return effectiveKeys
.size();
198 public boolean isEmpty() {
199 return effectiveKeys
.size() == 0;
203 public Enumeration
<String
> keys() {
204 return Collections
.enumeration(effectiveKeys
);
208 public Enumeration
<Object
> elements() {
209 final Iterator
<String
> it
= effectiveKeys
.iterator();
210 return new Enumeration
<Object
>() {
213 public boolean hasMoreElements() {
218 public Object
nextElement() {
219 String key
= it
.next();
227 public Object
get(Object key
) {
229 Attribute attr
= !key
.equals(LdapAttrs
.objectClasses
.name()) ?
getAttributes().get(key
.toString())
230 : getAttributes().get(LdapAttrs
.objectClass
.name());
233 Object value
= attr
.get();
234 if (value
instanceof byte[]) {
235 if (key
.equals(LdapAttrs
.userPassword
.name()))
236 // TODO other cases (certificates, images)
238 value
= new String((byte[]) value
, StandardCharsets
.UTF_8
);
240 if (attr
.size() == 1)
242 // special case for object class
243 if (key
.equals(LdapAttrs
.objectClass
.name())) {
244 // TODO support multiple object classes
245 NamingEnumeration
<?
> en
= attr
.getAll();
247 attrs
: while (en
.hasMore()) {
248 String v
= en
.next().toString();
249 if (v
.equalsIgnoreCase(LdapObjs
.top
.name()))
253 if (v
.equalsIgnoreCase(getDirectory().getUserObjectClass()))
254 return getDirectory().getUserObjectClass();
255 else if (v
.equalsIgnoreCase(getDirectory().getGroupObjectClass()))
256 return getDirectory().getGroupObjectClass();
260 throw new IllegalStateException("Cannot find objectClass in " + value
);
262 NamingEnumeration
<?
> en
= attr
.getAll();
263 StringJoiner values
= new StringJoiner("\n");
264 while (en
.hasMore()) {
265 String v
= en
.next().toString();
268 return values
.toString();
272 } catch (NamingException e
) {
273 throw new IllegalStateException("Cannot get value for attribute " + key
, e
);
278 public Object
put(String key
, Object value
) {
280 // TODO persist to other sources (like PKCS12)
281 char[] password
= DirectoryDigestUtils
.bytesToChars(value
);
282 byte[] hashedPassword
= sha1hash(password
);
283 return put(LdapAttrs
.userPassword
.name(), hashedPassword
);
285 if (key
.startsWith("X-")) {
286 return put(LdapAttrs
.authPassword
.name(), value
);
289 getDirectory().checkEdit();
293 if (!(value
instanceof String
|| value
instanceof byte[]))
294 throw new IllegalArgumentException("Value must be String or byte[]");
296 if (includeFilter
&& !attrFilter
.contains(key
))
297 throw new IllegalArgumentException("Key " + key
+ " not included");
298 else if (!includeFilter
&& attrFilter
.contains(key
))
299 throw new IllegalArgumentException("Key " + key
+ " excluded");
302 Attribute attribute
= getModifiedAttributes().get(key
.toString());
303 // if (attribute == null) // block unit tests
304 attribute
= new BasicAttribute(key
.toString());
305 if (value
instanceof String
&& !isAsciiPrintable(((String
) value
)))
306 attribute
.add(((String
) value
).getBytes(StandardCharsets
.UTF_8
));
308 attribute
.add(value
);
309 Attribute previousAttribute
= getModifiedAttributes().put(attribute
);
310 if (previousAttribute
!= null)
311 return previousAttribute
.get();
314 } catch (NamingException e
) {
315 throw new IllegalStateException("Cannot get value for attribute " + key
, e
);
320 public Object
remove(Object key
) {
321 getDirectory().checkEdit();
325 if (includeFilter
&& !attrFilter
.contains(key
))
326 throw new IllegalArgumentException("Key " + key
+ " not included");
327 else if (!includeFilter
&& attrFilter
.contains(key
))
328 throw new IllegalArgumentException("Key " + key
+ " excluded");
331 Attribute attr
= getModifiedAttributes().remove(key
.toString());
336 } catch (NamingException e
) {
337 throw new IllegalStateException("Cannot remove attribute " + key
, e
);
342 private static boolean isAsciiPrintable(String str
) {
346 int sz
= str
.length();
347 for (int i
= 0; i
< sz
; i
++) {
348 if (isAsciiPrintable(str
.charAt(i
)) == false) {
355 private static boolean isAsciiPrintable(char ch
) {
356 return ch
>= 32 && ch
< 127;
359 static class LdifPerson
extends LdifUser
implements Person
{
361 public LdifPerson(DirectoryUserAdmin userAdmin
, LdapName dn
, Attributes attributes
) {
362 super(userAdmin
, dn
, attributes
);