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
.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
;
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);
44 * Stronger with 256, but causes problem with Oracle JVM, force 128 in this case
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";
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
;
57 private final Repository repository
;
58 // TODO remove thread local session ; open a session each time
59 private ThreadLocal
<Session
> sessionThreadLocal
= new ThreadLocal
<Session
>() {
62 protected Session
initialValue() {
68 // FIXME is it really still needed?
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.
76 // private ThreadLocal<Node> notYetSavedKeyring = new ThreadLocal<Node>() {
79 // protected Node initialValue() {
84 public JcrKeyring(Repository repository
) {
85 this.repository
= repository
;
88 private Session
session() {
89 Session session
= this.sessionThreadLocal
.get();
90 if (!session
.isLive()) {
92 sessionThreadLocal
.set(session
);
97 private Session
login() {
99 return repository
.login(NodeConstants
.HOME_WORKSPACE
);
100 } catch (RepositoryException e
) {
101 throw new JcrException("Cannot login key ring session", e
);
106 protected synchronized Boolean
isSetup() {
107 Session session
= null;
109 // if (notYetSavedKeyring.get() != null)
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
);
118 JcrUtils
.logoutQuietly(session
);
123 protected synchronized void setup(char[] password
) {
124 Binary binary
= null;
125 // InputStream in = null;
127 session().refresh(true);
128 Node userHome
= CmsJcrUtils
.getUserHome(session());
130 if (userHome
.hasNode(ARGEO_KEYRING
)) {
131 throw new IllegalArgumentException("Keyring already set up");
133 keyring
= userHome
.addNode(ARGEO_KEYRING
);
135 keyring
.addMixin(ArgeoTypes
.ARGEO_PBE_SPEC
);
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
];
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
);
154 Integer iterationCount
= username
.length() * iterationCountFactor
;
155 keyring
.setProperty(ARGEO_ITERATION_COUNT
, iterationCount
);
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
);
164 keyring
.getSession().save();
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);
174 // notYetSavedKeyring.set(keyring);
175 } catch (RepositoryException e
) {
176 throw new JcrException("Cannot setup keyring", e
);
178 JcrUtils
.closeQuietly(binary
);
179 // IOUtils.closeQuietly(in);
180 // JcrUtils.discardQuietly(session());
185 protected synchronized void handleKeySpecCallback(PBEKeySpecCallback pbeCallback
) {
186 Session session
= null;
189 session
.refresh(true);
190 Node userHome
= CmsJcrUtils
.getUserHome(session
);
192 if (userHome
.hasNode(ARGEO_KEYRING
))
193 keyring
= userHome
.getNode(ARGEO_KEYRING
);
194 // else if (notYetSavedKeyring.get() != null)
195 // keyring = notYetSavedKeyring.get();
197 throw new IllegalStateException("Keyring not setup");
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());
205 // if (notYetSavedKeyring.get() != null)
206 // notYetSavedKeyring.remove();
207 } catch (RepositoryException e
) {
208 throw new JcrException("Cannot handle key spec callback", e
);
210 JcrUtils
.logoutQuietly(session
);
214 /** The parent node must already exist at this path. */
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();
221 // Binary binary = null;
222 // InputStream in = null;
224 session().refresh(true);
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
));
233 node
= session().getNode(path
);
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);
243 // try (InputStream in = new CipherInputStream(unencrypted, cipher);) {
244 // binary = session().getValueFactory().createBinary(in);
245 // node.setProperty(Property.JCR_DATA, binary);
248 } catch (RepositoryException e
) {
249 throw new JcrException("Cannot encrypt", e
);
253 } catch (IOException e
) {
256 // IOUtils.closeQuietly(unencrypted);
257 // IOUtils.closeQuietly(in);
258 // JcrUtils.closeQuietly(binary);
259 JcrUtils
.logoutQuietly(session());
263 protected synchronized void encrypt(SecretKey secretKey
, Cipher cipher
, Node node
, InputStream unencrypted
) {
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
);
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
);
278 JcrUtils
.closeQuietly(binary
);
280 } catch (RepositoryException e
) {
281 throw new JcrException("Cannot encrypt", e
);
282 } catch (GeneralSecurityException
| IOException e
) {
283 throw new RuntimeException("Cannot encrypt", e
);
288 protected synchronized InputStream
decrypt(String path
) {
289 Binary binary
= null;
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
));
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
);
303 } catch (RepositoryException e
) {
304 throw new JcrException("Cannot decrypt", e
);
305 } catch (GeneralSecurityException
| IOException e
) {
306 throw new RuntimeException("Cannot decrypt", e
);
308 JcrUtils
.closeQuietly(binary
);
309 JcrUtils
.logoutQuietly(session());
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
));
319 cipher
.init(Cipher
.DECRYPT_MODE
, secretKey
);
322 Binary binary
= node
.getProperty(Property
.JCR_DATA
).getBinary();
323 InputStream encrypted
= binary
.getStream();
324 return new CipherInputStream(encrypted
, cipher
);
327 protected Cipher
createCipher() {
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();
336 if (securityProvider
== null)// TODO use BC?
337 cipher
= Cipher
.getInstance(cipherName
);
339 cipher
= Cipher
.getInstance(cipherName
, securityProvider
);
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
);
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();
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());
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
);
370 JcrUtils
.logoutQuietly(session
);
374 // public synchronized void setSession(Session session) {
375 // this.session = session;
378 public void setIterationCountFactor(Integer iterationCountFactor
) {
379 this.iterationCountFactor
= iterationCountFactor
;
382 public void setSecretKeyLength(Long keyLength
) {
383 this.secretKeyLength
= keyLength
;
386 public void setSecretKeyFactoryName(String secreteKeyFactoryName
) {
387 this.secretKeyFactoryName
= secreteKeyFactoryName
;
390 public void setSecretKeyEncryption(String secreteKeyEncryption
) {
391 this.secretKeyEncryption
= secreteKeyEncryption
;
394 public void setCipherName(String cipherName
) {
395 this.cipherName
= cipherName
;