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