1 package org
.argeo
.cms
.security
;
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
.Provider
;
14 import java
.security
.Security
;
15 import java
.util
.Arrays
;
16 import java
.util
.Iterator
;
18 import javax
.crypto
.SecretKey
;
19 import javax
.security
.auth
.Subject
;
20 import javax
.security
.auth
.callback
.Callback
;
21 import javax
.security
.auth
.callback
.CallbackHandler
;
22 import javax
.security
.auth
.callback
.PasswordCallback
;
23 import javax
.security
.auth
.callback
.TextOutputCallback
;
24 import javax
.security
.auth
.callback
.UnsupportedCallbackException
;
25 import javax
.security
.auth
.login
.LoginContext
;
26 import javax
.security
.auth
.login
.LoginException
;
28 import org
.apache
.commons
.io
.IOUtils
;
29 import org
.argeo
.api
.cms
.CmsAuth
;
30 import org
.argeo
.cms
.CmsException
;
32 /** username / password based keyring. TODO internationalize */
33 public abstract class AbstractKeyring
implements Keyring
, CryptoKeyring
{
34 // public final static String DEFAULT_KEYRING_LOGIN_CONTEXT = "KEYRING";
36 // private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT;
37 private CallbackHandler defaultCallbackHandler
;
39 private String charset
= "UTF-8";
42 * Default provider is bouncy castle, in order to have consistent behaviour
43 * across implementations
45 private String securityProviderName
= "BC";
48 * Whether the keyring has already been created in the past with a master
51 protected abstract Boolean
isSetup();
54 * Setup the keyring persistently, {@link #isSetup()} must return true
57 protected abstract void setup(char[] password
);
59 /** Populates the key spec callback */
60 protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback
);
62 protected abstract void encrypt(String path
, InputStream unencrypted
);
64 protected abstract InputStream
decrypt(String path
);
66 /** Triggers lazy initialization */
67 protected SecretKey
getSecretKey(char[] password
) {
68 Subject subject
= Subject
.getSubject(AccessController
.getContext());
69 // we assume only one secrete key is available
70 Iterator
<SecretKey
> iterator
= subject
.getPrivateCredentials(SecretKey
.class).iterator();
71 if (!iterator
.hasNext() || password
!=null) {// not initialized
72 CallbackHandler callbackHandler
= password
== null ?
new KeyringCallbackHandler()
73 : new PasswordProvidedCallBackHandler(password
);
74 ClassLoader currentContextClassLoader
= Thread
.currentThread().getContextClassLoader();
75 Thread
.currentThread().setContextClassLoader(getClass().getClassLoader());
77 LoginContext loginContext
= new LoginContext(CmsAuth
.LOGIN_CONTEXT_KEYRING
, subject
,
80 // FIXME will login even if password is wrong
81 iterator
= subject
.getPrivateCredentials(SecretKey
.class).iterator();
82 return iterator
.next();
83 } catch (LoginException e
) {
84 throw new CmsException("Keyring login failed", e
);
86 Thread
.currentThread().setContextClassLoader(currentContextClassLoader
);
90 SecretKey secretKey
= iterator
.next();
91 if (iterator
.hasNext())
92 throw new CmsException("More than one secret key in private credentials");
97 public InputStream
getAsStream(String path
) {
101 public void set(String path
, InputStream in
) {
105 public char[] getAsChars(String path
) {
106 // InputStream in = getAsStream(path);
107 // CharArrayWriter writer = null;
108 // Reader reader = null;
109 try (InputStream in
= getAsStream(path
);
110 CharArrayWriter writer
= new CharArrayWriter();
111 Reader reader
= new InputStreamReader(in
, charset
);) {
112 IOUtils
.copy(reader
, writer
);
113 return writer
.toCharArray();
114 } catch (IOException e
) {
115 throw new CmsException("Cannot decrypt to char array", e
);
117 // IOUtils.closeQuietly(reader);
118 // IOUtils.closeQuietly(in);
119 // IOUtils.closeQuietly(writer);
123 public void set(String path
, char[] arr
) {
124 // ByteArrayOutputStream out = new ByteArrayOutputStream();
125 // ByteArrayInputStream in = null;
126 // Writer writer = null;
127 try (ByteArrayOutputStream out
= new ByteArrayOutputStream();
128 Writer writer
= new OutputStreamWriter(out
, charset
);) {
129 // writer = new OutputStreamWriter(out, charset);
132 // in = new ByteArrayInputStream(out.toByteArray());
133 try (ByteArrayInputStream in
= new ByteArrayInputStream(out
.toByteArray());) {
136 } catch (IOException e
) {
137 throw new CmsException("Cannot encrypt to char array", e
);
139 // IOUtils.closeQuietly(writer);
140 // IOUtils.closeQuietly(out);
141 // IOUtils.closeQuietly(in);
145 public void unlock(char[] password
) {
148 SecretKey secretKey
= getSecretKey(password
);
149 if (secretKey
== null)
150 throw new CmsException("Could not unlock keyring");
153 protected Provider
getSecurityProvider() {
154 return Security
.getProvider(securityProviderName
);
157 public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler
) {
158 this.defaultCallbackHandler
= defaultCallbackHandler
;
161 public void setCharset(String charset
) {
162 this.charset
= charset
;
165 public void setSecurityProviderName(String securityProviderName
) {
166 this.securityProviderName
= securityProviderName
;
170 // protected static byte[] hash(char[] password, byte[] salt, Integer
172 // ByteArrayOutputStream out = null;
173 // OutputStreamWriter writer = null;
175 // out = new ByteArrayOutputStream();
176 // writer = new OutputStreamWriter(out, "UTF-8");
177 // writer.write(password);
178 // MessageDigest pwDigest = MessageDigest.getInstance("SHA-256");
180 // pwDigest.update(salt);
181 // byte[] btPass = pwDigest.digest(out.toByteArray());
182 // for (int i = 0; i < iterationCount; i++) {
184 // btPass = pwDigest.digest(btPass);
187 // } catch (Exception e) {
188 // throw new CmsException("Cannot hash", e);
190 // IOUtils.closeQuietly(out);
191 // IOUtils.closeQuietly(writer);
197 * Convenience method using the underlying callback to ask for a password
198 * (typically used when the password is not saved in the keyring)
200 protected char[] ask() {
201 PasswordCallback passwordCb
= new PasswordCallback("Password", false);
202 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
204 defaultCallbackHandler
.handle(dialogCbs
);
205 char[] password
= passwordCb
.getPassword();
207 } catch (Exception e
) {
208 throw new CmsException("Cannot ask for a password", e
);
213 class KeyringCallbackHandler
implements CallbackHandler
{
214 public void handle(Callback
[] callbacks
) throws IOException
, UnsupportedCallbackException
{
216 if (callbacks
.length
!= 2)
217 throw new IllegalArgumentException(
218 "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
219 if (!(callbacks
[0] instanceof PasswordCallback
))
220 throw new UnsupportedCallbackException(callbacks
[0]);
221 if (!(callbacks
[1] instanceof PBEKeySpecCallback
))
222 throw new UnsupportedCallbackException(callbacks
[0]);
224 PasswordCallback passwordCb
= (PasswordCallback
) callbacks
[0];
225 PBEKeySpecCallback pbeCb
= (PBEKeySpecCallback
) callbacks
[1];
228 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
229 defaultCallbackHandler
.handle(dialogCbs
);
230 } else {// setup keyring
231 TextOutputCallback textCb1
= new TextOutputCallback(TextOutputCallback
.INFORMATION
,
232 "Enter a master password which will protect your private data");
233 TextOutputCallback textCb2
= new TextOutputCallback(TextOutputCallback
.INFORMATION
,
234 "(for example your credentials to third-party services)");
235 TextOutputCallback textCb3
= new TextOutputCallback(TextOutputCallback
.INFORMATION
,
236 "Don't forget this password since the data cannot be read without it");
237 PasswordCallback confirmPasswordCb
= new PasswordCallback("Confirm password", false);
239 Callback
[] dialogCbs
= new Callback
[] { textCb1
, textCb2
, textCb3
, passwordCb
, confirmPasswordCb
};
240 defaultCallbackHandler
.handle(dialogCbs
);
242 // if passwords different, retry (except if cancelled)
243 while (passwordCb
.getPassword() != null
244 && !Arrays
.equals(passwordCb
.getPassword(), confirmPasswordCb
.getPassword())) {
245 TextOutputCallback textCb
= new TextOutputCallback(TextOutputCallback
.ERROR
,
246 "The passwords do not match");
247 dialogCbs
= new Callback
[] { textCb
, passwordCb
, confirmPasswordCb
};
248 defaultCallbackHandler
.handle(dialogCbs
);
251 if (passwordCb
.getPassword() != null) {// not cancelled
252 setup(passwordCb
.getPassword());
256 if (passwordCb
.getPassword() != null)
257 handleKeySpecCallback(pbeCb
);
262 class PasswordProvidedCallBackHandler
implements CallbackHandler
{
263 private final char[] password
;
265 public PasswordProvidedCallBackHandler(char[] password
) {
266 this.password
= password
;
270 public void handle(Callback
[] callbacks
) throws IOException
, UnsupportedCallbackException
{
272 if (callbacks
.length
!= 2)
273 throw new IllegalArgumentException(
274 "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
275 if (!(callbacks
[0] instanceof PasswordCallback
))
276 throw new UnsupportedCallbackException(callbacks
[0]);
277 if (!(callbacks
[1] instanceof PBEKeySpecCallback
))
278 throw new UnsupportedCallbackException(callbacks
[0]);
280 PasswordCallback passwordCb
= (PasswordCallback
) callbacks
[0];
281 passwordCb
.setPassword(password
);
282 PBEKeySpecCallback pbeCb
= (PBEKeySpecCallback
) callbacks
[1];
283 handleKeySpecCallback(pbeCb
);