1 package org
.argeo
.util
.crypto
;
3 import java
.io
.ByteArrayInputStream
;
4 import java
.io
.ByteArrayOutputStream
;
5 import java
.io
.CharArrayWriter
;
6 import java
.io
.IOException
;
7 import java
.io
.InputStream
;
8 import java
.io
.InputStreamReader
;
9 import java
.io
.OutputStreamWriter
;
10 import java
.io
.Reader
;
11 import java
.io
.Writer
;
12 import java
.security
.AccessController
;
13 import java
.security
.MessageDigest
;
14 import java
.util
.Arrays
;
15 import java
.util
.Iterator
;
17 import javax
.crypto
.SecretKey
;
18 import javax
.security
.auth
.Subject
;
19 import javax
.security
.auth
.callback
.Callback
;
20 import javax
.security
.auth
.callback
.CallbackHandler
;
21 import javax
.security
.auth
.callback
.PasswordCallback
;
22 import javax
.security
.auth
.callback
.TextOutputCallback
;
23 import javax
.security
.auth
.callback
.UnsupportedCallbackException
;
24 import javax
.security
.auth
.login
.LoginContext
;
25 import javax
.security
.auth
.login
.LoginException
;
27 import org
.argeo
.ArgeoException
;
28 import org
.argeo
.StreamUtils
;
30 /** username / password based keyring. TODO internationalize */
31 public abstract class AbstractKeyring
implements Keyring
{
32 public final static String DEFAULT_KEYRING_LOGIN_CONTEXT
= "KEYRING";
34 private String loginContextName
= DEFAULT_KEYRING_LOGIN_CONTEXT
;
35 private CallbackHandler defaultCallbackHandler
;
37 private String charset
= "UTF-8";
40 * Whether the keyring has already been created in the past with a master
43 protected abstract Boolean
isSetup();
46 * Setup the keyring persistently, {@link #isSetup()} must return true
49 protected abstract void setup(char[] password
);
51 /** Populates the key spec callback */
52 protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback
);
54 protected abstract void encrypt(String path
, InputStream unencrypted
);
56 protected abstract InputStream
decrypt(String path
);
58 /** Triggers lazy initialization */
59 protected SecretKey
getSecretKey() {
60 Subject subject
= Subject
.getSubject(AccessController
.getContext());
61 // we assume only one secrete key is available
62 Iterator
<SecretKey
> iterator
= subject
.getPrivateCredentials(
63 SecretKey
.class).iterator();
64 if (!iterator
.hasNext()) {// not initialized
65 CallbackHandler callbackHandler
= new KeyringCallbackHandler();
67 LoginContext loginContext
= new LoginContext(loginContextName
,
68 subject
, callbackHandler
);
70 // FIXME will login even if password is wrong
71 iterator
= subject
.getPrivateCredentials(SecretKey
.class)
73 return iterator
.next();
74 } catch (LoginException e
) {
75 throw new ArgeoException("Keyring login failed", e
);
79 SecretKey secretKey
= iterator
.next();
80 if (iterator
.hasNext())
81 throw new ArgeoException(
82 "More than one secret key in private credentials");
87 public InputStream
getAsStream(String path
) {
91 public void set(String path
, InputStream in
) {
95 public char[] getAsChars(String path
) {
96 InputStream in
= getAsStream(path
);
97 CharArrayWriter writer
= null;
100 writer
= new CharArrayWriter();
101 reader
= new InputStreamReader(in
, charset
);
102 StreamUtils
.copy(reader
, writer
);
103 return writer
.toCharArray();
104 } catch (IOException e
) {
105 throw new ArgeoException("Cannot decrypt to char array", e
);
107 StreamUtils
.closeQuietly(reader
);
108 StreamUtils
.closeQuietly(in
);
109 StreamUtils
.closeQuietly(writer
);
113 public void set(String path
, char[] arr
) {
114 ByteArrayOutputStream out
= new ByteArrayOutputStream();
115 ByteArrayInputStream in
= null;
116 Writer writer
= null;
118 writer
= new OutputStreamWriter(out
, charset
);
121 in
= new ByteArrayInputStream(out
.toByteArray());
123 } catch (IOException e
) {
124 throw new ArgeoException("Cannot encrypt to char array", e
);
126 StreamUtils
.closeQuietly(writer
);
127 StreamUtils
.closeQuietly(out
);
128 StreamUtils
.closeQuietly(in
);
132 public void setLoginContextName(String loginContextName
) {
133 this.loginContextName
= loginContextName
;
136 public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler
) {
137 this.defaultCallbackHandler
= defaultCallbackHandler
;
140 public void setCharset(String charset
) {
141 this.charset
= charset
;
144 protected static byte[] hash(char[] password
, byte[] salt
,
145 Integer iterationCount
) {
146 ByteArrayOutputStream out
= null;
147 OutputStreamWriter writer
= null;
149 out
= new ByteArrayOutputStream();
150 writer
= new OutputStreamWriter(out
, "UTF-8");
151 writer
.write(password
);
152 MessageDigest pwDigest
= MessageDigest
.getInstance("SHA-256");
154 pwDigest
.update(salt
);
155 byte[] btPass
= pwDigest
.digest(out
.toByteArray());
156 for (int i
= 0; i
< iterationCount
; i
++) {
158 btPass
= pwDigest
.digest(btPass
);
161 } catch (Exception e
) {
162 throw new ArgeoException("Cannot hash", e
);
164 StreamUtils
.closeQuietly(out
);
165 StreamUtils
.closeQuietly(writer
);
170 class KeyringCallbackHandler
implements CallbackHandler
{
171 public void handle(Callback
[] callbacks
) throws IOException
,
172 UnsupportedCallbackException
{
174 if (callbacks
.length
!= 2)
175 throw new IllegalArgumentException(
176 "Keyring required 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
177 if (!(callbacks
[0] instanceof PasswordCallback
))
178 throw new UnsupportedCallbackException(callbacks
[0]);
179 if (!(callbacks
[1] instanceof PBEKeySpecCallback
))
180 throw new UnsupportedCallbackException(callbacks
[0]);
182 PasswordCallback passwordCb
= (PasswordCallback
) callbacks
[0];
183 PBEKeySpecCallback pbeCb
= (PBEKeySpecCallback
) callbacks
[1];
186 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
187 defaultCallbackHandler
.handle(dialogCbs
);
188 } else {// setup keyring
189 TextOutputCallback textCb1
= new TextOutputCallback(
190 TextOutputCallback
.INFORMATION
,
191 "Enter a master password which will protect your private data");
192 TextOutputCallback textCb2
= new TextOutputCallback(
193 TextOutputCallback
.INFORMATION
,
194 "(for example your credentials to third-party services)");
195 TextOutputCallback textCb3
= new TextOutputCallback(
196 TextOutputCallback
.INFORMATION
,
197 "Don't forget this password since the data cannot be read without it");
198 PasswordCallback confirmPasswordCb
= new PasswordCallback(
199 "Confirm password", false);
201 Callback
[] dialogCbs
= new Callback
[] { textCb1
, textCb2
,
202 textCb3
, passwordCb
, confirmPasswordCb
};
203 defaultCallbackHandler
.handle(dialogCbs
);
205 // if passwords different, retry (except if cancelled)
206 while (passwordCb
.getPassword() != null
207 && !Arrays
.equals(passwordCb
.getPassword(),
208 confirmPasswordCb
.getPassword())) {
209 TextOutputCallback textCb
= new TextOutputCallback(
210 TextOutputCallback
.ERROR
,
211 "The passwords do not match");
212 dialogCbs
= new Callback
[] { textCb
, passwordCb
,
214 defaultCallbackHandler
.handle(dialogCbs
);
217 if (passwordCb
.getPassword() != null) {// not cancelled
218 setup(passwordCb
.getPassword());
222 if (passwordCb
.getPassword() != null)
223 handleKeySpecCallback(pbeCb
);