#979 split implementation of ScmClientConfigFileHandler in order to create new more secure implementation

This commit is contained in:
Sebastian Sdorra
2018-04-17 22:00:54 +02:00
parent b8456d364c
commit a55dd9873b
7 changed files with 439 additions and 197 deletions

View File

@@ -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);
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -66,7 +66,7 @@ public class ScmClientConfig
* Constructs ...
*
*/
private ScmClientConfig()
ScmClientConfig()
{
this.serverConfigMap = new HashMap<String, ServerConfig>();
}

View File

@@ -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;
}

View File

@@ -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 <a href="https://bitbucket.org/sdorra/scm-manager/issues/978/iteration-count-for-password-based">Issue 978</a>
* @see <a href="https://bitbucket.org/sdorra/scm-manager/issues/979/constant-salts-for-pbe-are-insecure">Issue 979</a>
*/
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);
}
}

View File

@@ -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;
}
}
}