From a55dd9873bf5e79873a839f11d9fced303ca727b Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 17 Apr 2018 22:00:54 +0200 Subject: [PATCH 1/8] #979 split implementation of ScmClientConfigFileHandler in order to create new more secure implementation --- .../scm/cli/config/CipherStreamHandler.java | 60 +++++ .../java/sonia/scm/cli/config/KeyStore.java | 57 ++++ .../sonia/scm/cli/config/PrefsKeyStore.java | 64 +++++ .../sonia/scm/cli/config/ScmClientConfig.java | 2 +- .../config/ScmClientConfigFileHandler.java | 246 ++++-------------- .../cli/config/WeakCipherStreamHandler.java | 109 ++++++++ .../ScmClientConfigFileHandlerTest.java | 98 +++++++ 7 files changed, 439 insertions(+), 197 deletions(-) create mode 100644 scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/CipherStreamHandler.java create mode 100644 scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/KeyStore.java create mode 100644 scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsKeyStore.java create mode 100644 scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java create mode 100644 scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/CipherStreamHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/CipherStreamHandler.java new file mode 100644 index 0000000000..e687988be3 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/CipherStreamHandler.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +package sonia.scm.cli.config; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * The CipherStreamHandler is able to encrypt and decrypt streams. + */ +public interface CipherStreamHandler { + + /** + * Decrypts the given input stream. + * + * @param inputStream encrypted input stream + * + * @return raw input stream + */ + InputStream decrypt(InputStream inputStream); + + /** + * Encrypts the given output stream. + * + * @param outputStream raw output stream + * + * @return encrypting output stream + */ + OutputStream encrypt(OutputStream outputStream); +} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/KeyStore.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/KeyStore.java new file mode 100644 index 0000000000..39874b499c --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/KeyStore.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.cli.config; + +/** + * KeyStore is able to read and write keys. + */ +public interface KeyStore { + + /** + * Writes the given secret key to the store. + * + * @param secretKey secret key to write + */ + void set(String secretKey); + + /** + * Reads the secret key from the store. The method returns {@code null} if no secret key was stored. + * + * @return secret key of {@code null} + */ + String get(); + + /** + * Removes the secret key from store. + */ + void remove(); +} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsKeyStore.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsKeyStore.java new file mode 100644 index 0000000000..26a7e67b05 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsKeyStore.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.cli.config; + +import java.util.prefs.Preferences; + +/** + * KeyStore implementation with uses {@link Preferences}. + */ +public class PrefsKeyStore implements KeyStore { + + private static final String PREF_SECRET_KEY = "scm.client.key"; + + private final Preferences preferences; + + public PrefsKeyStore() { + // we use ScmClientConfigFileHandler as base for backward compatibility + preferences = Preferences.userNodeForPackage(ScmClientConfigFileHandler.class); + } + + @Override + public void set(String secretKey) { + preferences.put(PREF_SECRET_KEY, secretKey); + } + + @Override + public String get() { + return preferences.get(PREF_SECRET_KEY, null); + } + + @Override + public void remove() { + preferences.remove(PREF_SECRET_KEY); + } +} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfig.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfig.java index 9edb2b382a..7bec710974 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfig.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfig.java @@ -66,7 +66,7 @@ public class ScmClientConfig * Constructs ... * */ - private ScmClientConfig() + ScmClientConfig() { this.serverConfigMap = new HashMap(); } diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java index 5cc2c46978..bbb0fd85c1 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java @@ -35,38 +35,17 @@ package sonia.scm.cli.config; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.util.IOUtil; +import sonia.scm.security.KeyGenerator; +import sonia.scm.security.UUIDKeyGenerator; import sonia.scm.util.Util; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; - -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.spec.InvalidKeySpecException; - -import java.util.UUID; -import java.util.prefs.Preferences; - -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.PBEParameterSpec; - import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; +import java.io.*; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -81,62 +60,66 @@ public class ScmClientConfigFileHandler /** Field description */ public static final String ENV_CONFIG_FILE = "SCM_CLI_CONFIG"; - /** Field description */ - public static final String PREF_SECRET_KEY = "scm.client.key"; - - /** Field description */ - public static final String SALT = "AE16347F"; - - /** Field description */ - public static final int SPEC_ITERATION = 12; - - /** Field description */ - private static final String CIPHER_NAME = "PBEWithMD5AndDES"; - //~--- constructors --------------------------------------------------------- + private final KeyStore keyStore; + private final KeyGenerator keyGenerator; + private final File file; + private final JAXBContext context; + + private final CipherStreamHandler cipherStreamHandler; + + /** * Constructs ... * */ - public ScmClientConfigFileHandler() - { - prefs = Preferences.userNodeForPackage(ScmClientConfigFileHandler.class); - key = prefs.get(PREF_SECRET_KEY, null); + public ScmClientConfigFileHandler() { + this(new PrefsKeyStore(), new UUIDKeyGenerator(), getDefaultConfigFile()); + } - if (Util.isEmpty(key)) - { - key = createNewKey(); - prefs.put(PREF_SECRET_KEY, key); + ScmClientConfigFileHandler(KeyStore keyStore, KeyGenerator keyGenerator, File file) { + this.keyStore = keyStore; + this.keyGenerator = keyGenerator; + this.file = file; + + String key = keyStore.get(); + + if (Util.isEmpty(key)) { + key = keyGenerator.createKey(); + keyStore.set(key); } - try - { + cipherStreamHandler = new WeakCipherStreamHandler(key.toCharArray()); + + try { context = JAXBContext.newInstance(ScmClientConfig.class); - } - catch (JAXBException ex) - { - throw new ScmConfigException( - "could not create JAXBContext for ScmClientConfig", ex); + } catch (JAXBException ex) { + throw new ScmConfigException("could not create JAXBContext for ScmClientConfig", ex); } } //~--- methods -------------------------------------------------------------- + private static File getDefaultConfigFile() { + String configPath = System.getenv(ENV_CONFIG_FILE); + + if (Util.isNotEmpty(configPath)){ + return new File(configPath); + } + return new File(System.getProperty("user.home"), DEFAULT_CONFIG_NAME); + } + /** * Method description * */ - public void delete() - { - File configFile = getConfigFile(); - - if (configFile.exists() &&!configFile.delete()) - { + public void delete() { + if (file.exists() &&!file.delete()) { throw new ScmConfigException("could not delete config file"); } - prefs.remove(PREF_SECRET_KEY); + keyStore.remove(); } /** @@ -145,33 +128,16 @@ public class ScmClientConfigFileHandler * * @return */ - public ScmClientConfig read() - { + public ScmClientConfig read() { ScmClientConfig config = null; - File configFile = getConfigFile(); - - if (configFile.exists()) - { - InputStream input = null; - - try - { - Cipher c = createCipher(Cipher.DECRYPT_MODE); - - input = new CipherInputStream(new FileInputStream(configFile), c); + if (file.exists()) { + try (InputStream input = cipherStreamHandler.decrypt(new FileInputStream(file))) { Unmarshaller um = context.createUnmarshaller(); - config = (ScmClientConfig) um.unmarshal(input); - } - catch (Exception ex) - { + } catch (IOException | JAXBException ex) { throw new ScmConfigException("could not read config file", ex); } - finally - { - IOUtil.close(input); - } } return config; @@ -183,124 +149,12 @@ public class ScmClientConfigFileHandler * * @param config */ - public void write(ScmClientConfig config) - { - File configFile = getConfigFile(); - OutputStream output = null; - - try - { - Cipher c = createCipher(Cipher.ENCRYPT_MODE); - - output = new CipherOutputStream(new FileOutputStream(configFile), c); - + public void write(ScmClientConfig config) { + try (OutputStream output = cipherStreamHandler.encrypt(new FileOutputStream(file))) { Marshaller m = context.createMarshaller(); - m.marshal(config, output); - } - catch (Exception ex) - { + } catch (IOException | JAXBException ex) { throw new ScmConfigException("could not write config file", ex); } - finally - { - IOUtil.close(output); - } } - - /** - * Method description - * - * - * @param mode - * - * @return - * - * - * @throws InvalidAlgorithmParameterException - * @throws InvalidKeyException - * @throws InvalidKeySpecException - * @throws NoSuchAlgorithmException - * @throws NoSuchPaddingException - */ - private Cipher createCipher(int mode) - throws NoSuchAlgorithmException, NoSuchPaddingException, - InvalidKeySpecException, InvalidKeyException, - InvalidAlgorithmParameterException - { - SecretKey sk = createSecretKey(); - Cipher cipher = Cipher.getInstance(CIPHER_NAME); - PBEParameterSpec spec = new PBEParameterSpec(SALT.getBytes(), - SPEC_ITERATION); - - cipher.init(mode, sk, spec); - - return cipher; - } - - /** - * Method description - * - * - * @return - */ - private String createNewKey() - { - return UUID.randomUUID().toString(); - } - - /** - * Method description - * - * - * @return - * - * @throws InvalidKeySpecException - * @throws NoSuchAlgorithmException - */ - private SecretKey createSecretKey() - throws NoSuchAlgorithmException, InvalidKeySpecException - { - PBEKeySpec keySpec = new PBEKeySpec(key.toCharArray()); - SecretKeyFactory factory = SecretKeyFactory.getInstance(CIPHER_NAME); - - return factory.generateSecret(keySpec); - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - */ - private File getConfigFile() - { - File configFile = null; - String configPath = System.getenv(ENV_CONFIG_FILE); - - if (Util.isEmpty(configPath)) - { - configFile = new File(System.getProperty("user.home"), - DEFAULT_CONFIG_NAME); - } - else - { - configFile = new File(configPath); - } - - return configFile; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private JAXBContext context; - - /** Field description */ - private String key; - - /** Field description */ - private Preferences prefs; } diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java new file mode 100644 index 0000000000..0a867d5b30 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.cli.config; + +import javax.crypto.*; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.PBEParameterSpec; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +/** + * Weak implementation of {@link CipherStreamHandler}. + * + * @see Issue 978 + * @see Issue 979 + */ +public class WeakCipherStreamHandler implements CipherStreamHandler { + + private static final String SALT = "AE16347F"; + private static final int SPEC_ITERATION = 12; + private static final String CIPHER_NAME = "PBEWithMD5AndDES"; + + private final char[] secretKey; + + /** + * Creates a new handler with the given secret key. + * + * @param secretKey secret key + */ + public WeakCipherStreamHandler(char[] secretKey) { + this.secretKey = secretKey; + } + + @Override + public InputStream decrypt(InputStream inputStream) { + try { + Cipher c = createCipher(Cipher.DECRYPT_MODE); + return new CipherInputStream(inputStream, c); + } catch (Exception ex) { + throw new ScmConfigException("could not encrypt output stream", ex); + } + } + + @Override + public OutputStream encrypt(OutputStream outputStream) { + try { + Cipher c = createCipher(Cipher.ENCRYPT_MODE); + return new CipherOutputStream(outputStream, c); + } catch (Exception ex) { + throw new ScmConfigException("could not encrypt output stream", ex); + } + } + + private Cipher createCipher(int mode) + throws NoSuchAlgorithmException, NoSuchPaddingException, + InvalidKeySpecException, InvalidKeyException, + InvalidAlgorithmParameterException + { + SecretKey sk = createSecretKey(); + Cipher cipher = Cipher.getInstance(CIPHER_NAME); + PBEParameterSpec spec = new PBEParameterSpec(SALT.getBytes(), SPEC_ITERATION); + + cipher.init(mode, sk, spec); + + return cipher; + } + + private SecretKey createSecretKey() + throws NoSuchAlgorithmException, InvalidKeySpecException + { + PBEKeySpec keySpec = new PBEKeySpec(secretKey); + SecretKeyFactory factory = SecretKeyFactory.getInstance(CIPHER_NAME); + + return factory.generateSecret(keySpec); + } +} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java new file mode 100644 index 0000000000..ec598fd2d4 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.cli.config; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import sonia.scm.security.UUIDKeyGenerator; + +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ScmClientConfigFileHandlerTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testClientConfigFileHandler() throws IOException { + File configFile = temporaryFolder.newFile(); + + ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler( + new InMemoryKeyStore(), new UUIDKeyGenerator(), configFile + ); + + ScmClientConfig config = new ScmClientConfig(); + ServerConfig defaultConfig = config.getDefaultConfig(); + defaultConfig.setServerUrl("http://localhost:8080/scm"); + defaultConfig.setUsername("scmadmin"); + defaultConfig.setPassword("admin123"); + handler.write(config); + + assertTrue(configFile.exists()); + + config = handler.read(); + defaultConfig = config.getDefaultConfig(); + assertEquals("http://localhost:8080/scm", defaultConfig.getServerUrl()); + assertEquals("scmadmin", defaultConfig.getUsername()); + assertEquals("admin123", defaultConfig.getPassword()); + + handler.delete(); + + assertFalse(configFile.exists()); + } + + private static class InMemoryKeyStore implements KeyStore { + + private String secretKey; + + @Override + public void set(String secretKey) { + this.secretKey = secretKey; + } + + @Override + public String get() { + return secretKey; + } + + @Override + public void remove() { + this.secretKey = null; + } + } +} From d9e7de82022beb80c7812b435970f12357d3c7c9 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 18 Apr 2018 08:09:28 +0200 Subject: [PATCH 2/8] #979 improve javadoc --- .../src/main/java/sonia/scm/cli/config/KeyStore.java | 2 +- .../java/sonia/scm/cli/config/WeakCipherStreamHandler.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/KeyStore.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/KeyStore.java index 39874b499c..7ce64750ce 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/KeyStore.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/KeyStore.java @@ -46,7 +46,7 @@ public interface KeyStore { /** * Reads the secret key from the store. The method returns {@code null} if no secret key was stored. * - * @return secret key of {@code null} + * @return secret key or {@code null} */ String get(); diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java index 0a867d5b30..a9e3e5ef42 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java @@ -42,7 +42,8 @@ import java.security.NoSuchAlgorithmException; import java.security.spec.InvalidKeySpecException; /** - * Weak implementation of {@link CipherStreamHandler}. + * Weak implementation of {@link CipherStreamHandler}. This is the old implementation, which was used in versions prior + * 1.60. * * @see Issue 978 * @see Issue 979 From 3ee0bcedac4ce72cffb8f474ac33ae4d5c1b77a4 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 18 Apr 2018 14:41:38 +0200 Subject: [PATCH 3/8] #979 encrypt cli configuration with aes instead of pbe --- .../cli/config/AesCipherStreamHandler.java | 108 +++++++++++++ .../scm/cli/config/CipherStreamHandler.java | 8 +- .../sonia/scm/cli/config/ConfigFiles.java | 151 ++++++++++++++++++ .../config/ScmClientConfigFileHandler.java | 64 +++----- .../cli/config/SecureRandomKeyGenerator.java | 69 ++++++++ .../cli/config/WeakCipherStreamHandler.java | 7 +- .../config/AesCipherStreamHandlerTest.java | 68 ++++++++ .../cli/config/ClientConfigurationTests.java | 96 +++++++++++ .../sonia/scm/cli/config/ConfigFilesTest.java | 105 ++++++++++++ .../scm/cli/config/InMemoryKeyStore.java | 53 ++++++ .../ScmClientConfigFileHandlerTest.java | 76 ++++++--- .../config/SecureRandomKeyGeneratorTest.java | 47 ++++++ .../scm/cli/config/scm-cli-config.enc.xml | Bin 0 -> 296 bytes 13 files changed, 790 insertions(+), 62 deletions(-) create mode 100644 scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/AesCipherStreamHandler.java create mode 100644 scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigFiles.java create mode 100644 scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecureRandomKeyGenerator.java create mode 100644 scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/AesCipherStreamHandlerTest.java create mode 100644 scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ClientConfigurationTests.java create mode 100644 scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ConfigFilesTest.java create mode 100644 scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemoryKeyStore.java create mode 100644 scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/SecureRandomKeyGeneratorTest.java create mode 100644 scm-clients/scm-cli-client/src/test/resources/sonia/scm/cli/config/scm-cli-config.enc.xml diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/AesCipherStreamHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/AesCipherStreamHandler.java new file mode 100644 index 0000000000..5955b8b762 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/AesCipherStreamHandler.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +package sonia.scm.cli.config; + +import com.google.common.base.Charsets; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.SecureRandom; + +/** + * Implementation of {@link CipherStreamHandler} which uses AES. This version is used since version 1.60 for the + * cli client encryption. + * + * @author Sebastian Sdorra + * @since 1.60 + */ +public class AesCipherStreamHandler implements CipherStreamHandler { + + private static final String ALGORITHM = "AES/GCM/NoPadding"; + + private final SecureRandom random = new SecureRandom(); + + private final byte[] secretKey; + + AesCipherStreamHandler(String secretKey) { + this.secretKey = secretKey.getBytes(Charsets.UTF_8); + } + + @Override + public OutputStream encrypt(OutputStream outputStream) throws IOException { + Cipher cipher = createCipherForEncryption(); + outputStream.write(cipher.getIV()); + return new CipherOutputStream(outputStream, cipher); + } + + @Override + public InputStream decrypt(InputStream inputStream) throws IOException { + Cipher cipher = createCipherForDecryption(inputStream); + return new CipherInputStream(inputStream, cipher); + } + + private Cipher createCipherForDecryption(InputStream inputStream) throws IOException { + byte[] iv =new byte[12]; + inputStream.read(iv); + return createCipher(Cipher.DECRYPT_MODE, iv); + } + + private Cipher createCipherForEncryption() { + byte[] iv = generateIV(); + return createCipher(Cipher.ENCRYPT_MODE, iv); + } + + private byte[] generateIV() { + // use 12 byte as described at nist + // https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf + byte[] iv = new byte[12]; + random.nextBytes(iv); + return iv; + } + + private Cipher createCipher(int mode, byte[] iv) { + try { + Cipher cipher = Cipher.getInstance(ALGORITHM); + GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); + cipher.init(mode, new SecretKeySpec(secretKey, "AES"), parameterSpec); + return cipher; + } catch (Exception ex) { + throw new ScmConfigException("failed to create cipher", ex); + } + } +} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/CipherStreamHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/CipherStreamHandler.java index e687988be3..2321b3d9d9 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/CipherStreamHandler.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/CipherStreamHandler.java @@ -32,11 +32,15 @@ package sonia.scm.cli.config; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * The CipherStreamHandler is able to encrypt and decrypt streams. + * + * @author Sebastian Sdorra + * @since 1.60 */ public interface CipherStreamHandler { @@ -47,7 +51,7 @@ public interface CipherStreamHandler { * * @return raw input stream */ - InputStream decrypt(InputStream inputStream); + InputStream decrypt(InputStream inputStream) throws IOException; /** * Encrypts the given output stream. @@ -56,5 +60,5 @@ public interface CipherStreamHandler { * * @return encrypting output stream */ - OutputStream encrypt(OutputStream outputStream); + OutputStream encrypt(OutputStream outputStream) throws IOException; } diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigFiles.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigFiles.java new file mode 100644 index 0000000000..5087c50746 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigFiles.java @@ -0,0 +1,151 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.cli.config; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; +import com.google.common.base.Preconditions; +import com.google.common.base.Strings; +import sonia.scm.security.KeyGenerator; + +import javax.xml.bind.JAXB; +import java.io.*; +import java.util.Arrays; + +/** + * Util methods for configuration files. + * + * @author Sebastian Sdorra + * @since 1.60 + */ +final class ConfigFiles { + + private static final KeyGenerator keyGenerator = new SecureRandomKeyGenerator(); + + // SCM Config Version 2 + @VisibleForTesting + static final byte[] VERSION_IDENTIFIER = "SCV2".getBytes(Charsets.US_ASCII); + + private ConfigFiles() { + } + + /** + * Returns {@code true} if the file is encrypted with the v2 format. + * + * @param file configuration file + * + * @return {@code true} for format v2 + * + * @throws IOException + */ + static boolean isFormatV2(File file) throws IOException { + try (InputStream input = new FileInputStream(file)) { + byte[] bytes = new byte[VERSION_IDENTIFIER.length]; + input.read(bytes); + return Arrays.equals(VERSION_IDENTIFIER, bytes); + } + } + + /** + * Decrypt and parse v1 configuration file. + * + * @param keyStore key store + * @param file configuration file + * + * @return client configuration + * + * @throws IOException + */ + static ScmClientConfig parseV1(KeyStore keyStore, File file) throws IOException { + String secretKey = secretKey(keyStore); + CipherStreamHandler cipherStreamHandler = new WeakCipherStreamHandler(secretKey); + return decrypt(cipherStreamHandler, new FileInputStream(file)); + } + + /** + * Decrypt and parse v12configuration file. + * + * @param keyStore key store + * @param file configuration file + * + * @return client configuration + * + * @throws IOException + */ + static ScmClientConfig parseV2(KeyStore keyStore, File file) throws IOException { + String secretKey = secretKey(keyStore); + CipherStreamHandler cipherStreamHandler = new AesCipherStreamHandler(secretKey); + try (InputStream input = new FileInputStream(file)) { + input.skip(VERSION_IDENTIFIER.length); + return decrypt(cipherStreamHandler, input); + } + } + + /** + * Store encrypt and write the configuration to the given file. + * Note the method uses always the latest available format. + * + * @param keyStore key store + * @param config configuration + * @param file configuration file + * + * @throws IOException + */ + static void store(KeyStore keyStore, ScmClientConfig config, File file) throws IOException { + String secretKey = keyGenerator.createKey(); + CipherStreamHandler cipherStreamHandler = new AesCipherStreamHandler(secretKey); + try (OutputStream output = new FileOutputStream(file)) { + output.write(VERSION_IDENTIFIER); + encrypt(cipherStreamHandler, output, config); + } + keyStore.set(secretKey); + } + + private static String secretKey(KeyStore keyStore) { + String secretKey = keyStore.get(); + Preconditions.checkState(!Strings.isNullOrEmpty(secretKey), "no stored secret key found"); + return secretKey; + } + + private static ScmClientConfig decrypt(CipherStreamHandler cipherStreamHandler, InputStream input) throws IOException { + try ( InputStream decryptedInputStream = cipherStreamHandler.decrypt(input) ) { + return JAXB.unmarshal(decryptedInputStream, ScmClientConfig.class); + } + } + + private static void encrypt(CipherStreamHandler cipherStreamHandler, OutputStream output, ScmClientConfig clientConfig) throws IOException { + try ( OutputStream encryptedOutputStream = cipherStreamHandler.encrypt(output) ) { + JAXB.marshal(clientConfig, encryptedOutputStream); + } + } + +} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java index bbb0fd85c1..00a0eb6cf6 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java @@ -35,15 +35,10 @@ package sonia.scm.cli.config; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.security.KeyGenerator; -import sonia.scm.security.UUIDKeyGenerator; import sonia.scm.util.Util; -import javax.xml.bind.JAXBContext; -import javax.xml.bind.JAXBException; -import javax.xml.bind.Marshaller; -import javax.xml.bind.Unmarshaller; -import java.io.*; +import java.io.File; +import java.io.IOException; //~--- JDK imports ------------------------------------------------------------ @@ -63,40 +58,20 @@ public class ScmClientConfigFileHandler //~--- constructors --------------------------------------------------------- private final KeyStore keyStore; - private final KeyGenerator keyGenerator; private final File file; - private final JAXBContext context; - - private final CipherStreamHandler cipherStreamHandler; /** - * Constructs ... + * Constructs a new ScmClientConfigFileHandler * */ public ScmClientConfigFileHandler() { - this(new PrefsKeyStore(), new UUIDKeyGenerator(), getDefaultConfigFile()); + this(new PrefsKeyStore(), getDefaultConfigFile()); } - ScmClientConfigFileHandler(KeyStore keyStore, KeyGenerator keyGenerator, File file) { + ScmClientConfigFileHandler(KeyStore keyStore,File file) { this.keyStore = keyStore; - this.keyGenerator = keyGenerator; this.file = file; - - String key = keyStore.get(); - - if (Util.isEmpty(key)) { - key = keyGenerator.createKey(); - keyStore.set(key); - } - - cipherStreamHandler = new WeakCipherStreamHandler(key.toCharArray()); - - try { - context = JAXBContext.newInstance(ScmClientConfig.class); - } catch (JAXBException ex) { - throw new ScmConfigException("could not create JAXBContext for ScmClientConfig", ex); - } } //~--- methods -------------------------------------------------------------- @@ -132,17 +107,27 @@ public class ScmClientConfigFileHandler ScmClientConfig config = null; if (file.exists()) { - try (InputStream input = cipherStreamHandler.decrypt(new FileInputStream(file))) { - Unmarshaller um = context.createUnmarshaller(); - config = (ScmClientConfig) um.unmarshal(input); - } catch (IOException | JAXBException ex) { - throw new ScmConfigException("could not read config file", ex); - } + config = readFromFile(); } return config; } + private ScmClientConfig readFromFile() { + ScmClientConfig config; + try { + if (ConfigFiles.isFormatV2(file)) { + config = ConfigFiles.parseV2(keyStore, file); + } else { + config = ConfigFiles.parseV1(keyStore, file); + ConfigFiles.store(keyStore, config, file); + } + } catch (IOException ex) { + throw new ScmConfigException("could not read config file", ex); + } + return config; + } + /** * Method description * @@ -150,10 +135,9 @@ public class ScmClientConfigFileHandler * @param config */ public void write(ScmClientConfig config) { - try (OutputStream output = cipherStreamHandler.encrypt(new FileOutputStream(file))) { - Marshaller m = context.createMarshaller(); - m.marshal(config, output); - } catch (IOException | JAXBException ex) { + try { + ConfigFiles.store(keyStore, config, file); + } catch (IOException ex) { throw new ScmConfigException("could not write config file", ex); } } diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecureRandomKeyGenerator.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecureRandomKeyGenerator.java new file mode 100644 index 0000000000..19ae135461 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecureRandomKeyGenerator.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +package sonia.scm.cli.config; + +import com.google.common.annotations.VisibleForTesting; +import sonia.scm.security.KeyGenerator; + +import java.security.SecureRandom; +import java.util.Locale; + +/** + * Create keys by using {@link SecureRandom}. The SecureRandomKeyGenerator produces aes compatible keys. + * Warning the class is not thread safe. + * + * @author Sebastian Sdorra + * @since 1.60 + */ +public class SecureRandomKeyGenerator implements KeyGenerator { + + private SecureRandom random = new SecureRandom(); + + // key length 16 for aes128 + @VisibleForTesting + static final int KEY_LENGTH = 16; + + private static final String UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final String LOWER = UPPER.toLowerCase(Locale.ENGLISH); + private static final String DIGITS = "0123456789"; + private static final char[] ALL = (UPPER + LOWER + DIGITS).toCharArray(); + + @Override + public String createKey() { + char[] key = new char[KEY_LENGTH]; + for (int idx = 0; idx < KEY_LENGTH; ++idx) { + key[idx] = ALL[random.nextInt(ALL.length)]; + } + return new String(key); + } +} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java index a9e3e5ef42..175d1986c6 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java @@ -45,6 +45,9 @@ import java.security.spec.InvalidKeySpecException; * Weak implementation of {@link CipherStreamHandler}. This is the old implementation, which was used in versions prior * 1.60. * + * @author Sebastian Sdorra + * @since 1.60 + * * @see Issue 978 * @see Issue 979 */ @@ -61,8 +64,8 @@ public class WeakCipherStreamHandler implements CipherStreamHandler { * * @param secretKey secret key */ - public WeakCipherStreamHandler(char[] secretKey) { - this.secretKey = secretKey; + WeakCipherStreamHandler(String secretKey) { + this.secretKey = secretKey.toCharArray(); } @Override diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/AesCipherStreamHandlerTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/AesCipherStreamHandlerTest.java new file mode 100644 index 0000000000..14000faa33 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/AesCipherStreamHandlerTest.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +package sonia.scm.cli.config; + +import com.google.common.base.Charsets; +import com.google.common.io.ByteStreams; +import org.junit.Test; +import sonia.scm.security.KeyGenerator; + +import java.io.*; + +import static org.junit.Assert.assertEquals; + +public class AesCipherStreamHandlerTest { + + private final KeyGenerator keyGenerator = new SecureRandomKeyGenerator(); + + @Test + public void testEncryptAndDecrypt() throws IOException { + AesCipherStreamHandler cipherStreamHandler = new AesCipherStreamHandler(keyGenerator.createKey()); + + // douglas adams + String content = "If you try and take a cat apart to see how it works, the first thing you have on your hands is a nonworking cat."; + + // encrypt + ByteArrayOutputStream output = new ByteArrayOutputStream(); + OutputStream encryptedOutput = cipherStreamHandler.encrypt(output); + encryptedOutput.write(content.getBytes(Charsets.UTF_8)); + encryptedOutput.close(); + + InputStream input = new ByteArrayInputStream(output.toByteArray()); + input = cipherStreamHandler.decrypt(input); + byte[] decrypted = ByteStreams.toByteArray(input); + + assertEquals(content, new String(decrypted, Charsets.UTF_8)); + } + +} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ClientConfigurationTests.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ClientConfigurationTests.java new file mode 100644 index 0000000000..20422df7ed --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ClientConfigurationTests.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +package sonia.scm.cli.config; + +import com.google.common.base.Charsets; +import com.google.common.io.ByteStreams; + +import javax.xml.bind.JAXB; +import java.io.*; + +import static org.junit.Assert.assertEquals; + +final class ClientConfigurationTests { + + private ClientConfigurationTests() { + } + + static void testCipherStream(CipherStreamHandler cipherStreamHandler, String content) throws IOException { + byte[] encrypted = encrypt(cipherStreamHandler, content); + String decrypted = decrypt(cipherStreamHandler, encrypted); + assertEquals(content, decrypted); + } + + + static byte[] encrypt(CipherStreamHandler cipherStreamHandler, String content) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + OutputStream encryptedOutput = cipherStreamHandler.encrypt(output); + encryptedOutput.write(content.getBytes(Charsets.UTF_8)); + encryptedOutput.close(); + return output.toByteArray(); + } + + static String decrypt(CipherStreamHandler cipherStreamHandler, byte[] encrypted) throws IOException { + InputStream input = new ByteArrayInputStream(encrypted); + input = cipherStreamHandler.decrypt(input); + byte[] decrypted = ByteStreams.toByteArray(input); + input.close(); + + return new String(decrypted, Charsets.UTF_8); + } + + static void assertSampleConfig(ScmClientConfig config) { + ServerConfig defaultConfig; + defaultConfig = config.getDefaultConfig(); + + assertEquals("http://localhost:8080/scm", defaultConfig.getServerUrl()); + assertEquals("admin", defaultConfig.getUsername()); + assertEquals("admin123", defaultConfig.getPassword()); + } + + static ScmClientConfig createSampleConfig() { + ScmClientConfig config = new ScmClientConfig(); + ServerConfig defaultConfig = config.getDefaultConfig(); + defaultConfig.setServerUrl("http://localhost:8080/scm"); + defaultConfig.setUsername("admin"); + defaultConfig.setPassword("admin123"); + return config; + } + + static void encrypt(CipherStreamHandler cipherStreamHandler, ScmClientConfig config, File file) throws IOException { + try (OutputStream output = cipherStreamHandler.encrypt(new FileOutputStream(file))) { + JAXB.marshal(config, output); + } + } + +} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ConfigFilesTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ConfigFilesTest.java new file mode 100644 index 0000000000..5fbdfdc0c0 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ConfigFilesTest.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.cli.config; + +import com.google.common.base.Charsets; +import com.google.common.io.Files; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; + +import static org.junit.Assert.*; + +public class ConfigFilesTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testIsFormatV2() throws IOException { + byte[] content = "The door was the way to... to... The Door was The Way".getBytes(Charsets.UTF_8); + + File fileV1 = temporaryFolder.newFile(); + Files.write(content, fileV1); + + assertFalse(ConfigFiles.isFormatV2(fileV1)); + + File fileV2 = temporaryFolder.newFile(); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(ConfigFiles.VERSION_IDENTIFIER); + baos.write(content); + Files.write(baos.toByteArray(), fileV2); + + assertTrue(ConfigFiles.isFormatV2(fileV2)); + } + + @Test + public void testParseV1() throws IOException { + InMemoryKeyStore keyStore = createKeyStore(); + WeakCipherStreamHandler handler = new WeakCipherStreamHandler(keyStore.get()); + + ScmClientConfig config = ClientConfigurationTests.createSampleConfig(); + File file = temporaryFolder.newFile(); + ClientConfigurationTests.encrypt(handler, config, file); + + config = ConfigFiles.parseV1(keyStore, file); + ClientConfigurationTests.assertSampleConfig(config); + } + + @Test + public void storeAndParseV2() throws IOException { + InMemoryKeyStore keyStore = new InMemoryKeyStore(); + ScmClientConfig config = ClientConfigurationTests.createSampleConfig(); + File file = temporaryFolder.newFile(); + + ConfigFiles.store(keyStore, config, file); + + String key = keyStore.get(); + assertNotNull(key); + + config = ConfigFiles.parseV2(keyStore, file); + ClientConfigurationTests.assertSampleConfig(config); + } + + private InMemoryKeyStore createKeyStore() { + String secretKey = new SecureRandomKeyGenerator().createKey(); + InMemoryKeyStore keyStore = new InMemoryKeyStore(); + keyStore.set(secretKey); + return keyStore; + } + +} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemoryKeyStore.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemoryKeyStore.java new file mode 100644 index 0000000000..1d0069a087 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemoryKeyStore.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +package sonia.scm.cli.config; + +public class InMemoryKeyStore implements KeyStore { + + private String secretKey; + + @Override + public void set(String secretKey) { + this.secretKey = secretKey; + } + + @Override + public String get() { + return secretKey; + } + + @Override + public void remove() { + this.secretKey = null; + } +} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java index ec598fd2d4..a9f6a46418 100644 --- a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java @@ -31,6 +31,8 @@ package sonia.scm.cli.config; +import com.google.common.io.Files; +import com.google.common.io.Resources; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -38,10 +40,9 @@ import sonia.scm.security.UUIDKeyGenerator; import java.io.File; import java.io.IOException; +import java.net.URL; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; +import static org.junit.Assert.*; public class ScmClientConfigFileHandlerTest { @@ -53,7 +54,7 @@ public class ScmClientConfigFileHandlerTest { File configFile = temporaryFolder.newFile(); ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler( - new InMemoryKeyStore(), new UUIDKeyGenerator(), configFile + new InMemoryKeyStore(), configFile ); ScmClientConfig config = new ScmClientConfig(); @@ -76,23 +77,62 @@ public class ScmClientConfigFileHandlerTest { assertFalse(configFile.exists()); } - private static class InMemoryKeyStore implements KeyStore { + @Test + public void testClientConfigFileHandlerWithOldConfiguration() throws IOException { + File configFile = temporaryFolder.newFile(); - private String secretKey; + // old implementation has used uuids as keys + String key = new UUIDKeyGenerator().createKey(); - @Override - public void set(String secretKey) { - this.secretKey = secretKey; - } + WeakCipherStreamHandler weakCipherStreamHandler = new WeakCipherStreamHandler(key); + ScmClientConfig clientConfig = ClientConfigurationTests.createSampleConfig(); + ClientConfigurationTests.encrypt(weakCipherStreamHandler, clientConfig, configFile); - @Override - public String get() { - return secretKey; - } + assertFalse(ConfigFiles.isFormatV2(configFile)); - @Override - public void remove() { - this.secretKey = null; - } + KeyStore keyStore = new InMemoryKeyStore(); + keyStore.set(key); + + ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler( + keyStore, configFile + ); + + ScmClientConfig config = handler.read(); + ClientConfigurationTests.assertSampleConfig(config); + + // ensure key has changed + assertNotEquals(key, keyStore.get()); + + // ensure config rewritten with v2 + assertTrue(ConfigFiles.isFormatV2(configFile)); + } + + @Test + public void testClientConfigFileHandlerWithRealMigration() throws IOException { + URL resource = Resources.getResource("sonia/scm/cli/config/scm-cli-config.enc.xml"); + byte[] bytes = Resources.toByteArray(resource); + + File configFile = temporaryFolder.newFile(); + Files.write(bytes, configFile); + + String key = "358e018a-0c3c-4339-8266-3874e597305f"; + KeyStore keyStore = new InMemoryKeyStore(); + keyStore.set(key); + + ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler( + keyStore, configFile + ); + + ScmClientConfig config = handler.read(); + ServerConfig defaultConfig = config.getDefaultConfig(); + assertEquals("http://hitchhicker.com/scm", defaultConfig.getServerUrl()); + assertEquals("tricia", defaultConfig.getUsername()); + assertEquals("trillian123", defaultConfig.getPassword()); + + // ensure key has changed + assertNotEquals(key, keyStore.get()); + + // ensure config rewritten with v2 + assertTrue(ConfigFiles.isFormatV2(configFile)); } } diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/SecureRandomKeyGeneratorTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/SecureRandomKeyGeneratorTest.java new file mode 100644 index 0000000000..e847e89cb1 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/SecureRandomKeyGeneratorTest.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + +package sonia.scm.cli.config; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class SecureRandomKeyGeneratorTest { + + @Test + public void testCreateKey() { + SecureRandomKeyGenerator keyGenerator = new SecureRandomKeyGenerator(); + assertNotNull(keyGenerator.createKey()); + assertEquals(SecureRandomKeyGenerator.KEY_LENGTH, keyGenerator.createKey().length()); + } + +} diff --git a/scm-clients/scm-cli-client/src/test/resources/sonia/scm/cli/config/scm-cli-config.enc.xml b/scm-clients/scm-cli-client/src/test/resources/sonia/scm/cli/config/scm-cli-config.enc.xml new file mode 100644 index 0000000000000000000000000000000000000000..94132772a470186bfb3f7fcef19147caa328fb43 GIT binary patch literal 296 zcmV+@0oVRHh)0-9KiSs&h;Gc9iYttD@?y@sK-DK9rt+UqcB$|LJIGtiuWhPCD8AV|${J04dxuLvemaJDR8bujM$6VA+!l-1QGeQ)~D{8o;7;j|7E z{HjWNPp_;e+jmnC$8b58PmorI<1lg5!<=30g;E*#1Zoe+=vFqAjR2LPW0GvPwTh7{dS&&BRDH%NNkQTUEVsSI^Vz!`vE}xPm}6UZF0e3HMZ?%rM^3;`HLf89 ul*7JXT?4Q0fR^`g-Fk}4l+hkg?Dkrwzz~`30=lveB36|b^%kxKF|9s{3XnMf literal 0 HcmV?d00001 From cbecb3731bb77822aaafddb2b9e261ff6115864e Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 30 Apr 2018 09:27:00 +0200 Subject: [PATCH 4/8] #979 use a java 7 compatible cipher spec --- .../cli/config/AesCipherStreamHandler.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/AesCipherStreamHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/AesCipherStreamHandler.java index 5955b8b762..680dd25563 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/AesCipherStreamHandler.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/AesCipherStreamHandler.java @@ -37,7 +37,7 @@ import com.google.common.base.Charsets; import javax.crypto.Cipher; import javax.crypto.CipherInputStream; import javax.crypto.CipherOutputStream; -import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.SecretKeySpec; import java.io.IOException; import java.io.InputStream; @@ -53,7 +53,9 @@ import java.security.SecureRandom; */ public class AesCipherStreamHandler implements CipherStreamHandler { - private static final String ALGORITHM = "AES/GCM/NoPadding"; + private static final String CIPHER_ALGORITHM = "AES/CBC/PKCS5PADDING"; + private static final String SECRET_KEY_ALGORITHM = "AES"; + private static final int IV_LENGTH = 16; private final SecureRandom random = new SecureRandom(); @@ -77,11 +79,15 @@ public class AesCipherStreamHandler implements CipherStreamHandler { } private Cipher createCipherForDecryption(InputStream inputStream) throws IOException { - byte[] iv =new byte[12]; + byte[] iv = createEmptyIvArray(); inputStream.read(iv); return createCipher(Cipher.DECRYPT_MODE, iv); } + private byte[] createEmptyIvArray() { + return new byte[IV_LENGTH]; + } + private Cipher createCipherForEncryption() { byte[] iv = generateIV(); return createCipher(Cipher.ENCRYPT_MODE, iv); @@ -90,16 +96,16 @@ public class AesCipherStreamHandler implements CipherStreamHandler { private byte[] generateIV() { // use 12 byte as described at nist // https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf - byte[] iv = new byte[12]; + byte[] iv = createEmptyIvArray(); random.nextBytes(iv); return iv; } private Cipher createCipher(int mode, byte[] iv) { try { - Cipher cipher = Cipher.getInstance(ALGORITHM); - GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); - cipher.init(mode, new SecretKeySpec(secretKey, "AES"), parameterSpec); + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); + cipher.init(mode, new SecretKeySpec(secretKey, SECRET_KEY_ALGORITHM), ivParameterSpec); return cipher; } catch (Exception ex) { throw new ScmConfigException("failed to create cipher", ex); From 40b5ef485b7b002f5bdbddd2b86741ccc8f3ee40 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 30 Apr 2018 09:36:51 +0200 Subject: [PATCH 5/8] #979 encrypt the configuration keys before they are written to prefs --- .../cli/config/EncryptionKeyStoreWrapper.java | 138 ++++++++++++++++++ .../config/ScmClientConfigFileHandler.java | 6 +- .../config/EncryptionKeyStoreWrapperTest.java | 60 ++++++++ .../ScmClientConfigFileHandlerTest.java | 6 +- 4 files changed, 204 insertions(+), 6 deletions(-) create mode 100644 scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionKeyStoreWrapper.java create mode 100644 scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionKeyStoreWrapperTest.java diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionKeyStoreWrapper.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionKeyStoreWrapper.java new file mode 100644 index 0000000000..826660f00f --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionKeyStoreWrapper.java @@ -0,0 +1,138 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + +package sonia.scm.cli.config; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; +import com.google.common.base.Strings; +import com.google.common.io.BaseEncoding; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +/** + * The EncryptionKeyStoreWrapper is a wrapper around the {@link KeyStore} interface. The wrapper will encrypt the passed + * keys, before they are written to the underlying {@link KeyStore} implementation. The wrapper will also honor old + * unencrypted keys. + * + * @author Sebastian Sdorra + * @since 1.60 + */ +public class EncryptionKeyStoreWrapper implements KeyStore { + + private static final String ALGORITHM = "AES"; + + private static final SecureRandom random = new SecureRandom(); + + // i know storing the key directly in the class is far away from a best practice, but this is a chicken egg type + // of problem. We need a key to encrypt the stored keys, however encrypting the keys with a static defined key + // is better as storing them as plain text. + private static final byte[] SECRET_KEY = new byte[]{ 0x50, 0x61, 0x41, 0x67, 0x55, 0x43, 0x48, 0x7a, 0x48, 0x59, + 0x7a, 0x57, 0x6b, 0x34, 0x54, 0x62 + }; + + @VisibleForTesting + static final String ENCRYPTED_PREFIX = "enc:"; + + private KeyStore wrappedKeyStore; + + EncryptionKeyStoreWrapper(KeyStore wrappedKeyStore) { + this.wrappedKeyStore = wrappedKeyStore; + } + + @Override + public void set(String secretKey) { + String encrypted = encrypt(secretKey); + wrappedKeyStore.set(ENCRYPTED_PREFIX.concat(encrypted)); + } + + private String encrypt(String value) { + try { + Cipher cipher = createCipher(Cipher.ENCRYPT_MODE); + byte[] raw = cipher.doFinal(value.getBytes(Charsets.UTF_8)); + return encode(raw); + } catch (IllegalBlockSizeException | BadPaddingException ex) { + throw new ScmConfigException("failed to encrypt key", ex); + } + } + + private String encode(byte[] raw) { + return BaseEncoding.base64().encode(raw); + } + + @Override + public String get() { + String value = wrappedKeyStore.get(); + if (Strings.nullToEmpty(value).startsWith(ENCRYPTED_PREFIX)) { + String encrypted = value.substring(ENCRYPTED_PREFIX.length()); + return decrypt(encrypted); + } + return value; + } + + private String decrypt(String encoded) { + try { + Cipher cipher = createCipher(Cipher.DECRYPT_MODE); + byte[] raw = decode(encoded); + return new String(cipher.doFinal(raw), Charsets.UTF_8); + } catch (IllegalBlockSizeException | BadPaddingException ex) { + throw new ScmConfigException("failed to decrypt key", ex); + } + } + + private byte[] decode(String encoded) { + return BaseEncoding.base64().decode(encoded); + } + + private Cipher createCipher(int mode) { + try { + Cipher cipher = Cipher.getInstance(ALGORITHM); + SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY, "AES"); + cipher.init(mode, secretKeySpec, random); + return cipher; + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException ex) { + throw new ScmConfigException("failed to create key", ex); + } + } + + @Override + public void remove() { + wrappedKeyStore.remove(); + } +} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java index 00a0eb6cf6..78efd5f149 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java @@ -62,11 +62,11 @@ public class ScmClientConfigFileHandler /** - * Constructs a new ScmClientConfigFileHandler - * + * Constructs a new ScmClientConfigFileHandler with a encrypted {@link java.util.prefs.Preferences} based + * {@link KeyStore} and a determined default location. */ public ScmClientConfigFileHandler() { - this(new PrefsKeyStore(), getDefaultConfigFile()); + this(new EncryptionKeyStoreWrapper(new PrefsKeyStore()), getDefaultConfigFile()); } ScmClientConfigFileHandler(KeyStore keyStore,File file) { diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionKeyStoreWrapperTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionKeyStoreWrapperTest.java new file mode 100644 index 0000000000..0a77c9d772 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionKeyStoreWrapperTest.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010, Sebastian Sdorra + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * 3. Neither the name of SCM-Manager; nor the names of its + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * http://bitbucket.org/sdorra/scm-manager + * + */ + + + +package sonia.scm.cli.config; + +import org.junit.Test; + +import static org.junit.Assert.*; + +public class EncryptionKeyStoreWrapperTest { + + private KeyStore keyStore = new InMemoryKeyStore(); + + @Test + public void testEncryptionKeyStoreWrapper() { + EncryptionKeyStoreWrapper wrapper = new EncryptionKeyStoreWrapper(keyStore); + wrapper.set("mysecretkey"); + + assertEquals("mysecretkey", wrapper.get()); + assertTrue(keyStore.get().startsWith(EncryptionKeyStoreWrapper.ENCRYPTED_PREFIX)); + } + + @Test + public void testEncryptionKeyStoreWrapperWithOldUnencryptedKey() { + keyStore.set("mysecretkey"); + EncryptionKeyStoreWrapper wrapper = new EncryptionKeyStoreWrapper(keyStore); + assertEquals("mysecretkey", wrapper.get()); + } + +} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java index a9f6a46418..f7dfc36deb 100644 --- a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java @@ -54,7 +54,7 @@ public class ScmClientConfigFileHandlerTest { File configFile = temporaryFolder.newFile(); ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler( - new InMemoryKeyStore(), configFile + new EncryptionKeyStoreWrapper(new InMemoryKeyStore()), configFile ); ScmClientConfig config = new ScmClientConfig(); @@ -90,7 +90,7 @@ public class ScmClientConfigFileHandlerTest { assertFalse(ConfigFiles.isFormatV2(configFile)); - KeyStore keyStore = new InMemoryKeyStore(); + KeyStore keyStore = new EncryptionKeyStoreWrapper(new InMemoryKeyStore()); keyStore.set(key); ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler( @@ -116,7 +116,7 @@ public class ScmClientConfigFileHandlerTest { Files.write(bytes, configFile); String key = "358e018a-0c3c-4339-8266-3874e597305f"; - KeyStore keyStore = new InMemoryKeyStore(); + KeyStore keyStore = new EncryptionKeyStoreWrapper(new InMemoryKeyStore()); keyStore.set(key); ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler( From f3459729353b6c29c9570a225d72f8afcb9a173d Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Mon, 30 Apr 2018 11:01:00 +0200 Subject: [PATCH 6/8] #979 change encryption key prefix from enc to SKV2 (scm key version 2) --- .../java/sonia/scm/cli/config/EncryptionKeyStoreWrapper.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionKeyStoreWrapper.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionKeyStoreWrapper.java index 826660f00f..d86bfa40b6 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionKeyStoreWrapper.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionKeyStoreWrapper.java @@ -68,7 +68,7 @@ public class EncryptionKeyStoreWrapper implements KeyStore { }; @VisibleForTesting - static final String ENCRYPTED_PREFIX = "enc:"; + static final String ENCRYPTED_PREFIX = "SKV2:"; private KeyStore wrappedKeyStore; From 41dea4741337414ba96b89c659b5a75f2c63c8e2 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 4 May 2018 07:20:07 +0200 Subject: [PATCH 7/8] #979 rename KeyStore to SecretKeyStore --- .../sonia/scm/cli/config/ConfigFiles.java | 22 +++++++++---------- ...a => EncryptionSecretKeyStoreWrapper.java} | 20 ++++++++--------- ...KeyStore.java => PrefsSecretKeyStore.java} | 9 +++++--- .../config/ScmClientConfigFileHandler.java | 20 ++++++++--------- .../{KeyStore.java => SecretKeyStore.java} | 7 ++++-- .../sonia/scm/cli/config/ConfigFilesTest.java | 8 +++---- ... EncryptionSecretKeyStoreWrapperTest.java} | 12 +++++----- ...Store.java => InMemorySecretKeyStore.java} | 2 +- .../ScmClientConfigFileHandlerTest.java | 18 +++++++-------- 9 files changed, 62 insertions(+), 56 deletions(-) rename scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/{EncryptionKeyStoreWrapper.java => EncryptionSecretKeyStoreWrapper.java} (86%) rename scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/{PrefsKeyStore.java => PrefsSecretKeyStore.java} (91%) rename scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/{KeyStore.java => SecretKeyStore.java} (93%) rename scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/{EncryptionKeyStoreWrapperTest.java => EncryptionSecretKeyStoreWrapperTest.java} (81%) rename scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/{InMemoryKeyStore.java => InMemorySecretKeyStore.java} (96%) diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigFiles.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigFiles.java index 5087c50746..0f88ef662a 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigFiles.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ConfigFiles.java @@ -78,15 +78,15 @@ final class ConfigFiles { /** * Decrypt and parse v1 configuration file. * - * @param keyStore key store + * @param secretKeyStore key store * @param file configuration file * * @return client configuration * * @throws IOException */ - static ScmClientConfig parseV1(KeyStore keyStore, File file) throws IOException { - String secretKey = secretKey(keyStore); + static ScmClientConfig parseV1(SecretKeyStore secretKeyStore, File file) throws IOException { + String secretKey = secretKey(secretKeyStore); CipherStreamHandler cipherStreamHandler = new WeakCipherStreamHandler(secretKey); return decrypt(cipherStreamHandler, new FileInputStream(file)); } @@ -94,15 +94,15 @@ final class ConfigFiles { /** * Decrypt and parse v12configuration file. * - * @param keyStore key store + * @param secretKeyStore key store * @param file configuration file * * @return client configuration * * @throws IOException */ - static ScmClientConfig parseV2(KeyStore keyStore, File file) throws IOException { - String secretKey = secretKey(keyStore); + static ScmClientConfig parseV2(SecretKeyStore secretKeyStore, File file) throws IOException { + String secretKey = secretKey(secretKeyStore); CipherStreamHandler cipherStreamHandler = new AesCipherStreamHandler(secretKey); try (InputStream input = new FileInputStream(file)) { input.skip(VERSION_IDENTIFIER.length); @@ -114,24 +114,24 @@ final class ConfigFiles { * Store encrypt and write the configuration to the given file. * Note the method uses always the latest available format. * - * @param keyStore key store + * @param secretKeyStore key store * @param config configuration * @param file configuration file * * @throws IOException */ - static void store(KeyStore keyStore, ScmClientConfig config, File file) throws IOException { + static void store(SecretKeyStore secretKeyStore, ScmClientConfig config, File file) throws IOException { String secretKey = keyGenerator.createKey(); CipherStreamHandler cipherStreamHandler = new AesCipherStreamHandler(secretKey); try (OutputStream output = new FileOutputStream(file)) { output.write(VERSION_IDENTIFIER); encrypt(cipherStreamHandler, output, config); } - keyStore.set(secretKey); + secretKeyStore.set(secretKey); } - private static String secretKey(KeyStore keyStore) { - String secretKey = keyStore.get(); + private static String secretKey(SecretKeyStore secretKeyStore) { + String secretKey = secretKeyStore.get(); Preconditions.checkState(!Strings.isNullOrEmpty(secretKey), "no stored secret key found"); return secretKey; } diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionKeyStoreWrapper.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapper.java similarity index 86% rename from scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionKeyStoreWrapper.java rename to scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapper.java index d86bfa40b6..123655a7e3 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionKeyStoreWrapper.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapper.java @@ -47,14 +47,14 @@ import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; /** - * The EncryptionKeyStoreWrapper is a wrapper around the {@link KeyStore} interface. The wrapper will encrypt the passed - * keys, before they are written to the underlying {@link KeyStore} implementation. The wrapper will also honor old - * unencrypted keys. + * The EncryptionSecretKeyStoreWrapper is a wrapper around the {@link SecretKeyStore} interface. The wrapper will + * encrypt the passed secret keys, before they are written to the underlying {@link SecretKeyStore} implementation. The + * wrapper will also honor old unencrypted keys. * * @author Sebastian Sdorra * @since 1.60 */ -public class EncryptionKeyStoreWrapper implements KeyStore { +public class EncryptionSecretKeyStoreWrapper implements SecretKeyStore { private static final String ALGORITHM = "AES"; @@ -70,16 +70,16 @@ public class EncryptionKeyStoreWrapper implements KeyStore { @VisibleForTesting static final String ENCRYPTED_PREFIX = "SKV2:"; - private KeyStore wrappedKeyStore; + private SecretKeyStore wrappedSecretKeyStore; - EncryptionKeyStoreWrapper(KeyStore wrappedKeyStore) { - this.wrappedKeyStore = wrappedKeyStore; + EncryptionSecretKeyStoreWrapper(SecretKeyStore wrappedSecretKeyStore) { + this.wrappedSecretKeyStore = wrappedSecretKeyStore; } @Override public void set(String secretKey) { String encrypted = encrypt(secretKey); - wrappedKeyStore.set(ENCRYPTED_PREFIX.concat(encrypted)); + wrappedSecretKeyStore.set(ENCRYPTED_PREFIX.concat(encrypted)); } private String encrypt(String value) { @@ -98,7 +98,7 @@ public class EncryptionKeyStoreWrapper implements KeyStore { @Override public String get() { - String value = wrappedKeyStore.get(); + String value = wrappedSecretKeyStore.get(); if (Strings.nullToEmpty(value).startsWith(ENCRYPTED_PREFIX)) { String encrypted = value.substring(ENCRYPTED_PREFIX.length()); return decrypt(encrypted); @@ -133,6 +133,6 @@ public class EncryptionKeyStoreWrapper implements KeyStore { @Override public void remove() { - wrappedKeyStore.remove(); + wrappedSecretKeyStore.remove(); } } diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsKeyStore.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsSecretKeyStore.java similarity index 91% rename from scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsKeyStore.java rename to scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsSecretKeyStore.java index 26a7e67b05..d3bd6847a8 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsKeyStore.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsSecretKeyStore.java @@ -34,15 +34,18 @@ package sonia.scm.cli.config; import java.util.prefs.Preferences; /** - * KeyStore implementation with uses {@link Preferences}. + * SecretKeyStore implementation with uses {@link Preferences}. + * + * @author Sebastian Sdorra + * @since 1.60 */ -public class PrefsKeyStore implements KeyStore { +public class PrefsSecretKeyStore implements SecretKeyStore { private static final String PREF_SECRET_KEY = "scm.client.key"; private final Preferences preferences; - public PrefsKeyStore() { + PrefsSecretKeyStore() { // we use ScmClientConfigFileHandler as base for backward compatibility preferences = Preferences.userNodeForPackage(ScmClientConfigFileHandler.class); } diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java index 78efd5f149..5619d85f7e 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/ScmClientConfigFileHandler.java @@ -57,20 +57,20 @@ public class ScmClientConfigFileHandler //~--- constructors --------------------------------------------------------- - private final KeyStore keyStore; + private final SecretKeyStore secretKeyStore; private final File file; /** * Constructs a new ScmClientConfigFileHandler with a encrypted {@link java.util.prefs.Preferences} based - * {@link KeyStore} and a determined default location. + * {@link SecretKeyStore} and a determined default location. */ public ScmClientConfigFileHandler() { - this(new EncryptionKeyStoreWrapper(new PrefsKeyStore()), getDefaultConfigFile()); + this(new EncryptionSecretKeyStoreWrapper(new PrefsSecretKeyStore()), getDefaultConfigFile()); } - ScmClientConfigFileHandler(KeyStore keyStore,File file) { - this.keyStore = keyStore; + ScmClientConfigFileHandler(SecretKeyStore secretKeyStore, File file) { + this.secretKeyStore = secretKeyStore; this.file = file; } @@ -94,7 +94,7 @@ public class ScmClientConfigFileHandler throw new ScmConfigException("could not delete config file"); } - keyStore.remove(); + secretKeyStore.remove(); } /** @@ -117,10 +117,10 @@ public class ScmClientConfigFileHandler ScmClientConfig config; try { if (ConfigFiles.isFormatV2(file)) { - config = ConfigFiles.parseV2(keyStore, file); + config = ConfigFiles.parseV2(secretKeyStore, file); } else { - config = ConfigFiles.parseV1(keyStore, file); - ConfigFiles.store(keyStore, config, file); + config = ConfigFiles.parseV1(secretKeyStore, file); + ConfigFiles.store(secretKeyStore, config, file); } } catch (IOException ex) { throw new ScmConfigException("could not read config file", ex); @@ -136,7 +136,7 @@ public class ScmClientConfigFileHandler */ public void write(ScmClientConfig config) { try { - ConfigFiles.store(keyStore, config, file); + ConfigFiles.store(secretKeyStore, config, file); } catch (IOException ex) { throw new ScmConfigException("could not write config file", ex); } diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/KeyStore.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecretKeyStore.java similarity index 93% rename from scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/KeyStore.java rename to scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecretKeyStore.java index 7ce64750ce..be600125b7 100644 --- a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/KeyStore.java +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecretKeyStore.java @@ -32,9 +32,12 @@ package sonia.scm.cli.config; /** - * KeyStore is able to read and write keys. + * SecretKeyStore is able to read and write secret keys. + * + * @author Sebastian Sdorra + * @since 1.60 */ -public interface KeyStore { +public interface SecretKeyStore { /** * Writes the given secret key to the store. diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ConfigFilesTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ConfigFilesTest.java index 5fbdfdc0c0..d9edc64884 100644 --- a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ConfigFilesTest.java +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ConfigFilesTest.java @@ -69,7 +69,7 @@ public class ConfigFilesTest { @Test public void testParseV1() throws IOException { - InMemoryKeyStore keyStore = createKeyStore(); + InMemorySecretKeyStore keyStore = createKeyStore(); WeakCipherStreamHandler handler = new WeakCipherStreamHandler(keyStore.get()); ScmClientConfig config = ClientConfigurationTests.createSampleConfig(); @@ -82,7 +82,7 @@ public class ConfigFilesTest { @Test public void storeAndParseV2() throws IOException { - InMemoryKeyStore keyStore = new InMemoryKeyStore(); + InMemorySecretKeyStore keyStore = new InMemorySecretKeyStore(); ScmClientConfig config = ClientConfigurationTests.createSampleConfig(); File file = temporaryFolder.newFile(); @@ -95,9 +95,9 @@ public class ConfigFilesTest { ClientConfigurationTests.assertSampleConfig(config); } - private InMemoryKeyStore createKeyStore() { + private InMemorySecretKeyStore createKeyStore() { String secretKey = new SecureRandomKeyGenerator().createKey(); - InMemoryKeyStore keyStore = new InMemoryKeyStore(); + InMemorySecretKeyStore keyStore = new InMemorySecretKeyStore(); keyStore.set(secretKey); return keyStore; } diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionKeyStoreWrapperTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapperTest.java similarity index 81% rename from scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionKeyStoreWrapperTest.java rename to scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapperTest.java index 0a77c9d772..c9190c3357 100644 --- a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionKeyStoreWrapperTest.java +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapperTest.java @@ -37,23 +37,23 @@ import org.junit.Test; import static org.junit.Assert.*; -public class EncryptionKeyStoreWrapperTest { +public class EncryptionSecretKeyStoreWrapperTest { - private KeyStore keyStore = new InMemoryKeyStore(); + private SecretKeyStore secretKeyStore = new InMemorySecretKeyStore(); @Test public void testEncryptionKeyStoreWrapper() { - EncryptionKeyStoreWrapper wrapper = new EncryptionKeyStoreWrapper(keyStore); + EncryptionSecretKeyStoreWrapper wrapper = new EncryptionSecretKeyStoreWrapper(secretKeyStore); wrapper.set("mysecretkey"); assertEquals("mysecretkey", wrapper.get()); - assertTrue(keyStore.get().startsWith(EncryptionKeyStoreWrapper.ENCRYPTED_PREFIX)); + assertTrue(secretKeyStore.get().startsWith(EncryptionSecretKeyStoreWrapper.ENCRYPTED_PREFIX)); } @Test public void testEncryptionKeyStoreWrapperWithOldUnencryptedKey() { - keyStore.set("mysecretkey"); - EncryptionKeyStoreWrapper wrapper = new EncryptionKeyStoreWrapper(keyStore); + secretKeyStore.set("mysecretkey"); + EncryptionSecretKeyStoreWrapper wrapper = new EncryptionSecretKeyStoreWrapper(secretKeyStore); assertEquals("mysecretkey", wrapper.get()); } diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemoryKeyStore.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemorySecretKeyStore.java similarity index 96% rename from scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemoryKeyStore.java rename to scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemorySecretKeyStore.java index 1d0069a087..4510a7a072 100644 --- a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemoryKeyStore.java +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemorySecretKeyStore.java @@ -32,7 +32,7 @@ package sonia.scm.cli.config; -public class InMemoryKeyStore implements KeyStore { +public class InMemorySecretKeyStore implements SecretKeyStore { private String secretKey; diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java index f7dfc36deb..9f65d34655 100644 --- a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.java @@ -54,7 +54,7 @@ public class ScmClientConfigFileHandlerTest { File configFile = temporaryFolder.newFile(); ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler( - new EncryptionKeyStoreWrapper(new InMemoryKeyStore()), configFile + new EncryptionSecretKeyStoreWrapper(new InMemorySecretKeyStore()), configFile ); ScmClientConfig config = new ScmClientConfig(); @@ -90,18 +90,18 @@ public class ScmClientConfigFileHandlerTest { assertFalse(ConfigFiles.isFormatV2(configFile)); - KeyStore keyStore = new EncryptionKeyStoreWrapper(new InMemoryKeyStore()); - keyStore.set(key); + SecretKeyStore secretKeyStore = new EncryptionSecretKeyStoreWrapper(new InMemorySecretKeyStore()); + secretKeyStore.set(key); ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler( - keyStore, configFile + secretKeyStore, configFile ); ScmClientConfig config = handler.read(); ClientConfigurationTests.assertSampleConfig(config); // ensure key has changed - assertNotEquals(key, keyStore.get()); + assertNotEquals(key, secretKeyStore.get()); // ensure config rewritten with v2 assertTrue(ConfigFiles.isFormatV2(configFile)); @@ -116,11 +116,11 @@ public class ScmClientConfigFileHandlerTest { Files.write(bytes, configFile); String key = "358e018a-0c3c-4339-8266-3874e597305f"; - KeyStore keyStore = new EncryptionKeyStoreWrapper(new InMemoryKeyStore()); - keyStore.set(key); + SecretKeyStore secretKeyStore = new EncryptionSecretKeyStoreWrapper(new InMemorySecretKeyStore()); + secretKeyStore.set(key); ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler( - keyStore, configFile + secretKeyStore, configFile ); ScmClientConfig config = handler.read(); @@ -130,7 +130,7 @@ public class ScmClientConfigFileHandlerTest { assertEquals("trillian123", defaultConfig.getPassword()); // ensure key has changed - assertNotEquals(key, keyStore.get()); + assertNotEquals(key, secretKeyStore.get()); // ensure config rewritten with v2 assertTrue(ConfigFiles.isFormatV2(configFile)); From 418ad370e2bed25b3c6a89e5199eacdbd9594fed Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Fri, 4 May 2018 09:07:18 +0200 Subject: [PATCH 8/8] close branch issue-979