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