]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/security/AbstractKeyring.java
Improve IPA
[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.MessageDigest;
29 import java.security.Provider;
30 import java.security.Security;
31 import java.util.Arrays;
32 import java.util.Iterator;
33
34 import javax.crypto.SecretKey;
35 import javax.security.auth.Subject;
36 import javax.security.auth.callback.Callback;
37 import javax.security.auth.callback.CallbackHandler;
38 import javax.security.auth.callback.PasswordCallback;
39 import javax.security.auth.callback.TextOutputCallback;
40 import javax.security.auth.callback.UnsupportedCallbackException;
41 import javax.security.auth.login.LoginContext;
42 import javax.security.auth.login.LoginException;
43
44 import org.apache.commons.io.IOUtils;
45 import org.argeo.cms.CmsException;
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() {
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()) {// not initialized
90 CallbackHandler callbackHandler = new KeyringCallbackHandler();
91 ClassLoader currentContextClassLoader = Thread.currentThread().getContextClassLoader();
92 Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
93 try {
94 LoginContext loginContext = new LoginContext(loginContextName, subject, callbackHandler);
95 loginContext.login();
96 // FIXME will login even if password is wrong
97 iterator = subject.getPrivateCredentials(SecretKey.class).iterator();
98 return iterator.next();
99 } catch (LoginException e) {
100 throw new CmsException("Keyring login failed", e);
101 } finally {
102 Thread.currentThread().setContextClassLoader(currentContextClassLoader);
103 }
104
105 } else {
106 SecretKey secretKey = iterator.next();
107 if (iterator.hasNext())
108 throw new CmsException("More than one secret key in private credentials");
109 return secretKey;
110 }
111 }
112
113 public InputStream getAsStream(String path) {
114 return decrypt(path);
115 }
116
117 public void set(String path, InputStream in) {
118 encrypt(path, in);
119 }
120
121 public char[] getAsChars(String path) {
122 InputStream in = getAsStream(path);
123 CharArrayWriter writer = null;
124 Reader reader = null;
125 try {
126 writer = new CharArrayWriter();
127 reader = new InputStreamReader(in, charset);
128 IOUtils.copy(reader, writer);
129 return writer.toCharArray();
130 } catch (IOException e) {
131 throw new CmsException("Cannot decrypt to char array", e);
132 } finally {
133 IOUtils.closeQuietly(reader);
134 IOUtils.closeQuietly(in);
135 IOUtils.closeQuietly(writer);
136 }
137 }
138
139 public void set(String path, char[] arr) {
140 ByteArrayOutputStream out = new ByteArrayOutputStream();
141 ByteArrayInputStream in = null;
142 Writer writer = null;
143 try {
144 writer = new OutputStreamWriter(out, charset);
145 writer.write(arr);
146 writer.flush();
147 in = new ByteArrayInputStream(out.toByteArray());
148 set(path, in);
149 } catch (IOException e) {
150 throw new CmsException("Cannot encrypt to char array", e);
151 } finally {
152 IOUtils.closeQuietly(writer);
153 IOUtils.closeQuietly(out);
154 IOUtils.closeQuietly(in);
155 }
156 }
157
158 protected Provider getSecurityProvider() {
159 return Security.getProvider(securityProviderName);
160 }
161
162 public void setLoginContextName(String loginContextName) {
163 this.loginContextName = loginContextName;
164 }
165
166 public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler) {
167 this.defaultCallbackHandler = defaultCallbackHandler;
168 }
169
170 public void setCharset(String charset) {
171 this.charset = charset;
172 }
173
174 public void setSecurityProviderName(String securityProviderName) {
175 this.securityProviderName = securityProviderName;
176 }
177
178 @Deprecated
179 protected static byte[] hash(char[] password, byte[] salt, Integer iterationCount) {
180 ByteArrayOutputStream out = null;
181 OutputStreamWriter writer = null;
182 try {
183 out = new ByteArrayOutputStream();
184 writer = new OutputStreamWriter(out, "UTF-8");
185 writer.write(password);
186 MessageDigest pwDigest = MessageDigest.getInstance("SHA-256");
187 pwDigest.reset();
188 pwDigest.update(salt);
189 byte[] btPass = pwDigest.digest(out.toByteArray());
190 for (int i = 0; i < iterationCount; i++) {
191 pwDigest.reset();
192 btPass = pwDigest.digest(btPass);
193 }
194 return btPass;
195 } catch (Exception e) {
196 throw new CmsException("Cannot hash", e);
197 } finally {
198 IOUtils.closeQuietly(out);
199 IOUtils.closeQuietly(writer);
200 }
201
202 }
203
204 /**
205 * Convenience method using the underlying callback to ask for a password
206 * (typically used when the password is not saved in the keyring)
207 */
208 protected char[] ask() {
209 PasswordCallback passwordCb = new PasswordCallback("Password", false);
210 Callback[] dialogCbs = new Callback[] { passwordCb };
211 try {
212 defaultCallbackHandler.handle(dialogCbs);
213 char[] password = passwordCb.getPassword();
214 return password;
215 } catch (Exception e) {
216 throw new CmsException("Cannot ask for a password", e);
217 }
218
219 }
220
221 class KeyringCallbackHandler implements CallbackHandler {
222 public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
223 // checks
224 if (callbacks.length != 2)
225 throw new IllegalArgumentException(
226 "Keyring required 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
227 if (!(callbacks[0] instanceof PasswordCallback))
228 throw new UnsupportedCallbackException(callbacks[0]);
229 if (!(callbacks[1] instanceof PBEKeySpecCallback))
230 throw new UnsupportedCallbackException(callbacks[0]);
231
232 PasswordCallback passwordCb = (PasswordCallback) callbacks[0];
233 PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1];
234
235 if (isSetup()) {
236 Callback[] dialogCbs = new Callback[] { passwordCb };
237 defaultCallbackHandler.handle(dialogCbs);
238 } else {// setup keyring
239 TextOutputCallback textCb1 = new TextOutputCallback(TextOutputCallback.INFORMATION,
240 "Enter a master password which will protect your private data");
241 TextOutputCallback textCb2 = new TextOutputCallback(TextOutputCallback.INFORMATION,
242 "(for example your credentials to third-party services)");
243 TextOutputCallback textCb3 = new TextOutputCallback(TextOutputCallback.INFORMATION,
244 "Don't forget this password since the data cannot be read without it");
245 PasswordCallback confirmPasswordCb = new PasswordCallback("Confirm password", false);
246 // first try
247 Callback[] dialogCbs = new Callback[] { textCb1, textCb2, textCb3, passwordCb, confirmPasswordCb };
248 defaultCallbackHandler.handle(dialogCbs);
249
250 // if passwords different, retry (except if cancelled)
251 while (passwordCb.getPassword() != null
252 && !Arrays.equals(passwordCb.getPassword(), confirmPasswordCb.getPassword())) {
253 TextOutputCallback textCb = new TextOutputCallback(TextOutputCallback.ERROR,
254 "The passwords do not match");
255 dialogCbs = new Callback[] { textCb, passwordCb, confirmPasswordCb };
256 defaultCallbackHandler.handle(dialogCbs);
257 }
258
259 if (passwordCb.getPassword() != null) {// not cancelled
260 setup(passwordCb.getPassword());
261 }
262 }
263
264 if (passwordCb.getPassword() != null)
265 handleKeySpecCallback(pbeCb);
266 }
267
268 }
269 }