import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.Reader;
+import java.io.Writer;
/** Utilities to be used when APache COmmons IO is not available. */
public class StreamUtils {
+ private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
- public static void copy(InputStream in, OutputStream out)
+ /** @return the number of bytes */
+ public static Long copy(InputStream in, OutputStream out)
throws IOException {
- byte[] buf = new byte[8192];
+ Long count = 0l;
+ byte[] buf = new byte[DEFAULT_BUFFER_SIZE];
while (true) {
int length = in.read(buf);
if (length < 0)
break;
out.write(buf, 0, length);
+ count = count + length;
}
+ return count;
+ }
+
+ /** @return the number of chars */
+ public static Long copy(Reader in, Writer out) throws IOException {
+ Long count = 0l;
+ char[] buf = new char[DEFAULT_BUFFER_SIZE];
+ while (true) {
+ int length = in.read(buf);
+ if (length < 0)
+ break;
+ out.write(buf, 0, length);
+ count = count + length;
+ }
+ return count;
}
public static void closeQuietly(InputStream in) {
}
}
+ public static void closeQuietly(Reader in) {
+ if (in != null)
+ try {
+ in.close();
+ } catch (Exception e) {
+ //
+ }
+ }
+
+ public static void closeQuietly(Writer out) {
+ if (out != null)
+ try {
+ out.close();
+ } catch (Exception e) {
+ //
+ }
+ }
+
private StreamUtils() {
}
--- /dev/null
+package org.argeo.util.crypto;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.CharArrayWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Reader;
+import java.io.Writer;
+import java.security.AccessController;
+import java.util.Arrays;
+import java.util.Iterator;
+
+import javax.crypto.SecretKey;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.TextOutputCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
+
+import org.argeo.ArgeoException;
+import org.argeo.StreamUtils;
+
+/** username / password based keyring. TODO internationalize */
+public abstract class AbstractKeyring implements Keyring {
+ public final static String DEFAULT_KEYRING_LOGIN_CONTEXT = "KEYRING";
+
+ private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT;
+ private CallbackHandler defaultCallbackHandler;
+
+ private String charset = "UTF-8";
+
+ /**
+ * Whether the keyring has already been created in the past with a master
+ * password
+ */
+ protected abstract Boolean isSetup();
+
+ /**
+ * Setup the keyring persistently, {@link #isSetup()} must return true
+ * afterwards
+ */
+ protected abstract void setup();
+
+ /** Populates the key spec callback */
+ protected abstract void handleKeySpecCallback(PBEKeySpecCallback pbeCallback);
+
+ protected abstract void encrypt(String path, InputStream unencrypted);
+
+ protected abstract InputStream decrypt(String path);
+
+ /** Triggers lazy initialization */
+ protected SecretKey getSecretKey() {
+ Subject subject = Subject.getSubject(AccessController.getContext());
+ // we assume only one secrete key is available
+ Iterator<SecretKey> iterator = subject.getPrivateCredentials(
+ SecretKey.class).iterator();
+ if (!iterator.hasNext()) {// not initialized
+ CallbackHandler callbackHandler = new KeyringCallbackHandler();
+ try {
+ LoginContext loginContext = new LoginContext(loginContextName,
+ subject, callbackHandler);
+ loginContext.login();
+ // FIXME will login even if password is wrong
+ iterator = subject.getPrivateCredentials(SecretKey.class)
+ .iterator();
+ return iterator.next();
+ } catch (LoginException e) {
+ throw new ArgeoException("Keyring login failed", e);
+ }
+
+ } else {
+ SecretKey secretKey = iterator.next();
+ if (iterator.hasNext())
+ throw new ArgeoException(
+ "More than one secret key in private credentials");
+ return secretKey;
+ }
+ }
+
+ public InputStream getAsStream(String path) {
+ return decrypt(path);
+ }
+
+ public void set(String path, InputStream in) {
+ encrypt(path, in);
+ }
+
+ public char[] getAsChars(String path) {
+ InputStream in = getAsStream(path);
+ CharArrayWriter writer = null;
+ Reader reader = null;
+ try {
+ writer = new CharArrayWriter();
+ reader = new InputStreamReader(in, charset);
+ StreamUtils.copy(reader, writer);
+ return writer.toCharArray();
+ } catch (IOException e) {
+ throw new ArgeoException("Cannot decrypt to char array", e);
+ } finally {
+ StreamUtils.closeQuietly(reader);
+ StreamUtils.closeQuietly(in);
+ StreamUtils.closeQuietly(writer);
+ }
+ }
+
+ public void set(String path, char[] arr) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ ByteArrayInputStream in = null;
+ Writer writer = null;
+ try {
+ writer = new OutputStreamWriter(out, charset);
+ writer.write(arr);
+ in = new ByteArrayInputStream(out.toByteArray());
+ set(path, in);
+ } catch (IOException e) {
+ throw new ArgeoException("Cannot encrypt to char array", e);
+ } finally {
+ StreamUtils.closeQuietly(writer);
+ StreamUtils.closeQuietly(out);
+ StreamUtils.closeQuietly(in);
+ }
+ }
+
+ public void setLoginContextName(String loginContextName) {
+ this.loginContextName = loginContextName;
+ }
+
+ public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler) {
+ this.defaultCallbackHandler = defaultCallbackHandler;
+ }
+
+ public void setCharset(String charset) {
+ this.charset = charset;
+ }
+
+ class KeyringCallbackHandler implements CallbackHandler {
+ public void handle(Callback[] callbacks) throws IOException,
+ UnsupportedCallbackException {
+ // checks
+ if (callbacks.length != 2)
+ throw new IllegalArgumentException(
+ "Keyring required 2 and only 2 callbacks: {PasswordCallback,PBEKeySpecCallback}");
+ if (!(callbacks[0] instanceof PasswordCallback))
+ throw new UnsupportedCallbackException(callbacks[0]);
+ if (!(callbacks[1] instanceof PBEKeySpecCallback))
+ throw new UnsupportedCallbackException(callbacks[0]);
+
+ PasswordCallback passwordCb = (PasswordCallback) callbacks[0];
+ PBEKeySpecCallback pbeCb = (PBEKeySpecCallback) callbacks[1];
+
+ if (isSetup()) {
+ Callback[] dialogCbs = new Callback[] { passwordCb };
+ defaultCallbackHandler.handle(dialogCbs);
+ } else {// setup keyring
+ TextOutputCallback textCb1 = new TextOutputCallback(
+ TextOutputCallback.INFORMATION,
+ "Enter a master password");
+ TextOutputCallback textCb2 = new TextOutputCallback(
+ TextOutputCallback.INFORMATION,
+ "It will encrypt your private data");
+ TextOutputCallback textCb3 = new TextOutputCallback(
+ TextOutputCallback.INFORMATION,
+ "Don't forget it or your data is lost");
+ PasswordCallback confirmPasswordCb = new PasswordCallback(
+ "Confirm password", false);
+ // first try
+ Callback[] dialogCbs = new Callback[] { textCb1, textCb2,
+ textCb3, passwordCb, confirmPasswordCb };
+ defaultCallbackHandler.handle(dialogCbs);
+
+ // if passwords different, retry (except if cancelled)
+ while (passwordCb.getPassword() != null
+ && !Arrays.equals(passwordCb.getPassword(),
+ confirmPasswordCb.getPassword())) {
+ TextOutputCallback textCb = new TextOutputCallback(
+ TextOutputCallback.ERROR,
+ "The passwords do not match");
+ dialogCbs = new Callback[] { textCb, passwordCb,
+ confirmPasswordCb };
+ defaultCallbackHandler.handle(dialogCbs);
+ }
+
+ if (passwordCb.getPassword() != null)// not cancelled
+ setup();
+ }
+
+ if (passwordCb.getPassword() != null)
+ handleKeySpecCallback(pbeCb);
+ }
+
+ }
+}
--- /dev/null
+package org.argeo.util.crypto;
+
+import java.io.InputStream;
+
+/**
+ * Access to private (typically encrypted) data. The keyring is responsible for
+ * retrieving the necessary credentials.
+ */
+public interface Keyring {
+ public void changePassword(char[] oldPassword, char[] newPassword);
+
+ public char[] getAsChars(String path);
+
+ public InputStream getAsStream(String path);
+
+ public void set(String path, char[] arr);
+
+ public void set(String path, InputStream in);
+}
--- /dev/null
+package org.argeo.util.crypto;
+
+import java.security.AccessController;
+import java.util.Map;
+import java.util.Set;
+
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.PBEKeySpec;
+import javax.crypto.spec.SecretKeySpec;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.login.LoginException;
+import javax.security.auth.spi.LoginModule;
+
+/** Adds a secret key to the private credentials */
+public class KeyringLoginModule implements LoginModule {
+ private Subject subject;
+ private CallbackHandler callbackHandler;
+ private SecretKey secretKey;
+
+ public void initialize(Subject subject, CallbackHandler callbackHandler,
+ Map<String, ?> sharedState, Map<String, ?> options) {
+ this.subject = subject;
+ if (subject == null) {
+ subject = Subject.getSubject(AccessController.getContext());
+ }
+ this.callbackHandler = callbackHandler;
+ }
+
+ public boolean login() throws LoginException {
+ Set<SecretKey> pbes = subject.getPrivateCredentials(SecretKey.class);
+ if (pbes.size() > 0)
+ return true;
+ PasswordCallback pc = new PasswordCallback("Master password", false);
+ PBEKeySpecCallback pbeCb = new PBEKeySpecCallback();
+ Callback[] callbacks = { pc, pbeCb };
+ try {
+ callbackHandler.handle(callbacks);
+ char[] password = pc.getPassword();
+
+ SecretKeyFactory keyFac = SecretKeyFactory.getInstance(pbeCb
+ .getSecretKeyFactory());
+ PBEKeySpec keySpec;
+ if (pbeCb.getKeyLength() != null)
+ keySpec = new PBEKeySpec(password, pbeCb.getSalt(),
+ pbeCb.getIterationCount(), pbeCb.getKeyLength());
+ else
+ keySpec = new PBEKeySpec(password, pbeCb.getSalt(),
+ pbeCb.getIterationCount());
+
+ String secKeyEncryption = pbeCb.getSecretKeyEncryption();
+ if (secKeyEncryption != null) {
+ SecretKey tmp = keyFac.generateSecret(keySpec);
+ secretKey = new SecretKeySpec(tmp.getEncoded(),
+ secKeyEncryption);
+ } else {
+ secretKey = keyFac.generateSecret(keySpec);
+ }
+ } catch (Exception e) {
+ LoginException le = new LoginException("Cannot login keyring");
+ le.initCause(e);
+ throw le;
+ }
+ return true;
+ }
+
+ public boolean commit() throws LoginException {
+ if (secretKey != null)
+ subject.getPrivateCredentials().add(secretKey);
+ return true;
+ }
+
+ public boolean abort() throws LoginException {
+ return true;
+ }
+
+ public boolean logout() throws LoginException {
+ Set<PasswordBasedEncryption> pbes = subject
+ .getPrivateCredentials(PasswordBasedEncryption.class);
+ pbes.clear();
+ return true;
+ }
+
+}
--- /dev/null
+package org.argeo.util.crypto;
+
+import javax.crypto.spec.PBEKeySpec;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.PasswordCallback;
+
+/**
+ * All information required to set up a {@link PBEKeySpec} bar the password
+ * itself (use a {@link PasswordCallback})
+ */
+public class PBEKeySpecCallback implements Callback {
+ private String secretKeyFactory;
+ private byte[] salt;
+ private Integer iterationCount;
+ /** Can be null for some algorithms */
+ private Integer keyLength;
+ /** Can be null, will trigger secret key encryption if not */
+ private String secretKeyEncryption;
+
+ public void set(String secretKeyFactory, byte[] salt,
+ Integer iterationCount, Integer keyLength,
+ String secretKeyEncryption) {
+ this.secretKeyFactory = secretKeyFactory;
+ this.salt = salt;
+ this.iterationCount = iterationCount;
+ this.keyLength = keyLength;
+ this.secretKeyEncryption = secretKeyEncryption;
+ }
+
+ public String getSecretKeyFactory() {
+ return secretKeyFactory;
+ }
+
+ public byte[] getSalt() {
+ return salt;
+ }
+
+ public Integer getIterationCount() {
+ return iterationCount;
+ }
+
+ public Integer getKeyLength() {
+ return keyLength;
+ }
+
+ public String getSecretKeyEncryption() {
+ return secretKeyEncryption;
+ }
+
+}
private final Cipher dcipher;
public PasswordBasedEncryption(char[] password) {
+ this(password, DEFAULT_SALT_8, DEFAULT_IV_16);
+ }
+
+ public PasswordBasedEncryption(char[] password, byte[] passwordSalt,
+ byte[] initializationVector) {
try {
byte[] salt = new byte[8];
- System.arraycopy(DEFAULT_SALT_8, 0, salt, 0, salt.length);
- for (int i = 0; i < password.length && i < salt.length; i++)
- salt[i] = (byte) password[i];
+ System.arraycopy(passwordSalt, 0, salt, 0, salt.length);
+ // for (int i = 0; i < password.length && i < salt.length; i++)
+ // salt[i] = (byte) password[i];
byte[] iv = new byte[16];
- System.arraycopy(DEFAULT_IV_16, 0, iv, 0, iv.length);
- for (int i = 0; i < password.length && i < iv.length; i++)
- iv[i] = (byte) password[i];
+ System.arraycopy(initializationVector, 0, iv, 0, iv.length);
+ // for (int i = 0; i < password.length && i < iv.length; i++)
+ // iv[i] = (byte) password[i];
SecretKeyFactory keyFac = SecretKeyFactory
.getInstance(getSecretKeyFactoryName());