]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.util/src/org/argeo/util/security/AbstractKeyring.java
68ae1a20e8d32590b3289dd60e7f0aa97e9c685e
[lgpl/argeo-commons.git] / org.argeo.util / src / org / argeo / util / 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.util.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.argeo.util.internal.UtilsException;
45 import org.argeo.util.internal.StreamUtils;
46
47 /** username / password based keyring. TODO internationalize */
48 public abstract class AbstractKeyring implements Keyring, CryptoKeyring {
49 public final static String DEFAULT_KEYRING_LOGIN_CONTEXT = "KEYRING";
50
51 private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT;
52 private CallbackHandler defaultCallbackHandler;
53
54 private String charset = "UTF-8";
55
56 /**
57 * Default provider is bouncy castle, in order to have consistent behaviour
58 * across implementations
59 */
60 private String securityProviderName = "BC";
61
62 /**
63 * Whether the keyring has already been created in the past with a master
64 * password
65 */
66 protected abstract Boolean isSetup();
67
68 /**
69 * Setup the keyring persistently, {@link #isSetup()} must return true
70 * afterwards
71 */
72 protected abstract void setup(char[] password);
73
74 /** Populates the key spec callback */
75 protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback);
76
77 protected abstract void encrypt(String path, InputStream unencrypted);
78
79 protected abstract InputStream decrypt(String path);
80
81 /** Triggers lazy initialization */
82 protected SecretKey getSecretKey() {
83 Subject subject = Subject.getSubject(AccessController.getContext());
84 // we assume only one secrete key is available
85 Iterator<SecretKey> iterator = subject.getPrivateCredentials(
86 SecretKey.class).iterator();
87 if (!iterator.hasNext()) {// not initialized
88 CallbackHandler callbackHandler = new KeyringCallbackHandler();
89 try {
90 LoginContext loginContext = new LoginContext(loginContextName,
91 subject, callbackHandler);
92 loginContext.login();
93 // FIXME will login even if password is wrong
94 iterator = subject.getPrivateCredentials(SecretKey.class)
95 .iterator();
96 return iterator.next();
97 } catch (LoginException e) {
98 throw new UtilsException("Keyring login failed", e);
99 }
100
101 } else {
102 SecretKey secretKey = iterator.next();
103 if (iterator.hasNext())
104 throw new UtilsException(
105 "More than one secret key in private credentials");
106 return secretKey;
107 }
108 }
109
110 public InputStream getAsStream(String path) {
111 return decrypt(path);
112 }
113
114 public void set(String path, InputStream in) {
115 encrypt(path, in);
116 }
117
118 public char[] getAsChars(String path) {
119 InputStream in = getAsStream(path);
120 CharArrayWriter writer = null;
121 Reader reader = null;
122 try {
123 writer = new CharArrayWriter();
124 reader = new InputStreamReader(in, charset);
125 StreamUtils.copy(reader, writer);
126 return writer.toCharArray();
127 } catch (IOException e) {
128 throw new UtilsException("Cannot decrypt to char array", e);
129 } finally {
130 StreamUtils.closeQuietly(reader);
131 StreamUtils.closeQuietly(in);
132 StreamUtils.closeQuietly(writer);
133 }
134 }
135
136 public void set(String path, char[] arr) {
137 ByteArrayOutputStream out = new ByteArrayOutputStream();
138 ByteArrayInputStream in = null;
139 Writer writer = null;
140 try {
141 writer = new OutputStreamWriter(out, charset);
142 writer.write(arr);
143 writer.flush();
144 in = new ByteArrayInputStream(out.toByteArray());
145 set(path, in);
146 } catch (IOException e) {
147 throw new UtilsException("Cannot encrypt to char array", e);
148 } finally {
149 StreamUtils.closeQuietly(writer);
150 StreamUtils.closeQuietly(out);
151 StreamUtils.closeQuietly(in);
152 }
153 }
154
155 protected Provider getSecurityProvider() {
156 return Security.getProvider(securityProviderName);
157 }
158
159 public void setLoginContextName(String loginContextName) {
160 this.loginContextName = loginContextName;
161 }
162
163 public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler) {
164 this.defaultCallbackHandler = defaultCallbackHandler;
165 }
166
167 public void setCharset(String charset) {
168 this.charset = charset;
169 }
170
171 public void setSecurityProviderName(String securityProviderName) {
172 this.securityProviderName = securityProviderName;
173 }
174
175 @Deprecated
176 protected static byte[] hash(char[] password, byte[] salt,
177 Integer iterationCount) {
178 ByteArrayOutputStream out = null;
179 OutputStreamWriter writer = null;
180 try {
181 out = new ByteArrayOutputStream();
182 writer = new OutputStreamWriter(out, "UTF-8");
183 writer.write(password);
184 MessageDigest pwDigest = MessageDigest.getInstance("SHA-256");
185 pwDigest.reset();
186 pwDigest.update(salt);
187 byte[] btPass = pwDigest.digest(out.toByteArray());
188 for (int i = 0; i < iterationCount; i++) {
189 pwDigest.reset();
190 btPass = pwDigest.digest(btPass);
191 }
192 return btPass;
193 } catch (Exception e) {
194 throw new UtilsException("Cannot hash", e);
195 } finally {
196 StreamUtils.closeQuietly(out);
197 StreamUtils.closeQuietly(writer);
198 }
199
200 }
201
202 /**
203 * Convenience method using the underlying callback to ask for a password
204 * (typically used when the password is not saved in the keyring)
205 */
206 protected char[] ask() {
207 PasswordCallback passwordCb = new PasswordCallback("Password", false);
208 Callback[] dialogCbs = new Callback[] { passwordCb };
209 try {
210 defaultCallbackHandler.handle(dialogCbs);
211 char[] password = passwordCb.getPassword();
212 return password;
213 } catch (Exception e) {
214 throw new UtilsException("Cannot ask for a password", e);
215 }
216
217 }
218
219 class KeyringCallbackHandler implements CallbackHandler {
220 public void handle(Callback[] callbacks) throws IOException,
221 UnsupportedCallbackException {
222 // checks
223 if (callbacks.length != 2)
224 throw new IllegalArgumentException(
225 "Keyring required 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
226 if (!(callbacks[0] instanceof PasswordCallback))
227 throw new UnsupportedCallbackException(callbacks[0]);
228 if (!(callbacks[1] instanceof PBEKeySpecCallback))
229 throw new UnsupportedCallbackException(callbacks[0]);
230
231 PasswordCallback passwordCb = (PasswordCallback) callbacks[0];
232 PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1];
233
234 if (isSetup()) {
235 Callback[] dialogCbs = new Callback[] { passwordCb };
236 defaultCallbackHandler.handle(dialogCbs);
237 } else {// setup keyring
238 TextOutputCallback textCb1 = new TextOutputCallback(
239 TextOutputCallback.INFORMATION,
240 "Enter a master password which will protect your private data");
241 TextOutputCallback textCb2 = new TextOutputCallback(
242 TextOutputCallback.INFORMATION,
243 "(for example your credentials to third-party services)");
244 TextOutputCallback textCb3 = new TextOutputCallback(
245 TextOutputCallback.INFORMATION,
246 "Don't forget this password since the data cannot be read without it");
247 PasswordCallback confirmPasswordCb = new PasswordCallback(
248 "Confirm password", false);
249 // first try
250 Callback[] dialogCbs = new Callback[] { textCb1, textCb2,
251 textCb3, passwordCb, confirmPasswordCb };
252 defaultCallbackHandler.handle(dialogCbs);
253
254 // if passwords different, retry (except if cancelled)
255 while (passwordCb.getPassword() != null
256 && !Arrays.equals(passwordCb.getPassword(),
257 confirmPasswordCb.getPassword())) {
258 TextOutputCallback textCb = new TextOutputCallback(
259 TextOutputCallback.ERROR,
260 "The passwords do not match");
261 dialogCbs = new Callback[] { textCb, passwordCb,
262 confirmPasswordCb };
263 defaultCallbackHandler.handle(dialogCbs);
264 }
265
266 if (passwordCb.getPassword() != null) {// not cancelled
267 setup(passwordCb.getPassword());
268 }
269 }
270
271 if (passwordCb.getPassword() != null)
272 handleKeySpecCallback(pbeCb);
273 }
274
275 }
276 }