]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java
Set update policy to 'always' for SNAPSHOT repositories.
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / security / AbstractKeyring.java
1 /*
2 * Copyright (C) 2007-2012 Argeo GmbH
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
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.cms.CmsException;
45 import org.argeo.node.NodeConstants;
46 import org.argeo.node.security.CryptoKeyring;
47 import org.argeo.node.security.Keyring;
48 import org.argeo.node.security.PBEKeySpecCallback;
49
50 /** username / password based keyring. TODO internationalize */
51 public abstract class AbstractKeyring implements Keyring, CryptoKeyring {
52 // public final static String DEFAULT_KEYRING_LOGIN_CONTEXT = "KEYRING";
53
54 // private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT;
55 private CallbackHandler defaultCallbackHandler;
56
57 private String charset = "UTF-8";
58
59 /**
60 * Default provider is bouncy castle, in order to have consistent behaviour
61 * across implementations
62 */
63 private String securityProviderName = "BC";
64
65 /**
66 * Whether the keyring has already been created in the past with a master
67 * password
68 */
69 protected abstract Boolean isSetup();
70
71 /**
72 * Setup the keyring persistently, {@link #isSetup()} must return true
73 * afterwards
74 */
75 protected abstract void setup(char[] password);
76
77 /** Populates the key spec callback */
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 /** Triggers lazy initialization */
85 protected SecretKey getSecretKey(char[] password) {
86 Subject subject = Subject.getSubject(AccessController.getContext());
87 // we assume only one secrete key is available
88 Iterator<SecretKey> iterator = subject.getPrivateCredentials(SecretKey.class).iterator();
89 if (!iterator.hasNext() || password!=null) {// not initialized
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 // FIXME will login even if password is wrong
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 // InputStream in = getAsStream(path);
125 // CharArrayWriter writer = null;
126 // Reader reader = null;
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 // IOUtils.closeQuietly(reader);
136 // IOUtils.closeQuietly(in);
137 // IOUtils.closeQuietly(writer);
138 }
139 }
140
141 public void set(String path, char[] arr) {
142 // ByteArrayOutputStream out = new ByteArrayOutputStream();
143 // ByteArrayInputStream in = null;
144 // Writer writer = null;
145 try (ByteArrayOutputStream out = new ByteArrayOutputStream();
146 Writer writer = new OutputStreamWriter(out, charset);) {
147 // writer = new OutputStreamWriter(out, charset);
148 writer.write(arr);
149 writer.flush();
150 // in = new ByteArrayInputStream(out.toByteArray());
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 // IOUtils.closeQuietly(writer);
158 // IOUtils.closeQuietly(out);
159 // IOUtils.closeQuietly(in);
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 // @Deprecated
188 // protected static byte[] hash(char[] password, byte[] salt, Integer
189 // iterationCount) {
190 // ByteArrayOutputStream out = null;
191 // OutputStreamWriter writer = null;
192 // try {
193 // out = new ByteArrayOutputStream();
194 // writer = new OutputStreamWriter(out, "UTF-8");
195 // writer.write(password);
196 // MessageDigest pwDigest = MessageDigest.getInstance("SHA-256");
197 // pwDigest.reset();
198 // pwDigest.update(salt);
199 // byte[] btPass = pwDigest.digest(out.toByteArray());
200 // for (int i = 0; i < iterationCount; i++) {
201 // pwDigest.reset();
202 // btPass = pwDigest.digest(btPass);
203 // }
204 // return btPass;
205 // } catch (Exception e) {
206 // throw new CmsException("Cannot hash", e);
207 // } finally {
208 // IOUtils.closeQuietly(out);
209 // IOUtils.closeQuietly(writer);
210 // }
211 //
212 // }
213
214 /**
215 * Convenience method using the underlying callback to ask for a password
216 * (typically used when the password is not saved in the keyring)
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 // checks
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 {// setup keyring
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 // first try
257 Callback[] dialogCbs = new Callback[] { textCb1, textCb2, textCb3, passwordCb, confirmPasswordCb };
258 defaultCallbackHandler.handle(dialogCbs);
259
260 // if passwords different, retry (except if cancelled)
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) {// not cancelled
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 // checks
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 }