]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java
Use runtime namespace context as default.
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / security / AbstractKeyring.java
1 package org.argeo.cms.security;
2
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;
17
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;
27
28 import org.apache.commons.io.IOUtils;
29 import org.argeo.api.cms.CmsAuth;
30 import org.argeo.cms.CmsException;
31
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";
35
36 // private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT;
37 private CallbackHandler defaultCallbackHandler;
38
39 private String charset = "UTF-8";
40
41 /**
42 * Default provider is bouncy castle, in order to have consistent behaviour
43 * across implementations
44 */
45 private String securityProviderName = "BC";
46
47 /**
48 * Whether the keyring has already been created in the past with a master
49 * password
50 */
51 protected abstract Boolean isSetup();
52
53 /**
54 * Setup the keyring persistently, {@link #isSetup()} must return true
55 * afterwards
56 */
57 protected abstract void setup(char[] password);
58
59 /** Populates the key spec callback */
60 protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback);
61
62 protected abstract void encrypt(String path, InputStream unencrypted);
63
64 protected abstract InputStream decrypt(String path);
65
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());
76 try {
77 LoginContext loginContext = new LoginContext(CmsAuth.LOGIN_CONTEXT_KEYRING, subject,
78 callbackHandler);
79 loginContext.login();
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);
85 } finally {
86 Thread.currentThread().setContextClassLoader(currentContextClassLoader);
87 }
88
89 } else {
90 SecretKey secretKey = iterator.next();
91 if (iterator.hasNext())
92 throw new CmsException("More than one secret key in private credentials");
93 return secretKey;
94 }
95 }
96
97 public InputStream getAsStream(String path) {
98 return decrypt(path);
99 }
100
101 public void set(String path, InputStream in) {
102 encrypt(path, in);
103 }
104
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);
116 } finally {
117 // IOUtils.closeQuietly(reader);
118 // IOUtils.closeQuietly(in);
119 // IOUtils.closeQuietly(writer);
120 }
121 }
122
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);
130 writer.write(arr);
131 writer.flush();
132 // in = new ByteArrayInputStream(out.toByteArray());
133 try (ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());) {
134 set(path, in);
135 }
136 } catch (IOException e) {
137 throw new CmsException("Cannot encrypt to char array", e);
138 } finally {
139 // IOUtils.closeQuietly(writer);
140 // IOUtils.closeQuietly(out);
141 // IOUtils.closeQuietly(in);
142 }
143 }
144
145 public void unlock(char[] password) {
146 if (!isSetup())
147 setup(password);
148 SecretKey secretKey = getSecretKey(password);
149 if (secretKey == null)
150 throw new CmsException("Could not unlock keyring");
151 }
152
153 protected Provider getSecurityProvider() {
154 return Security.getProvider(securityProviderName);
155 }
156
157 public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler) {
158 this.defaultCallbackHandler = defaultCallbackHandler;
159 }
160
161 public void setCharset(String charset) {
162 this.charset = charset;
163 }
164
165 public void setSecurityProviderName(String securityProviderName) {
166 this.securityProviderName = securityProviderName;
167 }
168
169 // @Deprecated
170 // protected static byte[] hash(char[] password, byte[] salt, Integer
171 // iterationCount) {
172 // ByteArrayOutputStream out = null;
173 // OutputStreamWriter writer = null;
174 // try {
175 // out = new ByteArrayOutputStream();
176 // writer = new OutputStreamWriter(out, "UTF-8");
177 // writer.write(password);
178 // MessageDigest pwDigest = MessageDigest.getInstance("SHA-256");
179 // pwDigest.reset();
180 // pwDigest.update(salt);
181 // byte[] btPass = pwDigest.digest(out.toByteArray());
182 // for (int i = 0; i < iterationCount; i++) {
183 // pwDigest.reset();
184 // btPass = pwDigest.digest(btPass);
185 // }
186 // return btPass;
187 // } catch (Exception e) {
188 // throw new CmsException("Cannot hash", e);
189 // } finally {
190 // IOUtils.closeQuietly(out);
191 // IOUtils.closeQuietly(writer);
192 // }
193 //
194 // }
195
196 /**
197 * Convenience method using the underlying callback to ask for a password
198 * (typically used when the password is not saved in the keyring)
199 */
200 protected char[] ask() {
201 PasswordCallback passwordCb = new PasswordCallback("Password", false);
202 Callback[] dialogCbs = new Callback[] { passwordCb };
203 try {
204 defaultCallbackHandler.handle(dialogCbs);
205 char[] password = passwordCb.getPassword();
206 return password;
207 } catch (Exception e) {
208 throw new CmsException("Cannot ask for a password", e);
209 }
210
211 }
212
213 class KeyringCallbackHandler implements CallbackHandler {
214 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
215 // checks
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]);
223
224 PasswordCallback passwordCb = (PasswordCallback) callbacks[0];
225 PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1];
226
227 if (isSetup()) {
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);
238 // first try
239 Callback[] dialogCbs = new Callback[] { textCb1, textCb2, textCb3, passwordCb, confirmPasswordCb };
240 defaultCallbackHandler.handle(dialogCbs);
241
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);
249 }
250
251 if (passwordCb.getPassword() != null) {// not cancelled
252 setup(passwordCb.getPassword());
253 }
254 }
255
256 if (passwordCb.getPassword() != null)
257 handleKeySpecCallback(pbeCb);
258 }
259
260 }
261
262 class PasswordProvidedCallBackHandler implements CallbackHandler {
263 private final char[] password;
264
265 public PasswordProvidedCallBackHandler(char[] password) {
266 this.password = password;
267 }
268
269 @Override
270 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
271 // checks
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]);
279
280 PasswordCallback passwordCb = (PasswordCallback) callbacks[0];
281 passwordCb.setPassword(password);
282 PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1];
283 handleKeySpecCallback(pbeCb);
284 }
285
286 }
287 }