1 package org
.argeo
.cms
.jcr
.internal
;
3 import java
.io
.ByteArrayInputStream
;
4 import java
.io
.CharArrayReader
;
5 import java
.io
.IOException
;
6 import java
.io
.InputStream
;
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
;
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
;
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
.api
.cms
.keyring
.PBEKeySpecCallback
;
32 import org
.argeo
.cms
.AbstractKeyring
;
33 import org
.argeo
.cms
.ArgeoNames
;
34 import org
.argeo
.cms
.ArgeoTypes
;
35 import org
.argeo
.cms
.jcr
.CmsJcrUtils
;
36 import org
.argeo
.jcr
.JcrException
;
37 import org
.argeo
.jcr
.JcrUtils
;
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);
43 * Stronger with 256, but causes problem with Oracle JVM, force 128 in this case
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";
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
;
56 private final Repository repository
;
57 // TODO remove thread local session ; open a session each time
58 private ThreadLocal
<Session
> sessionThreadLocal
= new ThreadLocal
<Session
>() {
61 protected Session
initialValue() {
67 // FIXME is it really still needed?
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.
75 // private ThreadLocal<Node> notYetSavedKeyring = new ThreadLocal<Node>() {
78 // protected Node initialValue() {
83 public JcrKeyring(Repository repository
) {
84 this.repository
= repository
;
87 private Session
session() {
88 Session session
= this.sessionThreadLocal
.get();
89 if (!session
.isLive()) {
91 sessionThreadLocal
.set(session
);
96 private Session
login() {
98 return repository
.login(CmsConstants
.HOME_WORKSPACE
);
99 } catch (RepositoryException e
) {
100 throw new JcrException("Cannot login key ring session", e
);
105 protected synchronized Boolean
isSetup() {
106 Session session
= null;
108 // if (notYetSavedKeyring.get() != null)
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
);
117 JcrUtils
.logoutQuietly(session
);
122 protected synchronized void setup(char[] password
) {
123 Binary binary
= null;
124 // InputStream in = null;
126 session().refresh(true);
127 Node userHome
= CmsJcrUtils
.getUserHome(session());
129 if (userHome
.hasNode(ARGEO_KEYRING
)) {
130 throw new IllegalArgumentException("Keyring already set up");
132 keyring
= userHome
.addNode(ARGEO_KEYRING
);
134 keyring
.addMixin(ArgeoTypes
.ARGEO_PBE_SPEC
);
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
];
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
);
153 Integer iterationCount
= username
.length() * iterationCountFactor
;
154 keyring
.setProperty(ARGEO_ITERATION_COUNT
, iterationCount
);
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
);
163 keyring
.getSession().save();
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);
173 // notYetSavedKeyring.set(keyring);
174 } catch (RepositoryException e
) {
175 throw new JcrException("Cannot setup keyring", e
);
177 JcrUtils
.closeQuietly(binary
);
178 // IOUtils.closeQuietly(in);
179 // JcrUtils.discardQuietly(session());
184 protected synchronized void handleKeySpecCallback(PBEKeySpecCallback pbeCallback
) {
185 Session session
= null;
188 session
.refresh(true);
189 Node userHome
= CmsJcrUtils
.getUserHome(session
);
191 if (userHome
.hasNode(ARGEO_KEYRING
))
192 keyring
= userHome
.getNode(ARGEO_KEYRING
);
193 // else if (notYetSavedKeyring.get() != null)
194 // keyring = notYetSavedKeyring.get();
196 throw new IllegalStateException("Keyring not setup");
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());
204 // if (notYetSavedKeyring.get() != null)
205 // notYetSavedKeyring.remove();
206 } catch (RepositoryException e
) {
207 throw new JcrException("Cannot handle key spec callback", e
);
209 JcrUtils
.logoutQuietly(session
);
213 /** The parent node must already exist at this path. */
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();
220 // Binary binary = null;
221 // InputStream in = null;
223 session().refresh(true);
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
));
232 node
= session().getNode(path
);
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);
242 // try (InputStream in = new CipherInputStream(unencrypted, cipher);) {
243 // binary = session().getValueFactory().createBinary(in);
244 // node.setProperty(Property.JCR_DATA, binary);
247 } catch (RepositoryException e
) {
248 throw new JcrException("Cannot encrypt", e
);
252 } catch (IOException e
) {
255 // IOUtils.closeQuietly(unencrypted);
256 // IOUtils.closeQuietly(in);
257 // JcrUtils.closeQuietly(binary);
258 JcrUtils
.logoutQuietly(session());
262 protected synchronized void encrypt(SecretKey secretKey
, Cipher cipher
, Node node
, InputStream unencrypted
) {
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
);
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
);
277 JcrUtils
.closeQuietly(binary
);
279 } catch (RepositoryException e
) {
280 throw new JcrException("Cannot encrypt", e
);
281 } catch (GeneralSecurityException
| IOException e
) {
282 throw new RuntimeException("Cannot encrypt", e
);
287 protected synchronized InputStream
decrypt(String path
) {
288 Binary binary
= null;
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
));
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
);
302 } catch (RepositoryException e
) {
303 throw new JcrException("Cannot decrypt", e
);
304 } catch (GeneralSecurityException
| IOException e
) {
305 throw new RuntimeException("Cannot decrypt", e
);
307 JcrUtils
.closeQuietly(binary
);
308 JcrUtils
.logoutQuietly(session());
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
));
318 cipher
.init(Cipher
.DECRYPT_MODE
, secretKey
);
321 Binary binary
= node
.getProperty(Property
.JCR_DATA
).getBinary();
322 InputStream encrypted
= binary
.getStream();
323 return new CipherInputStream(encrypted
, cipher
);
326 protected Cipher
createCipher() {
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();
335 if (securityProvider
== null)// TODO use BC?
336 cipher
= Cipher
.getInstance(cipherName
);
338 cipher
= Cipher
.getInstance(cipherName
, securityProvider
);
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
);
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();
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());
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
);
369 JcrUtils
.logoutQuietly(session
);
373 // public synchronized void setSession(Session session) {
374 // this.session = session;
377 public void setIterationCountFactor(Integer iterationCountFactor
) {
378 this.iterationCountFactor
= iterationCountFactor
;
381 public void setSecretKeyLength(Long keyLength
) {
382 this.secretKeyLength
= keyLength
;
385 public void setSecretKeyFactoryName(String secreteKeyFactoryName
) {
386 this.secretKeyFactoryName
= secreteKeyFactoryName
;
389 public void setSecretKeyEncryption(String secreteKeyEncryption
) {
390 this.secretKeyEncryption
= secreteKeyEncryption
;
393 public void setCipherName(String cipherName
) {
394 this.cipherName
= cipherName
;