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