Initial PBE symmetric encrypted secure.config Change-Id: Ic253c3686c21e0eceec54c3fdf311b27df74826f
diff --git a/BUCK b/BUCK index c339d5b..a4c0227 100644 --- a/BUCK +++ b/BUCK
@@ -1,4 +1,4 @@ -include_defs('//lib/maven.defs') +include_defs('//bucklets/gerrit_plugin.bucklet') gerrit_plugin( name = 'secure-config', @@ -16,8 +16,7 @@ name = 'secure-config_tests', srcs = glob(['src/test/java/**/*.java']), labels = ['secure-config'], - deps = [ + deps = GERRIT_PLUGIN_API + GERRIT_TESTS + [ ':secure-config__plugin', - '//gerrit-acceptance-framework:lib', ], )
diff --git a/README.md b/README.md index 519f1cd..128fcb4 100644 --- a/README.md +++ b/README.md
@@ -11,12 +11,13 @@ $ buck build plugins/secure-config ``` -Resulting plugin jar is generated under /buck-out/gen/plugins/secure-config/secure-config.jar +Resulting plugin jar is generated under +/buck-out/gen/plugins/secure-config/secure-config.jar ## How to install -Differently from the other plugins, secure-config needs to be copied to the /lib directory of -Gerrit installation. +Differently from the other plugins, secure-config needs to be copied to the /lib +directory of Gerrit installation. Example: @@ -26,9 +27,9 @@ ## How to configure -Add the gerrit.secureStoreClass configuration entry in gerrit.config to instruct Gerrit -to use the secure-store plugin for the encryption and decryption of all values contained -in your secure.config file. +Add the gerrit.secureStoreClass configuration entry in gerrit.config to instruct +Gerrit to use the secure-store plugin for the encryption and decryption of all +values contained in your secure.config file. Example: @@ -39,3 +40,75 @@ ^D ``` +## How to run + +Gerrit secure.config properties need to be generated and managed using the +Gerrit init wizard. All the passwords entered at init will be stored as +encrypted values and then decrypted *on-the-fly* when needed at runtime. + +Example: + +``` + $ cd $GERRIT_SITE && java -jar bin/gerrit.war init + Using secure store: com.googlesource.gerrit.plugins.secureconfig.SecureConfigStore + + *** Gerrit Code Review 2.13.2-1146-ga89e6a3 + [...] + + + $ cat etc/secure.config + [auth] + registerEmailPrivateKey = hfMC1Yi9NF5N3Yz7cVNUdJNPQfbb2g47RnaPElTraTh0MMB2OE+xeg== + +``` + +## Customising encryption settings + +Default settings are fully working but are meant to be use for DEMO purpose +only. You typicallty need to customize them according to your Company's Policies +about passwords and confidential data encryption standards. + +See below the gerrit.config parameters to customize the encryption security +settings. + +### secureConfig.jceProvider + +The JCE cryptographic provider for the encryption algorithms +and security keys. + +Default: SunJCE + +### secureConfig.cipher + +The encyrption algorithm to be used for encryption. Different JCE providers +provide a different set of cryptographic algorithms. + +Default: PBEWithMD5AndDES. + +*NOTE - The default value is considered insecure and should not be used in +production* + +### secureConfig.passwordDevice + +The device or file where to retrieve the encryption passphrase. + +Default: /dev/zero + +*NOTE - The all-zeros password is considered insecure and should not be used in +production* + +### secureConfig.passwordLength + +The length in bytes of the password read from the passwordDevice. + +Default: 8 + +*NOTE - A 8-bytes (64-bit) password length is considered insecure and should not +be used in production* + +### secureConfig.encoding + +Encoding to use when encrypting/decrypting values from secure.config. + +Default: UTF-8 +
diff --git a/src/main/java/com/googlesource/gerrit/plugins/secureconfig/Codec.java b/src/main/java/com/googlesource/gerrit/plugins/secureconfig/Codec.java new file mode 100644 index 0000000..27868a5 --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/secureconfig/Codec.java
@@ -0,0 +1,22 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.secureconfig; + +public interface Codec { + + public abstract String encode(String plain); + + public abstract String decode(String encoded); +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/secureconfig/PBECodec.java b/src/main/java/com/googlesource/gerrit/plugins/secureconfig/PBECodec.java new file mode 100644 index 0000000..9b7c1ba --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/secureconfig/PBECodec.java
@@ -0,0 +1,104 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.secureconfig; + +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.Security; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.PBEParameterSpec; + +@Singleton +public class PBECodec implements Codec { + private static final Logger log = LoggerFactory.getLogger(PBECodec.class); + byte[] salt = new byte[] {0x7d, 0x60, 0x43, 0x5f, 0x02, (byte) 0xe9, + (byte) 0xe0, (byte) 0xae}; + private static final int iterationCount = 2048; + + private final SecureConfigSettings config; + + @Inject + public PBECodec(SecureConfigSettings config) { + Provider provider = Security.getProvider(config.getJCEProvider()); + Security.addProvider(provider); + this.config = config; + } + + @Override + public String encode(String s) { + try { + Key sKey = generateKey(); + Cipher encoder = getCipher(); + + encoder.init(Cipher.ENCRYPT_MODE, sKey, getCipherParameterSpec()); + return new String(Base64.getEncoder().encodeToString( + encoder.doFinal(s.getBytes(config.getEncoding())))); + + } catch (Exception e) { + log.error("encode() failed", e); + throw new IllegalArgumentException("encode() failed", e); + } + } + + @Override + public String decode(String s) { + try { + Cipher encoder = getCipher(); + Key sKey = generateKey(); + + encoder.init(Cipher.DECRYPT_MODE, sKey, getCipherParameterSpec()); + return new String(encoder.doFinal(Base64.getDecoder().decode(s)), + config.getEncoding()); + + } catch (Exception e) { + log.error("decode() failed", e); + throw new IllegalArgumentException("encode() failed", e); + } + } + + private PBEParameterSpec getCipherParameterSpec() { + return new PBEParameterSpec(salt, iterationCount); + } + + private Cipher getCipher() throws NoSuchAlgorithmException, + NoSuchProviderException, NoSuchPaddingException { + Cipher encoder = + Cipher.getInstance(config.getCipher(), config.getJCEProvider()); + return encoder; + } + + private Key generateKey() throws NoSuchAlgorithmException, + NoSuchProviderException, InvalidKeySpecException { + PBEKeySpec pbeSpec = new PBEKeySpec(config.getPassword()); + SecretKeyFactory keyFact = + SecretKeyFactory.getInstance(config.getCipher(), + config.getJCEProvider()); + return keyFact.generateSecret(pbeSpec); + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/secureconfig/SecureConfigSettings.java b/src/main/java/com/googlesource/gerrit/plugins/secureconfig/SecureConfigSettings.java new file mode 100644 index 0000000..e5f3b3c --- /dev/null +++ b/src/main/java/com/googlesource/gerrit/plugins/secureconfig/SecureConfigSettings.java
@@ -0,0 +1,90 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.secureconfig; + +import com.google.gerrit.server.config.SitePaths; +import com.google.inject.Inject; +import com.google.inject.Singleton; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.lib.Config; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; + +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Base64; + +@Singleton +public class SecureConfigSettings { + private static final String SECURE_CONFIG = "secureConfig"; + private static final String PASSWORD_DEVICE = "passwordDevice"; + private static final String PASSWORD_LENGTH = "passwordLength"; + private static final int DEF_PASSWORD_LENGTH = 8; + private static final String CIPHER = "cipher"; + private static final String ENCODING = "encoding"; + private static final String DEF_ENCODING = "UTF-8"; + private static final String JCE_PROVIDER = "jceProvider"; + private static final String DEF_JCE_PROVIDER = "SunJCE"; + private final FileBasedConfig gerritConfig; + private final char[] password; + + @Inject + SecureConfigSettings(SitePaths site) throws IOException, + ConfigInvalidException { + Config baseConfig = new Config(); + baseConfig.setString(SECURE_CONFIG, null, CIPHER, "PBEWithMD5AndDES"); + baseConfig.setString(SECURE_CONFIG, null, PASSWORD_DEVICE, "/dev/zero"); + baseConfig.setInt(SECURE_CONFIG, null, PASSWORD_LENGTH, DEF_PASSWORD_LENGTH); + baseConfig.setString(SECURE_CONFIG, null, ENCODING, DEF_ENCODING); + baseConfig.setString(SECURE_CONFIG, null, JCE_PROVIDER, DEF_JCE_PROVIDER); + + gerritConfig = + new FileBasedConfig(baseConfig, site.gerrit_config.toFile(), + FS.DETECTED); + gerritConfig.load(); + password = readPassword(); + } + + String getCipher() { + return gerritConfig.getString(SECURE_CONFIG, null, CIPHER); + } + + public char[] getPassword() { + return password; + } + + public String getEncoding() { + return gerritConfig.getString(SECURE_CONFIG, null, ENCODING); + } + + public String getJCEProvider() { + return gerritConfig.getString(SECURE_CONFIG, null, JCE_PROVIDER); + } + + private char[] readPassword() throws IOException { + Path passwordDevice = + Paths.get(gerritConfig.getString(SECURE_CONFIG, null, PASSWORD_DEVICE)); + try (FileInputStream in = new FileInputStream(passwordDevice.toFile())) { + byte[] passphrase = + new byte[gerritConfig.getInt(SECURE_CONFIG, PASSWORD_LENGTH, + DEF_PASSWORD_LENGTH)]; + in.read(passphrase); + return new String(Base64.getEncoder().encode(passphrase)).toCharArray(); + } + } +}
diff --git a/src/main/java/com/googlesource/gerrit/plugins/secureconfig/SecureConfigStore.java b/src/main/java/com/googlesource/gerrit/plugins/secureconfig/SecureConfigStore.java index 3736364..9d657d3 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/secureconfig/SecureConfigStore.java +++ b/src/main/java/com/googlesource/gerrit/plugins/secureconfig/SecureConfigStore.java
@@ -14,9 +14,10 @@ package com.googlesource.gerrit.plugins.secureconfig; +import com.google.common.collect.FluentIterable; import com.google.gerrit.common.FileUtil; -import com.google.gerrit.server.securestore.SecureStore; import com.google.gerrit.server.config.SitePaths; +import com.google.gerrit.server.securestore.SecureStore; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -29,19 +30,23 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; @Singleton public class SecureConfigStore extends SecureStore { private final FileBasedConfig sec; private final Map<String, FileBasedConfig> pluginSec; private final SitePaths site; + private final Codec codec; @Inject - SecureConfigStore(SitePaths site) { + SecureConfigStore(SitePaths site, PBECodec codec) { this.site = site; + this.codec = codec; sec = new FileBasedConfig(site.secure_config.toFile(), FS.DETECTED); try { sec.load(); @@ -53,12 +58,14 @@ @Override public String[] getList(String section, String subsection, String name) { - return sec.getStringList(section, subsection, name); + return Arrays.stream(sec.getStringList(section, subsection, name)) + .map(codec::decode) + .toArray(String[]::new); } @Override - public synchronized String[] getListForPlugin(String pluginName, String section, - String subsection, String name) { + public synchronized String[] getListForPlugin(String pluginName, + String section, String subsection, String name) { FileBasedConfig cfg = null; if (pluginSec.containsKey(pluginName)) { cfg = pluginSec.get(pluginName); @@ -75,14 +82,19 @@ } } } - return cfg != null ? cfg.getStringList(section, subsection, name) : null; + return cfg != null ? FluentIterable + .from(cfg.getStringList(section, subsection, name)) + .transform(codec::decode).toArray(String.class) : null; } @Override public void setList(String section, String subsection, String name, List<String> values) { if (values != null) { - sec.setStringList(section, subsection, name, values); + sec.setStringList(section, subsection, name, + values.stream() + .map(codec::encode) + .collect(Collectors.toList())); } else { sec.unset(section, subsection, name); } @@ -119,7 +131,7 @@ } } - private static void saveSecure(final FileBasedConfig sec) throws IOException { + private void saveSecure(final FileBasedConfig sec) throws IOException { if (FileUtil.modified(sec)) { final byte[] out = Constants.encode(sec.toText()); final File path = sec.getFile();
diff --git a/src/test/java/com/googlesource/gerrit/plugins/secureconfig/CodecTest.java b/src/test/java/com/googlesource/gerrit/plugins/secureconfig/CodecTest.java new file mode 100644 index 0000000..1b97980 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/secureconfig/CodecTest.java
@@ -0,0 +1,78 @@ +// Copyright (C) 2016 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.googlesource.gerrit.plugins.secureconfig; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; + +import com.google.gerrit.server.config.SitePaths; + +import org.eclipse.jgit.errors.ConfigInvalidException; +import org.eclipse.jgit.storage.file.FileBasedConfig; +import org.eclipse.jgit.util.FS; +import org.junit.BeforeClass; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; + +public class CodecTest { + static String sitePath; + static File gerritConfigFile; + + @BeforeClass + public static void setUp() throws IOException { + sitePath = "/tmp/" + System.currentTimeMillis(); + new File(sitePath + "/etc").mkdirs(); + File gerritConfigFile = new File(sitePath + "/etc/gerrit.config"); + FileBasedConfig gerritConfig = + new FileBasedConfig(gerritConfigFile, + FS.DETECTED); + gerritConfig.save(); + gerritConfigFile.deleteOnExit(); + } + + @Test + public void encodedStringShouldBeDifferent() throws Exception { + Codec codec = newPBECodec(); + + String plainText = "a value"; + String cipherText = codec.encode(plainText); + + assertThat(cipherText, is(not(plainText))); + } + + @Test + public void decodedOrEncodedShouldBeTheSameValue() throws Exception { + Codec codec = newPBECodec(); + + String plainText = "plainText value"; + String cipherText = codec.encode(plainText); + String decodedText = codec.decode(cipherText); + + assertThat(decodedText, is(equalTo(plainText))); + } + + private Codec newPBECodec() throws IOException, ConfigInvalidException { + SitePaths sitePaths = new SitePaths(Paths.get("/tmp")); + SecureConfigSettings config = new SecureConfigSettings(sitePaths); + Codec codec = new PBECodec(config); + return codec; + } + +}