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
.Provider
;
29 import java
.security
.Security
;
30 import java
.util
.Arrays
;
31 import java
.util
.Iterator
;
33 import javax
.crypto
.SecretKey
;
34 import javax
.security
.auth
.Subject
;
35 import javax
.security
.auth
.callback
.Callback
;
36 import javax
.security
.auth
.callback
.CallbackHandler
;
37 import javax
.security
.auth
.callback
.PasswordCallback
;
38 import javax
.security
.auth
.callback
.TextOutputCallback
;
39 import javax
.security
.auth
.callback
.UnsupportedCallbackException
;
40 import javax
.security
.auth
.login
.LoginContext
;
41 import javax
.security
.auth
.login
.LoginException
;
43 import org
.apache
.commons
.io
.IOUtils
;
44 import org
.argeo
.cms
.CmsException
;
45 import org
.argeo
.node
.NodeConstants
;
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(char[] password
) {
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() || password
!=null) {// not initialized
90 CallbackHandler callbackHandler
= password
== null ?
new KeyringCallbackHandler()
91 : new PasswordProvidedCallBackHandler(password
);
92 ClassLoader currentContextClassLoader
= Thread
.currentThread().getContextClassLoader();
93 Thread
.currentThread().setContextClassLoader(getClass().getClassLoader());
95 LoginContext loginContext
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_KEYRING
, subject
,
98 // FIXME will login even if password is wrong
99 iterator
= subject
.getPrivateCredentials(SecretKey
.class).iterator();
100 return iterator
.next();
101 } catch (LoginException e
) {
102 throw new CmsException("Keyring login failed", e
);
104 Thread
.currentThread().setContextClassLoader(currentContextClassLoader
);
108 SecretKey secretKey
= iterator
.next();
109 if (iterator
.hasNext())
110 throw new CmsException("More than one secret key in private credentials");
115 public InputStream
getAsStream(String path
) {
116 return decrypt(path
);
119 public void set(String path
, InputStream in
) {
123 public char[] getAsChars(String path
) {
124 // InputStream in = getAsStream(path);
125 // CharArrayWriter writer = null;
126 // Reader reader = null;
127 try (InputStream in
= getAsStream(path
);
128 CharArrayWriter writer
= new CharArrayWriter();
129 Reader reader
= new InputStreamReader(in
, charset
);) {
130 IOUtils
.copy(reader
, writer
);
131 return writer
.toCharArray();
132 } catch (IOException e
) {
133 throw new CmsException("Cannot decrypt to char array", e
);
135 // IOUtils.closeQuietly(reader);
136 // IOUtils.closeQuietly(in);
137 // IOUtils.closeQuietly(writer);
141 public void set(String path
, char[] arr
) {
142 // ByteArrayOutputStream out = new ByteArrayOutputStream();
143 // ByteArrayInputStream in = null;
144 // Writer writer = null;
145 try (ByteArrayOutputStream out
= new ByteArrayOutputStream();
146 Writer writer
= new OutputStreamWriter(out
, charset
);) {
147 // writer = new OutputStreamWriter(out, charset);
150 // in = new ByteArrayInputStream(out.toByteArray());
151 try (ByteArrayInputStream in
= new ByteArrayInputStream(out
.toByteArray());) {
154 } catch (IOException e
) {
155 throw new CmsException("Cannot encrypt to char array", e
);
157 // IOUtils.closeQuietly(writer);
158 // IOUtils.closeQuietly(out);
159 // IOUtils.closeQuietly(in);
163 public void unlock(char[] password
) {
166 SecretKey secretKey
= getSecretKey(password
);
167 if (secretKey
== null)
168 throw new CmsException("Could not unlock keyring");
171 protected Provider
getSecurityProvider() {
172 return Security
.getProvider(securityProviderName
);
175 public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler
) {
176 this.defaultCallbackHandler
= defaultCallbackHandler
;
179 public void setCharset(String charset
) {
180 this.charset
= charset
;
183 public void setSecurityProviderName(String securityProviderName
) {
184 this.securityProviderName
= securityProviderName
;
188 // protected static byte[] hash(char[] password, byte[] salt, Integer
190 // ByteArrayOutputStream out = null;
191 // OutputStreamWriter writer = null;
193 // out = new ByteArrayOutputStream();
194 // writer = new OutputStreamWriter(out, "UTF-8");
195 // writer.write(password);
196 // MessageDigest pwDigest = MessageDigest.getInstance("SHA-256");
198 // pwDigest.update(salt);
199 // byte[] btPass = pwDigest.digest(out.toByteArray());
200 // for (int i = 0; i < iterationCount; i++) {
202 // btPass = pwDigest.digest(btPass);
205 // } catch (Exception e) {
206 // throw new CmsException("Cannot hash", e);
208 // IOUtils.closeQuietly(out);
209 // IOUtils.closeQuietly(writer);
215 * Convenience method using the underlying callback to ask for a password
216 * (typically used when the password is not saved in the keyring)
218 protected char[] ask() {
219 PasswordCallback passwordCb
= new PasswordCallback("Password", false);
220 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
222 defaultCallbackHandler
.handle(dialogCbs
);
223 char[] password
= passwordCb
.getPassword();
225 } catch (Exception e
) {
226 throw new CmsException("Cannot ask for a password", e
);
231 class KeyringCallbackHandler
implements CallbackHandler
{
232 public void handle(Callback
[] callbacks
) throws IOException
, UnsupportedCallbackException
{
234 if (callbacks
.length
!= 2)
235 throw new IllegalArgumentException(
236 "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
237 if (!(callbacks
[0] instanceof PasswordCallback
))
238 throw new UnsupportedCallbackException(callbacks
[0]);
239 if (!(callbacks
[1] instanceof PBEKeySpecCallback
))
240 throw new UnsupportedCallbackException(callbacks
[0]);
242 PasswordCallback passwordCb
= (PasswordCallback
) callbacks
[0];
243 PBEKeySpecCallback pbeCb
= (PBEKeySpecCallback
) callbacks
[1];
246 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
247 defaultCallbackHandler
.handle(dialogCbs
);
248 } else {// setup keyring
249 TextOutputCallback textCb1
= new TextOutputCallback(TextOutputCallback
.INFORMATION
,
250 "Enter a master password which will protect your private data");
251 TextOutputCallback textCb2
= new TextOutputCallback(TextOutputCallback
.INFORMATION
,
252 "(for example your credentials to third-party services)");
253 TextOutputCallback textCb3
= new TextOutputCallback(TextOutputCallback
.INFORMATION
,
254 "Don't forget this password since the data cannot be read without it");
255 PasswordCallback confirmPasswordCb
= new PasswordCallback("Confirm password", false);
257 Callback
[] dialogCbs
= new Callback
[] { textCb1
, textCb2
, textCb3
, passwordCb
, confirmPasswordCb
};
258 defaultCallbackHandler
.handle(dialogCbs
);
260 // if passwords different, retry (except if cancelled)
261 while (passwordCb
.getPassword() != null
262 && !Arrays
.equals(passwordCb
.getPassword(), confirmPasswordCb
.getPassword())) {
263 TextOutputCallback textCb
= new TextOutputCallback(TextOutputCallback
.ERROR
,
264 "The passwords do not match");
265 dialogCbs
= new Callback
[] { textCb
, passwordCb
, confirmPasswordCb
};
266 defaultCallbackHandler
.handle(dialogCbs
);
269 if (passwordCb
.getPassword() != null) {// not cancelled
270 setup(passwordCb
.getPassword());
274 if (passwordCb
.getPassword() != null)
275 handleKeySpecCallback(pbeCb
);
280 class PasswordProvidedCallBackHandler
implements CallbackHandler
{
281 private final char[] password
;
283 public PasswordProvidedCallBackHandler(char[] password
) {
284 this.password
= password
;
288 public void handle(Callback
[] callbacks
) throws IOException
, UnsupportedCallbackException
{
290 if (callbacks
.length
!= 2)
291 throw new IllegalArgumentException(
292 "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
293 if (!(callbacks
[0] instanceof PasswordCallback
))
294 throw new UnsupportedCallbackException(callbacks
[0]);
295 if (!(callbacks
[1] instanceof PBEKeySpecCallback
))
296 throw new UnsupportedCallbackException(callbacks
[0]);
298 PasswordCallback passwordCb
= (PasswordCallback
) callbacks
[0];
299 passwordCb
.setPassword(password
);
300 PBEKeySpecCallback pbeCb
= (PBEKeySpecCallback
) callbacks
[1];
301 handleKeySpecCallback(pbeCb
);