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
.NodeConstants
;
30 import org
.argeo
.api
.security
.CryptoKeyring
;
31 import org
.argeo
.api
.security
.Keyring
;
32 import org
.argeo
.api
.security
.PBEKeySpecCallback
;
33 import org
.argeo
.cms
.CmsException
;
35 /** username / password based keyring. TODO internationalize */
36 public abstract class AbstractKeyring
implements Keyring
, CryptoKeyring
{
37 // public final static String DEFAULT_KEYRING_LOGIN_CONTEXT = "KEYRING";
39 // private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT;
40 private CallbackHandler defaultCallbackHandler
;
42 private String charset
= "UTF-8";
45 * Default provider is bouncy castle, in order to have consistent behaviour
46 * across implementations
48 private String securityProviderName
= "BC";
51 * Whether the keyring has already been created in the past with a master
54 protected abstract Boolean
isSetup();
57 * Setup the keyring persistently, {@link #isSetup()} must return true
60 protected abstract void setup(char[] password
);
62 /** Populates the key spec callback */
63 protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback
);
65 protected abstract void encrypt(String path
, InputStream unencrypted
);
67 protected abstract InputStream
decrypt(String path
);
69 /** Triggers lazy initialization */
70 protected SecretKey
getSecretKey(char[] password
) {
71 Subject subject
= Subject
.getSubject(AccessController
.getContext());
72 // we assume only one secrete key is available
73 Iterator
<SecretKey
> iterator
= subject
.getPrivateCredentials(SecretKey
.class).iterator();
74 if (!iterator
.hasNext() || password
!=null) {// not initialized
75 CallbackHandler callbackHandler
= password
== null ?
new KeyringCallbackHandler()
76 : new PasswordProvidedCallBackHandler(password
);
77 ClassLoader currentContextClassLoader
= Thread
.currentThread().getContextClassLoader();
78 Thread
.currentThread().setContextClassLoader(getClass().getClassLoader());
80 LoginContext loginContext
= new LoginContext(NodeConstants
.LOGIN_CONTEXT_KEYRING
, subject
,
83 // FIXME will login even if password is wrong
84 iterator
= subject
.getPrivateCredentials(SecretKey
.class).iterator();
85 return iterator
.next();
86 } catch (LoginException e
) {
87 throw new CmsException("Keyring login failed", e
);
89 Thread
.currentThread().setContextClassLoader(currentContextClassLoader
);
93 SecretKey secretKey
= iterator
.next();
94 if (iterator
.hasNext())
95 throw new CmsException("More than one secret key in private credentials");
100 public InputStream
getAsStream(String path
) {
101 return decrypt(path
);
104 public void set(String path
, InputStream in
) {
108 public char[] getAsChars(String path
) {
109 // InputStream in = getAsStream(path);
110 // CharArrayWriter writer = null;
111 // Reader reader = null;
112 try (InputStream in
= getAsStream(path
);
113 CharArrayWriter writer
= new CharArrayWriter();
114 Reader reader
= new InputStreamReader(in
, charset
);) {
115 IOUtils
.copy(reader
, writer
);
116 return writer
.toCharArray();
117 } catch (IOException e
) {
118 throw new CmsException("Cannot decrypt to char array", e
);
120 // IOUtils.closeQuietly(reader);
121 // IOUtils.closeQuietly(in);
122 // IOUtils.closeQuietly(writer);
126 public void set(String path
, char[] arr
) {
127 // ByteArrayOutputStream out = new ByteArrayOutputStream();
128 // ByteArrayInputStream in = null;
129 // Writer writer = null;
130 try (ByteArrayOutputStream out
= new ByteArrayOutputStream();
131 Writer writer
= new OutputStreamWriter(out
, charset
);) {
132 // writer = new OutputStreamWriter(out, charset);
135 // in = new ByteArrayInputStream(out.toByteArray());
136 try (ByteArrayInputStream in
= new ByteArrayInputStream(out
.toByteArray());) {
139 } catch (IOException e
) {
140 throw new CmsException("Cannot encrypt to char array", e
);
142 // IOUtils.closeQuietly(writer);
143 // IOUtils.closeQuietly(out);
144 // IOUtils.closeQuietly(in);
148 public void unlock(char[] password
) {
151 SecretKey secretKey
= getSecretKey(password
);
152 if (secretKey
== null)
153 throw new CmsException("Could not unlock keyring");
156 protected Provider
getSecurityProvider() {
157 return Security
.getProvider(securityProviderName
);
160 public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler
) {
161 this.defaultCallbackHandler
= defaultCallbackHandler
;
164 public void setCharset(String charset
) {
165 this.charset
= charset
;
168 public void setSecurityProviderName(String securityProviderName
) {
169 this.securityProviderName
= securityProviderName
;
173 // protected static byte[] hash(char[] password, byte[] salt, Integer
175 // ByteArrayOutputStream out = null;
176 // OutputStreamWriter writer = null;
178 // out = new ByteArrayOutputStream();
179 // writer = new OutputStreamWriter(out, "UTF-8");
180 // writer.write(password);
181 // MessageDigest pwDigest = MessageDigest.getInstance("SHA-256");
183 // pwDigest.update(salt);
184 // byte[] btPass = pwDigest.digest(out.toByteArray());
185 // for (int i = 0; i < iterationCount; i++) {
187 // btPass = pwDigest.digest(btPass);
190 // } catch (Exception e) {
191 // throw new CmsException("Cannot hash", e);
193 // IOUtils.closeQuietly(out);
194 // IOUtils.closeQuietly(writer);
200 * Convenience method using the underlying callback to ask for a password
201 * (typically used when the password is not saved in the keyring)
203 protected char[] ask() {
204 PasswordCallback passwordCb
= new PasswordCallback("Password", false);
205 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
207 defaultCallbackHandler
.handle(dialogCbs
);
208 char[] password
= passwordCb
.getPassword();
210 } catch (Exception e
) {
211 throw new CmsException("Cannot ask for a password", e
);
216 class KeyringCallbackHandler
implements CallbackHandler
{
217 public void handle(Callback
[] callbacks
) throws IOException
, UnsupportedCallbackException
{
219 if (callbacks
.length
!= 2)
220 throw new IllegalArgumentException(
221 "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
222 if (!(callbacks
[0] instanceof PasswordCallback
))
223 throw new UnsupportedCallbackException(callbacks
[0]);
224 if (!(callbacks
[1] instanceof PBEKeySpecCallback
))
225 throw new UnsupportedCallbackException(callbacks
[0]);
227 PasswordCallback passwordCb
= (PasswordCallback
) callbacks
[0];
228 PBEKeySpecCallback pbeCb
= (PBEKeySpecCallback
) callbacks
[1];
231 Callback
[] dialogCbs
= new Callback
[] { passwordCb
};
232 defaultCallbackHandler
.handle(dialogCbs
);
233 } else {// setup keyring
234 TextOutputCallback textCb1
= new TextOutputCallback(TextOutputCallback
.INFORMATION
,
235 "Enter a master password which will protect your private data");
236 TextOutputCallback textCb2
= new TextOutputCallback(TextOutputCallback
.INFORMATION
,
237 "(for example your credentials to third-party services)");
238 TextOutputCallback textCb3
= new TextOutputCallback(TextOutputCallback
.INFORMATION
,
239 "Don't forget this password since the data cannot be read without it");
240 PasswordCallback confirmPasswordCb
= new PasswordCallback("Confirm password", false);
242 Callback
[] dialogCbs
= new Callback
[] { textCb1
, textCb2
, textCb3
, passwordCb
, confirmPasswordCb
};
243 defaultCallbackHandler
.handle(dialogCbs
);
245 // if passwords different, retry (except if cancelled)
246 while (passwordCb
.getPassword() != null
247 && !Arrays
.equals(passwordCb
.getPassword(), confirmPasswordCb
.getPassword())) {
248 TextOutputCallback textCb
= new TextOutputCallback(TextOutputCallback
.ERROR
,
249 "The passwords do not match");
250 dialogCbs
= new Callback
[] { textCb
, passwordCb
, confirmPasswordCb
};
251 defaultCallbackHandler
.handle(dialogCbs
);
254 if (passwordCb
.getPassword() != null) {// not cancelled
255 setup(passwordCb
.getPassword());
259 if (passwordCb
.getPassword() != null)
260 handleKeySpecCallback(pbeCb
);
265 class PasswordProvidedCallBackHandler
implements CallbackHandler
{
266 private final char[] password
;
268 public PasswordProvidedCallBackHandler(char[] password
) {
269 this.password
= password
;
273 public void handle(Callback
[] callbacks
) throws IOException
, UnsupportedCallbackException
{
275 if (callbacks
.length
!= 2)
276 throw new IllegalArgumentException(
277 "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
278 if (!(callbacks
[0] instanceof PasswordCallback
))
279 throw new UnsupportedCallbackException(callbacks
[0]);
280 if (!(callbacks
[1] instanceof PBEKeySpecCallback
))
281 throw new UnsupportedCallbackException(callbacks
[0]);
283 PasswordCallback passwordCb
= (PasswordCallback
) callbacks
[0];
284 passwordCb
.setPassword(password
);
285 PBEKeySpecCallback pbeCb
= (PBEKeySpecCallback
) callbacks
[1];
286 handleKeySpecCallback(pbeCb
);