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
.util
.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
.argeo
.util
.internal
.UtilsException
;
45 import org
.argeo
.util
.internal
.StreamUtils
;
47 /** username / password based keyring. TODO internationalize */
48 public abstract class AbstractKeyring
implements Keyring
, CryptoKeyring
{
49 public final static String DEFAULT_KEYRING_LOGIN_CONTEXT
= "KEYRING";
51 private String loginContextName
= DEFAULT_KEYRING_LOGIN_CONTEXT
;
52 private CallbackHandler defaultCallbackHandler
;
54 private String charset
= "UTF-8";
57 * Default provider is bouncy castle, in order to have consistent behaviour
58 * across implementations
60 private String securityProviderName
= "BC";
63 * Whether the keyring has already been created in the past with a master
66 protected abstract Boolean
isSetup();
69 * Setup the keyring persistently, {@link #isSetup()} must return true
72 protected abstract void setup(char[] password
);
74 /** Populates the key spec callback */
75 protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback
);
77 protected abstract void encrypt(String path
, InputStream unencrypted
);
79 protected abstract InputStream
decrypt(String path
);
81 /** Triggers lazy initialization */
82 protected SecretKey
getSecretKey() {
83 Subject subject
= Subject
.getSubject(AccessController
.getContext());
84 // we assume only one secrete key is available
85 Iterator
<SecretKey
> iterator
= subject
.getPrivateCredentials(
86 SecretKey
.class).iterator();
87 if (!iterator
.hasNext()) {// not initialized
88 CallbackHandler callbackHandler
= new KeyringCallbackHandler();
90 LoginContext loginContext
= new LoginContext(loginContextName
,
91 subject
, callbackHandler
);
93 // FIXME will login even if password is wrong
94 iterator
= subject
.getPrivateCredentials(SecretKey
.class)
96 return iterator
.next();
97 } catch (LoginException e
) {
98 throw new UtilsException("Keyring login failed", e
);
102 SecretKey secretKey
= iterator
.next();
103 if (iterator
.hasNext())
104 throw new UtilsException(
105 "More than one secret key in private credentials");
110 public InputStream
getAsStream(String path
) {
111 return decrypt(path
);
114 public void set(String path
, InputStream in
) {
118 public char[] getAsChars(String path
) {
119 InputStream in
= getAsStream(path
);
120 CharArrayWriter writer
= null;
121 Reader reader
= null;
123 writer
= new CharArrayWriter();
124 reader
= new InputStreamReader(in
, charset
);
125 StreamUtils
.copy(reader
, writer
);
126 return writer
.toCharArray();
127 } catch (IOException e
) {
128 throw new UtilsException("Cannot decrypt to char array", e
);
130 StreamUtils
.closeQuietly(reader
);
131 StreamUtils
.closeQuietly(in
);
132 StreamUtils
.closeQuietly(writer
);
136 public void set(String path
, char[] arr
) {
137 ByteArrayOutputStream out
= new ByteArrayOutputStream();
138 ByteArrayInputStream in
= null;
139 Writer writer
= null;
141 writer
= new OutputStreamWriter(out
, charset
);
144 in
= new ByteArrayInputStream(out
.toByteArray());
146 } catch (IOException e
) {
147 throw new UtilsException("Cannot encrypt to char array", e
);
149 StreamUtils
.closeQuietly(writer
);
150 StreamUtils
.closeQuietly(out
);
151 StreamUtils
.closeQuietly(in
);
155 protected Provider
getSecurityProvider() {
156 return Security
.getProvider(securityProviderName
);
159 public void setLoginContextName(String loginContextName
) {
160 this.loginContextName
= loginContextName
;
163 public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler
) {
164 this.defaultCallbackHandler
= defaultCallbackHandler
;
167 public void setCharset(String charset
) {
168 this.charset
= charset
;
171 public void setSecurityProviderName(String securityProviderName
) {
172 this.securityProviderName
= securityProviderName
;
176 protected static byte[] hash(char[] password
, byte[] salt
,
177 Integer iterationCount
) {
178 ByteArrayOutputStream out
= null;
179 OutputStreamWriter writer
= null;
181 out
= new ByteArrayOutputStream();
182 writer
= new OutputStreamWriter(out
, "UTF-8");
183 writer
.write(password
);
184 MessageDigest pwDigest
= MessageDigest
.getInstance("SHA-256");
186 pwDigest
.update(salt
);
187 byte[] btPass
= pwDigest
.digest(out
.toByteArray());
188 for (int i
= 0; i
< iterationCount
; i
++) {
190 btPass
= pwDigest
.digest(btPass
);
193 } catch (Exception e
) {
194 throw new UtilsException("Cannot hash", e
);
196 StreamUtils
.closeQuietly(out
);
197 StreamUtils
.closeQuietly(writer
);
203 * Convenience method using the underlying callback to ask for a password
204 * (typically used when the password is not saved in the keyring)
206 protected char[] ask() {
207 PasswordCallback passwordCb
= new PasswordCallback("Password", false);
208 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
210 defaultCallbackHandler
.handle(dialogCbs
);
211 char[] password
= passwordCb
.getPassword();
213 } catch (Exception e
) {
214 throw new UtilsException("Cannot ask for a password", e
);
219 class KeyringCallbackHandler
implements CallbackHandler
{
220 public void handle(Callback
[] callbacks
) throws IOException
,
221 UnsupportedCallbackException
{
223 if (callbacks
.length
!= 2)
224 throw new IllegalArgumentException(
225 "Keyring required 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
226 if (!(callbacks
[0] instanceof PasswordCallback
))
227 throw new UnsupportedCallbackException(callbacks
[0]);
228 if (!(callbacks
[1] instanceof PBEKeySpecCallback
))
229 throw new UnsupportedCallbackException(callbacks
[0]);
231 PasswordCallback passwordCb
= (PasswordCallback
) callbacks
[0];
232 PBEKeySpecCallback pbeCb
= (PBEKeySpecCallback
) callbacks
[1];
235 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
236 defaultCallbackHandler
.handle(dialogCbs
);
237 } else {// setup keyring
238 TextOutputCallback textCb1
= new TextOutputCallback(
239 TextOutputCallback
.INFORMATION
,
240 "Enter a master password which will protect your private data");
241 TextOutputCallback textCb2
= new TextOutputCallback(
242 TextOutputCallback
.INFORMATION
,
243 "(for example your credentials to third-party services)");
244 TextOutputCallback textCb3
= new TextOutputCallback(
245 TextOutputCallback
.INFORMATION
,
246 "Don't forget this password since the data cannot be read without it");
247 PasswordCallback confirmPasswordCb
= new PasswordCallback(
248 "Confirm password", false);
250 Callback
[] dialogCbs
= new Callback
[] { textCb1
, textCb2
,
251 textCb3
, passwordCb
, confirmPasswordCb
};
252 defaultCallbackHandler
.handle(dialogCbs
);
254 // if passwords different, retry (except if cancelled)
255 while (passwordCb
.getPassword() != null
256 && !Arrays
.equals(passwordCb
.getPassword(),
257 confirmPasswordCb
.getPassword())) {
258 TextOutputCallback textCb
= new TextOutputCallback(
259 TextOutputCallback
.ERROR
,
260 "The passwords do not match");
261 dialogCbs
= new Callback
[] { textCb
, passwordCb
,
263 defaultCallbackHandler
.handle(dialogCbs
);
266 if (passwordCb
.getPassword() != null) {// not cancelled
267 setup(passwordCb
.getPassword());
271 if (passwordCb
.getPassword() != null)
272 handleKeySpecCallback(pbeCb
);