diff options
author | Simon Brandhof <simon.brandhof@gmail.com> | 2012-03-13 16:06:51 +0100 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@gmail.com> | 2012-03-13 16:06:51 +0100 |
commit | 896acd53cbdf4cb16536055a7d48bfcb4b613c3d (patch) | |
tree | 274b33b1846da9a916ea310c67a283e6a8cccb3f /sonar-plugin-api | |
parent | 1d631531e60d1897f1c0dc7f20e39ec9bf59c60f (diff) | |
download | sonarqube-896acd53cbdf4cb16536055a7d48bfcb4b613c3d.tar.gz sonarqube-896acd53cbdf4cb16536055a7d48bfcb4b613c3d.zip |
SONAR-2084 use RSA to encrypt settings
Diffstat (limited to 'sonar-plugin-api')
9 files changed, 418 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..32a67d2b529 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,23 @@ 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_PRIVATE_KEY = "sonar.encryption.privateKeyPath"; + + /** + * @since 2.15 + */ + String ENCRYPTION_PUBLIC_KEY = "sonar.encryption.publicKey"; + + + /** * @since 2.11 */ String CATEGORY_GENERAL = "general"; @@ -85,7 +96,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 +159,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 +180,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 +191,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 +200,7 @@ public interface CoreProperties { /** * Indicates whether Java bytecode analysis should be skipped. - * + * * @since 2.0 */ String DESIGN_SKIP_DESIGN_PROPERTY = "sonar.skipDesign"; @@ -197,7 +208,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/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..edc7dfab7b8 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/Encryption.java @@ -0,0 +1,89 @@ +/* + * 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 RSA_ALGORITHM = "rsa"; + private final RsaCipher rsaEncryption; + + private final Map<String, Cipher> encryptions; + private static final Pattern ENCRYPTED_PATTERN = Pattern.compile("\\{(.*?)\\}(.*)"); + + Encryption(Settings settings) { + base64Encryption = new Base64Cipher(); + rsaEncryption = new RsaCipher(settings); + encryptions = ImmutableMap.of( + BASE64_ALGORITHM, base64Encryption, + RSA_ALGORITHM, rsaEncryption + ); + } + + public boolean isEncrypted(String value) { + return value.startsWith("{") && value.indexOf("}") > 1; + } + + public String encrypt(String clearText) { + return encrypt(RSA_ALGORITHM, clearText); + } + + public String scramble(String clearText) { + return encrypt(BASE64_ALGORITHM, clearText); + } + + /** + * @return an array of 2 strings: {public key, private key} + */ + public String[] generateRandomKeys() { + return rsaEncryption.generateRandomKeys(); + } + + public String decrypt(String encryptedText) { + Matcher matcher = ENCRYPTED_PATTERN.matcher(encryptedText); + if (matcher.matches()) { + Cipher cipher = encryptions.get(matcher.group(0).toLowerCase(Locale.ENGLISH)); + if (cipher != null) { + return cipher.decrypt(matcher.group(1)); + } + } + 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/RsaCipher.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/RsaCipher.java new file mode 100644 index 00000000000..3e96c096c12 --- /dev/null +++ b/sonar-plugin-api/src/main/java/org/sonar/api/config/RsaCipher.java @@ -0,0 +1,137 @@ +/* + * 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.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 java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.security.*; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; + +final class RsaCipher extends Cipher { + + private final Settings settings; + + RsaCipher(Settings settings) { + this.settings = settings; + } + + String encrypt(String clearText) { + String publicKey = settings.getClearString(CoreProperties.ENCRYPTION_PUBLIC_KEY); + if (StringUtils.isBlank(publicKey)) { + throw new IllegalStateException("RSA public key is missing. Please generate one."); + } + return encrypt(clearText, publicKey); + } + + private String encrypt(String clearText, String publicKey) { + try { + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("RSA"); + cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, toPublicKey(publicKey)); + return new String(Base64.encodeBase64(cipher.doFinal(clearText.getBytes()))); + } catch (Exception e) { + throw Throwables.propagate(e); + } + } + + String decrypt(String encryptedText) { + try { + PrivateKey privateKey = loadPrivateKey(); + javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("RSA"); + cipher.init(javax.crypto.Cipher.DECRYPT_MODE, privateKey); + byte[] cipherData = cipher.doFinal(Base64.decodeBase64(StringUtils.trim(encryptedText))); + return new String(cipherData); + } catch (Exception e) { + throw Throwables.propagate(e); + } + } + + private PrivateKey loadPrivateKey() throws NoSuchAlgorithmException, IOException, InvalidKeySpecException, InvalidKeyException { + String path = settings.getClearString(CoreProperties.ENCRYPTION_PATH_TO_PRIVATE_KEY); + return loadPrivateKeyFromFile(path); + } + + @VisibleForTesting + PrivateKey loadPrivateKeyFromFile(String path) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, InvalidKeyException { + if (StringUtils.isBlank(path)) { + throw new IllegalStateException("Impossible to decrypt text without the private key. Please set the property " + CoreProperties.ENCRYPTION_PATH_TO_PRIVATE_KEY); + } + File file = new File(path); + if (!file.exists() || !file.isFile()) { + throw new IllegalStateException("The property " + CoreProperties.ENCRYPTION_PATH_TO_PRIVATE_KEY + " does not link to a valid file: " + path); + } + + String s = FileUtils.readFileToString(file); + if (StringUtils.isBlank(s)) { + throw new IllegalStateException("No private key in the file: " + path); + } + String[] fields = StringUtils.split(StringUtils.trim(s), ","); + if (fields.length != 2) { + throw new IllegalStateException("Badly formatted private key in the file: " + path); + } + BigInteger modulus = new BigInteger(fields[0]); + BigInteger exponent = new BigInteger(fields[1]); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePrivate(new RSAPrivateKeySpec(modulus, exponent)); + } + + @VisibleForTesting + PublicKey toPublicKey(String text) throws InvalidKeySpecException, NoSuchAlgorithmException { + if (StringUtils.isBlank(text)) { + throw new IllegalArgumentException("The public key is blank"); + } + String[] fields = StringUtils.split(StringUtils.trim(text), ","); + if (fields.length != 2) { + throw new IllegalStateException("Unknown format of public key: " + text); + } + BigInteger modulus = new BigInteger(fields[0]); + BigInteger exponent = new BigInteger(fields[1]); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePublic(new RSAPublicKeySpec(modulus, exponent)); + } + + String[] generateRandomKeys() { + try { + KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA"); + gen.initialize(1024, new SecureRandom()); + KeyPair pair = gen.generateKeyPair(); + + KeyFactory fact = KeyFactory.getInstance("RSA"); + RSAPublicKeySpec pub = fact.getKeySpec(pair.getPublic(), RSAPublicKeySpec.class); + RSAPrivateKeySpec priv = fact.getKeySpec(pair.getPrivate(), RSAPrivateKeySpec.class); + + String publicKey = pub.getModulus() + "," + pub.getPublicExponent(); + String privateKey = priv.getModulus() + "," + priv.getPrivateExponent(); + return new String[]{publicKey, privateKey}; + + } 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/Settings.java b/sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java index 755345be3f5..8de950ac7f5 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,19 @@ public class Settings implements BatchComponent, ServerComponent { String value = properties.get(key); if (value == null) { value = getDefaultValue(key); + } else if (encryption.isEncrypted(value)) { + value = encryption.decrypt(value); + } + 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 +233,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/RsaCipherTest.java b/sonar-plugin-api/src/test/java/org/sonar/api/config/RsaCipherTest.java new file mode 100644 index 00000000000..2bd4a538c45 --- /dev/null +++ b/sonar-plugin-api/src/test/java/org/sonar/api/config/RsaCipherTest.java @@ -0,0 +1,95 @@ +/* + * 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.io.IOUtils; +import org.apache.commons.lang.StringUtils; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.security.PrivateKey; +import java.security.PublicKey; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +public class RsaCipherTest { + @Test + public void encrypt() throws IOException { + Settings settings = new Settings(); + settings.setProperty("sonar.encryption.publicKey", loadPublicKey()); + RsaCipher cipher = new RsaCipher(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 URISyntaxException, IOException { + File file = new File(getClass().getResource("/org/sonar/api/config/RsaCipherTest/rsa_private_key.txt").toURI()); + Settings settings = new Settings(); + settings.setProperty("sonar.encryption.privateKeyPath", file.getCanonicalPath()); + RsaCipher cipher = new RsaCipher(settings); + + // the following value has been encrypted with the public key /org/sonar/api/config/RsaCipherTest/rsa_public_key.txt + String clearText = cipher.decrypt("bnFlXnB5A8kLV4VR1FSGI4BmKd9I1E7euOQq/yB8a8RIpW34YYQX0toM5GTymY5EwkMO+KvfcpKXIvvhthr+5beW8v2nDux8n3VSH+tb+3wJZ+UYZQBQAQj2G8FVvYxbvRk3WVGn9bpw3x6195/gEneGvcG/A41/YsDHDce9zLw="); + assertThat(clearText, is("this is a secret")); + } + + @Test + public void encryptThenDecrypt() throws URISyntaxException, IOException { + File file = new File(getClass().getResource("/org/sonar/api/config/RsaCipherTest/rsa_private_key.txt").toURI()); + Settings settings = new Settings(); + settings.setProperty("sonar.encryption.publicKey", loadPublicKey()); + settings.setProperty("sonar.encryption.privateKeyPath", file.getCanonicalPath()); + RsaCipher cipher = new RsaCipher(settings); + + assertThat(cipher.decrypt(cipher.encrypt("foo")), is("foo")); + } + + @Test + public void loadPrivateKeyFromFile() throws Exception { + File file = new File(getClass().getResource("/org/sonar/api/config/RsaCipherTest/rsa_private_key.txt").toURI()); + RsaCipher cipher = new RsaCipher(new Settings()); + PrivateKey privateKey = cipher.loadPrivateKeyFromFile(file.getPath()); + assertThat(privateKey.getAlgorithm(), is("RSA")); + } + + @Test + public void toPublicKey() throws Exception { + RsaCipher cipher = new RsaCipher(new Settings()); + PublicKey publicKey = cipher.toPublicKey(loadPublicKey()); + assertThat(publicKey.getAlgorithm(), is("RSA")); + } + + private String loadPublicKey() throws IOException { + InputStream input = getClass().getResourceAsStream("/org/sonar/api/config/RsaCipherTest/rsa_public_key.txt"); + try { + return IOUtils.toString(input); + } finally { + IOUtils.closeQuietly(input); + } + } +} diff --git a/sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_private_key.txt b/sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_private_key.txt new file mode 100644 index 00000000000..10be545d264 --- /dev/null +++ b/sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_private_key.txt @@ -0,0 +1 @@ +90219154459460484635307049294251309350624400174513872842964935995590426792849850754956692979878580134173903984923579664828287537160023584656524734039928278121145700539672753499137027823143447317638477535928797385199093031615075304372662494208460458746505946857452591645907526128623572362647338861106567346733,28487650981645105345729039749992166191644740180529930949117542540744133726768616377360480408404524731015987443184896433830608393640073974409602558039310242973125348929164889362700042142142217737063061860660679199646988663544428331146992861271726257946205621825278746735752228856292372558114153659087908459073
\ No newline at end of file diff --git a/sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_public_key.txt b/sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_public_key.txt new file mode 100644 index 00000000000..ef746000bcc --- /dev/null +++ b/sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_public_key.txt @@ -0,0 +1 @@ +90219154459460484635307049294251309350624400174513872842964935995590426792849850754956692979878580134173903984923579664828287537160023584656524734039928278121145700539672753499137027823143447317638477535928797385199093031615075304372662494208460458746505946857452591645907526128623572362647338861106567346733,65537
\ No newline at end of file |