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