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