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