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