diff options
9 files changed, 431 insertions, 10 deletions
diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java b/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java index 75d3620545a..8cf0e3cb576 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java @@ -22,12 +22,18 @@ package org.sonar.api; /** * CoreProperties is used to group various properties of Sonar as well * as default values of configuration in a single place - * + * * @since 1.11 */ public interface CoreProperties { /** + * @since 2.15 + */ + String ENCRYPTION_PATH_TO_SECRET_KEY = "sonar.pathToSecretKey"; + + + /** * @since 2.11 */ String CATEGORY_GENERAL = "general"; @@ -85,7 +91,7 @@ public interface CoreProperties { /** * To determine value of this property use {@link org.sonar.api.resources.ProjectFileSystem#getSourceCharset()}. - * + * * @since 2.6 */ String ENCODING_PROPERTY = "sonar.sourceEncoding"; @@ -148,8 +154,8 @@ public interface CoreProperties { String SERVER_BASE_URL = "sonar.core.serverBaseURL"; /** - * @since 2.10 * @see #SERVER_BASE_URL + * @since 2.10 */ String SERVER_BASE_URL_DEFAULT_VALUE = "http://localhost:9000"; @@ -169,8 +175,8 @@ public interface CoreProperties { String CPD_ENGINE = "sonar.cpd.engine"; /** - * @since 2.11 * @see #CPD_ENGINE + * @since 2.11 */ String CPD_ENGINE_DEFAULT_VALUE = "sonar"; @@ -180,8 +186,8 @@ public interface CoreProperties { String CPD_CROSS_RPOJECT = "sonar.cpd.cross_project"; /** - * @since 2.11 * @see #CPD_CROSS_RPOJECT + * @since 2.11 */ boolean CPD_CROSS_RPOJECT_DEFAULT_VALUE = false; @@ -189,7 +195,7 @@ public interface CoreProperties { /** * Indicates whether Java bytecode analysis should be skipped. - * + * * @since 2.0 */ String DESIGN_SKIP_DESIGN_PROPERTY = "sonar.skipDesign"; @@ -197,7 +203,7 @@ public interface CoreProperties { /** * Indicates whether Package Design Analysis should be skipped. - * + * * @since 2.9 */ String DESIGN_SKIP_PACKAGE_DESIGN_PROPERTY = "sonar.skipPackageDesign"; diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/AesCipher.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/AesCipher.java new file mode 100644 index 00000000000..e8ed181966c --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/AesCipher.java @@ -0,0 +1,99 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.config; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Charsets; +import com.google.common.base.Throwables; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; +import org.sonar.api.CoreProperties; + +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.io.File; +import java.io.IOException; +import java.security.*; +import java.security.spec.InvalidKeySpecException; + +final class AesCipher extends Cipher { + + public static final int KEY_SIZE_IN_BITS = 128; + private final Settings settings; + + AesCipher(Settings settings) { + this.settings = settings; + } + + String encrypt(String clearText) { + String path = settings.getClearString(CoreProperties.ENCRYPTION_PATH_TO_SECRET_KEY); + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES"); + cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, loadSecretFileFromFile(path)); + return new String(Base64.encodeBase64(cipher.doFinal(clearText.getBytes(Charsets.UTF_8)))); + } catch (Exception e) { + throw Throwables.propagate(e); + } + } + + + String decrypt(String encryptedText) { + String path = settings.getClearString(CoreProperties.ENCRYPTION_PATH_TO_SECRET_KEY); + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES"); + cipher.init(javax.crypto.Cipher.DECRYPT_MODE, loadSecretFileFromFile(path)); + byte[] cipherData = cipher.doFinal(Base64.decodeBase64(StringUtils.trim(encryptedText))); + return new String(cipherData); + } catch (Exception e) { + throw Throwables.propagate(e); + } + } + + @VisibleForTesting + Key loadSecretFileFromFile(String path) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, InvalidKeyException { + if (StringUtils.isBlank(path)) { + throw new IllegalStateException("Secret key not found. Please set the property " + CoreProperties.ENCRYPTION_PATH_TO_SECRET_KEY); + } + File file = new File(path); + if (!file.exists() || !file.isFile()) { + throw new IllegalStateException("The property " + CoreProperties.ENCRYPTION_PATH_TO_SECRET_KEY + " does not link to a valid file: " + path); + } + + String s = FileUtils.readFileToString(file); + if (StringUtils.isBlank(s)) { + throw new IllegalStateException("No secret key in the file: " + path); + } + return new SecretKeySpec(Base64.decodeBase64(s), "AES"); + } + + String generateRandomSecretKey() { + try { + KeyGenerator keyGen = KeyGenerator.getInstance("AES"); + keyGen.init(KEY_SIZE_IN_BITS, new SecureRandom()); + SecretKey secretKey = keyGen.generateKey(); + return new String(Base64.encodeBase64(secretKey.getEncoded())); + + } catch (Exception e) { + throw new IllegalStateException("Fail to generate random RSA keys", e); + } + } +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/Base64Cipher.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/Base64Cipher.java new file mode 100644 index 00000000000..a04b40e953b --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/Base64Cipher.java @@ -0,0 +1,32 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.config; + +import org.apache.commons.codec.binary.Base64; + +final class Base64Cipher extends Cipher { + String encrypt(String clearText) { + return new String(Base64.encodeBase64(clearText.getBytes())); + } + + String decrypt(String encryptedText) { + return new String(Base64.decodeBase64(encryptedText)); + } +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/Cipher.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/Cipher.java new file mode 100644 index 00000000000..7a39c7c7653 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/Cipher.java @@ -0,0 +1,25 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.config; + +abstract class Cipher { + abstract String encrypt(String clearText); + abstract String decrypt(String encryptedText); +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/Encryption.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/Encryption.java new file mode 100644 index 00000000000..def3164985f --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/Encryption.java @@ -0,0 +1,86 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.config; + +import com.google.common.collect.ImmutableMap; + +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @since 2.15 + */ +public final class Encryption { + + private static final String BASE64_ALGORITHM = "b64"; + private final Base64Cipher base64Encryption; + + private static final String AES_ALGORITHM = "aes"; + private final AesCipher aesEncryption; + + private final Map<String, Cipher> encryptions; + private static final Pattern ENCRYPTED_PATTERN = Pattern.compile("\\{(.*?)\\}(.*)"); + + Encryption(Settings settings) { + base64Encryption = new Base64Cipher(); + aesEncryption = new AesCipher(settings); + encryptions = ImmutableMap.of( + BASE64_ALGORITHM, base64Encryption, + AES_ALGORITHM, aesEncryption + ); + } + + public boolean isEncrypted(String value) { + return value.startsWith("{") && value.indexOf("}") > 1; + } + + public String encrypt(String clearText) { + return encrypt(AES_ALGORITHM, clearText); + } + + public String scramble(String clearText) { + return encrypt(BASE64_ALGORITHM, clearText); + } + + public String generateRandomSecretKey() { + return aesEncryption.generateRandomSecretKey(); + } + + public String decrypt(String encryptedText) { + Matcher matcher = ENCRYPTED_PATTERN.matcher(encryptedText); + if (matcher.matches()) { + Cipher cipher = encryptions.get(matcher.group(1).toLowerCase(Locale.ENGLISH)); + if (cipher != null) { + return cipher.decrypt(matcher.group(2)); + } + } + return encryptedText; + } + + private String encrypt(String algorithm, String clearText) { + Cipher cipher = encryptions.get(algorithm); + if (cipher == null) { + throw new IllegalArgumentException("Unknown cipher algorithm: " + algorithm); + } + return String.format("{%s}%s", algorithm, cipher.encrypt(clearText)); + } +} diff --git a/sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java index 755345be3f5..8e88562a2bb 100644 --- a/sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java +++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java @@ -38,15 +38,18 @@ import java.util.*; */ public class Settings implements BatchComponent, ServerComponent { - protected Map<String, String> properties = Maps.newHashMap(); - protected PropertyDefinitions definitions; + protected final Map<String, String> properties; + protected final PropertyDefinitions definitions; + private final Encryption encryption; public Settings() { this(new PropertyDefinitions()); } public Settings(PropertyDefinitions definitions) { + this.properties = Maps.newHashMap(); this.definitions = definitions; + this.encryption = new Encryption(this); } public final String getDefaultValue(String key) { @@ -65,6 +68,23 @@ public class Settings implements BatchComponent, ServerComponent { String value = properties.get(key); if (value == null) { value = getDefaultValue(key); + } else if (encryption.isEncrypted(value)) { + try { + value = encryption.decrypt(value); + } catch (Exception e) { + throw new IllegalStateException("Fail to decrypt the property " + key + ". Please check your secret key."); + } + } + return value; + } + + /** + * Does not decrypt value. + */ + protected String getClearString(String key) { + String value = properties.get(key); + if (value == null) { + value = getDefaultValue(key); } return value; } @@ -217,7 +237,8 @@ public class Settings implements BatchComponent, ServerComponent { } public final Settings setProperties(Map<String, String> props) { - properties = Maps.newHashMap(props); + properties.clear(); + properties.putAll(props); return this; } diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/config/AesCipherTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/config/AesCipherTest.java new file mode 100644 index 00000000000..47b432f75d5 --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/config/AesCipherTest.java @@ -0,0 +1,92 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.config; + +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang.StringUtils; +import org.junit.Test; +import org.sonar.api.CoreProperties; + +import java.io.File; +import java.net.URL; +import java.security.Key; + +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +public class AesCipherTest { + + @Test + public void generateRandomSecretKey() { + AesCipher cipher = new AesCipher(new Settings()); + + String key = cipher.generateRandomSecretKey(); + + assertThat(StringUtils.isNotBlank(key), is(true)); + assertThat(Base64.isArrayByteBase64(key.getBytes()), is(true)); + } + + @Test + public void encrypt() throws Exception { + Settings settings = new Settings(); + settings.setProperty(CoreProperties.ENCRYPTION_PATH_TO_SECRET_KEY, pathToSecretKey()); + AesCipher cipher = new AesCipher(settings); + + String encryptedText = cipher.encrypt("sonar"); + System.out.println(encryptedText); + assertThat(StringUtils.isNotBlank(encryptedText), is(true)); + assertThat(Base64.isArrayByteBase64(encryptedText.getBytes()), is(true)); + } + + @Test + public void decrypt() throws Exception { + Settings settings = new Settings(); + settings.setProperty(CoreProperties.ENCRYPTION_PATH_TO_SECRET_KEY, pathToSecretKey()); + AesCipher cipher = new AesCipher(settings); + + // the following value has been encrypted with the key /org/sonar/api/config/AesCipherTest/aes_secret_key.txt + String clearText = cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY="); + + assertThat(clearText, is("this is a secret")); + } + + @Test + public void encryptThenDecrypt() throws Exception { + Settings settings = new Settings(); + settings.setProperty(CoreProperties.ENCRYPTION_PATH_TO_SECRET_KEY, pathToSecretKey()); + AesCipher cipher = new AesCipher(settings); + + assertThat(cipher.decrypt(cipher.encrypt("foo")), is("foo")); + } + + @Test + public void loadSecretKeyFromFile() throws Exception { + AesCipher cipher = new AesCipher(new Settings()); + Key secretKey = cipher.loadSecretFileFromFile(pathToSecretKey()); + assertThat(secretKey.getAlgorithm(), is("AES")); + assertThat(secretKey.getEncoded().length, greaterThan(10)); + } + + private String pathToSecretKey() throws Exception { + URL resource = getClass().getResource("/org/sonar/api/config/AesCipherTest/aes_secret_key.txt"); + return new File(resource.toURI()).getCanonicalPath(); + } +} diff --git a/sonar-plugin-api/src/test/java/org/sonar/api/config/EncryptionTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/config/EncryptionTest.java new file mode 100644 index 00000000000..1333120422c --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/config/EncryptionTest.java @@ -0,0 +1,59 @@ +/* + * Sonar, open source software quality management tool. + * Copyright (C) 2008-2012 SonarSource + * mailto:contact AT sonarsource DOT com + * + * Sonar is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * Sonar is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Sonar; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02 + */ +package org.sonar.api.config; + +import org.junit.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; + +public class EncryptionTest { + + @Test + public void isEncrypted() { + Encryption encryption = new Encryption(new Settings()); + assertThat(encryption.isEncrypted("{aes}ADASDASAD"), is(true)); + assertThat(encryption.isEncrypted("{b64}ADASDASAD"), is(true)); + assertThat(encryption.isEncrypted("{abc}ADASDASAD"), is(true)); + + assertThat(encryption.isEncrypted("{}"), is(false)); + assertThat(encryption.isEncrypted("{foo"), is(false)); + assertThat(encryption.isEncrypted("foo{aes}"), is(false)); + } + + @Test + public void decrypt() { + Encryption encryption = new Encryption(new Settings()); + assertThat(encryption.decrypt("{b64}Zm9v"), is("foo")); + } + + @Test + public void decrypt_unknown_algorithm() { + Encryption encryption = new Encryption(new Settings()); + assertThat(encryption.decrypt("{xxx}Zm9v"), is("{xxx}Zm9v")); + } + + @Test + public void decrypt_uncrypted_text() { + Encryption encryption = new Encryption(new Settings()); + assertThat(encryption.decrypt("foo"), is("foo")); + + } +} diff --git a/sonar-plugin-api/src/test/resources/org/sonar/api/config/AesCipherTest/aes_secret_key.txt b/sonar-plugin-api/src/test/resources/org/sonar/api/config/AesCipherTest/aes_secret_key.txt new file mode 100644 index 00000000000..65b98c522da --- /dev/null +++ b/sonar-plugin-api/src/test/resources/org/sonar/api/config/AesCipherTest/aes_secret_key.txt @@ -0,0 +1 @@ +0PZz+G+f8mjr3sPn4+AhHg==
\ No newline at end of file |