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