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