Browse Source

SONAR-2084 use RSA to encrypt settings

tags/3.0
Simon Brandhof 12 years ago
parent
commit
896acd53cb

+ 18
- 7
sonar-plugin-api/src/main/java/org/sonar/api/CoreProperties.java View File

@@ -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";

+ 32
- 0
sonar-plugin-api/src/main/java/org/sonar/api/config/Base64Cipher.java View File

@@ -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));
}
}

+ 25
- 0
sonar-plugin-api/src/main/java/org/sonar/api/config/Cipher.java View File

@@ -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);
}

+ 89
- 0
sonar-plugin-api/src/main/java/org/sonar/api/config/Encryption.java View File

@@ -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));
}
}

+ 137
- 0
sonar-plugin-api/src/main/java/org/sonar/api/config/RsaCipher.java View File

@@ -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);
}
}
}

+ 20
- 3
sonar-plugin-api/src/main/java/org/sonar/api/config/Settings.java View File

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


+ 95
- 0
sonar-plugin-api/src/test/java/org/sonar/api/config/RsaCipherTest.java View File

@@ -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);
}
}
}

+ 1
- 0
sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_private_key.txt View File

@@ -0,0 +1 @@
90219154459460484635307049294251309350624400174513872842964935995590426792849850754956692979878580134173903984923579664828287537160023584656524734039928278121145700539672753499137027823143447317638477535928797385199093031615075304372662494208460458746505946857452591645907526128623572362647338861106567346733,28487650981645105345729039749992166191644740180529930949117542540744133726768616377360480408404524731015987443184896433830608393640073974409602558039310242973125348929164889362700042142142217737063061860660679199646988663544428331146992861271726257946205621825278746735752228856292372558114153659087908459073

+ 1
- 0
sonar-plugin-api/src/test/resources/org/sonar/api/config/RsaCipherTest/rsa_public_key.txt View File

@@ -0,0 +1 @@
90219154459460484635307049294251309350624400174513872842964935995590426792849850754956692979878580134173903984923579664828287537160023584656524734039928278121145700539672753499137027823143447317638477535928797385199093031615075304372662494208460458746505946857452591645907526128623572362647338861106567346733,65537

Loading…
Cancel
Save