2 * Copyright (C) 2007-2012 Mathieu Baudier
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
.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
.util
.Arrays
;
30 import java
.util
.Iterator
;
32 import javax
.crypto
.SecretKey
;
33 import javax
.security
.auth
.Subject
;
34 import javax
.security
.auth
.callback
.Callback
;
35 import javax
.security
.auth
.callback
.CallbackHandler
;
36 import javax
.security
.auth
.callback
.PasswordCallback
;
37 import javax
.security
.auth
.callback
.TextOutputCallback
;
38 import javax
.security
.auth
.callback
.UnsupportedCallbackException
;
39 import javax
.security
.auth
.login
.LoginContext
;
40 import javax
.security
.auth
.login
.LoginException
;
42 import org
.argeo
.ArgeoException
;
43 import org
.argeo
.StreamUtils
;
45 /** username / password based keyring. TODO internationalize */
46 public abstract class AbstractKeyring
implements Keyring
{
47 public final static String DEFAULT_KEYRING_LOGIN_CONTEXT
= "KEYRING";
49 private String loginContextName
= DEFAULT_KEYRING_LOGIN_CONTEXT
;
50 private CallbackHandler defaultCallbackHandler
;
52 private String charset
= "UTF-8";
55 * Whether the keyring has already been created in the past with a master
58 protected abstract Boolean
isSetup();
61 * Setup the keyring persistently, {@link #isSetup()} must return true
64 protected abstract void setup(char[] password
);
66 /** Populates the key spec callback */
67 protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback
);
69 protected abstract void encrypt(String path
, InputStream unencrypted
);
71 protected abstract InputStream
decrypt(String path
);
73 /** Triggers lazy initialization */
74 protected SecretKey
getSecretKey() {
75 Subject subject
= Subject
.getSubject(AccessController
.getContext());
76 // we assume only one secrete key is available
77 Iterator
<SecretKey
> iterator
= subject
.getPrivateCredentials(
78 SecretKey
.class).iterator();
79 if (!iterator
.hasNext()) {// not initialized
80 CallbackHandler callbackHandler
= new KeyringCallbackHandler();
82 LoginContext loginContext
= new LoginContext(loginContextName
,
83 subject
, callbackHandler
);
85 // FIXME will login even if password is wrong
86 iterator
= subject
.getPrivateCredentials(SecretKey
.class)
88 return iterator
.next();
89 } catch (LoginException e
) {
90 throw new ArgeoException("Keyring login failed", e
);
94 SecretKey secretKey
= iterator
.next();
95 if (iterator
.hasNext())
96 throw new ArgeoException(
97 "More than one secret key in private credentials");
102 public InputStream
getAsStream(String path
) {
103 return decrypt(path
);
106 public void set(String path
, InputStream in
) {
110 public char[] getAsChars(String path
) {
111 InputStream in
= getAsStream(path
);
112 CharArrayWriter writer
= null;
113 Reader reader
= null;
115 writer
= new CharArrayWriter();
116 reader
= new InputStreamReader(in
, charset
);
117 StreamUtils
.copy(reader
, writer
);
118 return writer
.toCharArray();
119 } catch (IOException e
) {
120 throw new ArgeoException("Cannot decrypt to char array", e
);
122 StreamUtils
.closeQuietly(reader
);
123 StreamUtils
.closeQuietly(in
);
124 StreamUtils
.closeQuietly(writer
);
128 public void set(String path
, char[] arr
) {
129 ByteArrayOutputStream out
= new ByteArrayOutputStream();
130 ByteArrayInputStream in
= null;
131 Writer writer
= null;
133 writer
= new OutputStreamWriter(out
, charset
);
136 in
= new ByteArrayInputStream(out
.toByteArray());
138 } catch (IOException e
) {
139 throw new ArgeoException("Cannot encrypt to char array", e
);
141 StreamUtils
.closeQuietly(writer
);
142 StreamUtils
.closeQuietly(out
);
143 StreamUtils
.closeQuietly(in
);
147 public void setLoginContextName(String loginContextName
) {
148 this.loginContextName
= loginContextName
;
151 public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler
) {
152 this.defaultCallbackHandler
= defaultCallbackHandler
;
155 public void setCharset(String charset
) {
156 this.charset
= charset
;
159 protected static byte[] hash(char[] password
, byte[] salt
,
160 Integer iterationCount
) {
161 ByteArrayOutputStream out
= null;
162 OutputStreamWriter writer
= null;
164 out
= new ByteArrayOutputStream();
165 writer
= new OutputStreamWriter(out
, "UTF-8");
166 writer
.write(password
);
167 MessageDigest pwDigest
= MessageDigest
.getInstance("SHA-256");
169 pwDigest
.update(salt
);
170 byte[] btPass
= pwDigest
.digest(out
.toByteArray());
171 for (int i
= 0; i
< iterationCount
; i
++) {
173 btPass
= pwDigest
.digest(btPass
);
176 } catch (Exception e
) {
177 throw new ArgeoException("Cannot hash", e
);
179 StreamUtils
.closeQuietly(out
);
180 StreamUtils
.closeQuietly(writer
);
185 class KeyringCallbackHandler
implements CallbackHandler
{
186 public void handle(Callback
[] callbacks
) throws IOException
,
187 UnsupportedCallbackException
{
189 if (callbacks
.length
!= 2)
190 throw new IllegalArgumentException(
191 "Keyring required 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
192 if (!(callbacks
[0] instanceof PasswordCallback
))
193 throw new UnsupportedCallbackException(callbacks
[0]);
194 if (!(callbacks
[1] instanceof PBEKeySpecCallback
))
195 throw new UnsupportedCallbackException(callbacks
[0]);
197 PasswordCallback passwordCb
= (PasswordCallback
) callbacks
[0];
198 PBEKeySpecCallback pbeCb
= (PBEKeySpecCallback
) callbacks
[1];
201 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
202 defaultCallbackHandler
.handle(dialogCbs
);
203 } else {// setup keyring
204 TextOutputCallback textCb1
= new TextOutputCallback(
205 TextOutputCallback
.INFORMATION
,
206 "Enter a master password which will protect your private data");
207 TextOutputCallback textCb2
= new TextOutputCallback(
208 TextOutputCallback
.INFORMATION
,
209 "(for example your credentials to third-party services)");
210 TextOutputCallback textCb3
= new TextOutputCallback(
211 TextOutputCallback
.INFORMATION
,
212 "Don't forget this password since the data cannot be read without it");
213 PasswordCallback confirmPasswordCb
= new PasswordCallback(
214 "Confirm password", false);
216 Callback
[] dialogCbs
= new Callback
[] { textCb1
, textCb2
,
217 textCb3
, passwordCb
, confirmPasswordCb
};
218 defaultCallbackHandler
.handle(dialogCbs
);
220 // if passwords different, retry (except if cancelled)
221 while (passwordCb
.getPassword() != null
222 && !Arrays
.equals(passwordCb
.getPassword(),
223 confirmPasswordCb
.getPassword())) {
224 TextOutputCallback textCb
= new TextOutputCallback(
225 TextOutputCallback
.ERROR
,
226 "The passwords do not match");
227 dialogCbs
= new Callback
[] { textCb
, passwordCb
,
229 defaultCallbackHandler
.handle(dialogCbs
);
232 if (passwordCb
.getPassword() != null) {// not cancelled
233 setup(passwordCb
.getPassword());
237 if (passwordCb
.getPassword() != null)
238 handleKeySpecCallback(pbeCb
);