View Javadoc
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.HashSet;
14  import java.util.Iterator;
15  import java.util.List;
16  import java.util.Set;
17  
18  import javax.naming.NamingEnumeration;
19  import javax.naming.NamingException;
20  import javax.naming.directory.Attribute;
21  import javax.naming.directory.Attributes;
22  import javax.naming.directory.BasicAttribute;
23  import javax.naming.ldap.LdapName;
24  
25  import org.argeo.naming.AuthPassword;
26  import org.argeo.naming.LdapAttrs;
27  import org.argeo.naming.SharedSecret;
28  
29  /** Directory user implementation */
30  class LdifUser implements DirectoryUser {
31  	private final AbstractUserDirectory userAdmin;
32  
33  	private final LdapName dn;
34  
35  	private final boolean frozen;
36  	private Attributes publishedAttributes;
37  
38  	private final AttributeDictionary properties;
39  	private final AttributeDictionary credentials;
40  
41  	LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes) {
42  		this(userAdmin, dn, attributes, false);
43  	}
44  
45  	private LdifUser(AbstractUserDirectory userAdmin, LdapName dn, Attributes attributes, boolean frozen) {
46  		this.userAdmin = userAdmin;
47  		this.dn = dn;
48  		this.publishedAttributes = attributes;
49  		properties = new AttributeDictionary(false);
50  		credentials = new AttributeDictionary(true);
51  		this.frozen = frozen;
52  	}
53  
54  	@Override
55  	public String getName() {
56  		return dn.toString();
57  	}
58  
59  	@Override
60  	public int getType() {
61  		return USER;
62  	}
63  
64  	@Override
65  	public Dictionary<String, Object> getProperties() {
66  		return properties;
67  	}
68  
69  	@Override
70  	public Dictionary<String, Object> getCredentials() {
71  		return credentials;
72  	}
73  
74  	@Override
75  	public boolean hasCredential(String key, Object value) {
76  		if (key == null) {
77  			// TODO check other sources (like PKCS12)
78  			// String pwd = new String((char[]) value);
79  			// authPassword (RFC 312 https://tools.ietf.org/html/rfc3112)
80  			char[] password = DigestUtils.bytesToChars(value);
81  			AuthPassword authPassword = AuthPassword.matchAuthValue(getAttributes(), password);
82  			if (authPassword != null) {
83  				if (authPassword.getAuthScheme().equals(SharedSecret.X_SHARED_SECRET)) {
84  					SharedSecret onceToken = new SharedSecret(authPassword);
85  					if (onceToken.isExpired()) {
86  						// AuthPassword.remove(getAttributes(), onceToken);
87  						return false;
88  					} else {
89  						// boolean wasRemoved = AuthPassword.remove(getAttributes(), onceToken);
90  						return true;
91  					}
92  					// TODO delete expired tokens?
93  				} else {
94  					// TODO implement SHA
95  					throw new UnsupportedOperationException(
96  							"Unsupported authPassword scheme " + authPassword.getAuthScheme());
97  				}
98  			}
99  
100 			// Regular password
101 //			byte[] hashedPassword = hash(password, DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256);
102 			if (hasCredential(LdapAttrs.userPassword.name(), DigestUtils.charsToBytes(password)))
103 				return true;
104 			return false;
105 		}
106 
107 		Object storedValue = getCredentials().get(key);
108 		if (storedValue == null || value == null)
109 			return false;
110 		if (!(value instanceof String || value instanceof byte[]))
111 			return false;
112 		if (storedValue instanceof String && value instanceof String)
113 			return storedValue.equals(value);
114 		if (storedValue instanceof byte[] && value instanceof byte[]) {
115 			String storedBase64 = new String((byte[]) storedValue, US_ASCII);
116 			String passwordScheme = null;
117 			if (storedBase64.charAt(0) == '{') {
118 				int index = storedBase64.indexOf('}');
119 				if (index > 0) {
120 					passwordScheme = storedBase64.substring(1, index);
121 					String storedValueBase64 = storedBase64.substring(index + 1);
122 					byte[] storedValueBytes = Base64.getDecoder().decode(storedValueBase64);
123 					char[] passwordValue = DigestUtils.bytesToChars((byte[]) value);
124 					byte[] valueBytes;
125 					if (DigestUtils.PASSWORD_SCHEME_SHA.equals(passwordScheme)) {
126 						valueBytes = DigestUtils.toPasswordScheme(passwordScheme, passwordValue, null, null, null);
127 					} else if (DigestUtils.PASSWORD_SCHEME_PBKDF2_SHA256.equals(passwordScheme)) {
128 						// see https://www.thesubtlety.com/post/a-389-ds-pbkdf2-password-checker/
129 						byte[] iterationsArr = Arrays.copyOfRange(storedValueBytes, 0, 4);
130 						BigInteger iterations = new BigInteger(iterationsArr);
131 						byte[] salt = Arrays.copyOfRange(storedValueBytes, iterationsArr.length,
132 								iterationsArr.length + 64);
133 						byte[] keyArr = Arrays.copyOfRange(storedValueBytes, iterationsArr.length + salt.length,
134 								storedValueBytes.length);
135 						int keyLengthBits = keyArr.length * 8;
136 						valueBytes = DigestUtils.toPasswordScheme(passwordScheme, passwordValue, salt,
137 								iterations.intValue(), keyLengthBits);
138 					} else {
139 						throw new UnsupportedOperationException("Unknown password scheme " + passwordScheme);
140 					}
141 					return Arrays.equals(storedValueBytes, valueBytes);
142 				}
143 			}
144 		}
145 //		if (storedValue instanceof byte[] && value instanceof byte[]) {
146 //			return Arrays.equals((byte[]) storedValue, (byte[]) value);
147 //		}
148 		return false;
149 	}
150 
151 	/** Hash the password */
152 	byte[] sha1hash(char[] password) {
153 		byte[] hashedPassword = ("{SHA}"
154 				+ Base64.getEncoder().encodeToString(DigestUtils.sha1(DigestUtils.charsToBytes(password))))
155 						.getBytes(StandardCharsets.UTF_8);
156 		return hashedPassword;
157 	}
158 
159 //	byte[] hash(char[] password, String passwordScheme) {
160 //		if (passwordScheme == null)
161 //			passwordScheme = DigestUtils.PASSWORD_SCHEME_SHA;
162 //		byte[] hashedPassword = ("{" + passwordScheme + "}"
163 //				+ Base64.getEncoder().encodeToString(DigestUtils.toPasswordScheme(passwordScheme, password)))
164 //						.getBytes(US_ASCII);
165 //		return hashedPassword;
166 //	}
167 
168 	@Override
169 	public LdapName getDn() {
170 		return dn;
171 	}
172 
173 	@Override
174 	public synchronized Attributes getAttributes() {
175 		return isEditing() ? getModifiedAttributes() : publishedAttributes;
176 	}
177 
178 	/** Should only be called from working copy thread. */
179 	private synchronized Attributes getModifiedAttributes() {
180 		assert getWc() != null;
181 		return getWc().getAttributes(getDn());
182 	}
183 
184 	protected synchronized boolean isEditing() {
185 		return getWc() != null && getModifiedAttributes() != null;
186 	}
187 
188 	private synchronized UserDirectoryWorkingCopy getWc() {
189 		return userAdmin.getWorkingCopy();
190 	}
191 
192 	protected synchronized void startEditing() {
193 		if (frozen)
194 			throw new UserDirectoryException("Cannot edit frozen view");
195 		if (getUserAdmin().isReadOnly())
196 			throw new UserDirectoryException("User directory is read-only");
197 		assert getModifiedAttributes() == null;
198 		getWc().startEditing(this);
199 		// modifiedAttributes = (Attributes) publishedAttributes.clone();
200 	}
201 
202 	public synchronized void publishAttributes(Attributes modifiedAttributes) {
203 		publishedAttributes = modifiedAttributes;
204 	}
205 
206 	public DirectoryUser getPublished() {
207 		return new LdifUser(userAdmin, dn, publishedAttributes, true);
208 	}
209 
210 	@Override
211 	public int hashCode() {
212 		return dn.hashCode();
213 	}
214 
215 	@Override
216 	public boolean equals(Object obj) {
217 		if (this == obj)
218 			return true;
219 		if (obj instanceof LdifUser) {
220 			LdifUser that = (LdifUser) obj;
221 			return this.dn.equals(that.dn);
222 		}
223 		return false;
224 	}
225 
226 	@Override
227 	public String toString() {
228 		return dn.toString();
229 	}
230 
231 	protected AbstractUserDirectory getUserAdmin() {
232 		return userAdmin;
233 	}
234 
235 	private class AttributeDictionary extends Dictionary<String, Object> {
236 		private final List<String> effectiveKeys = new ArrayList<String>();
237 		private final List<String> attrFilter;
238 		private final Boolean includeFilter;
239 
240 		public AttributeDictionary(Boolean includeFilter) {
241 			this.attrFilter = userAdmin.getCredentialAttributeIds();
242 			this.includeFilter = includeFilter;
243 			try {
244 				NamingEnumeration<String> ids = getAttributes().getIDs();
245 				while (ids.hasMore()) {
246 					String id = ids.next();
247 					if (includeFilter && attrFilter.contains(id))
248 						effectiveKeys.add(id);
249 					else if (!includeFilter && !attrFilter.contains(id))
250 						effectiveKeys.add(id);
251 				}
252 			} catch (NamingException e) {
253 				throw new UserDirectoryException("Cannot initialise attribute dictionary", e);
254 			}
255 		}
256 
257 		@Override
258 		public int size() {
259 			return effectiveKeys.size();
260 		}
261 
262 		@Override
263 		public boolean isEmpty() {
264 			return effectiveKeys.size() == 0;
265 		}
266 
267 		@Override
268 		public Enumeration<String> keys() {
269 			return Collections.enumeration(effectiveKeys);
270 		}
271 
272 		@Override
273 		public Enumeration<Object> elements() {
274 			final Iterator<String> it = effectiveKeys.iterator();
275 			return new Enumeration<Object>() {
276 
277 				@Override
278 				public boolean hasMoreElements() {
279 					return it.hasNext();
280 				}
281 
282 				@Override
283 				public Object nextElement() {
284 					String key = it.next();
285 					return get(key);
286 				}
287 
288 			};
289 		}
290 
291 		@Override
292 		public Object get(Object key) {
293 			try {
294 				Attribute attr = getAttributes().get(key.toString());
295 				if (attr == null)
296 					return null;
297 				Object value = attr.get();
298 				if (value instanceof byte[]) {
299 					if (key.equals(LdapAttrs.userPassword.name()))
300 						// TODO other cases (certificates, images)
301 						return value;
302 					value = new String((byte[]) value, StandardCharsets.UTF_8);
303 				}
304 				if (attr.size() == 1)
305 					return value;
306 				if (!attr.getID().equals(LdapAttrs.objectClass.name()))
307 					return value;
308 				// special case for object class
309 				NamingEnumeration<?> en = attr.getAll();
310 				Set<String> objectClasses = new HashSet<String>();
311 				while (en.hasMore()) {
312 					String objectClass = en.next().toString();
313 					objectClasses.add(objectClass);
314 				}
315 
316 				if (objectClasses.contains(userAdmin.getUserObjectClass()))
317 					return userAdmin.getUserObjectClass();
318 				else if (objectClasses.contains(userAdmin.getGroupObjectClass()))
319 					return userAdmin.getGroupObjectClass();
320 				else
321 					return value;
322 			} catch (NamingException e) {
323 				throw new UserDirectoryException("Cannot get value for attribute " + key, e);
324 			}
325 		}
326 
327 		@Override
328 		public Object put(String key, Object value) {
329 			if (key == null) {
330 				// TODO persist to other sources (like PKCS12)
331 				char[] password = DigestUtils.bytesToChars(value);
332 				byte[] hashedPassword = sha1hash(password);
333 				return put(LdapAttrs.userPassword.name(), hashedPassword);
334 			}
335 			if (key.startsWith("X-")) {
336 				return put(LdapAttrs.authPassword.name(), value);
337 			}
338 
339 			userAdmin.checkEdit();
340 			if (!isEditing())
341 				startEditing();
342 
343 			if (!(value instanceof String || value instanceof byte[]))
344 				throw new IllegalArgumentException("Value must be String or byte[]");
345 
346 			if (includeFilter && !attrFilter.contains(key))
347 				throw new IllegalArgumentException("Key " + key + " not included");
348 			else if (!includeFilter && attrFilter.contains(key))
349 				throw new IllegalArgumentException("Key " + key + " excluded");
350 
351 			try {
352 				Attribute attribute = getModifiedAttributes().get(key.toString());
353 				// if (attribute == null) // block unit tests
354 				attribute = new BasicAttribute(key.toString());
355 				if (value instanceof String && !isAsciiPrintable(((String) value)))
356 					attribute.add(((String) value).getBytes(StandardCharsets.UTF_8));
357 				else
358 					attribute.add(value);
359 				Attribute previousAttribute = getModifiedAttributes().put(attribute);
360 				if (previousAttribute != null)
361 					return previousAttribute.get();
362 				else
363 					return null;
364 			} catch (NamingException e) {
365 				throw new UserDirectoryException("Cannot get value for attribute " + key, e);
366 			}
367 		}
368 
369 		@Override
370 		public Object remove(Object key) {
371 			userAdmin.checkEdit();
372 			if (!isEditing())
373 				startEditing();
374 
375 			if (includeFilter && !attrFilter.contains(key))
376 				throw new IllegalArgumentException("Key " + key + " not included");
377 			else if (!includeFilter && attrFilter.contains(key))
378 				throw new IllegalArgumentException("Key " + key + " excluded");
379 
380 			try {
381 				Attribute attr = getModifiedAttributes().remove(key.toString());
382 				if (attr != null)
383 					return attr.get();
384 				else
385 					return null;
386 			} catch (NamingException e) {
387 				throw new UserDirectoryException("Cannot remove attribute " + key, e);
388 			}
389 		}
390 	}
391 
392 	private static boolean isAsciiPrintable(String str) {
393 		if (str == null) {
394 			return false;
395 		}
396 		int sz = str.length();
397 		for (int i = 0; i < sz; i++) {
398 			if (isAsciiPrintable(str.charAt(i)) == false) {
399 				return false;
400 			}
401 		}
402 		return true;
403 	}
404 
405 	private static boolean isAsciiPrintable(char ch) {
406 		return ch >= 32 && ch < 127;
407 	}
408 
409 }