1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.argeo.cms.security;
17
18 import java.io.ByteArrayInputStream;
19 import java.io.ByteArrayOutputStream;
20 import java.io.CharArrayWriter;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.InputStreamReader;
24 import java.io.OutputStreamWriter;
25 import java.io.Reader;
26 import java.io.Writer;
27 import java.security.AccessController;
28 import java.security.Provider;
29 import java.security.Security;
30 import java.util.Arrays;
31 import java.util.Iterator;
32
33 import javax.crypto.SecretKey;
34 import javax.security.auth.Subject;
35 import javax.security.auth.callback.Callback;
36 import javax.security.auth.callback.CallbackHandler;
37 import javax.security.auth.callback.PasswordCallback;
38 import javax.security.auth.callback.TextOutputCallback;
39 import javax.security.auth.callback.UnsupportedCallbackException;
40 import javax.security.auth.login.LoginContext;
41 import javax.security.auth.login.LoginException;
42
43 import org.apache.commons.io.IOUtils;
44 import org.argeo.api.NodeConstants;
45 import org.argeo.api.security.CryptoKeyring;
46 import org.argeo.api.security.Keyring;
47 import org.argeo.api.security.PBEKeySpecCallback;
48 import org.argeo.cms.CmsException;
49
50
51 public abstract class AbstractKeyring implements Keyring, CryptoKeyring {
52
53
54
55 private CallbackHandler defaultCallbackHandler;
56
57 private String charset = "UTF-8";
58
59
60
61
62
63 private String securityProviderName = "BC";
64
65
66
67
68
69 protected abstract Boolean isSetup();
70
71
72
73
74
75 protected abstract void setup(char[] password);
76
77
78 protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback);
79
80 protected abstract void encrypt(String path, InputStream unencrypted);
81
82 protected abstract InputStream decrypt(String path);
83
84
85 protected SecretKey getSecretKey(char[] password) {
86 Subject subject = Subject.getSubject(AccessController.getContext());
87
88 Iterator<SecretKey> iterator = subject.getPrivateCredentials(SecretKey.class).iterator();
89 if (!iterator.hasNext() || password!=null) {
90 CallbackHandler callbackHandler = password == null ? new KeyringCallbackHandler()
91 : new PasswordProvidedCallBackHandler(password);
92 ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader();
93 Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
94 try {
95 LoginContext loginContext = new LoginContext(NodeConstants.LOGIN_CONTEXT_KEYRING, subject,
96 callbackHandler);
97 loginContext.login();
98
99 iterator = subject.getPrivateCredentials(SecretKey.class).iterator();
100 return iterator.next();
101 } catch (LoginException e) {
102 throw new CmsException("Keyring login failed", e);
103 } finally {
104 Thread.currentThread().setContextClassLoader(currentContextClassLoader);
105 }
106
107 } else {
108 SecretKey secretKey = iterator.next();
109 if (iterator.hasNext())
110 throw new CmsException("More than one secret key in private credentials");
111 return secretKey;
112 }
113 }
114
115 public InputStream getAsStream(String path) {
116 return decrypt(path);
117 }
118
119 public void set(String path, InputStream in) {
120 encrypt(path, in);
121 }
122
123 public char[] getAsChars(String path) {
124
125
126
127 try (InputStream in = getAsStream(path);
128 CharArrayWriter writer = new CharArrayWriter();
129 Reader reader = new InputStreamReader(in, charset);) {
130 IOUtils.copy(reader, writer);
131 return writer.toCharArray();
132 } catch (IOException e) {
133 throw new CmsException("Cannot decrypt to char array", e);
134 } finally {
135
136
137
138 }
139 }
140
141 public void set(String path, char[] arr) {
142
143
144
145 try (ByteArrayOutputStream out = new ByteArrayOutputStream();
146 Writer writer = new OutputStreamWriter(out, charset);) {
147
148 writer.write(arr);
149 writer.flush();
150
151 try (ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());) {
152 set(path, in);
153 }
154 } catch (IOException e) {
155 throw new CmsException("Cannot encrypt to char array", e);
156 } finally {
157
158
159
160 }
161 }
162
163 public void unlock(char[] password) {
164 if (!isSetup())
165 setup(password);
166 SecretKey secretKey = getSecretKey(password);
167 if (secretKey == null)
168 throw new CmsException("Could not unlock keyring");
169 }
170
171 protected Provider getSecurityProvider() {
172 return Security.getProvider(securityProviderName);
173 }
174
175 public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler) {
176 this.defaultCallbackHandler = defaultCallbackHandler;
177 }
178
179 public void setCharset(String charset) {
180 this.charset = charset;
181 }
182
183 public void setSecurityProviderName(String securityProviderName) {
184 this.securityProviderName = securityProviderName;
185 }
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218 protected char[] ask() {
219 PasswordCallback passwordCb = new PasswordCallback("Password", false);
220 Callback[] dialogCbs = new Callback[] { passwordCb };
221 try {
222 defaultCallbackHandler.handle(dialogCbs);
223 char[] password = passwordCb.getPassword();
224 return password;
225 } catch (Exception e) {
226 throw new CmsException("Cannot ask for a password", e);
227 }
228
229 }
230
231 class KeyringCallbackHandler implements CallbackHandler {
232 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
233
234 if (callbacks.length != 2)
235 throw new IllegalArgumentException(
236 "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
237 if (!(callbacks[0] instanceof PasswordCallback))
238 throw new UnsupportedCallbackException(callbacks[0]);
239 if (!(callbacks[1] instanceof PBEKeySpecCallback))
240 throw new UnsupportedCallbackException(callbacks[0]);
241
242 PasswordCallback passwordCb = (PasswordCallback) callbacks[0];
243 PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1];
244
245 if (isSetup()) {
246 Callback[] dialogCbs = new Callback[] { passwordCb };
247 defaultCallbackHandler.handle(dialogCbs);
248 } else {
249 TextOutputCallback textCb1 = new TextOutputCallback(TextOutputCallback.INFORMATION,
250 "Enter a master password which will protect your private data");
251 TextOutputCallback textCb2 = new TextOutputCallback(TextOutputCallback.INFORMATION,
252 "(for example your credentials to third-party services)");
253 TextOutputCallback textCb3 = new TextOutputCallback(TextOutputCallback.INFORMATION,
254 "Don't forget this password since the data cannot be read without it");
255 PasswordCallback confirmPasswordCb = new PasswordCallback("Confirm password", false);
256
257 Callback[] dialogCbs = new Callback[] { textCb1, textCb2, textCb3, passwordCb, confirmPasswordCb };
258 defaultCallbackHandler.handle(dialogCbs);
259
260
261 while (passwordCb.getPassword() != null
262 && !Arrays.equals(passwordCb.getPassword(), confirmPasswordCb.getPassword())) {
263 TextOutputCallback textCb = new TextOutputCallback(TextOutputCallback.ERROR,
264 "The passwords do not match");
265 dialogCbs = new Callback[] { textCb, passwordCb, confirmPasswordCb };
266 defaultCallbackHandler.handle(dialogCbs);
267 }
268
269 if (passwordCb.getPassword() != null) {
270 setup(passwordCb.getPassword());
271 }
272 }
273
274 if (passwordCb.getPassword() != null)
275 handleKeySpecCallback(pbeCb);
276 }
277
278 }
279
280 class PasswordProvidedCallBackHandler implements CallbackHandler {
281 private final char[] password;
282
283 public PasswordProvidedCallBackHandler(char[] password) {
284 this.password = password;
285 }
286
287 @Override
288 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
289
290 if (callbacks.length != 2)
291 throw new IllegalArgumentException(
292 "Keyring requires 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
293 if (!(callbacks[0] instanceof PasswordCallback))
294 throw new UnsupportedCallbackException(callbacks[0]);
295 if (!(callbacks[1] instanceof PBEKeySpecCallback))
296 throw new UnsupportedCallbackException(callbacks[0]);
297
298 PasswordCallback passwordCb = (PasswordCallback) callbacks[0];
299 passwordCb.setPassword(password);
300 PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1];
301 handleKeySpecCallback(pbeCb);
302 }
303
304 }
305 }