diff options
author | Simon Brandhof <simon.brandhof@gmail.com> | 2012-03-13 23:08:17 +0100 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@gmail.com> | 2012-03-13 23:08:17 +0100 |
commit | e9b5cded528c6ad33d91fa61cd1be4b917983738 (patch) | |
tree | 4e02c019b8df5f3c55d51414d5606f433fc8367b | |
parent | 017f39d3bdda35f97c564cfb31f9dc34ef5708af (diff) | |
download | sonarqube-e9b5cded528c6ad33d91fa61cd1be4b917983738.tar.gz sonarqube-e9b5cded528c6ad33d91fa61cd1be4b917983738.zip |
SONAR-2084 complete web UI to generate secret key and to encrypt passwords
13 files changed, 126 insertions, 46 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 8cf0e3cb576..dea53e74b72 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 @@ -30,7 +30,7 @@ public interface CoreProperties { /** * @since 2.15 */ - String ENCRYPTION_PATH_TO_SECRET_KEY = "sonar.pathToSecretKey"; + String ENCRYPTION_SECRET_KEY_FILE = "sonar.secretKeyFile"; /** 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 index 51232587bbd..52a64c626d3 100644 --- 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 @@ -40,6 +40,9 @@ import java.security.spec.InvalidKeySpecException; final class AesCipher extends Cipher { + // Can't be increased because of Java 6 policy files : + // https://confluence.terena.org/display/~visser/No+256+bit+ciphers+for+Java+apps + // http://java.sun.com/javase/6/webnotes/install/jre/README public static final int KEY_SIZE_IN_BITS = 128; private final Settings settings; @@ -57,7 +60,6 @@ final class AesCipher extends Cipher { } } - String decrypt(String encryptedText) { try { javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance("AES"); @@ -69,34 +71,37 @@ final class AesCipher extends Cipher { } } - public boolean canEncrypt() { - try { - return loadSecretFile() != null; - } catch (Exception e) { - return false; + /** + * This method checks the existence of the file, but not the validity of the contained key. + */ + boolean hasSecretKey() { + String path = getPathToSecretKey(); + if (StringUtils.isNotBlank(path)) { + File file = new File(path); + return file.exists() && file.isFile(); } + return false; } - Key loadSecretFile() throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, InvalidKeyException { - String path = settings.getClearString(CoreProperties.ENCRYPTION_PATH_TO_SECRET_KEY); + private Key loadSecretFile() throws NoSuchAlgorithmException, InvalidKeySpecException, IOException, InvalidKeyException { + String path = getPathToSecretKey(); return loadSecretFileFromFile(path); } @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); + throw new IllegalStateException("Secret key not found. Please set the property " + CoreProperties.ENCRYPTION_SECRET_KEY_FILE); } 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); + throw new IllegalStateException("The property " + CoreProperties.ENCRYPTION_SECRET_KEY_FILE + " 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"); + return new SecretKeySpec(Base64.decodeBase64(StringUtils.trim(s)), "AES"); } String generateRandomSecretKey() { @@ -107,9 +112,11 @@ final class AesCipher extends Cipher { return new String(Base64.encodeBase64(secretKey.getEncoded())); } catch (Exception e) { - throw new IllegalStateException("Fail to generate random RSA keys", e); + throw new IllegalStateException("Fail to generate AES secret key", e); } } - + private String getPathToSecretKey() { + return settings.getClearString(CoreProperties.ENCRYPTION_SECRET_KEY_FILE); + } } 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 index 6cbc08d01d2..3bc88f0cc20 100644 --- 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 @@ -32,25 +32,28 @@ import java.util.regex.Pattern; public final class Encryption { private static final String BASE64_ALGORITHM = "b64"; - private final Base64Cipher base64Encryption; + private final Base64Cipher base64Cipher; private static final String AES_ALGORITHM = "aes"; - private final AesCipher aesEncryption; + private final AesCipher aesCipher; - private final Map<String, Cipher> encryptions; + private final Map<String, Cipher> ciphers; 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 + base64Cipher = new Base64Cipher(); + aesCipher = new AesCipher(settings); + ciphers = ImmutableMap.of( + BASE64_ALGORITHM, base64Cipher, + AES_ALGORITHM, aesCipher ); } - public boolean canEncrypt() { - return aesEncryption.canEncrypt(); + /** + * Checks the availability of the secret key, that is required to encrypt and decrypt. + */ + public boolean hasSecretKey() { + return aesCipher.hasSecretKey(); } public boolean isEncrypted(String value) { @@ -66,13 +69,13 @@ public final class Encryption { } public String generateRandomSecretKey() { - return aesEncryption.generateRandomSecretKey(); + return aesCipher.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)); + Cipher cipher = ciphers.get(matcher.group(1).toLowerCase(Locale.ENGLISH)); if (cipher != null) { return cipher.decrypt(matcher.group(2)); } @@ -81,7 +84,7 @@ public final class Encryption { } private String encrypt(String algorithm, String clearText) { - Cipher cipher = encryptions.get(algorithm); + Cipher cipher = ciphers.get(algorithm); if (cipher == null) { throw new IllegalArgumentException("Unknown cipher algorithm: " + algorithm); } 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 index 47b432f75d5..2adf627e289 100644 --- 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 @@ -24,13 +24,16 @@ import org.apache.commons.lang.StringUtils; import org.junit.Test; import org.sonar.api.CoreProperties; +import javax.crypto.BadPaddingException; import java.io.File; import java.net.URL; +import java.security.InvalidKeyException; import java.security.Key; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.core.Is.is; import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; public class AesCipherTest { @@ -47,11 +50,11 @@ public class AesCipherTest { @Test public void encrypt() throws Exception { Settings settings = new Settings(); - settings.setProperty(CoreProperties.ENCRYPTION_PATH_TO_SECRET_KEY, pathToSecretKey()); + settings.setProperty(CoreProperties.ENCRYPTION_SECRET_KEY_FILE, pathToSecretKey()); AesCipher cipher = new AesCipher(settings); - String encryptedText = cipher.encrypt("sonar"); - System.out.println(encryptedText); + String encryptedText = cipher.encrypt("this is a secret"); + assertThat(StringUtils.isNotBlank(encryptedText), is(true)); assertThat(Base64.isArrayByteBase64(encryptedText.getBytes()), is(true)); } @@ -59,7 +62,7 @@ public class AesCipherTest { @Test public void decrypt() throws Exception { Settings settings = new Settings(); - settings.setProperty(CoreProperties.ENCRYPTION_PATH_TO_SECRET_KEY, pathToSecretKey()); + settings.setProperty(CoreProperties.ENCRYPTION_SECRET_KEY_FILE, pathToSecretKey()); AesCipher cipher = new AesCipher(settings); // the following value has been encrypted with the key /org/sonar/api/config/AesCipherTest/aes_secret_key.txt @@ -69,9 +72,42 @@ public class AesCipherTest { } @Test + public void decrypt_bad_key() throws Exception { + URL resource = getClass().getResource("/org/sonar/api/config/AesCipherTest/bad_secret_key.txt"); + Settings settings = new Settings(); + settings.setProperty(CoreProperties.ENCRYPTION_SECRET_KEY_FILE, new File(resource.toURI()).getCanonicalPath()); + AesCipher cipher = new AesCipher(settings); + + try { + cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY="); + fail(); + + } catch (RuntimeException e) { + assertThat(e.getCause(), is(InvalidKeyException.class)); + } + } + + @Test + public void decrypt_other_key() throws Exception { + URL resource = getClass().getResource("/org/sonar/api/config/AesCipherTest/other_secret_key.txt"); + Settings settings = new Settings(); + settings.setProperty(CoreProperties.ENCRYPTION_SECRET_KEY_FILE, new File(resource.toURI()).getCanonicalPath()); + AesCipher cipher = new AesCipher(settings); + + try { + // text encrypted with another key + cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY="); + fail(); + + } catch (RuntimeException e) { + assertThat(e.getCause(), is(BadPaddingException.class)); + } + } + + @Test public void encryptThenDecrypt() throws Exception { Settings settings = new Settings(); - settings.setProperty(CoreProperties.ENCRYPTION_PATH_TO_SECRET_KEY, pathToSecretKey()); + settings.setProperty(CoreProperties.ENCRYPTION_SECRET_KEY_FILE, pathToSecretKey()); AesCipher cipher = new AesCipher(settings); assertThat(cipher.decrypt(cipher.encrypt("foo")), is("foo")); @@ -85,6 +121,30 @@ public class AesCipherTest { assertThat(secretKey.getEncoded().length, greaterThan(10)); } + @Test + public void loadSecretKeyFromFile_trim_content() throws Exception { + URL resource = getClass().getResource("/org/sonar/api/config/AesCipherTest/non_trimmed_secret_key.txt"); + String path = new File(resource.toURI()).getCanonicalPath(); + AesCipher cipher = new AesCipher(new Settings()); + + Key secretKey = cipher.loadSecretFileFromFile(path); + + assertThat(secretKey.getAlgorithm(), is("AES")); + assertThat(secretKey.getEncoded().length, greaterThan(10)); + } + + @Test(expected = IllegalStateException.class) + public void loadSecretKeyFromFile_file_does_not_exist() throws Exception { + AesCipher cipher = new AesCipher(new Settings()); + cipher.loadSecretFileFromFile("/file/does/not/exist"); + } + + @Test(expected = IllegalStateException.class) + public void loadSecretKeyFromFile_no_property() throws Exception { + AesCipher cipher = new AesCipher(new Settings()); + cipher.loadSecretFileFromFile(null); + } + 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 index 1333120422c..93a04537d7c 100644 --- 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 @@ -39,6 +39,12 @@ public class EncryptionTest { } @Test + public void scramble() { + Encryption encryption = new Encryption(new Settings()); + assertThat(encryption.scramble("foo"), is("{b64}Zm9v")); + } + + @Test public void decrypt() { Encryption encryption = new Encryption(new Settings()); assertThat(encryption.decrypt("{b64}Zm9v"), is("foo")); @@ -54,6 +60,5 @@ public class EncryptionTest { 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/bad_secret_key.txt b/sonar-plugin-api/src/test/resources/org/sonar/api/config/AesCipherTest/bad_secret_key.txt new file mode 100644 index 00000000000..b33e179e5c8 --- /dev/null +++ b/sonar-plugin-api/src/test/resources/org/sonar/api/config/AesCipherTest/bad_secret_key.txt @@ -0,0 +1 @@ +badbadbad==
\ No newline at end of file diff --git a/sonar-plugin-api/src/test/resources/org/sonar/api/config/AesCipherTest/non_trimmed_secret_key.txt b/sonar-plugin-api/src/test/resources/org/sonar/api/config/AesCipherTest/non_trimmed_secret_key.txt new file mode 100644 index 00000000000..ab83e4adc03 --- /dev/null +++ b/sonar-plugin-api/src/test/resources/org/sonar/api/config/AesCipherTest/non_trimmed_secret_key.txt @@ -0,0 +1,3 @@ + + 0PZz+G+f8mjr3sPn4+AhHg== + diff --git a/sonar-plugin-api/src/test/resources/org/sonar/api/config/AesCipherTest/other_secret_key.txt b/sonar-plugin-api/src/test/resources/org/sonar/api/config/AesCipherTest/other_secret_key.txt new file mode 100644 index 00000000000..23f5ecf5104 --- /dev/null +++ b/sonar-plugin-api/src/test/resources/org/sonar/api/config/AesCipherTest/other_secret_key.txt @@ -0,0 +1 @@ +IBxEUxZ41c8XTxyaah1Qlg==
\ No newline at end of file diff --git a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java index 0e7774cdf34..82f899f3e22 100644 --- a/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java +++ b/sonar-server/src/main/java/org/sonar/server/ui/JRubyFacade.java @@ -399,8 +399,8 @@ public final class JRubyFacade { LoggerFactory.getLogger(getClass()).error(message); } - public boolean canEncrypt() { - return getContainer().getComponentByType(Settings.class).getEncryption().canEncrypt(); + public boolean hasSecretKey() { + return getContainer().getComponentByType(Settings.class).getEncryption().hasSecretKey(); } public String encrypt(String clearText) { diff --git a/sonar-server/src/main/webapp/WEB-INF/app/controllers/encryption_controller.rb b/sonar-server/src/main/webapp/WEB-INF/app/controllers/encryption_controller.rb index 789b60972a6..cc7d49ee848 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/controllers/encryption_controller.rb +++ b/sonar-server/src/main/webapp/WEB-INF/app/controllers/encryption_controller.rb @@ -24,7 +24,7 @@ class EncryptionController < ApplicationController verify :method => :post, :only => [:generate_secret, :encrypt], :redirect_to => {:action => :index} def index - @can_encrypt=java_facade.canEncrypt() + @has_secret_key=java_facade.hasSecretKey() end def generate_secret @@ -37,7 +37,7 @@ class EncryptionController < ApplicationController end def encrypt - bad_request('No secret key') unless java_facade.canEncrypt() + bad_request('No secret key') unless java_facade.hasSecretKey() @encrypted=java_facade.encrypt(params[:text]) end diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/encryption/encrypt.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/encryption/encrypt.html.erb new file mode 100644 index 00000000000..83d55e7db1c --- /dev/null +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/encryption/encrypt.html.erb @@ -0,0 +1 @@ +<code><%= @encrypted -%></code>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/encryption/generate_secret.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/encryption/generate_secret.html.erb index 7323895aedc..15b8f4b5fbf 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/encryption/generate_secret.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/encryption/generate_secret.html.erb @@ -1,4 +1 @@ -<p> - Secret is: - <input type="text" value="<%= @secret -%>"/> -</p>
\ No newline at end of file +<span class=""><%= @secret -%></span>
\ No newline at end of file diff --git a/sonar-server/src/main/webapp/WEB-INF/app/views/encryption/index.html.erb b/sonar-server/src/main/webapp/WEB-INF/app/views/encryption/index.html.erb index 06fd70d605f..b81da062400 100644 --- a/sonar-server/src/main/webapp/WEB-INF/app/views/encryption/index.html.erb +++ b/sonar-server/src/main/webapp/WEB-INF/app/views/encryption/index.html.erb @@ -1,10 +1,12 @@ -<p>bla bla</p> - -<% if @can_encrypt %> - <form action="<%= ApplicationController.root_context -%>/encryption/encrypt" method="POST"> +<% if @has_secret_key %> + <%= form_remote_tag :url => {:action => 'encrypt'}, + :update => { :success => 'encryption_result', :failure => 'encryption_error' }, + :failure => "$('encryption_error').show()" -%> <input type="text" name="text" id="text"/> <input type="submit" value="Encrypt" id="submit_encrypt"/> </form> + <div id="encryption_result"></div> + <div id="encryption_error" class="error" style="display:none"></div> <% else %> <form action="<%= ApplicationController.root_context -%>/encryption/generate_secret" method="POST"> <input type="submit" value="Generate secret" id="submit_generate_secret"/> |