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
.ByteArrayOutputStream
;
20 import java
.io
.CharArrayWriter
;
21 import java
.io
.IOException
;
22 import java
.io
.InputStream
;
23 import java
.io
.InputStreamReader
;
24 import java
.io
.OutputStreamWriter
;
25 import java
.io
.Reader
;
26 import java
.io
.Writer
;
27 import java
.security
.AccessController
;
28 import java
.security
.MessageDigest
;
29 import java
.security
.Provider
;
30 import java
.security
.Security
;
31 import java
.util
.Arrays
;
32 import java
.util
.Iterator
;
34 import javax
.crypto
.SecretKey
;
35 import javax
.security
.auth
.Subject
;
36 import javax
.security
.auth
.callback
.Callback
;
37 import javax
.security
.auth
.callback
.CallbackHandler
;
38 import javax
.security
.auth
.callback
.PasswordCallback
;
39 import javax
.security
.auth
.callback
.TextOutputCallback
;
40 import javax
.security
.auth
.callback
.UnsupportedCallbackException
;
41 import javax
.security
.auth
.login
.LoginContext
;
42 import javax
.security
.auth
.login
.LoginException
;
44 import org
.apache
.commons
.io
.IOUtils
;
45 import org
.argeo
.cms
.CmsException
;
46 import org
.argeo
.node
.security
.CryptoKeyring
;
47 import org
.argeo
.node
.security
.Keyring
;
48 import org
.argeo
.node
.security
.PBEKeySpecCallback
;
50 /** username / password based keyring. TODO internationalize */
51 public abstract class AbstractKeyring
implements Keyring
, CryptoKeyring
{
52 public final static String DEFAULT_KEYRING_LOGIN_CONTEXT
= "KEYRING";
54 private String loginContextName
= DEFAULT_KEYRING_LOGIN_CONTEXT
;
55 private CallbackHandler defaultCallbackHandler
;
57 private String charset
= "UTF-8";
60 * Default provider is bouncy castle, in order to have consistent behaviour
61 * across implementations
63 private String securityProviderName
= "BC";
66 * Whether the keyring has already been created in the past with a master
69 protected abstract Boolean
isSetup();
72 * Setup the keyring persistently, {@link #isSetup()} must return true
75 protected abstract void setup(char[] password
);
77 /** Populates the key spec callback */
78 protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback
);
80 protected abstract void encrypt(String path
, InputStream unencrypted
);
82 protected abstract InputStream
decrypt(String path
);
84 /** Triggers lazy initialization */
85 protected SecretKey
getSecretKey() {
86 Subject subject
= Subject
.getSubject(AccessController
.getContext());
87 // we assume only one secrete key is available
88 Iterator
<SecretKey
> iterator
= subject
.getPrivateCredentials(SecretKey
.class).iterator();
89 if (!iterator
.hasNext()) {// not initialized
90 CallbackHandler callbackHandler
= new KeyringCallbackHandler();
91 ClassLoader currentContextClassLoader
= Thread
.currentThread().getContextClassLoader();
92 Thread
.currentThread().setContextClassLoader(getClass().getClassLoader());
94 LoginContext loginContext
= new LoginContext(loginContextName
, subject
, callbackHandler
);
96 // FIXME will login even if password is wrong
97 iterator
= subject
.getPrivateCredentials(SecretKey
.class).iterator();
98 return iterator
.next();
99 } catch (LoginException e
) {
100 throw new CmsException("Keyring login failed", e
);
102 Thread
.currentThread().setContextClassLoader(currentContextClassLoader
);
106 SecretKey secretKey
= iterator
.next();
107 if (iterator
.hasNext())
108 throw new CmsException("More than one secret key in private credentials");
113 public InputStream
getAsStream(String path
) {
114 return decrypt(path
);
117 public void set(String path
, InputStream in
) {
121 public char[] getAsChars(String path
) {
122 InputStream in
= getAsStream(path
);
123 CharArrayWriter writer
= null;
124 Reader reader
= null;
126 writer
= new CharArrayWriter();
127 reader
= new InputStreamReader(in
, charset
);
128 IOUtils
.copy(reader
, writer
);
129 return writer
.toCharArray();
130 } catch (IOException e
) {
131 throw new CmsException("Cannot decrypt to char array", e
);
133 IOUtils
.closeQuietly(reader
);
134 IOUtils
.closeQuietly(in
);
135 IOUtils
.closeQuietly(writer
);
139 public void set(String path
, char[] arr
) {
140 ByteArrayOutputStream out
= new ByteArrayOutputStream();
141 ByteArrayInputStream in
= null;
142 Writer writer
= null;
144 writer
= new OutputStreamWriter(out
, charset
);
147 in
= new ByteArrayInputStream(out
.toByteArray());
149 } catch (IOException e
) {
150 throw new CmsException("Cannot encrypt to char array", e
);
152 IOUtils
.closeQuietly(writer
);
153 IOUtils
.closeQuietly(out
);
154 IOUtils
.closeQuietly(in
);
158 protected Provider
getSecurityProvider() {
159 return Security
.getProvider(securityProviderName
);
162 public void setLoginContextName(String loginContextName
) {
163 this.loginContextName
= loginContextName
;
166 public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler
) {
167 this.defaultCallbackHandler
= defaultCallbackHandler
;
170 public void setCharset(String charset
) {
171 this.charset
= charset
;
174 public void setSecurityProviderName(String securityProviderName
) {
175 this.securityProviderName
= securityProviderName
;
179 protected static byte[] hash(char[] password
, byte[] salt
, Integer iterationCount
) {
180 ByteArrayOutputStream out
= null;
181 OutputStreamWriter writer
= null;
183 out
= new ByteArrayOutputStream();
184 writer
= new OutputStreamWriter(out
, "UTF-8");
185 writer
.write(password
);
186 MessageDigest pwDigest
= MessageDigest
.getInstance("SHA-256");
188 pwDigest
.update(salt
);
189 byte[] btPass
= pwDigest
.digest(out
.toByteArray());
190 for (int i
= 0; i
< iterationCount
; i
++) {
192 btPass
= pwDigest
.digest(btPass
);
195 } catch (Exception e
) {
196 throw new CmsException("Cannot hash", e
);
198 IOUtils
.closeQuietly(out
);
199 IOUtils
.closeQuietly(writer
);
205 * Convenience method using the underlying callback to ask for a password
206 * (typically used when the password is not saved in the keyring)
208 protected char[] ask() {
209 PasswordCallback passwordCb
= new PasswordCallback("Password", false);
210 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
212 defaultCallbackHandler
.handle(dialogCbs
);
213 char[] password
= passwordCb
.getPassword();
215 } catch (Exception e
) {
216 throw new CmsException("Cannot ask for a password", e
);
221 class KeyringCallbackHandler
implements CallbackHandler
{
222 public void handle(Callback
[] callbacks
) throws IOException
, UnsupportedCallbackException
{
224 if (callbacks
.length
!= 2)
225 throw new IllegalArgumentException(
226 "Keyring required 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
227 if (!(callbacks
[0] instanceof PasswordCallback
))
228 throw new UnsupportedCallbackException(callbacks
[0]);
229 if (!(callbacks
[1] instanceof PBEKeySpecCallback
))
230 throw new UnsupportedCallbackException(callbacks
[0]);
232 PasswordCallback passwordCb
= (PasswordCallback
) callbacks
[0];
233 PBEKeySpecCallback pbeCb
= (PBEKeySpecCallback
) callbacks
[1];
236 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
237 defaultCallbackHandler
.handle(dialogCbs
);
238 } else {// setup keyring
239 TextOutputCallback textCb1
= new TextOutputCallback(TextOutputCallback
.INFORMATION
,
240 "Enter a master password which will protect your private data");
241 TextOutputCallback textCb2
= new TextOutputCallback(TextOutputCallback
.INFORMATION
,
242 "(for example your credentials to third-party services)");
243 TextOutputCallback textCb3
= new TextOutputCallback(TextOutputCallback
.INFORMATION
,
244 "Don't forget this password since the data cannot be read without it");
245 PasswordCallback confirmPasswordCb
= new PasswordCallback("Confirm password", false);
247 Callback
[] dialogCbs
= new Callback
[] { textCb1
, textCb2
, textCb3
, passwordCb
, confirmPasswordCb
};
248 defaultCallbackHandler
.handle(dialogCbs
);
250 // if passwords different, retry (except if cancelled)
251 while (passwordCb
.getPassword() != null
252 && !Arrays
.equals(passwordCb
.getPassword(), confirmPasswordCb
.getPassword())) {
253 TextOutputCallback textCb
= new TextOutputCallback(TextOutputCallback
.ERROR
,
254 "The passwords do not match");
255 dialogCbs
= new Callback
[] { textCb
, passwordCb
, confirmPasswordCb
};
256 defaultCallbackHandler
.handle(dialogCbs
);
259 if (passwordCb
.getPassword() != null) {// not cancelled
260 setup(passwordCb
.getPassword());
264 if (passwordCb
.getPassword() != null)
265 handleKeySpecCallback(pbeCb
);