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