From: Mathieu Baudier Date: Thu, 11 Oct 2012 18:38:04 +0000 (+0000) Subject: Refactor cryptography and keyring X-Git-Tag: argeo-commons-2.1.30~821 X-Git-Url: https://git.argeo.org/?a=commitdiff_plain;h=9884b3225a86b831917b10376925eebcbf99e513;p=lgpl%2Fargeo-commons.git Refactor cryptography and keyring git-svn-id: https://svn.argeo.org/commons/trunk@5599 4cfe0d0a-d680-48aa-b62c-e0a02a3f76cc --- diff --git a/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/AbstractKeyring.java b/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/AbstractKeyring.java deleted file mode 100644 index e42451325..000000000 --- a/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/AbstractKeyring.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright (C) 2007-2012 Mathieu Baudier - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -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.security.MessageDigest; -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(char[] password); - - /** 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 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); - writer.flush(); - 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; - } - - protected static byte[] hash(char[] password, byte[] salt, - Integer iterationCount) { - ByteArrayOutputStream out = null; - OutputStreamWriter writer = null; - try { - out = new ByteArrayOutputStream(); - writer = new OutputStreamWriter(out, "UTF-8"); - writer.write(password); - MessageDigest pwDigest = MessageDigest.getInstance("SHA-256"); - pwDigest.reset(); - pwDigest.update(salt); - byte[] btPass = pwDigest.digest(out.toByteArray()); - for (int i = 0; i < iterationCount; i++) { - pwDigest.reset(); - btPass = pwDigest.digest(btPass); - } - return btPass; - } catch (Exception e) { - throw new ArgeoException("Cannot hash", e); - } finally { - StreamUtils.closeQuietly(out); - StreamUtils.closeQuietly(writer); - } - - } - - 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 which will protect your private data"); - TextOutputCallback textCb2 = new TextOutputCallback( - TextOutputCallback.INFORMATION, - "(for example your credentials to third-party services)"); - TextOutputCallback textCb3 = new TextOutputCallback( - TextOutputCallback.INFORMATION, - "Don't forget this password since the data cannot be read without it"); - 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(passwordCb.getPassword()); - } - } - - if (passwordCb.getPassword() != null) - handleKeySpecCallback(pbeCb); - } - - } -} diff --git a/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/Keyring.java b/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/Keyring.java deleted file mode 100644 index eb876df54..000000000 --- a/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/Keyring.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2007-2012 Mathieu Baudier - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -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); -} diff --git a/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/KeyringLoginModule.java b/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/KeyringLoginModule.java deleted file mode 100644 index e8b6928f8..000000000 --- a/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/KeyringLoginModule.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2007-2012 Mathieu Baudier - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -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 sharedState, Map options) { - this.subject = subject; - if (subject == null) { - subject = Subject.getSubject(AccessController.getContext()); - } - this.callbackHandler = callbackHandler; - } - - public boolean login() throws LoginException { - Set 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 pbes = subject - .getPrivateCredentials(PasswordBasedEncryption.class); - pbes.clear(); - return true; - } - -} diff --git a/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/PBEKeySpecCallback.java b/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/PBEKeySpecCallback.java deleted file mode 100644 index 676b9706f..000000000 --- a/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/PBEKeySpecCallback.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright (C) 2007-2012 Mathieu Baudier - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -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; - - private String encryptedPasswordHashCipher; - private byte[] encryptedPasswordHash; - - 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; -// this.encryptedPasswordHashCipher = encryptedPasswordHashCipher; -// this.encryptedPasswordHash = encryptedPasswordHash; - } - - 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; - } - - public String getEncryptedPasswordHashCipher() { - return encryptedPasswordHashCipher; - } - - public byte[] getEncryptedPasswordHash() { - return encryptedPasswordHash; - } - -} diff --git a/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/PasswordBasedEncryption.java b/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/PasswordBasedEncryption.java deleted file mode 100644 index 62cc2400b..000000000 --- a/base/runtime/org.argeo.util/src/main/java/org/argeo/util/crypto/PasswordBasedEncryption.java +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright (C) 2007-2012 Mathieu Baudier - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.argeo.util.crypto; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.security.Key; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; - -import org.argeo.ArgeoException; -import org.argeo.StreamUtils; - -/** Simple password based encryption / decryption */ -public class PasswordBasedEncryption { - public final static Integer DEFAULT_ITERATION_COUNT = 1024; - public final static Integer DEFAULT_KEY_LENGTH = 256; - public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1"; - public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES"; - public final static String DEFAULT_CIPHER = "AES/CBC/PKCS5Padding"; - public final static String DEFAULT_CHARSET = "UTF-8"; - - private static byte[] DEFAULT_SALT_8 = { (byte) 0xA9, (byte) 0x9B, - (byte) 0xC8, (byte) 0x32, (byte) 0x56, (byte) 0x35, (byte) 0xE3, - (byte) 0x03 }; - private static byte[] DEFAULT_IV_16 = { (byte) 0xA9, (byte) 0x9B, - (byte) 0xC8, (byte) 0x32, (byte) 0x56, (byte) 0x35, (byte) 0xE3, - (byte) 0x03, (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, - (byte) 0x56, (byte) 0x35, (byte) 0xE3, (byte) 0x03 }; - - private final Key key; - private final Cipher ecipher; - private final Cipher dcipher; - - /** - * This is up to the caller to clear the passed array. Neither copy of nor - * reference to the passed array is kept - */ - public PasswordBasedEncryption(char[] password) { - this(password, DEFAULT_SALT_8, DEFAULT_IV_16); - } - - /** - * This is up to the caller to clear the passed array. Neither copies of nor - * references to the passed arrays are kept - */ - public PasswordBasedEncryption(char[] password, byte[] passwordSalt, - byte[] initializationVector) { - try { - byte[] salt = new byte[8]; - 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(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()); - PBEKeySpec keySpec = new PBEKeySpec(password, salt, - getIterationCount(), getKeyLength()); - String secKeyEncryption = getSecretKeyEncryption(); - if (secKeyEncryption != null) { - SecretKey tmp = keyFac.generateSecret(keySpec); - key = new SecretKeySpec(tmp.getEncoded(), - getSecretKeyEncryption()); - } else { - key = keyFac.generateSecret(keySpec); - } - ecipher = Cipher.getInstance(getCipherName()); - ecipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); - // AlgorithmParameters params = ecipher.getParameters(); - // byte[] iv = - // params.getParameterSpec(IvParameterSpec.class).getIV(); - dcipher = Cipher.getInstance(getCipherName()); - dcipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); - } catch (Exception e) { - throw new ArgeoException("Cannot get secret key", e); - } - } - - public void encrypt(InputStream decryptedIn, OutputStream encryptedOut) - throws IOException { - try { - CipherOutputStream out = new CipherOutputStream(encryptedOut, - ecipher); - StreamUtils.copy(decryptedIn, out); - StreamUtils.closeQuietly(out); - } catch (IOException e) { - throw e; - } catch (Exception e) { - throw new ArgeoException("Cannot encrypt", e); - } finally { - StreamUtils.closeQuietly(decryptedIn); - } - } - - public void decrypt(InputStream encryptedIn, OutputStream decryptedOut) - throws IOException { - try { - CipherInputStream decryptedIn = new CipherInputStream(encryptedIn, - dcipher); - StreamUtils.copy(decryptedIn, decryptedOut); - } catch (IOException e) { - throw e; - } catch (Exception e) { - throw new ArgeoException("Cannot decrypt", e); - } finally { - StreamUtils.closeQuietly(encryptedIn); - } - } - - public byte[] encryptString(String str) { - ByteArrayOutputStream out = null; - ByteArrayInputStream in = null; - try { - out = new ByteArrayOutputStream(); - in = new ByteArrayInputStream(str.getBytes(DEFAULT_CHARSET)); - encrypt(in, out); - return out.toByteArray(); - } catch (Exception e) { - throw new ArgeoException("Cannot encrypt", e); - } finally { - StreamUtils.closeQuietly(out); - } - } - - /** Closes the input stream */ - public String decryptAsString(InputStream in) { - ByteArrayOutputStream out = null; - try { - out = new ByteArrayOutputStream(); - decrypt(in, out); - return new String(out.toByteArray(), DEFAULT_CHARSET); - } catch (Exception e) { - throw new ArgeoException("Cannot decrypt", e); - } finally { - StreamUtils.closeQuietly(out); - } - } - - protected Key getKey() { - return key; - } - - protected Cipher getEcipher() { - return ecipher; - } - - protected Cipher getDcipher() { - return dcipher; - } - - protected Integer getIterationCount() { - return DEFAULT_ITERATION_COUNT; - } - - protected Integer getKeyLength() { - return DEFAULT_KEY_LENGTH; - } - - protected String getSecretKeyFactoryName() { - return DEFAULT_SECRETE_KEY_FACTORY; - } - - protected String getSecretKeyEncryption() { - return DEFAULT_SECRETE_KEY_ENCRYPTION; - } - - protected String getCipherName() { - return DEFAULT_CIPHER; - } -} diff --git a/base/runtime/org.argeo.util/src/main/java/org/argeo/util/security/Keyring.java b/base/runtime/org.argeo.util/src/main/java/org/argeo/util/security/Keyring.java new file mode 100644 index 000000000..552d2ef23 --- /dev/null +++ b/base/runtime/org.argeo.util/src/main/java/org/argeo/util/security/Keyring.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2007-2012 Mathieu Baudier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.argeo.util.security; + +import java.io.InputStream; + +/** + * Access to private (typically encrypted) data. The keyring is responsible for + * retrieving the necessary credentials. Experimental. This API may + * change. + */ +public interface Keyring { + public void changePassword(char[] oldPassword, char[] newPassword); + + /** + * Returns the confidential information as chars. Must ask for it if it is + * not stored. + */ + public char[] getAsChars(String path); + + /** + * Returns the confidential information as a stream. Must ask for it if it + * is not stored. + */ + public InputStream getAsStream(String path); + + public void set(String path, char[] arr); + + public void set(String path, InputStream in); +} diff --git a/base/runtime/org.argeo.util/src/test/java/org/argeo/util/crypto/PasswordBasedEncryptionTest.java b/base/runtime/org.argeo.util/src/test/java/org/argeo/util/crypto/PasswordBasedEncryptionTest.java deleted file mode 100644 index a42444539..000000000 --- a/base/runtime/org.argeo.util/src/test/java/org/argeo/util/crypto/PasswordBasedEncryptionTest.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright (C) 2007-2012 Mathieu Baudier - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.argeo.util.crypto; - -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.PBEParameterSpec; -import javax.crypto.spec.SecretKeySpec; -import javax.xml.bind.DatatypeConverter; - -import junit.framework.TestCase; - -import org.argeo.StreamUtils; - -public class PasswordBasedEncryptionTest extends TestCase { - public void testEncryptDecrypt() { - final String password = "test long password since they are safer"; - PasswordBasedEncryption pbeEnc = new PasswordBasedEncryption( - password.toCharArray()); - String message = "Hello World!"; - System.out.println("Password:\t'" + password + "'"); - System.out.println("Message:\t'" + message + "'"); - byte[] encrypted = pbeEnc.encryptString(message); - System.out.println("Encrypted:\t'" - + DatatypeConverter.printBase64Binary(encrypted) + "'"); - PasswordBasedEncryption pbeDec = new PasswordBasedEncryption( - password.toCharArray()); - InputStream in = null; - in = new ByteArrayInputStream(encrypted); - String decrypted = pbeDec.decryptAsString(in); - System.out.println("Decrypted:\t'" + decrypted + "'"); - StreamUtils.closeQuietly(in); - assertEquals(message, decrypted); - } - - public void testPBEWithMD5AndDES() throws Exception { - String password = "test"; - String message = "Hello World!"; - - byte[] salt = { (byte) 0xc7, (byte) 0x73, (byte) 0x21, (byte) 0x8c, - (byte) 0x7e, (byte) 0xc8, (byte) 0xee, (byte) 0x99 }; - - int count = 1024; - - String cipherAlgorithm = "PBEWithMD5AndDES"; - String secretKeyAlgorithm = "PBEWithMD5AndDES"; - SecretKeyFactory keyFac = SecretKeyFactory - .getInstance(secretKeyAlgorithm); - PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray()); - PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, count); - SecretKey pbeKey = keyFac.generateSecret(pbeKeySpec); - Cipher ecipher = Cipher.getInstance(cipherAlgorithm); - ecipher.init(Cipher.ENCRYPT_MODE, pbeKey, pbeParamSpec); - Cipher dcipher = Cipher.getInstance(cipherAlgorithm); - dcipher.init(Cipher.DECRYPT_MODE, pbeKey, pbeParamSpec); - - byte[] encrypted = ecipher.doFinal(message.getBytes()); - byte[] decrypted = dcipher.doFinal(encrypted); - assertEquals(message, new String(decrypted)); - - } - - public void testPBEWithSHA1AndAES() throws Exception { - String password = "test"; - String message = "Hello World!"; - - byte[] salt = { (byte) 0xc7, (byte) 0x73, (byte) 0x21, (byte) 0x8c, - (byte) 0x7e, (byte) 0xc8, (byte) 0xee, (byte) 0x99 }; - byte[] iv = { (byte) 0xc7, (byte) 0x73, (byte) 0x21, (byte) 0x8c, - (byte) 0x7e, (byte) 0xc8, (byte) 0xee, (byte) 0x99, - (byte) 0xc7, (byte) 0x73, (byte) 0x21, (byte) 0x8c, - (byte) 0x7e, (byte) 0xc8, (byte) 0xee, (byte) 0x99 }; - - int count = 1024; - // int keyLength = 256; - int keyLength = 128; - - String cipherAlgorithm = "AES/CBC/PKCS5Padding"; - String secretKeyAlgorithm = "PBKDF2WithHmacSHA1"; - SecretKeyFactory keyFac = SecretKeyFactory - .getInstance(secretKeyAlgorithm); - PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), salt, - count, keyLength); - SecretKey tmp = keyFac.generateSecret(pbeKeySpec); - SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES"); - Cipher ecipher = Cipher.getInstance(cipherAlgorithm); - ecipher.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(iv)); - - // decrypt - keyFac = SecretKeyFactory.getInstance(secretKeyAlgorithm); - pbeKeySpec = new PBEKeySpec(password.toCharArray(), salt, count, - keyLength); - tmp = keyFac.generateSecret(pbeKeySpec); - secret = new SecretKeySpec(tmp.getEncoded(), "AES"); - // AlgorithmParameters params = ecipher.getParameters(); - // byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV(); - Cipher dcipher = Cipher.getInstance(cipherAlgorithm); - dcipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv)); - - byte[] encrypted = ecipher.doFinal(message.getBytes()); - byte[] decrypted = dcipher.doFinal(encrypted); - assertEquals(message, new String(decrypted)); - - ByteArrayOutputStream out = new ByteArrayOutputStream(); - CipherOutputStream cipherOut = new CipherOutputStream(out, ecipher); - cipherOut.write(message.getBytes()); - StreamUtils.closeQuietly(cipherOut); - byte[] enc = out.toByteArray(); - - ByteArrayInputStream in = new ByteArrayInputStream(enc); - CipherInputStream cipherIn = new CipherInputStream(in, dcipher); - ByteArrayOutputStream dec = new ByteArrayOutputStream(); - StreamUtils.copy(cipherIn, dec); - assertEquals(message, new String(dec.toByteArray())); - } -} diff --git a/security/plugins/org.argeo.security.ui.rap/META-INF/jaas_default.txt b/security/plugins/org.argeo.security.ui.rap/META-INF/jaas_default.txt index 3829f93bb..c74797b93 100644 --- a/security/plugins/org.argeo.security.ui.rap/META-INF/jaas_default.txt +++ b/security/plugins/org.argeo.security.ui.rap/META-INF/jaas_default.txt @@ -19,5 +19,5 @@ SPRING_SECURITY_CONTEXT { }; KEYRING { - org.argeo.util.crypto.KeyringLoginModule required; + org.argeo.security.crypto.KeyringLoginModule required; }; diff --git a/security/plugins/org.argeo.security.ui.rcp/META-INF/jaas_default.txt b/security/plugins/org.argeo.security.ui.rcp/META-INF/jaas_default.txt index 124d7ad9f..16d476daf 100644 --- a/security/plugins/org.argeo.security.ui.rcp/META-INF/jaas_default.txt +++ b/security/plugins/org.argeo.security.ui.rcp/META-INF/jaas_default.txt @@ -23,5 +23,5 @@ REMOTE { }; KEYRING { - org.argeo.util.crypto.KeyringLoginModule required; + org.argeo.security.crypto.KeyringLoginModule required; }; diff --git a/security/runtime/org.argeo.security.core/pom.xml b/security/runtime/org.argeo.security.core/pom.xml index abb559ad7..527d052fd 100644 --- a/security/runtime/org.argeo.security.core/pom.xml +++ b/security/runtime/org.argeo.security.core/pom.xml @@ -68,10 +68,10 @@ - - - - + + org.argeo.tp + bcprov + org.argeo.tp org.apache.commons.codec diff --git a/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/crypto/AbstractKeyring.java b/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/crypto/AbstractKeyring.java new file mode 100644 index 000000000..b69fff95a --- /dev/null +++ b/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/crypto/AbstractKeyring.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2007-2012 Mathieu Baudier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.argeo.security.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.security.MessageDigest; +import java.security.Provider; +import java.security.Security; +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; +import org.argeo.util.security.Keyring; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +/** username / password based keyring. TODO internationalize */ +public abstract class AbstractKeyring implements Keyring { + static { + Security.addProvider(new BouncyCastleProvider()); + } + + public final static String DEFAULT_KEYRING_LOGIN_CONTEXT = "KEYRING"; + + private String loginContextName = DEFAULT_KEYRING_LOGIN_CONTEXT; + private CallbackHandler defaultCallbackHandler; + + private String charset = "UTF-8"; + + /** + * Default provider is bouncy castle, in order to have consistent behaviour + * across implementations + */ + private String securityProviderName = "BC"; + + /** + * 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(char[] password); + + /** 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 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); + writer.flush(); + 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); + } + } + + protected Provider getSecurityProvider() { + return Security.getProvider(securityProviderName); + } + + public void setLoginContextName(String loginContextName) { + this.loginContextName = loginContextName; + } + + public void setDefaultCallbackHandler(CallbackHandler defaultCallbackHandler) { + this.defaultCallbackHandler = defaultCallbackHandler; + } + + public void setCharset(String charset) { + this.charset = charset; + } + + public void setSecurityProviderName(String securityProviderName) { + this.securityProviderName = securityProviderName; + } + + @Deprecated + protected static byte[] hash(char[] password, byte[] salt, + Integer iterationCount) { + ByteArrayOutputStream out = null; + OutputStreamWriter writer = null; + try { + out = new ByteArrayOutputStream(); + writer = new OutputStreamWriter(out, "UTF-8"); + writer.write(password); + MessageDigest pwDigest = MessageDigest.getInstance("SHA-256"); + pwDigest.reset(); + pwDigest.update(salt); + byte[] btPass = pwDigest.digest(out.toByteArray()); + for (int i = 0; i < iterationCount; i++) { + pwDigest.reset(); + btPass = pwDigest.digest(btPass); + } + return btPass; + } catch (Exception e) { + throw new ArgeoException("Cannot hash", e); + } finally { + StreamUtils.closeQuietly(out); + StreamUtils.closeQuietly(writer); + } + + } + + /** + * Convenience method using the underlying callback to ask for a password + * (typically used when the password is not saved in the keyring) + */ + protected char[] ask() { + PasswordCallback passwordCb = new PasswordCallback("Password", false); + Callback[] dialogCbs = new Callback[] { passwordCb }; + try { + defaultCallbackHandler.handle(dialogCbs); + char[] password = passwordCb.getPassword(); + return password; + } catch (Exception e) { + throw new ArgeoException("Cannot ask for a password", e); + } + + } + + 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 which will protect your private data"); + TextOutputCallback textCb2 = new TextOutputCallback( + TextOutputCallback.INFORMATION, + "(for example your credentials to third-party services)"); + TextOutputCallback textCb3 = new TextOutputCallback( + TextOutputCallback.INFORMATION, + "Don't forget this password since the data cannot be read without it"); + 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(passwordCb.getPassword()); + } + } + + if (passwordCb.getPassword() != null) + handleKeySpecCallback(pbeCb); + } + + } +} diff --git a/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/crypto/KeyringLoginModule.java b/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/crypto/KeyringLoginModule.java new file mode 100644 index 000000000..24e0f1e9b --- /dev/null +++ b/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/crypto/KeyringLoginModule.java @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2007-2012 Mathieu Baudier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.argeo.security.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 sharedState, Map options) { + this.subject = subject; + if (subject == null) { + subject = Subject.getSubject(AccessController.getContext()); + } + this.callbackHandler = callbackHandler; + } + + public boolean login() throws LoginException { + Set 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 pbes = subject + .getPrivateCredentials(PasswordBasedEncryption.class); + pbes.clear(); + return true; + } + +} diff --git a/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/crypto/PBEKeySpecCallback.java b/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/crypto/PBEKeySpecCallback.java new file mode 100644 index 000000000..111212387 --- /dev/null +++ b/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/crypto/PBEKeySpecCallback.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2007-2012 Mathieu Baudier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.argeo.security.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; + + private String encryptedPasswordHashCipher; + private byte[] encryptedPasswordHash; + + 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; +// this.encryptedPasswordHashCipher = encryptedPasswordHashCipher; +// this.encryptedPasswordHash = encryptedPasswordHash; + } + + 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; + } + + public String getEncryptedPasswordHashCipher() { + return encryptedPasswordHashCipher; + } + + public byte[] getEncryptedPasswordHash() { + return encryptedPasswordHash; + } + +} diff --git a/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/crypto/PasswordBasedEncryption.java b/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/crypto/PasswordBasedEncryption.java new file mode 100644 index 000000000..63cdc6c0c --- /dev/null +++ b/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/crypto/PasswordBasedEncryption.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2007-2012 Mathieu Baudier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.argeo.security.crypto; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.Key; +import java.security.Security; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; + +import org.argeo.ArgeoException; +import org.argeo.StreamUtils; +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +/** Simple password based encryption / decryption */ +public class PasswordBasedEncryption { + static { + Security.addProvider(new BouncyCastleProvider()); + } + + public final static Integer DEFAULT_ITERATION_COUNT = 1024; + public final static Integer DEFAULT_KEY_LENGTH = 256; + public final static String DEFAULT_SECRETE_KEY_FACTORY = "PBKDF2WithHmacSHA1"; + public final static String DEFAULT_SECRETE_KEY_ENCRYPTION = "AES"; + public final static String DEFAULT_CIPHER = "AES/CBC/PKCS5Padding"; + public final static String DEFAULT_CHARSET = "UTF-8"; + + private static byte[] DEFAULT_SALT_8 = { (byte) 0xA9, (byte) 0x9B, + (byte) 0xC8, (byte) 0x32, (byte) 0x56, (byte) 0x35, (byte) 0xE3, + (byte) 0x03 }; + private static byte[] DEFAULT_IV_16 = { (byte) 0xA9, (byte) 0x9B, + (byte) 0xC8, (byte) 0x32, (byte) 0x56, (byte) 0x35, (byte) 0xE3, + (byte) 0x03, (byte) 0xA9, (byte) 0x9B, (byte) 0xC8, (byte) 0x32, + (byte) 0x56, (byte) 0x35, (byte) 0xE3, (byte) 0x03 }; + + private final Key key; + private final Cipher ecipher; + private final Cipher dcipher; + + /** + * Default provider is bouncy castle, in order to have consistent behaviour + * across implementations + */ + private String securityProviderName = "BC"; + + /** + * This is up to the caller to clear the passed array. Neither copy of nor + * reference to the passed array is kept + */ + public PasswordBasedEncryption(char[] password) { + this(password, DEFAULT_SALT_8, DEFAULT_IV_16); + } + + /** + * This is up to the caller to clear the passed array. Neither copies of nor + * references to the passed arrays are kept + */ + public PasswordBasedEncryption(char[] password, byte[] passwordSalt, + byte[] initializationVector) { + try { + byte[] salt = new byte[8]; + 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(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()); + PBEKeySpec keySpec = new PBEKeySpec(password, salt, + getIterationCount(), getKeyLength()); + String secKeyEncryption = getSecretKeyEncryption(); + if (secKeyEncryption != null) { + SecretKey tmp = keyFac.generateSecret(keySpec); + key = new SecretKeySpec(tmp.getEncoded(), + getSecretKeyEncryption()); + } else { + key = keyFac.generateSecret(keySpec); + } + ecipher = Cipher.getInstance(getCipherName(), securityProviderName); + ecipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv)); + // AlgorithmParameters params = ecipher.getParameters(); + // byte[] iv = + // params.getParameterSpec(IvParameterSpec.class).getIV(); + dcipher = Cipher.getInstance(getCipherName()); + dcipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv)); + } catch (Exception e) { + throw new ArgeoException("Cannot get secret key", e); + } + } + + public void encrypt(InputStream decryptedIn, OutputStream encryptedOut) + throws IOException { + try { + CipherOutputStream out = new CipherOutputStream(encryptedOut, + ecipher); + StreamUtils.copy(decryptedIn, out); + StreamUtils.closeQuietly(out); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new ArgeoException("Cannot encrypt", e); + } finally { + StreamUtils.closeQuietly(decryptedIn); + } + } + + public void decrypt(InputStream encryptedIn, OutputStream decryptedOut) + throws IOException { + try { + CipherInputStream decryptedIn = new CipherInputStream(encryptedIn, + dcipher); + StreamUtils.copy(decryptedIn, decryptedOut); + } catch (IOException e) { + throw e; + } catch (Exception e) { + throw new ArgeoException("Cannot decrypt", e); + } finally { + StreamUtils.closeQuietly(encryptedIn); + } + } + + public byte[] encryptString(String str) { + ByteArrayOutputStream out = null; + ByteArrayInputStream in = null; + try { + out = new ByteArrayOutputStream(); + in = new ByteArrayInputStream(str.getBytes(DEFAULT_CHARSET)); + encrypt(in, out); + return out.toByteArray(); + } catch (Exception e) { + throw new ArgeoException("Cannot encrypt", e); + } finally { + StreamUtils.closeQuietly(out); + } + } + + /** Closes the input stream */ + public String decryptAsString(InputStream in) { + ByteArrayOutputStream out = null; + try { + out = new ByteArrayOutputStream(); + decrypt(in, out); + return new String(out.toByteArray(), DEFAULT_CHARSET); + } catch (Exception e) { + throw new ArgeoException("Cannot decrypt", e); + } finally { + StreamUtils.closeQuietly(out); + } + } + + protected Key getKey() { + return key; + } + + protected Cipher getEcipher() { + return ecipher; + } + + protected Cipher getDcipher() { + return dcipher; + } + + protected Integer getIterationCount() { + return DEFAULT_ITERATION_COUNT; + } + + protected Integer getKeyLength() { + return DEFAULT_KEY_LENGTH; + } + + protected String getSecretKeyFactoryName() { + return DEFAULT_SECRETE_KEY_FACTORY; + } + + protected String getSecretKeyEncryption() { + return DEFAULT_SECRETE_KEY_ENCRYPTION; + } + + protected String getCipherName() { + return DEFAULT_CIPHER; + } + + public void setSecurityProviderName(String securityProviderName) { + this.securityProviderName = securityProviderName; + } +} diff --git a/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/jcr/JcrKeyring.java b/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/jcr/JcrKeyring.java new file mode 100644 index 000000000..04974bdd7 --- /dev/null +++ b/security/runtime/org.argeo.security.core/src/main/java/org/argeo/security/jcr/JcrKeyring.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2007-2012 Mathieu Baudier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.argeo.security.jcr; + +import java.io.ByteArrayInputStream; +import java.io.CharArrayReader; +import java.io.InputStream; +import java.io.Reader; +import java.security.SecureRandom; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.jcr.Binary; +import javax.jcr.Node; +import javax.jcr.Property; +import javax.jcr.RepositoryException; +import javax.jcr.Session; + +import org.apache.commons.io.IOUtils; +import org.argeo.ArgeoException; +import org.argeo.jcr.ArgeoNames; +import org.argeo.jcr.ArgeoTypes; +import org.argeo.jcr.JcrUtils; +import org.argeo.jcr.UserJcrUtils; +import org.argeo.security.crypto.AbstractKeyring; +import org.argeo.security.crypto.PBEKeySpecCallback; + +/** JCR based implementation of a keyring */ +public class JcrKeyring extends AbstractKeyring implements ArgeoNames { + private Session session; + + /** + * When setup is called the session has not yet been saved and we don't want + * to save it since there maybe other data which would be inconsistent. So + * we keep a reference to this node which will then be used (an reset to + * null) when handling the PBE callback. We keep one per thread in case + * multiple users are accessing the same instance of a keyring. + */ + private ThreadLocal notYetSavedKeyring = new ThreadLocal() { + + @Override + protected Node initialValue() { + return null; + } + }; + + @Override + protected Boolean isSetup() { + try { + if (notYetSavedKeyring.get() != null) + return true; + + Node userHome = UserJcrUtils.getUserHome(session); + return userHome.hasNode(ARGEO_KEYRING); + } catch (RepositoryException e) { + throw new ArgeoException("Cannot check whether keyring is setup", e); + } + } + + @Override + protected void setup(char[] password) { + Binary binary = null; + InputStream in = null; + try { + Node userHome = UserJcrUtils.getUserHome(session); + if (userHome.hasNode(ARGEO_KEYRING)) + throw new ArgeoException("Keyring already setup"); + Node keyring = userHome.addNode(ARGEO_KEYRING); + keyring.addMixin(ArgeoTypes.ARGEO_PBE_SPEC); + + // deterministic salt and iteration count based on username + String username = session.getUserID(); + byte[] salt = new byte[8]; + byte[] usernameBytes = username.getBytes(); + for (int i = 0; i < salt.length; i++) { + if (i < usernameBytes.length) + salt[i] = usernameBytes[i]; + else + salt[i] = 0; + } + in = new ByteArrayInputStream(salt); + binary = session.getValueFactory().createBinary(in); + keyring.setProperty(ARGEO_SALT, binary); + + Integer iterationCount = username.length() * 200; + keyring.setProperty(ARGEO_ITERATION_COUNT, iterationCount); + + // default algo + // TODO check if algo and key length are available, use DES if not + keyring.setProperty(ARGEO_SECRET_KEY_FACTORY, "PBKDF2WithHmacSHA1"); + keyring.setProperty(ARGEO_KEY_LENGTH, 256l); + keyring.setProperty(ARGEO_SECRET_KEY_ENCRYPTION, "AES"); + keyring.setProperty(ARGEO_CIPHER, "AES/CBC/PKCS5Padding"); + + // encrypted password hash + // IOUtils.closeQuietly(in); + // JcrUtils.closeQuietly(binary); + // byte[] btPass = hash(password, salt, iterationCount); + // in = new ByteArrayInputStream(btPass); + // binary = session.getValueFactory().createBinary(in); + // keyring.setProperty(ARGEO_PASSWORD, binary); + + notYetSavedKeyring.set(keyring); + } catch (Exception e) { + throw new ArgeoException("Cannot setup keyring", e); + } finally { + JcrUtils.closeQuietly(binary); + IOUtils.closeQuietly(in); + // JcrUtils.discardQuietly(session); + } + } + + @Override + protected void handleKeySpecCallback(PBEKeySpecCallback pbeCallback) { + try { + Node userHome = UserJcrUtils.getUserHome(session); + Node keyring; + if (userHome.hasNode(ARGEO_KEYRING)) + keyring = userHome.getNode(ARGEO_KEYRING); + else if (notYetSavedKeyring.get() != null) + keyring = notYetSavedKeyring.get(); + else + throw new ArgeoException("Keyring not setup"); + + pbeCallback.set(keyring.getProperty(ARGEO_SECRET_KEY_FACTORY) + .getString(), JcrUtils.getBinaryAsBytes(keyring + .getProperty(ARGEO_SALT)), + (int) keyring.getProperty(ARGEO_ITERATION_COUNT).getLong(), + (int) keyring.getProperty(ARGEO_KEY_LENGTH).getLong(), + keyring.getProperty(ARGEO_SECRET_KEY_ENCRYPTION) + .getString()); + + if (notYetSavedKeyring.get() != null) + notYetSavedKeyring.remove(); + } catch (RepositoryException e) { + throw new ArgeoException("Cannot handle key spec callback", e); + } + } + + /** The parent node must already exist at this path. */ + @Override + protected synchronized void encrypt(String path, InputStream unencrypted) { + // should be called first for lazy initialization + SecretKey secretKey = getSecretKey(); + + Binary binary = null; + InputStream in = null; + try { + Cipher cipher = createCipher(); + Node node; + if (!session.nodeExists(path)) { + String parentPath = JcrUtils.parentPath(path); + if (!session.nodeExists(parentPath)) + throw new ArgeoException("No parent node of " + path); + Node parentNode = session.getNode(parentPath); + node = parentNode.addNode(JcrUtils.nodeNameFromPath(path)); + } else { + node = session.getNode(path); + } + node.addMixin(ArgeoTypes.ARGEO_ENCRYPTED); + SecureRandom random = new SecureRandom(); + byte[] iv = new byte[16]; + random.nextBytes(iv); + cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv)); + JcrUtils.setBinaryAsBytes(node, ARGEO_IV, iv); + + in = new CipherInputStream(unencrypted, cipher); + binary = session.getValueFactory().createBinary(in); + node.setProperty(Property.JCR_DATA, binary); + session.save(); + } catch (Exception e) { + throw new ArgeoException("Cannot encrypt", e); + } finally { + IOUtils.closeQuietly(unencrypted); + IOUtils.closeQuietly(in); + JcrUtils.closeQuietly(binary); + } + } + + @Override + protected synchronized InputStream decrypt(String path) { + Binary binary = null; + InputStream encrypted = null; + Reader reader = null; + try { + if (!session.nodeExists(path)) { + char[] password = ask(); + reader = new CharArrayReader(password); + return new ByteArrayInputStream(IOUtils.toByteArray(reader)); + } else { + // should be called first for lazy initialisation + SecretKey secretKey = getSecretKey(); + + Cipher cipher = createCipher(); + + Node node = session.getNode(path); + if (node.hasProperty(ARGEO_IV)) { + byte[] iv = JcrUtils.getBinaryAsBytes(node + .getProperty(ARGEO_IV)); + cipher.init(Cipher.DECRYPT_MODE, secretKey, + new IvParameterSpec(iv)); + } else { + cipher.init(Cipher.DECRYPT_MODE, secretKey); + } + + binary = node.getProperty(Property.JCR_DATA).getBinary(); + encrypted = binary.getStream(); + return new CipherInputStream(encrypted, cipher); + } + } catch (Exception e) { + throw new ArgeoException("Cannot decrypt", e); + } finally { + IOUtils.closeQuietly(encrypted); + IOUtils.closeQuietly(reader); + JcrUtils.closeQuietly(binary); + } + } + + protected Cipher createCipher() { + try { + Node userHome = UserJcrUtils.getUserHome(session); + if (!userHome.hasNode(ARGEO_KEYRING)) + throw new ArgeoException("Keyring not setup"); + Node keyring = userHome.getNode(ARGEO_KEYRING); + Cipher cipher = Cipher.getInstance(keyring + .getProperty(ARGEO_CIPHER).getString(), + getSecurityProvider()); + return cipher; + } catch (Exception e) { + throw new ArgeoException("Cannot get cipher", e); + } + } + + public synchronized void changePassword(char[] oldPassword, + char[] newPassword) { + // TODO decrypt with old pw / encrypt with new pw all argeo:encrypted + } + + public synchronized void setSession(Session session) { + this.session = session; + } +} \ No newline at end of file diff --git a/security/runtime/org.argeo.security.core/src/test/java/org/argeo/security/crypto/ListBCCapabilities.java b/security/runtime/org.argeo.security.core/src/test/java/org/argeo/security/crypto/ListBCCapabilities.java new file mode 100644 index 000000000..a95fba274 --- /dev/null +++ b/security/runtime/org.argeo.security.core/src/test/java/org/argeo/security/crypto/ListBCCapabilities.java @@ -0,0 +1,43 @@ +package org.argeo.security.crypto; + +import java.security.Provider; +import java.security.Security; +import java.util.Iterator; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; + +/** + * List the available capabilities for ciphers, key agreement, macs, message + * digests, signatures and other objects in the BC provider. + */ +public class ListBCCapabilities { + public static void main(String[] args) { + Security.addProvider(new BouncyCastleProvider()); + + Provider[] providers = Security.getProviders(); + for (Provider provider : providers) { + System.out.println(provider.getName()); + System.out.println(" " + provider.getVersion()); + System.out.println(" " + provider.getInfo()); + } + Provider provider = Security.getProvider("BC"); + // Provider provider = Security.getProvider(null); + + Iterator it = provider.keySet().iterator(); + + while (it.hasNext()) { + String entry = (String) it.next(); + + // this indicates the entry refers to another entry + + if (entry.startsWith("Alg.Alias.")) { + entry = entry.substring("Alg.Alias.".length()); + } + + String factoryClass = entry.substring(0, entry.indexOf('.')); + String name = entry.substring(factoryClass.length() + 1); + + System.out.println(factoryClass + ": " + name); + } + } +} diff --git a/security/runtime/org.argeo.security.core/src/test/java/org/argeo/security/crypto/PasswordBasedEncryptionTest.java b/security/runtime/org.argeo.security.core/src/test/java/org/argeo/security/crypto/PasswordBasedEncryptionTest.java new file mode 100644 index 000000000..bea93188f --- /dev/null +++ b/security/runtime/org.argeo.security.core/src/test/java/org/argeo/security/crypto/PasswordBasedEncryptionTest.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2007-2012 Mathieu Baudier + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.argeo.security.crypto; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.PBEParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import javax.xml.bind.DatatypeConverter; + +import junit.framework.TestCase; + +import org.argeo.StreamUtils; +import org.argeo.security.crypto.PasswordBasedEncryption; + +public class PasswordBasedEncryptionTest extends TestCase { + public void testEncryptDecrypt() { + final String password = "test long password since they are safer"; + PasswordBasedEncryption pbeEnc = new PasswordBasedEncryption( + password.toCharArray()); + String message = "Hello World!"; + System.out.println("Password:\t'" + password + "'"); + System.out.println("Message:\t'" + message + "'"); + byte[] encrypted = pbeEnc.encryptString(message); + System.out.println("Encrypted:\t'" + + DatatypeConverter.printBase64Binary(encrypted) + "'"); + PasswordBasedEncryption pbeDec = new PasswordBasedEncryption( + password.toCharArray()); + InputStream in = null; + in = new ByteArrayInputStream(encrypted); + String decrypted = pbeDec.decryptAsString(in); + System.out.println("Decrypted:\t'" + decrypted + "'"); + StreamUtils.closeQuietly(in); + assertEquals(message, decrypted); + } + + public void testPBEWithMD5AndDES() throws Exception { + String password = "test"; + String message = "Hello World!"; + + byte[] salt = { (byte) 0xc7, (byte) 0x73, (byte) 0x21, (byte) 0x8c, + (byte) 0x7e, (byte) 0xc8, (byte) 0xee, (byte) 0x99 }; + + int count = 1024; + + String cipherAlgorithm = "PBEWithMD5AndDES"; + String secretKeyAlgorithm = "PBEWithMD5AndDES"; + SecretKeyFactory keyFac = SecretKeyFactory + .getInstance(secretKeyAlgorithm); + PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray()); + PBEParameterSpec pbeParamSpec = new PBEParameterSpec(salt, count); + SecretKey pbeKey = keyFac.generateSecret(pbeKeySpec); + Cipher ecipher = Cipher.getInstance(cipherAlgorithm); + ecipher.init(Cipher.ENCRYPT_MODE, pbeKey, pbeParamSpec); + Cipher dcipher = Cipher.getInstance(cipherAlgorithm); + dcipher.init(Cipher.DECRYPT_MODE, pbeKey, pbeParamSpec); + + byte[] encrypted = ecipher.doFinal(message.getBytes()); + byte[] decrypted = dcipher.doFinal(encrypted); + assertEquals(message, new String(decrypted)); + + } + + public void testPBEWithSHA1AndAES() throws Exception { + String password = "test"; + String message = "Hello World!"; + + byte[] salt = { (byte) 0xc7, (byte) 0x73, (byte) 0x21, (byte) 0x8c, + (byte) 0x7e, (byte) 0xc8, (byte) 0xee, (byte) 0x99 }; + byte[] iv = { (byte) 0xc7, (byte) 0x73, (byte) 0x21, (byte) 0x8c, + (byte) 0x7e, (byte) 0xc8, (byte) 0xee, (byte) 0x99, + (byte) 0xc7, (byte) 0x73, (byte) 0x21, (byte) 0x8c, + (byte) 0x7e, (byte) 0xc8, (byte) 0xee, (byte) 0x99 }; + + int count = 1024; + // int keyLength = 256; + int keyLength = 128; + + String cipherAlgorithm = "AES/CBC/PKCS5Padding"; + String secretKeyAlgorithm = "PBKDF2WithHmacSHA1"; + SecretKeyFactory keyFac = SecretKeyFactory + .getInstance(secretKeyAlgorithm); + PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray(), salt, + count, keyLength); + SecretKey tmp = keyFac.generateSecret(pbeKeySpec); + SecretKey secret = new SecretKeySpec(tmp.getEncoded(), "AES"); + Cipher ecipher = Cipher.getInstance(cipherAlgorithm); + ecipher.init(Cipher.ENCRYPT_MODE, secret, new IvParameterSpec(iv)); + + // decrypt + keyFac = SecretKeyFactory.getInstance(secretKeyAlgorithm); + pbeKeySpec = new PBEKeySpec(password.toCharArray(), salt, count, + keyLength); + tmp = keyFac.generateSecret(pbeKeySpec); + secret = new SecretKeySpec(tmp.getEncoded(), "AES"); + // AlgorithmParameters params = ecipher.getParameters(); + // byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV(); + Cipher dcipher = Cipher.getInstance(cipherAlgorithm); + dcipher.init(Cipher.DECRYPT_MODE, secret, new IvParameterSpec(iv)); + + byte[] encrypted = ecipher.doFinal(message.getBytes()); + byte[] decrypted = dcipher.doFinal(encrypted); + assertEquals(message, new String(decrypted)); + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + CipherOutputStream cipherOut = new CipherOutputStream(out, ecipher); + cipherOut.write(message.getBytes()); + StreamUtils.closeQuietly(cipherOut); + byte[] enc = out.toByteArray(); + + ByteArrayInputStream in = new ByteArrayInputStream(enc); + CipherInputStream cipherIn = new CipherInputStream(in, dcipher); + ByteArrayOutputStream dec = new ByteArrayOutputStream(); + StreamUtils.copy(cipherIn, dec); + assertEquals(message, new String(dec.toByteArray())); + } +} diff --git a/server/plugins/org.argeo.jcr.ui.explorer/META-INF/spring/commands.xml b/server/plugins/org.argeo.jcr.ui.explorer/META-INF/spring/commands.xml index e3bad2892..5e4a185c7 100644 --- a/server/plugins/org.argeo.jcr.ui.explorer/META-INF/spring/commands.xml +++ b/server/plugins/org.argeo.jcr.ui.explorer/META-INF/spring/commands.xml @@ -22,10 +22,12 @@ - + + - + - + diff --git a/server/plugins/org.argeo.jcr.ui.explorer/META-INF/spring/views.xml b/server/plugins/org.argeo.jcr.ui.explorer/META-INF/spring/views.xml index 84520ef55..14f1943d7 100644 --- a/server/plugins/org.argeo.jcr.ui.explorer/META-INF/spring/views.xml +++ b/server/plugins/org.argeo.jcr.ui.explorer/META-INF/spring/views.xml @@ -7,8 +7,9 @@ - + + diff --git a/server/plugins/org.argeo.jcr.ui.explorer/pom.xml b/server/plugins/org.argeo.jcr.ui.explorer/pom.xml index 3f92ce1ab..4fdd06434 100644 --- a/server/plugins/org.argeo.jcr.ui.explorer/pom.xml +++ b/server/plugins/org.argeo.jcr.ui.explorer/pom.xml @@ -1,4 +1,5 @@ - + 4.0.0 org.argeo.commons.server @@ -25,7 +26,6 @@ *, org.argeo.eclipse.spring, - org.argeo.util.crypto, org.argeo.jcr.ui.explorer.* @@ -34,6 +34,11 @@ + + org.argeo.commons.base + org.argeo.util + 1.1.4-SNAPSHOT + org.argeo.commons.base org.argeo.eclipse.ui.jcr diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/NodeContentProvider.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/NodeContentProvider.java index 34466d301..c438463d8 100644 --- a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/NodeContentProvider.java +++ b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/browser/NodeContentProvider.java @@ -27,10 +27,10 @@ import org.argeo.eclipse.ui.TreeParent; import org.argeo.jcr.ArgeoJcrConstants; import org.argeo.jcr.RepositoryRegister; import org.argeo.jcr.UserJcrUtils; -import org.argeo.jcr.security.JcrKeyring; import org.argeo.jcr.ui.explorer.model.RepositoriesNode; import org.argeo.jcr.ui.explorer.model.SingleJcrNode; import org.argeo.jcr.ui.explorer.utils.TreeObjectsComparator; +import org.argeo.util.security.Keyring; import org.eclipse.jface.viewers.ITreeContentProvider; import org.eclipse.jface.viewers.Viewer; @@ -40,14 +40,14 @@ import org.eclipse.jface.viewers.Viewer; * */ public class NodeContentProvider implements ITreeContentProvider { - // private final static Log log = - // LogFactory.getLog(NodeContentProvider.class); - - // Business Objects final private RepositoryRegister repositoryRegister; final private RepositoryFactory repositoryFactory; + /** + * A session of the logged in user on the default workspace of the node + * repository. + */ final private Session userSession; - final private JcrKeyring jcrKeyring; + final private Keyring keyring; final private boolean sortChildren; // reference for cleaning @@ -57,11 +57,11 @@ public class NodeContentProvider implements ITreeContentProvider { // Utils private TreeObjectsComparator itemComparator = new TreeObjectsComparator(); - public NodeContentProvider(JcrKeyring jcrKeyring, + public NodeContentProvider(Session userSession, Keyring keyring, RepositoryRegister repositoryRegister, RepositoryFactory repositoryFactory, Boolean sortChildren) { - this.userSession = jcrKeyring != null ? jcrKeyring.getSession() : null; - this.jcrKeyring = jcrKeyring; + this.userSession = userSession; + this.keyring = keyring; this.repositoryRegister = repositoryRegister; this.repositoryFactory = repositoryFactory; this.sortChildren = sortChildren; @@ -85,7 +85,8 @@ public class NodeContentProvider implements ITreeContentProvider { if (repositoriesNode != null) repositoriesNode.dispose(); repositoriesNode = new RepositoriesNode("Repositories", - repositoryRegister, repositoryFactory, null, jcrKeyring); + repositoryRegister, repositoryFactory, null, userSession, + keyring); } } diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/AddRemoteRepository.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/AddRemoteRepository.java index b7d4e030a..6187f40ee 100644 --- a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/AddRemoteRepository.java +++ b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/commands/AddRemoteRepository.java @@ -31,8 +31,8 @@ import org.argeo.jcr.ArgeoNames; import org.argeo.jcr.ArgeoTypes; import org.argeo.jcr.JcrUtils; import org.argeo.jcr.UserJcrUtils; -import org.argeo.jcr.security.JcrKeyring; import org.argeo.jcr.ui.explorer.JcrExplorerConstants; +import org.argeo.util.security.Keyring; import org.eclipse.core.commands.AbstractHandler; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; @@ -62,7 +62,8 @@ public class AddRemoteRepository extends AbstractHandler implements JcrExplorerConstants, ArgeoNames { private RepositoryFactory repositoryFactory; - private JcrKeyring keyring; + private Repository nodeRepository; + private Keyring keyring; public Object execute(ExecutionEvent event) throws ExecutionException { RemoteRepositoryLoginDialog dlg = new RemoteRepositoryLoginDialog( @@ -76,10 +77,14 @@ public class AddRemoteRepository extends AbstractHandler implements this.repositoryFactory = repositoryFactory; } - public void setKeyring(JcrKeyring keyring) { + public void setKeyring(Keyring keyring) { this.keyring = keyring; } + public void setNodeRepository(Repository nodeRepository) { + this.nodeRepository = nodeRepository; + } + class RemoteRepositoryLoginDialog extends TitleAreaDialog { private Text name; private Text uri; @@ -158,7 +163,7 @@ public class AddRemoteRepository extends AbstractHandler implements @Override protected void okPressed() { try { - Session nodeSession = keyring.getSession(); + Session nodeSession = nodeRepository.login(); Node home = UserJcrUtils.getUserHome(nodeSession); Node remote = home.hasNode(ARGEO_REMOTE) ? home @@ -171,11 +176,13 @@ public class AddRemoteRepository extends AbstractHandler implements ArgeoTypes.ARGEO_REMOTE_REPOSITORY); remoteRepository.setProperty(ARGEO_URI, uri.getText()); remoteRepository.setProperty(ARGEO_USER_ID, username.getText()); - Node pwd = remoteRepository.addNode(ARGEO_PASSWORD); - pwd.getSession().save(); - if (saveInKeyring.getSelection()) - keyring.set(pwd.getPath(), password.getText().toCharArray()); - keyring.getSession().save(); + nodeSession.save(); + if (saveInKeyring.getSelection()) { + String pwdPath = remoteRepository.getPath() + '/' + + ARGEO_PASSWORD; + keyring.set(pwdPath, password.getText().toCharArray()); + } + nodeSession.save(); MessageDialog.openInformation( getParentShell(), "Repository Added", diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/model/RemoteRepositoryNode.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/model/RemoteRepositoryNode.java index 8f71ace0a..19ca3990b 100644 --- a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/model/RemoteRepositoryNode.java +++ b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/model/RemoteRepositoryNode.java @@ -26,28 +26,37 @@ import javax.jcr.SimpleCredentials; import org.argeo.ArgeoException; import org.argeo.eclipse.ui.TreeParent; import org.argeo.jcr.ArgeoNames; -import org.argeo.jcr.security.JcrKeyring; +import org.argeo.util.security.Keyring; /** Root of a remote repository */ public class RemoteRepositoryNode extends RepositoryNode { - private JcrKeyring jcrKeyring; - private String remoteNodePath; + private final Keyring keyring; + /** + * A session of the logged in user on the default workspace of the node + * repository. + */ + private final Session userSession; + private final String remoteNodePath; public RemoteRepositoryNode(String alias, Repository repository, - TreeParent parent, JcrKeyring jcrKeyring, String remoteNodePath) { + TreeParent parent, Session userSession, Keyring keyring, + String remoteNodePath) { super(alias, repository, parent); - this.jcrKeyring = jcrKeyring; + this.keyring = keyring; + this.userSession = userSession; this.remoteNodePath = remoteNodePath; } @Override protected Session repositoryLogin(String workspaceName) throws RepositoryException { - Node remoteNode = jcrKeyring.getSession().getNode(remoteNodePath); - String userID = remoteNode.getProperty(ArgeoNames.ARGEO_USER_ID) + Node remoteRepository = userSession.getNode(remoteNodePath); + String userID = remoteRepository.getProperty(ArgeoNames.ARGEO_USER_ID) .getString(); - char[] password = jcrKeyring.getAsChars(remoteNodePath + "/" - + ArgeoNames.ARGEO_PASSWORD); + String pwdPath = remoteRepository.getPath() + '/' + + ArgeoNames.ARGEO_PASSWORD; + char[] password = keyring.getAsChars(pwdPath); + try { SimpleCredentials credentials = new SimpleCredentials(userID, password); @@ -59,7 +68,7 @@ public class RemoteRepositoryNode extends RepositoryNode { public void remove() { try { - Node remoteNode = jcrKeyring.getSession().getNode(remoteNodePath); + Node remoteNode = userSession.getNode(remoteNodePath); remoteNode.remove(); remoteNode.getSession().save(); } catch (RepositoryException e) { diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/model/RepositoriesNode.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/model/RepositoriesNode.java index 52ed4c2ef..fe8293235 100644 --- a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/model/RepositoriesNode.java +++ b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/model/RepositoriesNode.java @@ -32,7 +32,7 @@ import org.argeo.jcr.ArgeoJcrConstants; import org.argeo.jcr.ArgeoNames; import org.argeo.jcr.RepositoryRegister; import org.argeo.jcr.UserJcrUtils; -import org.argeo.jcr.security.JcrKeyring; +import org.argeo.util.security.Keyring; /** * UI Tree component. Implements the Argeo abstraction of a @@ -49,15 +49,21 @@ public class RepositoriesNode extends TreeParent implements ArgeoNames { private final RepositoryRegister repositoryRegister; private final RepositoryFactory repositoryFactory; - private final JcrKeyring jcrKeyring; + /** + * A session of the logged in user on the default workspace of the node + * repository. + */ + private final Session userSession; + private final Keyring keyring; public RepositoriesNode(String name, RepositoryRegister repositoryRegister, RepositoryFactory repositoryFactory, TreeParent parent, - JcrKeyring jcrKeyring) { + Session userSession, Keyring keyring) { super(name); this.repositoryRegister = repositoryRegister; this.repositoryFactory = repositoryFactory; - this.jcrKeyring = jcrKeyring; + this.userSession = userSession; + this.keyring = keyring; } /** @@ -78,9 +84,9 @@ public class RepositoriesNode extends TreeParent implements ArgeoNames { } // remote - if (jcrKeyring != null) { + if (keyring != null) { try { - addRemoteRepositories(jcrKeyring); + addRemoteRepositories(keyring); } catch (RepositoryException e) { throw new ArgeoException( "Cannot browse remote repositories", e); @@ -90,9 +96,8 @@ public class RepositoriesNode extends TreeParent implements ArgeoNames { } } - protected void addRemoteRepositories(JcrKeyring jcrKeyring) + protected void addRemoteRepositories(Keyring jcrKeyring) throws RepositoryException { - Session userSession = jcrKeyring.getSession(); Node userHome = UserJcrUtils.getUserHome(userSession); if (userHome != null && userHome.hasNode(ARGEO_REMOTE)) { NodeIterator it = userHome.getNode(ARGEO_REMOTE).getNodes(); @@ -107,8 +112,8 @@ public class RepositoriesNode extends TreeParent implements ArgeoNames { Repository repository = repositoryFactory .getRepository(params); RemoteRepositoryNode remoteRepositoryNode = new RemoteRepositoryNode( - remoteNode.getName(), repository, this, jcrKeyring, - remoteNode.getPath()); + remoteNode.getName(), repository, this, + userSession, jcrKeyring, remoteNode.getPath()); super.addChild(remoteRepositoryNode); } catch (Exception e) { ErrorFeedback.show("Cannot add remote repository " diff --git a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/views/GenericJcrBrowser.java b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/views/GenericJcrBrowser.java index 9ae1b0b63..5557d8109 100644 --- a/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/views/GenericJcrBrowser.java +++ b/server/plugins/org.argeo.jcr.ui.explorer/src/main/java/org/argeo/jcr/ui/explorer/views/GenericJcrBrowser.java @@ -19,22 +19,21 @@ import java.util.List; import javax.jcr.Property; import javax.jcr.PropertyType; +import javax.jcr.Repository; import javax.jcr.RepositoryException; import javax.jcr.RepositoryFactory; +import javax.jcr.Session; import javax.jcr.Value; import javax.jcr.observation.Event; import javax.jcr.observation.EventListener; import javax.jcr.observation.ObservationManager; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.argeo.ArgeoException; import org.argeo.eclipse.ui.TreeParent; import org.argeo.eclipse.ui.jcr.AsyncUiEventListener; import org.argeo.eclipse.ui.jcr.utils.NodeViewerComparer; import org.argeo.eclipse.ui.jcr.views.AbstractJcrBrowser; import org.argeo.jcr.RepositoryRegister; -import org.argeo.jcr.security.JcrKeyring; import org.argeo.jcr.ui.explorer.JcrExplorerPlugin; import org.argeo.jcr.ui.explorer.browser.NodeContentProvider; import org.argeo.jcr.ui.explorer.browser.NodeLabelProvider; @@ -42,6 +41,7 @@ import org.argeo.jcr.ui.explorer.browser.PropertiesContentProvider; import org.argeo.jcr.ui.explorer.model.SingleJcrNode; import org.argeo.jcr.ui.explorer.utils.GenericNodeDoubleClickListener; import org.argeo.jcr.ui.explorer.utils.JcrUiUtils; +import org.argeo.util.security.Keyring; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.viewers.ColumnLabelProvider; import org.eclipse.jface.viewers.ISelectionChangedListener; @@ -65,16 +65,20 @@ import org.eclipse.swt.widgets.Menu; * Basic View to display a sash form to browse a JCR compliant multirepository * environment */ - public class GenericJcrBrowser extends AbstractJcrBrowser { - private final static Log log = LogFactory.getLog(GenericJcrBrowser.class); public final static String ID = JcrExplorerPlugin.ID + ".browserView"; private boolean sortChildNodes = false; /* DEPENDENCY INJECTION */ - private JcrKeyring jcrKeyring; + private Keyring keyring; private RepositoryRegister repositoryRegister; private RepositoryFactory repositoryFactory; + private Repository nodeRepository; + /** + * A session of the logged in user on the default workspace of the node + * repository. + */ + private Session userSession; // This page widgets private TreeViewer nodesViewer; @@ -82,35 +86,8 @@ public class GenericJcrBrowser extends AbstractJcrBrowser { private TableViewer propertiesViewer; private EventListener resultsObserver; - // Manage documents - // private JcrFileProvider jcrFileProvider; - // private FileHandler fileHandler; - @Override public void createPartControl(Composite parent) { - - // look for session - // Session nodeSession = jcrKeyring != null ? jcrKeyring.getSession() - // : null; - // if (nodeSession == null) { - // Repository nodeRepository = JcrUtils.getRepositoryByAlias( - // repositoryRegister, ArgeoJcrConstants.ALIAS_NODE); - // if (nodeRepository != null) - // try { - // nodeSession = nodeRepository.login(); - // // TODO : enhance that to enable multirepository listener. - // } catch (RepositoryException e1) { - // throw new ArgeoException("Cannot login to node repository"); - // } - // } - - // Instantiate the generic object that fits for - // both RCP & RAP - // Note that in RAP, it registers a service handler that provide the - // access to the files. - // jcrFileProvider = new JcrFileProvider(); - // fileHandler = new FileHandler(jcrFileProvider); - parent.setLayout(new FillLayout()); SashForm sashForm = new SashForm(parent, SWT.VERTICAL); sashForm.setSashWidth(4); @@ -121,7 +98,13 @@ public class GenericJcrBrowser extends AbstractJcrBrowser { GridLayout gl = new GridLayout(1, false); top.setLayout(gl); - nodeContentProvider = new NodeContentProvider(jcrKeyring, + try { + this.userSession = this.nodeRepository.login(); + } catch (RepositoryException e) { + throw new ArgeoException("Cannot open user session", e); + } + + nodeContentProvider = new NodeContentProvider(userSession, keyring, repositoryRegister, repositoryFactory, sortChildNodes); // nodes viewer @@ -181,10 +164,9 @@ public class GenericJcrBrowser extends AbstractJcrBrowser { }); resultsObserver = new TreeObserver(tmpNodeViewer.getTree().getDisplay()); - if (jcrKeyring != null) + if (keyring != null) try { - log.debug("userID=" + jcrKeyring.getSession().getUserID()); - ObservationManager observationManager = jcrKeyring.getSession() + ObservationManager observationManager = userSession .getWorkspace().getObservationManager(); observationManager.addEventListener(resultsObserver, Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED, "/", @@ -335,12 +317,16 @@ public class GenericJcrBrowser extends AbstractJcrBrowser { this.repositoryRegister = repositoryRegister; } - public void setJcrKeyring(JcrKeyring jcrKeyring) { - this.jcrKeyring = jcrKeyring; + public void setKeyring(Keyring keyring) { + this.keyring = keyring; } public void setRepositoryFactory(RepositoryFactory repositoryFactory) { this.repositoryFactory = repositoryFactory; } + public void setNodeRepository(Repository nodeRepository) { + this.nodeRepository = nodeRepository; + } + } diff --git a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java index 1ace83fcd..91eaceeb2 100644 --- a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java +++ b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/JcrUtils.java @@ -113,6 +113,20 @@ public class JcrUtils implements ArgeoJcrConstants { return node; } + /** Retrieves the node name from the provided path */ + public static String nodeNameFromPath(String path) { + if (path.equals("/")) + return ""; + if (path.charAt(0) != '/') + throw new ArgeoException("Path " + path + " must start with a '/'"); + String pathT = path; + if (pathT.charAt(pathT.length() - 1) == '/') + pathT = pathT.substring(0, pathT.length() - 2); + + int index = pathT.lastIndexOf('/'); + return pathT.substring(index + 1); + } + /** Retrieves the parent path of the provided path */ public static String parentPath(String path) { if (path.equals("/")) diff --git a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/security/JcrKeyring.java b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/security/JcrKeyring.java deleted file mode 100644 index a35bbd272..000000000 --- a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/security/JcrKeyring.java +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (C) 2007-2012 Mathieu Baudier - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.argeo.jcr.security; - -import java.io.ByteArrayInputStream; -import java.io.InputStream; -import java.security.SecureRandom; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.SecretKey; -import javax.crypto.spec.IvParameterSpec; -import javax.jcr.Binary; -import javax.jcr.Node; -import javax.jcr.Property; -import javax.jcr.RepositoryException; -import javax.jcr.Session; - -import org.apache.commons.io.IOUtils; -import org.argeo.ArgeoException; -import org.argeo.jcr.ArgeoNames; -import org.argeo.jcr.ArgeoTypes; -import org.argeo.jcr.JcrUtils; -import org.argeo.jcr.UserJcrUtils; -import org.argeo.util.crypto.AbstractKeyring; -import org.argeo.util.crypto.PBEKeySpecCallback; - -/** JCR based implementation of a keyring */ -public class JcrKeyring extends AbstractKeyring implements ArgeoNames { - private Session session; - - /** - * When setup is called the session has not yet been saved and we don't want - * to save it since there maybe other data which would be inconsistent. So - * we keep a reference to this node which will then be used (an reset to - * null) when handling the PBE callback. We keep one per thread in case - * multiple users are accessing the same instance of a keyring. - */ - private ThreadLocal notYetSavedKeyring = new ThreadLocal() { - - @Override - protected Node initialValue() { - return null; - } - }; - - @Override - protected Boolean isSetup() { - try { - if (notYetSavedKeyring.get() != null) - return true; - - Node userHome = UserJcrUtils.getUserHome(session); - return userHome.hasNode(ARGEO_KEYRING); - } catch (RepositoryException e) { - throw new ArgeoException("Cannot check whether keyring is setup", e); - } - } - - @Override - protected void setup(char[] password) { - Binary binary = null; - InputStream in = null; - try { - Node userHome = UserJcrUtils.getUserHome(session); - if (userHome.hasNode(ARGEO_KEYRING)) - throw new ArgeoException("Keyring already setup"); - Node keyring = userHome.addNode(ARGEO_KEYRING); - keyring.addMixin(ArgeoTypes.ARGEO_PBE_SPEC); - - // deterministic salt and iteration count based on username - String username = session.getUserID(); - byte[] salt = new byte[8]; - byte[] usernameBytes = username.getBytes(); - for (int i = 0; i < salt.length; i++) { - if (i < usernameBytes.length) - salt[i] = usernameBytes[i]; - else - salt[i] = 0; - } - in = new ByteArrayInputStream(salt); - binary = session.getValueFactory().createBinary(in); - keyring.setProperty(ARGEO_SALT, binary); - - Integer iterationCount = username.length() * 200; - keyring.setProperty(ARGEO_ITERATION_COUNT, iterationCount); - - // default algo - // TODO check if algo and key length are available, use DES if not - keyring.setProperty(ARGEO_SECRET_KEY_FACTORY, "PBKDF2WithHmacSHA1"); - keyring.setProperty(ARGEO_KEY_LENGTH, 256l); - keyring.setProperty(ARGEO_SECRET_KEY_ENCRYPTION, "AES"); - keyring.setProperty(ARGEO_CIPHER, "AES/CBC/PKCS5Padding"); - - // encrypted password hash - // IOUtils.closeQuietly(in); - // JcrUtils.closeQuietly(binary); - // byte[] btPass = hash(password, salt, iterationCount); - // in = new ByteArrayInputStream(btPass); - // binary = session.getValueFactory().createBinary(in); - // keyring.setProperty(ARGEO_PASSWORD, binary); - - notYetSavedKeyring.set(keyring); - } catch (Exception e) { - throw new ArgeoException("Cannot setup keyring", e); - } finally { - JcrUtils.closeQuietly(binary); - IOUtils.closeQuietly(in); - // JcrUtils.discardQuietly(session); - } - } - - @Override - protected void handleKeySpecCallback(PBEKeySpecCallback pbeCallback) { - try { - Node userHome = UserJcrUtils.getUserHome(session); - Node keyring; - if (userHome.hasNode(ARGEO_KEYRING)) - keyring = userHome.getNode(ARGEO_KEYRING); - else if (notYetSavedKeyring.get() != null) - keyring = notYetSavedKeyring.get(); - else - throw new ArgeoException("Keyring not setup"); - - pbeCallback.set(keyring.getProperty(ARGEO_SECRET_KEY_FACTORY) - .getString(), JcrUtils.getBinaryAsBytes(keyring - .getProperty(ARGEO_SALT)), - (int) keyring.getProperty(ARGEO_ITERATION_COUNT).getLong(), - (int) keyring.getProperty(ARGEO_KEY_LENGTH).getLong(), - keyring.getProperty(ARGEO_SECRET_KEY_ENCRYPTION) - .getString()); - - if (notYetSavedKeyring.get() != null) - notYetSavedKeyring.remove(); - } catch (RepositoryException e) { - throw new ArgeoException("Cannot handle key spec callback", e); - } - } - - /** The node must already exist at this path. Session is saved. */ - @Override - protected synchronized void encrypt(String path, InputStream unencrypted) { - // should be called first for lazy initialization - SecretKey secretKey = getSecretKey(); - - Binary binary = null; - InputStream in = null; - // ByteArrayOutputStream out = null; - // OutputStream encrypted = null; - - try { - Cipher cipher = createCipher(); - if (!session.nodeExists(path)) - throw new ArgeoException("No node at " + path); - if (session.hasPendingChanges()) - session.save(); - Node node = session.getNode(path); - node.addMixin(ArgeoTypes.ARGEO_ENCRYPTED); - SecureRandom random = new SecureRandom(); - byte[] iv = new byte[16]; - random.nextBytes(iv); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv)); - // AlgorithmParameters params = cipher.getParameters(); - // byte[] iv = - // params.getParameterSpec(IvParameterSpec.class).getIV(); - // if (iv != null) - JcrUtils.setBinaryAsBytes(node, ARGEO_IV, iv); - - // out = new ByteArrayOutputStream(); - // // encrypted = new CipherOutputStream(out, cipher); - // IOUtils.copy(unencrypted, out); - // byte[] unenc = out.toByteArray(); - // byte[] crypted = cipher.doFinal(unenc); - - // Cipher decipher = createCipher(); - // decipher.init(Cipher.DECRYPT_MODE, secretKey, new - // IvParameterSpec( - // iv)); - // byte[] decrypted = decipher.doFinal(crypted); - // System.out.println("Password :'" + new String(decrypted) + "'"); - - // JcrUtils.setBinaryAsBytes(node, Property.JCR_DATA, crypted); - - in = new CipherInputStream(unencrypted, cipher); - binary = session.getValueFactory().createBinary(in); - node.setProperty(Property.JCR_DATA, binary); - session.save(); - } catch (Exception e) { - throw new ArgeoException("Cannot encrypt", e); - } finally { - // IOUtils.closeQuietly(out); - // IOUtils.closeQuietly(encrypted); - IOUtils.closeQuietly(unencrypted); - IOUtils.closeQuietly(in); - JcrUtils.closeQuietly(binary); - } - } - - @Override - protected synchronized InputStream decrypt(String path) { - // should be called first for lazy initialization - SecretKey secretKey = getSecretKey(); - - Binary binary = null; - InputStream encrypted = null; - - try { - Cipher cipher = createCipher(); - if (!session.nodeExists(path)) - throw new ArgeoException("No node at " + path); - Node node = session.getNode(path); - if (node.hasProperty(ARGEO_IV)) { - byte[] iv = JcrUtils.getBinaryAsBytes(node - .getProperty(ARGEO_IV)); - cipher.init(Cipher.DECRYPT_MODE, secretKey, - new IvParameterSpec(iv)); - } else { - cipher.init(Cipher.DECRYPT_MODE, secretKey); - } - - // byte[] arr = JcrUtils.getBinaryAsBytes(node - // .getProperty(Property.JCR_DATA)); - // byte[] arr2 = cipher.doFinal(arr); - // - // return new ByteArrayInputStream(arr2); - - binary = node.getProperty(Property.JCR_DATA).getBinary(); - encrypted = binary.getStream(); - return new CipherInputStream(encrypted, cipher); - } catch (Exception e) { - throw new ArgeoException("Cannot decrypt", e); - } finally { - IOUtils.closeQuietly(encrypted); - JcrUtils.closeQuietly(binary); - } - } - - protected Cipher createCipher() { - try { - Node userHome = UserJcrUtils.getUserHome(session); - if (!userHome.hasNode(ARGEO_KEYRING)) - throw new ArgeoException("Keyring not setup"); - Node keyring = userHome.getNode(ARGEO_KEYRING); - Cipher cipher = Cipher.getInstance(keyring - .getProperty(ARGEO_CIPHER).getString()); - return cipher; - } catch (Exception e) { - throw new ArgeoException("Cannot get cipher", e); - } - } - - public synchronized void changePassword(char[] oldPassword, - char[] newPassword) { - // TODO decrypt with old pw / encrypt with new pw all argeo:encrypted - } - - public synchronized Session getSession() { - return session; - } - - public synchronized void setSession(Session session) { - this.session = session; - } - -} diff --git a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/security/SecurityJcrUtils.java b/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/security/SecurityJcrUtils.java deleted file mode 100644 index 90a8d87bd..000000000 --- a/server/runtime/org.argeo.server.jcr/src/main/java/org/argeo/jcr/security/SecurityJcrUtils.java +++ /dev/null @@ -1,125 +0,0 @@ -package org.argeo.jcr.security; - -import javax.jcr.Node; -import javax.jcr.RepositoryException; -import javax.jcr.Session; -import javax.jcr.version.VersionManager; - -import org.argeo.ArgeoException; -import org.argeo.jcr.ArgeoJcrConstants; -import org.argeo.jcr.ArgeoNames; -import org.argeo.jcr.ArgeoTypes; -import org.argeo.jcr.JcrUtils; -import org.argeo.jcr.UserJcrUtils; - -/** Utilities related to Argeo security model in JCR */ -@Deprecated -public class SecurityJcrUtils implements ArgeoJcrConstants { - /** - * Creates an Argeo user home, does nothing if it already exists. Session is - * NOT saved. - */ - static Node createUserHomeIfNeeded(Session session, String username) { - try { - String homePath = generateUserHomePath(username); - if (session.itemExists(homePath)) - return session.getNode(homePath); - else { - Node userHome = JcrUtils.mkdirs(session, homePath); - userHome.addMixin(ArgeoTypes.ARGEO_USER_HOME); - userHome.setProperty(ArgeoNames.ARGEO_USER_ID, username); - - // JcrUtils.addPrivilege(session, homePath, username, - // "jcr:all"); - return userHome; - } - } catch (RepositoryException e) { - JcrUtils.discardQuietly(session); - throw new ArgeoException("Cannot create home for " + username - + " in workspace " + session.getWorkspace().getName(), e); - } - } - - private static String generateUserHomePath(String username) { - String homeBasePath = UserJcrUtils.DEFAULT_HOME_BASE_PATH; - return homeBasePath + '/' + JcrUtils.firstCharsToPath(username, 2) - + '/' + username; - } - - /** - * Creates a user profile in the home of this user. Creates the home if - * needed, but throw an exception if a profile already exists. The session - * is not saved and the node is in a checkedOut state (that is, it requires - * a subsequent checkin after saving the session). - */ - static Node createUserProfile(Session session, String username) { - try { - Node userHome = createUserHomeIfNeeded(session, username); - if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE)) - throw new ArgeoException( - "There is already a user profile under " + userHome); - Node userProfile = userHome.addNode(ArgeoNames.ARGEO_PROFILE); - userProfile.addMixin(ArgeoTypes.ARGEO_USER_PROFILE); - userProfile.setProperty(ArgeoNames.ARGEO_USER_ID, username); - userProfile.setProperty(ArgeoNames.ARGEO_ENABLED, true); - userProfile.setProperty(ArgeoNames.ARGEO_ACCOUNT_NON_EXPIRED, true); - userProfile.setProperty(ArgeoNames.ARGEO_ACCOUNT_NON_LOCKED, true); - userProfile.setProperty(ArgeoNames.ARGEO_CREDENTIALS_NON_EXPIRED, - true); - return userProfile; - } catch (RepositoryException e) { - JcrUtils.discardQuietly(session); - throw new ArgeoException("Cannot create user profile for " - + username + " in workspace " - + session.getWorkspace().getName(), e); - } - } - - /** - * Create user profile if needed, the session IS saved. - * - * @return the user profile - */ - static Node createUserProfileIfNeeded(Session securitySession, - String username) { - try { - Node userHome = createUserHomeIfNeeded(securitySession, username); - Node userProfile = userHome.hasNode(ArgeoNames.ARGEO_PROFILE) ? userHome - .getNode(ArgeoNames.ARGEO_PROFILE) : createUserProfile( - securitySession, username); - if (securitySession.hasPendingChanges()) - securitySession.save(); - VersionManager versionManager = securitySession.getWorkspace() - .getVersionManager(); - if (versionManager.isCheckedOut(userProfile.getPath())) - versionManager.checkin(userProfile.getPath()); - return userProfile; - } catch (RepositoryException e) { - JcrUtils.discardQuietly(securitySession); - throw new ArgeoException("Cannot create user profile for " - + username + " in workspace " - + securitySession.getWorkspace().getName(), e); - } - } - - /** - * @return null if not found * - */ - static Node getUserProfile(Session session, String username) { - try { - Node userHome = UserJcrUtils.getUserHome(session, username); - if (userHome == null) - return null; - if (userHome.hasNode(ArgeoNames.ARGEO_PROFILE)) - return userHome.getNode(ArgeoNames.ARGEO_PROFILE); - else - return null; - } catch (RepositoryException e) { - throw new ArgeoException( - "Cannot find profile for user " + username, e); - } - } - - private SecurityJcrUtils() { - } -}