]> git.argeo.org Git - lgpl/argeo-commons.git/blob - LdifUser.java
cceb6e4611c92fa06530c73e34471726da24df23
[lgpl/argeo-commons.git] / LdifUser.java
1 package org.argeo.osgi.useradmin;
2
3 import static java.nio.charset.StandardCharsets.US_ASCII;
4
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;
16
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;
23
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;
32
33 /** Directory user implementation */
34 abstract class LdifUser extends AbstractLdapEntry implements DirectoryUser {
35 private final AttributeDictionary properties;
36 private final AttributeDictionary credentials;
37
38 LdifUser(AbstractLdapDirectory userAdmin, LdapName dn, Attributes attributes) {
39 super(userAdmin, dn, attributes);
40 properties = new AttributeDictionary(false);
41 credentials = new AttributeDictionary(true);
42 }
43
44 @Override
45 public String getName() {
46 return getDn().toString();
47 }
48
49 @Override
50 public int getType() {
51 return USER;
52 }
53
54 @Override
55 public Dictionary<String, Object> getProperties() {
56 return properties;
57 }
58
59 @Override
60 public Dictionary<String, Object> getCredentials() {
61 return credentials;
62 }
63
64 @Override
65 public boolean hasCredential(String key, Object value) {
66 if (key == null) {
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);
71
72 if (getDirectory().getForcedPassword() != null
73 && getDirectory().getForcedPassword().equals(new String(password)))
74 return true;
75
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);
82 return false;
83 } else {
84 // boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken);
85 return true;
86 }
87 // TODO delete expired tokens?
88 } else {
89 // TODO implement SHA
90 throw new UnsupportedOperationException(
91 "Unsupported authPassword scheme " + authPassword.getAuthScheme());
92 }
93 }
94
95 // Regular password
96 // byte[] hashedPassword = hash(password, DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256);
97 if (hasCredential(LdapAttrs.userPassword.name(), DirectoryDigestUtils.charsToBytes(password)))
98 return true;
99 return false;
100 }
101
102 Object storedValue = getCredentials().get(key);
103 if (storedValue == null || value == null)
104 return false;
105 if (!(value instanceof String || value instanceof byte[]))
106 return false;
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('}');
114 if (index > 0) {
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);
119 byte[] valueBytes;
120 if (DirectoryDigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) {
121 valueBytes = DirectoryDigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null,
122 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);
134 } else {
135 throw new UnsupportedOperationException("Unknown password scheme " + passwordScheme);
136 }
137 return Arrays.equals(storedValueBytes, valueBytes);
138 }
139 }
140 }
141 // if (storedValue instanceof byte[] && value instanceof byte[]) {
142 // return Arrays.equals((byte[]) storedValue, (byte[]) value);
143 // }
144 return false;
145 }
146
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;
153 }
154
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;
162 // }
163
164 protected DirectoryUserAdmin getUserAdmin() {
165 return (DirectoryUserAdmin) getDirectory();
166 }
167
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;
172
173 public AttributeDictionary(Boolean credentials) {
174 this.attrFilter = getDirectory().getCredentialAttributeIds();
175 this.includeFilter = credentials;
176 try {
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);
184 }
185 } catch (NamingException e) {
186 throw new IllegalStateException("Cannot initialise attribute dictionary", e);
187 }
188 if (!credentials)
189 effectiveKeys.add(LdapAttrs.objectClasses.name());
190 }
191
192 @Override
193 public int size() {
194 return effectiveKeys.size();
195 }
196
197 @Override
198 public boolean isEmpty() {
199 return effectiveKeys.size() == 0;
200 }
201
202 @Override
203 public Enumeration<String> keys() {
204 return Collections.enumeration(effectiveKeys);
205 }
206
207 @Override
208 public Enumeration<Object> elements() {
209 final Iterator<String> it = effectiveKeys.iterator();
210 return new Enumeration<Object>() {
211
212 @Override
213 public boolean hasMoreElements() {
214 return it.hasNext();
215 }
216
217 @Override
218 public Object nextElement() {
219 String key = it.next();
220 return get(key);
221 }
222
223 };
224 }
225
226 @Override
227 public Object get(Object key) {
228 try {
229 Attribute attr = !key.equals(LdapAttrs.objectClasses.name()) ? getAttributes().get(key.toString())
230 : getAttributes().get(LdapAttrs.objectClass.name());
231 if (attr == null)
232 return null;
233 Object value = attr.get();
234 if (value instanceof byte[]) {
235 if (key.equals(LdapAttrs.userPassword.name()))
236 // TODO other cases (certificates, images)
237 return value;
238 value = new String((byte[]) value, StandardCharsets.UTF_8);
239 }
240 if (attr.size() == 1)
241 return value;
242 // special case for object class
243 if (key.equals(LdapAttrs.objectClass.name())) {
244 // TODO support multiple object classes
245 NamingEnumeration<?> en = attr.getAll();
246 String first = null;
247 attrs: while (en.hasMore()) {
248 String v = en.next().toString();
249 if (v.equalsIgnoreCase(LdapObjs.top.name()))
250 continue attrs;
251 if (first == null)
252 first = v;
253 if (v.equalsIgnoreCase(getDirectory().getUserObjectClass()))
254 return getDirectory().getUserObjectClass();
255 else if (v.equalsIgnoreCase(getDirectory().getGroupObjectClass()))
256 return getDirectory().getGroupObjectClass();
257 }
258 if (first != null)
259 return first;
260 throw new IllegalStateException("Cannot find objectClass in " + value);
261 } else {
262 NamingEnumeration<?> en = attr.getAll();
263 StringJoiner values = new StringJoiner("\n");
264 while (en.hasMore()) {
265 String v = en.next().toString();
266 values.add(v);
267 }
268 return values.toString();
269 }
270 // else
271 // return value;
272 } catch (NamingException e) {
273 throw new IllegalStateException("Cannot get value for attribute " + key, e);
274 }
275 }
276
277 @Override
278 public Object put(String key, Object value) {
279 if (key == null) {
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);
284 }
285 if (key.startsWith("X-")) {
286 return put(LdapAttrs.authPassword.name(), value);
287 }
288
289 getDirectory().checkEdit();
290 if (!isEditing())
291 startEditing();
292
293 if (!(value instanceof String || value instanceof byte[]))
294 throw new IllegalArgumentException("Value must be String or byte[]");
295
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");
300
301 try {
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));
307 else
308 attribute.add(value);
309 Attribute previousAttribute = getModifiedAttributes().put(attribute);
310 if (previousAttribute != null)
311 return previousAttribute.get();
312 else
313 return null;
314 } catch (NamingException e) {
315 throw new IllegalStateException("Cannot get value for attribute " + key, e);
316 }
317 }
318
319 @Override
320 public Object remove(Object key) {
321 getDirectory().checkEdit();
322 if (!isEditing())
323 startEditing();
324
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");
329
330 try {
331 Attribute attr = getModifiedAttributes().remove(key.toString());
332 if (attr != null)
333 return attr.get();
334 else
335 return null;
336 } catch (NamingException e) {
337 throw new IllegalStateException("Cannot remove attribute " + key, e);
338 }
339 }
340 }
341
342 private static boolean isAsciiPrintable(String str) {
343 if (str == null) {
344 return false;
345 }
346 int sz = str.length();
347 for (int i = 0; i < sz; i++) {
348 if (isAsciiPrintable(str.charAt(i)) == false) {
349 return false;
350 }
351 }
352 return true;
353 }
354
355 private static boolean isAsciiPrintable(char ch) {
356 return ch >= 32 && ch < 127;
357 }
358
359 static class LdifPerson extends LdifUser implements Person {
360
361 public LdifPerson(DirectoryUserAdmin userAdmin, LdapName dn, Attributes attributes) {
362 super(userAdmin, dn, attributes);
363 }
364
365 }
366 }