]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java
Make tree view more robust
[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.Provider;
13 import java.security.Security;
14 import java.util.Arrays;
15 import java.util.Iterator;
16
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;
26
27 import org.argeo.api.cms.CmsAuth;
28 import org.argeo.util.CurrentSubject;
29 import org.argeo.util.StreamUtils;
30
31 /** username / password based keyring. TODO internationalize */
32 public abstract class AbstractKeyring implements Keyring, CryptoKeyring {
33 // public final static String DEFAULT_KEYRING_LOGIN_CONTEXT = "KEYRING";
34
35 // private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT;
36 private CallbackHandler defaultCallbackHandler;
37
38 private String charset = "UTF-8";
39
40 /**
41 * Default provider is bouncy castle, in order to have consistent behaviour
42 * across implementations
43 */
44 private String securityProviderName = "BC";
45
46 /**
47 * Whether the keyring has already been created in the past with a master
48 * password
49 */
50 protected abstract Boolean isSetup();
51
52 /**
53 * Setup the keyring persistently, {@link #isSetup()} must return true
54 * afterwards
55 */
56 protected abstract void setup(char[] password);
57
58 /** Populates the key spec callback */
59 protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback);
60
61 protected abstract void encrypt(String path, InputStream unencrypted);
62
63 protected abstract InputStream decrypt(String path);
64
65 /** Triggers lazy initialization */
66 protected SecretKey getSecretKey(char[] password) {
67 Subject subject = CurrentSubject.current();
68 if (subject == null)
69 throw new IllegalStateException("Current subject cannot be null");
70 // we assume only one secrete key is available
71 Iterator<SecretKey> iterator = subject.getPrivateCredentials(SecretKey.class).iterator();
72 if (!iterator.hasNext() || password != null) {// not initialized
73 CallbackHandler callbackHandler = password == null ? new KeyringCallbackHandler()
74 : new PasswordProvidedCallBackHandler(password);
75 ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader();
76 Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
77 try {
78 LoginContext loginContext = new LoginContext(CmsAuth.LOGIN_CONTEXT_KEYRING, subject, 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 IllegalStateException("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 IllegalStateException("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 StreamUtils.copy(reader, writer);
113 return writer.toCharArray();
114 } catch (IOException e) {
115 throw new IllegalStateException("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 IllegalStateException("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 IllegalStateException("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 IllegalStateException("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 }