2 * Copyright (C) 2007-2012 Argeo GmbH
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package org
.argeo
.cms
.security
;
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
;
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
;
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
.api
.NodeConstants
;
45 import org
.argeo
.api
.NodeUtils
;
46 import org
.argeo
.api
.security
.PBEKeySpecCallback
;
47 import org
.argeo
.cms
.ArgeoNames
;
48 import org
.argeo
.cms
.ArgeoTypes
;
49 import org
.argeo
.cms
.CmsException
;
50 import org
.argeo
.jcr
.ArgeoJcrException
;
51 import org
.argeo
.jcr
.JcrUtils
;
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);
57 * Stronger with 256, but causes problem with Oracle JVM, force 128 in this case
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";
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
;
70 private final Repository repository
;
71 // TODO remove thread local session ; open a session each time
72 private ThreadLocal
<Session
> sessionThreadLocal
= new ThreadLocal
<Session
>() {
75 protected Session
initialValue() {
81 // FIXME is it really still needed?
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.
89 // private ThreadLocal<Node> notYetSavedKeyring = new ThreadLocal<Node>() {
92 // protected Node initialValue() {
97 public JcrKeyring(Repository repository
) {
98 this.repository
= repository
;
101 private Session
session() {
102 Session session
= this.sessionThreadLocal
.get();
103 if (!session
.isLive()) {
105 sessionThreadLocal
.set(session
);
110 private Session
login() {
112 return repository
.login(NodeConstants
.HOME
);
113 } catch (RepositoryException e
) {
114 throw new CmsException("Cannot login key ring session", e
);
119 protected synchronized Boolean
isSetup() {
120 Session session
= null;
122 // if (notYetSavedKeyring.get() != null)
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
);
131 JcrUtils
.logoutQuietly(session
);
136 protected synchronized void setup(char[] password
) {
137 Binary binary
= null;
138 // InputStream in = null;
140 session().refresh(true);
141 Node userHome
= NodeUtils
.getUserHome(session());
143 if (userHome
.hasNode(ARGEO_KEYRING
)) {
144 throw new CmsException("Keyring already set up");
146 keyring
= userHome
.addNode(ARGEO_KEYRING
);
148 keyring
.addMixin(ArgeoTypes
.ARGEO_PBE_SPEC
);
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
];
160 try (InputStream in
= new ByteArrayInputStream(salt
);) {
161 binary
= session().getValueFactory().createBinary(in
);
163 keyring
.setProperty(ARGEO_SALT
, binary
);
165 Integer iterationCount
= username
.length() * iterationCountFactor
;
166 keyring
.setProperty(ARGEO_ITERATION_COUNT
, iterationCount
);
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
);
175 keyring
.getSession().save();
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);
185 // notYetSavedKeyring.set(keyring);
186 } catch (Exception e
) {
187 throw new ArgeoJcrException("Cannot setup keyring", e
);
189 JcrUtils
.closeQuietly(binary
);
190 // IOUtils.closeQuietly(in);
191 // JcrUtils.discardQuietly(session());
196 protected synchronized void handleKeySpecCallback(PBEKeySpecCallback pbeCallback
) {
197 Session session
= null;
200 session
.refresh(true);
201 Node userHome
= NodeUtils
.getUserHome(session
);
203 if (userHome
.hasNode(ARGEO_KEYRING
))
204 keyring
= userHome
.getNode(ARGEO_KEYRING
);
205 // else if (notYetSavedKeyring.get() != null)
206 // keyring = notYetSavedKeyring.get();
208 throw new ArgeoJcrException("Keyring not setup");
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());
216 // if (notYetSavedKeyring.get() != null)
217 // notYetSavedKeyring.remove();
218 } catch (RepositoryException e
) {
219 throw new ArgeoJcrException("Cannot handle key spec callback", e
);
221 JcrUtils
.logoutQuietly(session
);
225 /** The parent node must already exist at this path. */
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();
232 // Binary binary = null;
233 // InputStream in = null;
235 session().refresh(true);
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
));
244 node
= session().getNode(path
);
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);
254 // try (InputStream in = new CipherInputStream(unencrypted, cipher);) {
255 // binary = session().getValueFactory().createBinary(in);
256 // node.setProperty(Property.JCR_DATA, binary);
259 } catch (RepositoryException e
) {
260 throw new ArgeoJcrException("Cannot encrypt", e
);
264 } catch (IOException e
) {
267 // IOUtils.closeQuietly(unencrypted);
268 // IOUtils.closeQuietly(in);
269 // JcrUtils.closeQuietly(binary);
270 JcrUtils
.logoutQuietly(session());
274 protected synchronized void encrypt(SecretKey secretKey
, Cipher cipher
, Node node
, InputStream unencrypted
) {
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
);
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
);
289 JcrUtils
.closeQuietly(binary
);
291 } catch (Exception e
) {
292 throw new ArgeoJcrException("Cannot encrypt", e
);
296 } catch (IOException e
) {
299 // IOUtils.closeQuietly(unencrypted);
300 // IOUtils.closeQuietly(in);
301 // JcrUtils.closeQuietly(binary);
302 // JcrUtils.logoutQuietly(session());
307 protected synchronized InputStream
decrypt(String path
) {
308 Binary binary
= null;
309 // InputStream encrypted = null;
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
));
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
);
323 } catch (Exception e
) {
324 throw new ArgeoJcrException("Cannot decrypt", e
);
326 // IOUtils.closeQuietly(encrypted);
327 // IOUtils.closeQuietly(reader);
328 JcrUtils
.closeQuietly(binary
);
329 JcrUtils
.logoutQuietly(session());
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
));
339 cipher
.init(Cipher
.DECRYPT_MODE
, secretKey
);
342 Binary binary
= node
.getProperty(Property
.JCR_DATA
).getBinary();
343 InputStream encrypted
= binary
.getStream();
344 return new CipherInputStream(encrypted
, cipher
);
347 protected Cipher
createCipher() {
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();
356 if (securityProvider
== null)// TODO use BC?
357 cipher
= Cipher
.getInstance(cipherName
);
359 cipher
= Cipher
.getInstance(cipherName
, securityProvider
);
361 } catch (Exception e
) {
362 throw new ArgeoJcrException("Cannot get cipher", e
);
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();
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());
381 } catch (RepositoryException
| GeneralSecurityException e
) {
382 throw new CmsException("Cannot change JCR keyring password", e
);
384 JcrUtils
.logoutQuietly(session
);
388 // public synchronized void setSession(Session session) {
389 // this.session = session;
392 public void setIterationCountFactor(Integer iterationCountFactor
) {
393 this.iterationCountFactor
= iterationCountFactor
;
396 public void setSecretKeyLength(Long keyLength
) {
397 this.secretKeyLength
= keyLength
;
400 public void setSecretKeyFactoryName(String secreteKeyFactoryName
) {
401 this.secretKeyFactoryName
= secreteKeyFactoryName
;
404 public void setSecretKeyEncryption(String secreteKeyEncryption
) {
405 this.secretKeyEncryption
= secreteKeyEncryption
;
408 public void setCipherName(String cipherName
) {
409 this.cipherName
= cipherName
;