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
.InputStream
;
21 import java
.io
.Reader
;
22 import java
.security
.Provider
;
23 import java
.security
.SecureRandom
;
25 import javax
.crypto
.Cipher
;
26 import javax
.crypto
.CipherInputStream
;
27 import javax
.crypto
.SecretKey
;
28 import javax
.crypto
.spec
.IvParameterSpec
;
29 import javax
.jcr
.Binary
;
30 import javax
.jcr
.Node
;
31 import javax
.jcr
.Property
;
32 import javax
.jcr
.Repository
;
33 import javax
.jcr
.RepositoryException
;
34 import javax
.jcr
.Session
;
36 import org
.apache
.commons
.io
.IOUtils
;
37 import org
.argeo
.cms
.CmsException
;
38 import org
.argeo
.jcr
.ArgeoJcrException
;
39 import org
.argeo
.jcr
.JcrUtils
;
40 import org
.argeo
.node
.ArgeoNames
;
41 import org
.argeo
.node
.ArgeoTypes
;
42 import org
.argeo
.node
.NodeUtils
;
43 import org
.argeo
.node
.security
.PBEKeySpecCallback
;
45 /** JCR based implementation of a keyring */
46 public class JcrKeyring
extends AbstractKeyring
implements ArgeoNames
{
48 * Stronger with 256, but causes problem with Oracle JVM, force 128 in this
51 public final static Long DEFAULT_SECRETE_KEY_LENGTH
= 256l;
52 public final static String DEFAULT_SECRETE_KEY_FACTORY
= "PBKDF2WithHmacSHA1";
53 public final static String DEFAULT_SECRETE_KEY_ENCRYPTION
= "AES";
54 public final static String DEFAULT_CIPHER_NAME
= "AES/CBC/PKCS5Padding";
56 private Integer iterationCountFactor
= 200;
57 private Long secreteKeyLength
= DEFAULT_SECRETE_KEY_LENGTH
;
58 private String secreteKeyFactoryName
= DEFAULT_SECRETE_KEY_FACTORY
;
59 private String secreteKeyEncryption
= DEFAULT_SECRETE_KEY_ENCRYPTION
;
60 private String cipherName
= DEFAULT_CIPHER_NAME
;
62 private final Repository repository
;
63 private ThreadLocal
<Session
> sessionThreadLocal
= new ThreadLocal
<Session
>() {
66 protected Session
initialValue() {
72 // FIXME is it really still needed?
74 * When setup is called the session has not yet been saved and we don't want
75 * to save it since there maybe other data which would be inconsistent. So
76 * we keep a reference to this node which will then be used (an reset to
77 * null) when handling the PBE callback. We keep one per thread in case
78 * multiple users are accessing the same instance of a keyring.
80 private ThreadLocal
<Node
> notYetSavedKeyring
= new ThreadLocal
<Node
>() {
83 protected Node
initialValue() {
88 public JcrKeyring(Repository repository
) {
89 this.repository
= repository
;
92 private Session
session() {
93 Session session
= this.sessionThreadLocal
.get();
94 if (!session
.isLive()) {
96 sessionThreadLocal
.set(session
);
101 private Session
login() {
103 return repository
.login();
104 } catch (RepositoryException e
) {
105 throw new CmsException("Cannot login key ring session", e
);
110 protected Boolean
isSetup() {
112 if (notYetSavedKeyring
.get() != null)
115 Node userHome
= NodeUtils
.getUserHome(session());
116 return userHome
.hasNode(ARGEO_KEYRING
);
117 } catch (RepositoryException e
) {
118 throw new ArgeoJcrException("Cannot check whether keyring is setup", e
);
123 protected void setup(char[] password
) {
124 Binary binary
= null;
125 InputStream in
= null;
127 Node userHome
= NodeUtils
.getUserHome(session());
128 if (userHome
.hasNode(ARGEO_KEYRING
))
129 throw new ArgeoJcrException("Keyring already setup");
130 Node keyring
= userHome
.addNode(ARGEO_KEYRING
);
131 keyring
.addMixin(ArgeoTypes
.ARGEO_PBE_SPEC
);
133 // deterministic salt and iteration count based on username
134 String username
= session().getUserID();
135 byte[] salt
= new byte[8];
136 byte[] usernameBytes
= username
.getBytes();
137 for (int i
= 0; i
< salt
.length
; i
++) {
138 if (i
< usernameBytes
.length
)
139 salt
[i
] = usernameBytes
[i
];
143 in
= new ByteArrayInputStream(salt
);
144 binary
= session().getValueFactory().createBinary(in
);
145 keyring
.setProperty(ARGEO_SALT
, binary
);
147 Integer iterationCount
= username
.length() * iterationCountFactor
;
148 keyring
.setProperty(ARGEO_ITERATION_COUNT
, iterationCount
);
151 // TODO check if algo and key length are available, use DES if not
152 keyring
.setProperty(ARGEO_SECRET_KEY_FACTORY
, secreteKeyFactoryName
);
153 keyring
.setProperty(ARGEO_KEY_LENGTH
, secreteKeyLength
);
154 keyring
.setProperty(ARGEO_SECRET_KEY_ENCRYPTION
, secreteKeyEncryption
);
155 keyring
.setProperty(ARGEO_CIPHER
, cipherName
);
157 // keyring.getSession().save();
159 // encrypted password hash
160 // IOUtils.closeQuietly(in);
161 // JcrUtils.closeQuietly(binary);
162 // byte[] btPass = hash(password, salt, iterationCount);
163 // in = new ByteArrayInputStream(btPass);
164 // binary = session().getValueFactory().createBinary(in);
165 // keyring.setProperty(ARGEO_PASSWORD, binary);
167 notYetSavedKeyring
.set(keyring
);
168 } catch (Exception e
) {
169 throw new ArgeoJcrException("Cannot setup keyring", e
);
171 JcrUtils
.closeQuietly(binary
);
172 IOUtils
.closeQuietly(in
);
173 // JcrUtils.discardQuietly(session());
178 protected void handleKeySpecCallback(PBEKeySpecCallback pbeCallback
) {
180 Node userHome
= NodeUtils
.getUserHome(session());
182 if (userHome
.hasNode(ARGEO_KEYRING
))
183 keyring
= userHome
.getNode(ARGEO_KEYRING
);
184 else if (notYetSavedKeyring
.get() != null)
185 keyring
= notYetSavedKeyring
.get();
187 throw new ArgeoJcrException("Keyring not setup");
189 pbeCallback
.set(keyring
.getProperty(ARGEO_SECRET_KEY_FACTORY
).getString(),
190 JcrUtils
.getBinaryAsBytes(keyring
.getProperty(ARGEO_SALT
)),
191 (int) keyring
.getProperty(ARGEO_ITERATION_COUNT
).getLong(),
192 (int) keyring
.getProperty(ARGEO_KEY_LENGTH
).getLong(),
193 keyring
.getProperty(ARGEO_SECRET_KEY_ENCRYPTION
).getString());
195 if (notYetSavedKeyring
.get() != null)
196 notYetSavedKeyring
.remove();
197 } catch (RepositoryException e
) {
198 throw new ArgeoJcrException("Cannot handle key spec callback", e
);
202 /** The parent node must already exist at this path. */
204 protected synchronized void encrypt(String path
, InputStream unencrypted
) {
205 // should be called first for lazy initialization
206 SecretKey secretKey
= getSecretKey();
208 Binary binary
= null;
209 InputStream in
= null;
211 Cipher cipher
= createCipher();
213 if (!session().nodeExists(path
)) {
214 String parentPath
= JcrUtils
.parentPath(path
);
215 if (!session().nodeExists(parentPath
))
216 throw new ArgeoJcrException("No parent node of " + path
);
217 Node parentNode
= session().getNode(parentPath
);
218 node
= parentNode
.addNode(JcrUtils
.nodeNameFromPath(path
));
220 node
= session().getNode(path
);
222 node
.addMixin(ArgeoTypes
.ARGEO_ENCRYPTED
);
223 SecureRandom random
= new SecureRandom();
224 byte[] iv
= new byte[16];
225 random
.nextBytes(iv
);
226 cipher
.init(Cipher
.ENCRYPT_MODE
, secretKey
, new IvParameterSpec(iv
));
227 JcrUtils
.setBinaryAsBytes(node
, ARGEO_IV
, iv
);
229 in
= new CipherInputStream(unencrypted
, cipher
);
230 binary
= session().getValueFactory().createBinary(in
);
231 node
.setProperty(Property
.JCR_DATA
, binary
);
233 } catch (Exception e
) {
234 throw new ArgeoJcrException("Cannot encrypt", e
);
236 IOUtils
.closeQuietly(unencrypted
);
237 IOUtils
.closeQuietly(in
);
238 JcrUtils
.closeQuietly(binary
);
239 JcrUtils
.logoutQuietly(session());
244 protected synchronized InputStream
decrypt(String path
) {
245 Binary binary
= null;
246 InputStream encrypted
= null;
247 Reader reader
= null;
249 if (!session().nodeExists(path
)) {
250 char[] password
= ask();
251 reader
= new CharArrayReader(password
);
252 return new ByteArrayInputStream(IOUtils
.toByteArray(reader
));
254 // should be called first for lazy initialisation
255 SecretKey secretKey
= getSecretKey();
257 Cipher cipher
= createCipher();
259 Node node
= session().getNode(path
);
260 if (node
.hasProperty(ARGEO_IV
)) {
261 byte[] iv
= JcrUtils
.getBinaryAsBytes(node
.getProperty(ARGEO_IV
));
262 cipher
.init(Cipher
.DECRYPT_MODE
, secretKey
, new IvParameterSpec(iv
));
264 cipher
.init(Cipher
.DECRYPT_MODE
, secretKey
);
267 binary
= node
.getProperty(Property
.JCR_DATA
).getBinary();
268 encrypted
= binary
.getStream();
269 return new CipherInputStream(encrypted
, cipher
);
271 } catch (Exception e
) {
272 throw new ArgeoJcrException("Cannot decrypt", e
);
274 IOUtils
.closeQuietly(encrypted
);
275 IOUtils
.closeQuietly(reader
);
276 JcrUtils
.closeQuietly(binary
);
277 JcrUtils
.logoutQuietly(session());
281 protected Cipher
createCipher() {
283 Node userHome
= NodeUtils
.getUserHome(session());
284 if (!userHome
.hasNode(ARGEO_KEYRING
))
285 throw new ArgeoJcrException("Keyring not setup");
286 Node keyring
= userHome
.getNode(ARGEO_KEYRING
);
287 String cipherName
= keyring
.getProperty(ARGEO_CIPHER
).getString();
288 Provider securityProvider
= getSecurityProvider();
290 if (securityProvider
== null)// TODO use BC?
291 cipher
= Cipher
.getInstance(cipherName
);
293 cipher
= Cipher
.getInstance(cipherName
, securityProvider
);
295 } catch (Exception e
) {
296 throw new ArgeoJcrException("Cannot get cipher", e
);
300 public synchronized void changePassword(char[] oldPassword
, char[] newPassword
) {
301 // TODO decrypt with old pw / encrypt with new pw all argeo:encrypted
304 // public synchronized void setSession(Session session) {
305 // this.session = session;
308 public void setIterationCountFactor(Integer iterationCountFactor
) {
309 this.iterationCountFactor
= iterationCountFactor
;
312 public void setSecreteKeyLength(Long keyLength
) {
313 this.secreteKeyLength
= keyLength
;
316 public void setSecreteKeyFactoryName(String secreteKeyFactoryName
) {
317 this.secreteKeyFactoryName
= secreteKeyFactoryName
;
320 public void setSecreteKeyEncryption(String secreteKeyEncryption
) {
321 this.secreteKeyEncryption
= secreteKeyEncryption
;
324 public void setCipherName(String cipherName
) {
325 this.cipherName
= cipherName
;