@@ -22,11 +22,22 @@ 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 | |||
*/ | |||
@@ -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"; |
@@ -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)); | |||
} | |||
} |
@@ -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); | |||
} |
@@ -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)); | |||
} | |||
} |
@@ -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); | |||
} | |||
} | |||
} |
@@ -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) { | |||
@@ -62,6 +65,19 @@ public class Settings implements BatchComponent, ServerComponent { | |||
} | |||
public final String getString(String key) { | |||
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); | |||
@@ -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; | |||
} | |||
@@ -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); | |||
} | |||
} | |||
} |
@@ -0,0 +1 @@ | |||
90219154459460484635307049294251309350624400174513872842964935995590426792849850754956692979878580134173903984923579664828287537160023584656524734039928278121145700539672753499137027823143447317638477535928797385199093031615075304372662494208460458746505946857452591645907526128623572362647338861106567346733,28487650981645105345729039749992166191644740180529930949117542540744133726768616377360480408404524731015987443184896433830608393640073974409602558039310242973125348929164889362700042142142217737063061860660679199646988663544428331146992861271726257946205621825278746735752228856292372558114153659087908459073 |
@@ -0,0 +1 @@ | |||
90219154459460484635307049294251309350624400174513872842964935995590426792849850754956692979878580134173903984923579664828287537160023584656524734039928278121145700539672753499137027823143447317638477535928797385199093031615075304372662494208460458746505946857452591645907526128623572362647338861106567346733,65537 |