View Javadoc
1   /*
2    * Copyright (C) 2007-2012 Argeo GmbH
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *         http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.argeo.cms.security;
17  
18  import java.io.ByteArrayInputStream;
19  import java.io.CharArrayReader;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.Reader;
23  import java.nio.charset.StandardCharsets;
24  import java.security.GeneralSecurityException;
25  import java.security.Provider;
26  import java.security.SecureRandom;
27  
28  import javax.crypto.Cipher;
29  import javax.crypto.CipherInputStream;
30  import javax.crypto.SecretKey;
31  import javax.crypto.spec.IvParameterSpec;
32  import javax.jcr.Binary;
33  import javax.jcr.Node;
34  import javax.jcr.NodeIterator;
35  import javax.jcr.Property;
36  import javax.jcr.Repository;
37  import javax.jcr.RepositoryException;
38  import javax.jcr.Session;
39  import javax.jcr.query.Query;
40  
41  import org.apache.commons.io.IOUtils;
42  import org.apache.commons.logging.Log;
43  import org.apache.commons.logging.LogFactory;
44  import org.argeo.api.NodeConstants;
45  import org.argeo.api.NodeUtils;
46  import org.argeo.api.security.PBEKeySpecCallback;
47  import org.argeo.cms.ArgeoNames;
48  import org.argeo.cms.ArgeoTypes;
49  import org.argeo.cms.CmsException;
50  import org.argeo.jcr.ArgeoJcrException;
51  import org.argeo.jcr.JcrUtils;
52  
53  /** JCR based implementation of a keyring */
54  public class JcrKeyring extends AbstractKeyring implements ArgeoNames {
55  	private final static Log log = LogFactory.getLog(JcrKeyring.class);
56  	/**
57  	 * Stronger with 256, but causes problem with Oracle JVM, force 128 in this case
58  	 */
59  	public final static Long DEFAULT_SECRETE_KEY_LENGTH = 256l;
60  	public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1";
61  	public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES";
62  	public final static String DEFAULT_CIPHER_NAME = "AES/CBC/PKCS5Padding";
63  
64  	private Integer iterationCountFactor = 200;
65  	private Long secretKeyLength = DEFAULT_SECRETE_KEY_LENGTH;
66  	private String secretKeyFactoryName = DEFAULT_SECRETE_KEY_FACTORY;
67  	private String secretKeyEncryption = DEFAULT_SECRETE_KEY_ENCRYPTION;
68  	private String cipherName = DEFAULT_CIPHER_NAME;
69  
70  	private final Repository repository;
71  	// TODO remove thread local session ; open a session each time
72  	private ThreadLocal<Session> sessionThreadLocal = new ThreadLocal<Session>() {
73  
74  		@Override
75  		protected Session initialValue() {
76  			return login();
77  		}
78  
79  	};
80  
81  	// FIXME is it really still needed?
82  	/**
83  	 * When setup is called the session has not yet been saved and we don't want to
84  	 * save it since there maybe other data which would be inconsistent. So we keep
85  	 * a reference to this node which will then be used (an reset to null) when
86  	 * handling the PBE callback. We keep one per thread in case multiple users are
87  	 * accessing the same instance of a keyring.
88  	 */
89  	// private ThreadLocal<Node> notYetSavedKeyring = new ThreadLocal<Node>() {
90  	//
91  	// @Override
92  	// protected Node initialValue() {
93  	// return null;
94  	// }
95  	// };
96  
97  	public JcrKeyring(Repository repository) {
98  		this.repository = repository;
99  	}
100 
101 	private Session session() {
102 		Session session = this.sessionThreadLocal.get();
103 		if (!session.isLive()) {
104 			session = login();
105 			sessionThreadLocal.set(session);
106 		}
107 		return session;
108 	}
109 
110 	private Session login() {
111 		try {
112 			return repository.login(NodeConstants.HOME_WORKSPACE);
113 		} catch (RepositoryException e) {
114 			throw new CmsException("Cannot login key ring session", e);
115 		}
116 	}
117 
118 	@Override
119 	protected synchronized Boolean isSetup() {
120 		Session session = null;
121 		try {
122 			// if (notYetSavedKeyring.get() != null)
123 			// return true;
124 			session = session();
125 			session.refresh(true);
126 			Node userHome = NodeUtils.getUserHome(session);
127 			return userHome.hasNode(ARGEO_KEYRING);
128 		} catch (RepositoryException e) {
129 			throw new ArgeoJcrException("Cannot check whether keyring is setup", e);
130 		} finally {
131 			JcrUtils.logoutQuietly(session);
132 		}
133 	}
134 
135 	@Override
136 	protected synchronized void setup(char[] password) {
137 		Binary binary = null;
138 		// InputStream in = null;
139 		try {
140 			session().refresh(true);
141 			Node userHome = NodeUtils.getUserHome(session());
142 			Node keyring;
143 			if (userHome.hasNode(ARGEO_KEYRING)) {
144 				throw new CmsException("Keyring already set up");
145 			} else {
146 				keyring = userHome.addNode(ARGEO_KEYRING);
147 			}
148 			keyring.addMixin(ArgeoTypes.ARGEO_PBE_SPEC);
149 
150 			// deterministic salt and iteration count based on username
151 			String username = session().getUserID();
152 			byte[] salt = new byte[8];
153 			byte[] usernameBytes = username.getBytes(StandardCharsets.UTF_8);
154 			for (int i = 0; i < salt.length; i++) {
155 				if (i < usernameBytes.length)
156 					salt[i] = usernameBytes[i];
157 				else
158 					salt[i] = 0;
159 			}
160 			try (InputStream in = new ByteArrayInputStream(salt);) {
161 				binary = session().getValueFactory().createBinary(in);
162 			}
163 			keyring.setProperty(ARGEO_SALT, binary);
164 
165 			Integer iterationCount = username.length() * iterationCountFactor;
166 			keyring.setProperty(ARGEO_ITERATION_COUNT, iterationCount);
167 
168 			// default algo
169 			// TODO check if algo and key length are available, use DES if not
170 			keyring.setProperty(ARGEO_SECRET_KEY_FACTORY, secretKeyFactoryName);
171 			keyring.setProperty(ARGEO_KEY_LENGTH, secretKeyLength);
172 			keyring.setProperty(ARGEO_SECRET_KEY_ENCRYPTION, secretKeyEncryption);
173 			keyring.setProperty(ARGEO_CIPHER, cipherName);
174 
175 			keyring.getSession().save();
176 
177 			// encrypted password hash
178 			// IOUtils.closeQuietly(in);
179 			// JcrUtils.closeQuietly(binary);
180 			// byte[] btPass = hash(password, salt, iterationCount);
181 			// in = new ByteArrayInputStream(btPass);
182 			// binary = session().getValueFactory().createBinary(in);
183 			// keyring.setProperty(ARGEO_PASSWORD, binary);
184 
185 			// notYetSavedKeyring.set(keyring);
186 		} catch (Exception e) {
187 			throw new ArgeoJcrException("Cannot setup keyring", e);
188 		} finally {
189 			JcrUtils.closeQuietly(binary);
190 			// IOUtils.closeQuietly(in);
191 			// JcrUtils.discardQuietly(session());
192 		}
193 	}
194 
195 	@Override
196 	protected synchronized void handleKeySpecCallback(PBEKeySpecCallback pbeCallback) {
197 		Session session = null;
198 		try {
199 			session = session();
200 			session.refresh(true);
201 			Node userHome = NodeUtils.getUserHome(session);
202 			Node keyring;
203 			if (userHome.hasNode(ARGEO_KEYRING))
204 				keyring = userHome.getNode(ARGEO_KEYRING);
205 			// else if (notYetSavedKeyring.get() != null)
206 			// keyring = notYetSavedKeyring.get();
207 			else
208 				throw new ArgeoJcrException("Keyring not setup");
209 
210 			pbeCallback.set(keyring.getProperty(ARGEO_SECRET_KEY_FACTORY).getString(),
211 					JcrUtils.getBinaryAsBytes(keyring.getProperty(ARGEO_SALT)),
212 					(int) keyring.getProperty(ARGEO_ITERATION_COUNT).getLong(),
213 					(int) keyring.getProperty(ARGEO_KEY_LENGTH).getLong(),
214 					keyring.getProperty(ARGEO_SECRET_KEY_ENCRYPTION).getString());
215 
216 			// if (notYetSavedKeyring.get() != null)
217 			// notYetSavedKeyring.remove();
218 		} catch (RepositoryException e) {
219 			throw new ArgeoJcrException("Cannot handle key spec callback", e);
220 		} finally {
221 			JcrUtils.logoutQuietly(session);
222 		}
223 	}
224 
225 	/** The parent node must already exist at this path. */
226 	@Override
227 	protected synchronized void encrypt(String path, InputStream unencrypted) {
228 		// should be called first for lazy initialization
229 		SecretKey secretKey = getSecretKey(null);
230 		Cipher cipher = createCipher();
231 
232 		// Binary binary = null;
233 		// InputStream in = null;
234 		try {
235 			session().refresh(true);
236 			Node node;
237 			if (!session().nodeExists(path)) {
238 				String parentPath = JcrUtils.parentPath(path);
239 				if (!session().nodeExists(parentPath))
240 					throw new ArgeoJcrException("No parent node of " + path);
241 				Node parentNode = session().getNode(parentPath);
242 				node = parentNode.addNode(JcrUtils.nodeNameFromPath(path));
243 			} else {
244 				node = session().getNode(path);
245 			}
246 			encrypt(secretKey, cipher, node, unencrypted);
247 			// node.addMixin(ArgeoTypes.ARGEO_ENCRYPTED);
248 			// SecureRandom random = new SecureRandom();
249 			// byte[] iv = new byte[16];
250 			// random.nextBytes(iv);
251 			// cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
252 			// JcrUtils.setBinaryAsBytes(node, ARGEO_IV, iv);
253 			//
254 			// try (InputStream in = new CipherInputStream(unencrypted, cipher);) {
255 			// binary = session().getValueFactory().createBinary(in);
256 			// node.setProperty(Property.JCR_DATA, binary);
257 			// session().save();
258 			// }
259 		} catch (RepositoryException e) {
260 			throw new ArgeoJcrException("Cannot encrypt", e);
261 		} finally {
262 			try {
263 				unencrypted.close();
264 			} catch (IOException e) {
265 				// silent
266 			}
267 			// IOUtils.closeQuietly(unencrypted);
268 			// IOUtils.closeQuietly(in);
269 			// JcrUtils.closeQuietly(binary);
270 			JcrUtils.logoutQuietly(session());
271 		}
272 	}
273 
274 	protected synchronized void encrypt(SecretKey secretKey, Cipher cipher, Node node, InputStream unencrypted) {
275 		try {
276 			node.addMixin(ArgeoTypes.ARGEO_ENCRYPTED);
277 			SecureRandom random = new SecureRandom();
278 			byte[] iv = new byte[16];
279 			random.nextBytes(iv);
280 			cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
281 			JcrUtils.setBinaryAsBytes(node, ARGEO_IV, iv);
282 
283 			Binary binary = null;
284 			try (InputStream in = new CipherInputStream(unencrypted, cipher);) {
285 				binary = session().getValueFactory().createBinary(in);
286 				node.setProperty(Property.JCR_DATA, binary);
287 				session().save();
288 			} finally {
289 				JcrUtils.closeQuietly(binary);
290 			}
291 		} catch (Exception e) {
292 			throw new ArgeoJcrException("Cannot encrypt", e);
293 		} finally {
294 			try {
295 				unencrypted.close();
296 			} catch (IOException e) {
297 				// silent
298 			}
299 			// IOUtils.closeQuietly(unencrypted);
300 			// IOUtils.closeQuietly(in);
301 			// JcrUtils.closeQuietly(binary);
302 			// JcrUtils.logoutQuietly(session());
303 		}
304 	}
305 
306 	@Override
307 	protected synchronized InputStream decrypt(String path) {
308 		Binary binary = null;
309 		// InputStream encrypted = null;
310 		try {
311 			session().refresh(true);
312 			if (!session().nodeExists(path)) {
313 				char[] password = ask();
314 				Reader reader = new CharArrayReader(password);
315 				return new ByteArrayInputStream(IOUtils.toByteArray(reader, StandardCharsets.UTF_8));
316 			} else {
317 				// should be called first for lazy initialisation
318 				SecretKey secretKey = getSecretKey(null);
319 				Cipher cipher = createCipher();
320 				Node node = session().getNode(path);
321 				return decrypt(secretKey, cipher, node);
322 			}
323 		} catch (Exception e) {
324 			throw new ArgeoJcrException("Cannot decrypt", e);
325 		} finally {
326 			// IOUtils.closeQuietly(encrypted);
327 			// IOUtils.closeQuietly(reader);
328 			JcrUtils.closeQuietly(binary);
329 			JcrUtils.logoutQuietly(session());
330 		}
331 	}
332 
333 	protected synchronized InputStream decrypt(SecretKey secretKey, Cipher cipher, Node node)
334 			throws RepositoryException, GeneralSecurityException {
335 		if (node.hasProperty(ARGEO_IV)) {
336 			byte[] iv = JcrUtils.getBinaryAsBytes(node.getProperty(ARGEO_IV));
337 			cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
338 		} else {
339 			cipher.init(Cipher.DECRYPT_MODE, secretKey);
340 		}
341 
342 		Binary binary = node.getProperty(Property.JCR_DATA).getBinary();
343 		InputStream encrypted = binary.getStream();
344 		return new CipherInputStream(encrypted, cipher);
345 	}
346 
347 	protected Cipher createCipher() {
348 		try {
349 			Node userHome = NodeUtils.getUserHome(session());
350 			if (!userHome.hasNode(ARGEO_KEYRING))
351 				throw new ArgeoJcrException("Keyring not setup");
352 			Node keyring = userHome.getNode(ARGEO_KEYRING);
353 			String cipherName = keyring.getProperty(ARGEO_CIPHER).getString();
354 			Provider securityProvider = getSecurityProvider();
355 			Cipher cipher;
356 			if (securityProvider == null)// TODO use BC?
357 				cipher = Cipher.getInstance(cipherName);
358 			else
359 				cipher = Cipher.getInstance(cipherName, securityProvider);
360 			return cipher;
361 		} catch (Exception e) {
362 			throw new ArgeoJcrException("Cannot get cipher", e);
363 		}
364 	}
365 
366 	public synchronized void changePassword(char[] oldPassword, char[] newPassword) {
367 		// TODO make it XA compatible
368 		SecretKey oldSecretKey = getSecretKey(oldPassword);
369 		SecretKey newSecretKey = getSecretKey(newPassword);
370 		Session session = session();
371 		try {
372 			NodeIterator encryptedNodes = session.getWorkspace().getQueryManager()
373 					.createQuery("select * from [argeo:encrypted]", Query.JCR_SQL2).execute().getNodes();
374 			while (encryptedNodes.hasNext()) {
375 				Node node = encryptedNodes.nextNode();
376 				InputStream in = decrypt(oldSecretKey, createCipher(), node);
377 				encrypt(newSecretKey, createCipher(), node, in);
378 				if (log.isDebugEnabled())
379 					log.debug("Converted keyring encrypted value of " + node.getPath());
380 			}
381 		} catch (RepositoryException | GeneralSecurityException e) {
382 			throw new CmsException("Cannot change JCR keyring password", e);
383 		} finally {
384 			JcrUtils.logoutQuietly(session);
385 		}
386 	}
387 
388 	// public synchronized void setSession(Session session) {
389 	// this.session = session;
390 	// }
391 
392 	public void setIterationCountFactor(Integer iterationCountFactor) {
393 		this.iterationCountFactor = iterationCountFactor;
394 	}
395 
396 	public void setSecretKeyLength(Long keyLength) {
397 		this.secretKeyLength = keyLength;
398 	}
399 
400 	public void setSecretKeyFactoryName(String secreteKeyFactoryName) {
401 		this.secretKeyFactoryName = secreteKeyFactoryName;
402 	}
403 
404 	public void setSecretKeyEncryption(String secreteKeyEncryption) {
405 		this.secretKeyEncryption = secreteKeyEncryption;
406 	}
407 
408 	public void setCipherName(String cipherName) {
409 		this.cipherName = cipherName;
410 	}
411 
412 }