]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/security/JcrKeyring.java
Set update policy to 'always' for SNAPSHOT repositories.
[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 Session session = null;
197 try {
198 session = session();
199 session.refresh(true);
200 Node userHome = NodeUtils.getUserHome(session);
201 Node keyring;
202 if (userHome.hasNode(ARGEO_KEYRING))
203 keyring = userHome.getNode(ARGEO_KEYRING);
204 // else if (notYetSavedKeyring.get() != null)
205 // keyring = notYetSavedKeyring.get();
206 else
207 throw new ArgeoJcrException("Keyring not setup");
208
209 pbeCallback.set(keyring.getProperty(ARGEO_SECRET_KEY_FACTORY).getString(),
210 JcrUtils.getBinaryAsBytes(keyring.getProperty(ARGEO_SALT)),
211 (int) keyring.getProperty(ARGEO_ITERATION_COUNT).getLong(),
212 (int) keyring.getProperty(ARGEO_KEY_LENGTH).getLong(),
213 keyring.getProperty(ARGEO_SECRET_KEY_ENCRYPTION).getString());
214
215 // if (notYetSavedKeyring.get() != null)
216 // notYetSavedKeyring.remove();
217 } catch (RepositoryException e) {
218 throw new ArgeoJcrException("Cannot handle key spec callback", e);
219 } finally {
220 JcrUtils.logoutQuietly(session);
221 }
222 }
223
224 /** The parent node must already exist at this path. */
225 @Override
226 protected synchronized void encrypt(String path, InputStream unencrypted) {
227 // should be called first for lazy initialization
228 SecretKey secretKey = getSecretKey(null);
229 Cipher cipher = createCipher();
230
231 // Binary binary = null;
232 // InputStream in = null;
233 try {
234 session().refresh(true);
235 Node node;
236 if (!session().nodeExists(path)) {
237 String parentPath = JcrUtils.parentPath(path);
238 if (!session().nodeExists(parentPath))
239 throw new ArgeoJcrException("No parent node of " + path);
240 Node parentNode = session().getNode(parentPath);
241 node = parentNode.addNode(JcrUtils.nodeNameFromPath(path));
242 } else {
243 node = session().getNode(path);
244 }
245 encrypt(secretKey, cipher, node, unencrypted);
246 // node.addMixin(ArgeoTypes.ARGEO_ENCRYPTED);
247 // SecureRandom random = new SecureRandom();
248 // byte[] iv = new byte[16];
249 // random.nextBytes(iv);
250 // cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
251 // JcrUtils.setBinaryAsBytes(node, ARGEO_IV, iv);
252 //
253 // try (InputStream in = new CipherInputStream(unencrypted, cipher);) {
254 // binary = session().getValueFactory().createBinary(in);
255 // node.setProperty(Property.JCR_DATA, binary);
256 // session().save();
257 // }
258 } catch (RepositoryException e) {
259 throw new ArgeoJcrException("Cannot encrypt", e);
260 } finally {
261 try {
262 unencrypted.close();
263 } catch (IOException e) {
264 // silent
265 }
266 // IOUtils.closeQuietly(unencrypted);
267 // IOUtils.closeQuietly(in);
268 // JcrUtils.closeQuietly(binary);
269 JcrUtils.logoutQuietly(session());
270 }
271 }
272
273 protected synchronized void encrypt(SecretKey secretKey, Cipher cipher, Node node, InputStream unencrypted) {
274 try {
275 node.addMixin(ArgeoTypes.ARGEO_ENCRYPTED);
276 SecureRandom random = new SecureRandom();
277 byte[] iv = new byte[16];
278 random.nextBytes(iv);
279 cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
280 JcrUtils.setBinaryAsBytes(node, ARGEO_IV, iv);
281
282 Binary binary = null;
283 try (InputStream in = new CipherInputStream(unencrypted, cipher);) {
284 binary = session().getValueFactory().createBinary(in);
285 node.setProperty(Property.JCR_DATA, binary);
286 session().save();
287 } finally {
288 JcrUtils.closeQuietly(binary);
289 }
290 } catch (Exception e) {
291 throw new ArgeoJcrException("Cannot encrypt", e);
292 } finally {
293 try {
294 unencrypted.close();
295 } catch (IOException e) {
296 // silent
297 }
298 // IOUtils.closeQuietly(unencrypted);
299 // IOUtils.closeQuietly(in);
300 // JcrUtils.closeQuietly(binary);
301 // JcrUtils.logoutQuietly(session());
302 }
303 }
304
305 @Override
306 protected synchronized InputStream decrypt(String path) {
307 Binary binary = null;
308 // InputStream encrypted = null;
309 try {
310 session().refresh(true);
311 if (!session().nodeExists(path)) {
312 char[] password = ask();
313 Reader reader = new CharArrayReader(password);
314 return new ByteArrayInputStream(IOUtils.toByteArray(reader, StandardCharsets.UTF_8));
315 } else {
316 // should be called first for lazy initialisation
317 SecretKey secretKey = getSecretKey(null);
318 Cipher cipher = createCipher();
319 Node node = session().getNode(path);
320 return decrypt(secretKey, cipher, node);
321 }
322 } catch (Exception e) {
323 throw new ArgeoJcrException("Cannot decrypt", e);
324 } finally {
325 // IOUtils.closeQuietly(encrypted);
326 // IOUtils.closeQuietly(reader);
327 JcrUtils.closeQuietly(binary);
328 JcrUtils.logoutQuietly(session());
329 }
330 }
331
332 protected synchronized InputStream decrypt(SecretKey secretKey, Cipher cipher, Node node)
333 throws RepositoryException, GeneralSecurityException {
334 if (node.hasProperty(ARGEO_IV)) {
335 byte[] iv = JcrUtils.getBinaryAsBytes(node.getProperty(ARGEO_IV));
336 cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));
337 } else {
338 cipher.init(Cipher.DECRYPT_MODE, secretKey);
339 }
340
341 Binary binary = node.getProperty(Property.JCR_DATA).getBinary();
342 InputStream encrypted = binary.getStream();
343 return new CipherInputStream(encrypted, cipher);
344 }
345
346 protected Cipher createCipher() {
347 try {
348 Node userHome = NodeUtils.getUserHome(session());
349 if (!userHome.hasNode(ARGEO_KEYRING))
350 throw new ArgeoJcrException("Keyring not setup");
351 Node keyring = userHome.getNode(ARGEO_KEYRING);
352 String cipherName = keyring.getProperty(ARGEO_CIPHER).getString();
353 Provider securityProvider = getSecurityProvider();
354 Cipher cipher;
355 if (securityProvider == null)// TODO use BC?
356 cipher = Cipher.getInstance(cipherName);
357 else
358 cipher = Cipher.getInstance(cipherName, securityProvider);
359 return cipher;
360 } catch (Exception e) {
361 throw new ArgeoJcrException("Cannot get cipher", e);
362 }
363 }
364
365 public synchronized void changePassword(char[] oldPassword, char[] newPassword) {
366 // TODO make it XA compatible
367 SecretKey oldSecretKey = getSecretKey(oldPassword);
368 SecretKey newSecretKey = getSecretKey(newPassword);
369 Session session = session();
370 try {
371 NodeIterator encryptedNodes = session.getWorkspace().getQueryManager()
372 .createQuery("select * from [argeo:encrypted]", Query.JCR_SQL2).execute().getNodes();
373 while (encryptedNodes.hasNext()) {
374 Node node = encryptedNodes.nextNode();
375 InputStream in = decrypt(oldSecretKey, createCipher(), node);
376 encrypt(newSecretKey, createCipher(), node, in);
377 if (log.isDebugEnabled())
378 log.debug("Converted keyring encrypted value of " + node.getPath());
379 }
380 } catch (RepositoryException | GeneralSecurityException e) {
381 throw new CmsException("Cannot change JCR keyring password", e);
382 } finally {
383 JcrUtils.logoutQuietly(session);
384 }
385 }
386
387 // public synchronized void setSession(Session session) {
388 // this.session = session;
389 // }
390
391 public void setIterationCountFactor(Integer iterationCountFactor) {
392 this.iterationCountFactor = iterationCountFactor;
393 }
394
395 public void setSecretKeyLength(Long keyLength) {
396 this.secretKeyLength = keyLength;
397 }
398
399 public void setSecretKeyFactoryName(String secreteKeyFactoryName) {
400 this.secretKeyFactoryName = secreteKeyFactoryName;
401 }
402
403 public void setSecretKeyEncryption(String secreteKeyEncryption) {
404 this.secretKeyEncryption = secreteKeyEncryption;
405 }
406
407 public void setCipherName(String cipherName) {
408 this.cipherName = cipherName;
409 }
410
411 }