]> git.argeo.org Git - lgpl/argeo-commons.git/blob - JcrKeyring.java
c75d38fc88f54c18fc80414ccba8b29acd076a09
[lgpl/argeo-commons.git] / 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.NodeConstants;
50 import org.argeo.node.NodeUtils;
51 import org.argeo.node.security.PBEKeySpecCallback;
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);
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 }