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
.security
.crypto
;
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
.ArgeoException
;
45 import org
.argeo
.StreamUtils
;
46 import org
.argeo
.util
.security
.Keyring
;
47 import org
.bouncycastle
.jce
.provider
.BouncyCastleProvider
;
49 /** username / password based keyring. TODO internationalize */
50 public abstract class AbstractKeyring
implements Keyring
, CryptoKeyring
{
52 Security
.addProvider(new BouncyCastleProvider());
55 public final static String DEFAULT_KEYRING_LOGIN_CONTEXT
= "KEYRING";
57 private String loginContextName
= DEFAULT_KEYRING_LOGIN_CONTEXT
;
58 private CallbackHandler defaultCallbackHandler
;
60 private String charset
= "UTF-8";
63 * Default provider is bouncy castle, in order to have consistent behaviour
64 * across implementations
66 private String securityProviderName
= "BC";
69 * Whether the keyring has already been created in the past with a master
72 protected abstract Boolean
isSetup();
75 * Setup the keyring persistently, {@link #isSetup()} must return true
78 protected abstract void setup(char[] password
);
80 /** Populates the key spec callback */
81 protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback
);
83 protected abstract void encrypt(String path
, InputStream unencrypted
);
85 protected abstract InputStream
decrypt(String path
);
87 /** Triggers lazy initialization */
88 protected SecretKey
getSecretKey() {
89 Subject subject
= Subject
.getSubject(AccessController
.getContext());
90 // we assume only one secrete key is available
91 Iterator
<SecretKey
> iterator
= subject
.getPrivateCredentials(
92 SecretKey
.class).iterator();
93 if (!iterator
.hasNext()) {// not initialized
94 CallbackHandler callbackHandler
= new KeyringCallbackHandler();
96 LoginContext loginContext
= new LoginContext(loginContextName
,
97 subject
, callbackHandler
);
99 // FIXME will login even if password is wrong
100 iterator
= subject
.getPrivateCredentials(SecretKey
.class)
102 return iterator
.next();
103 } catch (LoginException e
) {
104 throw new ArgeoException("Keyring login failed", e
);
108 SecretKey secretKey
= iterator
.next();
109 if (iterator
.hasNext())
110 throw new ArgeoException(
111 "More than one secret key in private credentials");
116 public InputStream
getAsStream(String path
) {
117 return decrypt(path
);
120 public void set(String path
, InputStream in
) {
124 public char[] getAsChars(String path
) {
125 InputStream in
= getAsStream(path
);
126 CharArrayWriter writer
= null;
127 Reader reader
= null;
129 writer
= new CharArrayWriter();
130 reader
= new InputStreamReader(in
, charset
);
131 StreamUtils
.copy(reader
, writer
);
132 return writer
.toCharArray();
133 } catch (IOException e
) {
134 throw new ArgeoException("Cannot decrypt to char array", e
);
136 StreamUtils
.closeQuietly(reader
);
137 StreamUtils
.closeQuietly(in
);
138 StreamUtils
.closeQuietly(writer
);
142 public void set(String path
, char[] arr
) {
143 ByteArrayOutputStream out
= new ByteArrayOutputStream();
144 ByteArrayInputStream in
= null;
145 Writer writer
= null;
147 writer
= new OutputStreamWriter(out
, charset
);
150 in
= new ByteArrayInputStream(out
.toByteArray());
152 } catch (IOException e
) {
153 throw new ArgeoException("Cannot encrypt to char array", e
);
155 StreamUtils
.closeQuietly(writer
);
156 StreamUtils
.closeQuietly(out
);
157 StreamUtils
.closeQuietly(in
);
161 protected Provider
getSecurityProvider() {
162 return Security
.getProvider(securityProviderName
);
165 public void setLoginContextName(String loginContextName
) {
166 this.loginContextName
= loginContextName
;
169 public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler
) {
170 this.defaultCallbackHandler
= defaultCallbackHandler
;
173 public void setCharset(String charset
) {
174 this.charset
= charset
;
177 public void setSecurityProviderName(String securityProviderName
) {
178 this.securityProviderName
= securityProviderName
;
182 protected static byte[] hash(char[] password
, byte[] salt
,
183 Integer iterationCount
) {
184 ByteArrayOutputStream out
= null;
185 OutputStreamWriter writer
= null;
187 out
= new ByteArrayOutputStream();
188 writer
= new OutputStreamWriter(out
, "UTF-8");
189 writer
.write(password
);
190 MessageDigest pwDigest
= MessageDigest
.getInstance("SHA-256");
192 pwDigest
.update(salt
);
193 byte[] btPass
= pwDigest
.digest(out
.toByteArray());
194 for (int i
= 0; i
< iterationCount
; i
++) {
196 btPass
= pwDigest
.digest(btPass
);
199 } catch (Exception e
) {
200 throw new ArgeoException("Cannot hash", e
);
202 StreamUtils
.closeQuietly(out
);
203 StreamUtils
.closeQuietly(writer
);
209 * Convenience method using the underlying callback to ask for a password
210 * (typically used when the password is not saved in the keyring)
212 protected char[] ask() {
213 PasswordCallback passwordCb
= new PasswordCallback("Password", false);
214 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
216 defaultCallbackHandler
.handle(dialogCbs
);
217 char[] password
= passwordCb
.getPassword();
219 } catch (Exception e
) {
220 throw new ArgeoException("Cannot ask for a password", e
);
225 class KeyringCallbackHandler
implements CallbackHandler
{
226 public void handle(Callback
[] callbacks
) throws IOException
,
227 UnsupportedCallbackException
{
229 if (callbacks
.length
!= 2)
230 throw new IllegalArgumentException(
231 "Keyring required 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
232 if (!(callbacks
[0] instanceof PasswordCallback
))
233 throw new UnsupportedCallbackException(callbacks
[0]);
234 if (!(callbacks
[1] instanceof PBEKeySpecCallback
))
235 throw new UnsupportedCallbackException(callbacks
[0]);
237 PasswordCallback passwordCb
= (PasswordCallback
) callbacks
[0];
238 PBEKeySpecCallback pbeCb
= (PBEKeySpecCallback
) callbacks
[1];
241 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
242 defaultCallbackHandler
.handle(dialogCbs
);
243 } else {// setup keyring
244 TextOutputCallback textCb1
= new TextOutputCallback(
245 TextOutputCallback
.INFORMATION
,
246 "Enter a master password which will protect your private data");
247 TextOutputCallback textCb2
= new TextOutputCallback(
248 TextOutputCallback
.INFORMATION
,
249 "(for example your credentials to third-party services)");
250 TextOutputCallback textCb3
= new TextOutputCallback(
251 TextOutputCallback
.INFORMATION
,
252 "Don't forget this password since the data cannot be read without it");
253 PasswordCallback confirmPasswordCb
= new PasswordCallback(
254 "Confirm password", false);
256 Callback
[] dialogCbs
= new Callback
[] { textCb1
, textCb2
,
257 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(),
263 confirmPasswordCb
.getPassword())) {
264 TextOutputCallback textCb
= new TextOutputCallback(
265 TextOutputCallback
.ERROR
,
266 "The passwords do not match");
267 dialogCbs
= new Callback
[] { textCb
, passwordCb
,
269 defaultCallbackHandler
.handle(dialogCbs
);
272 if (passwordCb
.getPassword() != null) {// not cancelled
273 setup(passwordCb
.getPassword());
277 if (passwordCb
.getPassword() != null)
278 handleKeySpecCallback(pbeCb
);