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