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..680dd25563 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/AesCipherStreamHandler.java @@ -0,0 +1,114 @@ +/** + * 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.IvParameterSpec; +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 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(); + + 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 = 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); + } + + private byte[] generateIV() { + // use 12 byte as described at nist + // https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf + byte[] iv = createEmptyIvArray(); + random.nextBytes(iv); + return iv; + } + + private Cipher createCipher(int mode, byte[] iv) { + try { + 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); + } + } +} 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..2321b3d9d9 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/CipherStreamHandler.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.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 { + + /** + * Decrypts the given input stream. + * + * @param inputStream encrypted input stream + * + * @return raw input stream + */ + InputStream decrypt(InputStream inputStream) throws IOException; + + /** + * Encrypts the given output stream. + * + * @param outputStream raw output stream + * + * @return encrypting output stream + */ + 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..0f88ef662a --- /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 secretKeyStore key store + * @param file configuration file + * + * @return client configuration + * + * @throws IOException + */ + static ScmClientConfig parseV1(SecretKeyStore secretKeyStore, File file) throws IOException { + String secretKey = secretKey(secretKeyStore); + CipherStreamHandler cipherStreamHandler = new WeakCipherStreamHandler(secretKey); + return decrypt(cipherStreamHandler, new FileInputStream(file)); + } + + /** + * Decrypt and parse v12configuration file. + * + * @param secretKeyStore key store + * @param file configuration file + * + * @return client configuration + * + * @throws IOException + */ + 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); + return decrypt(cipherStreamHandler, input); + } + } + + /** + * Store encrypt and write the configuration to the given file. + * Note the method uses always the latest available format. + * + * @param secretKeyStore key store + * @param config configuration + * @param file configuration 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); + } + secretKeyStore.set(secretKey); + } + + private static String secretKey(SecretKeyStore secretKeyStore) { + String secretKey = secretKeyStore.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/EncryptionSecretKeyStoreWrapper.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapper.java new file mode 100644 index 0000000000..123655a7e3 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapper.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 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 EncryptionSecretKeyStoreWrapper implements SecretKeyStore { + + 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 = "SKV2:"; + + private SecretKeyStore wrappedSecretKeyStore; + + EncryptionSecretKeyStoreWrapper(SecretKeyStore wrappedSecretKeyStore) { + this.wrappedSecretKeyStore = wrappedSecretKeyStore; + } + + @Override + public void set(String secretKey) { + String encrypted = encrypt(secretKey); + wrappedSecretKeyStore.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 = wrappedSecretKeyStore.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() { + wrappedSecretKeyStore.remove(); + } +} diff --git a/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsSecretKeyStore.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsSecretKeyStore.java new file mode 100644 index 0000000000..d3bd6847a8 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/PrefsSecretKeyStore.java @@ -0,0 +1,67 @@ +/** + * 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; + +/** + * SecretKeyStore implementation with uses {@link Preferences}. + * + * @author Sebastian Sdorra + * @since 1.60 + */ +public class PrefsSecretKeyStore implements SecretKeyStore { + + private static final String PREF_SECRET_KEY = "scm.client.key"; + + private final Preferences preferences; + + PrefsSecretKeyStore() { + // 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..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 @@ -35,38 +35,12 @@ package sonia.scm.cli.config; //~--- non-JDK imports -------------------------------------------------------- -import sonia.scm.util.IOUtil; 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.io.IOException; -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; +//~--- JDK imports ------------------------------------------------------------ /** * @@ -81,62 +55,46 @@ 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 SecretKeyStore secretKeyStore; + private final File file; + + /** - * Constructs ... - * + * Constructs a new ScmClientConfigFileHandler with a encrypted {@link java.util.prefs.Preferences} based + * {@link SecretKeyStore} and a determined default location. */ - public ScmClientConfigFileHandler() - { - prefs = Preferences.userNodeForPackage(ScmClientConfigFileHandler.class); - key = prefs.get(PREF_SECRET_KEY, null); + public ScmClientConfigFileHandler() { + this(new EncryptionSecretKeyStoreWrapper(new PrefsSecretKeyStore()), getDefaultConfigFile()); + } - if (Util.isEmpty(key)) - { - key = createNewKey(); - prefs.put(PREF_SECRET_KEY, key); - } - - try - { - context = JAXBContext.newInstance(ScmClientConfig.class); - } - catch (JAXBException ex) - { - throw new ScmConfigException( - "could not create JAXBContext for ScmClientConfig", ex); - } + ScmClientConfigFileHandler(SecretKeyStore secretKeyStore, File file) { + this.secretKeyStore = secretKeyStore; + this.file = file; } //~--- 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); + secretKeyStore.remove(); } /** @@ -145,162 +103,42 @@ 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); - - Unmarshaller um = context.createUnmarshaller(); - - config = (ScmClientConfig) um.unmarshal(input); - } - catch (Exception ex) - { - throw new ScmConfigException("could not read config file", ex); - } - finally - { - IOUtil.close(input); - } + if (file.exists()) { + config = readFromFile(); } return config; } + private ScmClientConfig readFromFile() { + ScmClientConfig config; + try { + if (ConfigFiles.isFormatV2(file)) { + config = ConfigFiles.parseV2(secretKeyStore, file); + } else { + config = ConfigFiles.parseV1(secretKeyStore, file); + ConfigFiles.store(secretKeyStore, config, file); + } + } catch (IOException ex) { + throw new ScmConfigException("could not read config file", ex); + } + return config; + } + /** * Method description * * * @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); - - Marshaller m = context.createMarshaller(); - - m.marshal(config, output); - } - catch (Exception ex) - { + public void write(ScmClientConfig config) { + try { + ConfigFiles.store(secretKeyStore, config, file); + } catch (IOException 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/SecretKeyStore.java b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecretKeyStore.java new file mode 100644 index 0000000000..be600125b7 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/SecretKeyStore.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; + +/** + * SecretKeyStore is able to read and write secret keys. + * + * @author Sebastian Sdorra + * @since 1.60 + */ +public interface SecretKeyStore { + + /** + * 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 or {@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/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 new file mode 100644 index 0000000000..175d1986c6 --- /dev/null +++ b/scm-clients/scm-cli-client/src/main/java/sonia/scm/cli/config/WeakCipherStreamHandler.java @@ -0,0 +1,113 @@ +/** + * 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}. 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 + */ +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 + */ + WeakCipherStreamHandler(String secretKey) { + this.secretKey = secretKey.toCharArray(); + } + + @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/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..d9edc64884 --- /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 { + InMemorySecretKeyStore 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 { + InMemorySecretKeyStore keyStore = new InMemorySecretKeyStore(); + 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 InMemorySecretKeyStore createKeyStore() { + String secretKey = new SecureRandomKeyGenerator().createKey(); + InMemorySecretKeyStore keyStore = new InMemorySecretKeyStore(); + keyStore.set(secretKey); + return keyStore; + } + +} diff --git a/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapperTest.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapperTest.java new file mode 100644 index 0000000000..c9190c3357 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/EncryptionSecretKeyStoreWrapperTest.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 EncryptionSecretKeyStoreWrapperTest { + + private SecretKeyStore secretKeyStore = new InMemorySecretKeyStore(); + + @Test + public void testEncryptionKeyStoreWrapper() { + EncryptionSecretKeyStoreWrapper wrapper = new EncryptionSecretKeyStoreWrapper(secretKeyStore); + wrapper.set("mysecretkey"); + + assertEquals("mysecretkey", wrapper.get()); + assertTrue(secretKeyStore.get().startsWith(EncryptionSecretKeyStoreWrapper.ENCRYPTED_PREFIX)); + } + + @Test + public void testEncryptionKeyStoreWrapperWithOldUnencryptedKey() { + 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/InMemorySecretKeyStore.java b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemorySecretKeyStore.java new file mode 100644 index 0000000000..4510a7a072 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/InMemorySecretKeyStore.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 InMemorySecretKeyStore implements SecretKeyStore { + + 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 new file mode 100644 index 0000000000..9f65d34655 --- /dev/null +++ b/scm-clients/scm-cli-client/src/test/java/sonia/scm/cli/config/ScmClientConfigFileHandlerTest.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.io.Files; +import com.google.common.io.Resources; +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 java.net.URL; + +import static org.junit.Assert.*; + +public class ScmClientConfigFileHandlerTest { + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Test + public void testClientConfigFileHandler() throws IOException { + File configFile = temporaryFolder.newFile(); + + ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler( + new EncryptionSecretKeyStoreWrapper(new InMemorySecretKeyStore()), 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()); + } + + @Test + public void testClientConfigFileHandlerWithOldConfiguration() throws IOException { + File configFile = temporaryFolder.newFile(); + + // old implementation has used uuids as keys + String key = new UUIDKeyGenerator().createKey(); + + WeakCipherStreamHandler weakCipherStreamHandler = new WeakCipherStreamHandler(key); + ScmClientConfig clientConfig = ClientConfigurationTests.createSampleConfig(); + ClientConfigurationTests.encrypt(weakCipherStreamHandler, clientConfig, configFile); + + assertFalse(ConfigFiles.isFormatV2(configFile)); + + SecretKeyStore secretKeyStore = new EncryptionSecretKeyStoreWrapper(new InMemorySecretKeyStore()); + secretKeyStore.set(key); + + ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler( + secretKeyStore, configFile + ); + + ScmClientConfig config = handler.read(); + ClientConfigurationTests.assertSampleConfig(config); + + // ensure key has changed + assertNotEquals(key, secretKeyStore.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"; + SecretKeyStore secretKeyStore = new EncryptionSecretKeyStoreWrapper(new InMemorySecretKeyStore()); + secretKeyStore.set(key); + + ScmClientConfigFileHandler handler = new ScmClientConfigFileHandler( + secretKeyStore, 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, secretKeyStore.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 0000000000..94132772a4 Binary files /dev/null and b/scm-clients/scm-cli-client/src/test/resources/sonia/scm/cli/config/scm-cli-config.enc.xml differ