]> git.argeo.org Git - lgpl/argeo-commons.git/blobdiff - basic/runtime/org.argeo.basic.nodeps/src/main/java/org/argeo/util/crypto/AbstractKeyring.java
Introduce Keyring
[lgpl/argeo-commons.git] / basic / runtime / org.argeo.basic.nodeps / src / main / java / org / argeo / util / crypto / AbstractKeyring.java
diff --git a/basic/runtime/org.argeo.basic.nodeps/src/main/java/org/argeo/util/crypto/AbstractKeyring.java b/basic/runtime/org.argeo.basic.nodeps/src/main/java/org/argeo/util/crypto/AbstractKeyring.java
new file mode 100644 (file)
index 0000000..3e9da4c
--- /dev/null
@@ -0,0 +1,198 @@
+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);
+               }
+
+       }
+}