]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-14426 Add support for AES-GCM encryption
authorZipeng WU <zipeng.wu@sonarsource.com>
Thu, 11 Feb 2021 17:25:14 +0000 (18:25 +0100)
committersonartech <sonartech@sonarsource.com>
Wed, 17 Feb 2021 20:07:15 +0000 (20:07 +0000)
17 files changed:
server/sonar-docs/src/images/encrypt-value.png
server/sonar-docs/src/pages/instance-administration/security.md
server/sonar-process/src/main/java/org/sonar/process/AesCipher.java [deleted file]
server/sonar-process/src/main/java/org/sonar/process/Encryption.java [deleted file]
server/sonar-process/src/main/java/org/sonar/process/Props.java
server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java [deleted file]
server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java [deleted file]
server/sonar-webserver-webapi/src/main/resources/org/sonar/server/setting/ws/encrypt-example.json
server/sonar-webserver-webapi/src/test/java/org/sonar/server/setting/ws/EncryptActionTest.java
sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/AesCipher.java
sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/AesECBCipher.java [new file with mode: 0644]
sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/AesGCMCipher.java [new file with mode: 0644]
sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/Encryption.java
sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/AesCipherTest.java [deleted file]
sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/AesECBCipherTest.java [new file with mode: 0644]
sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/AesGCMCipherTest.java [new file with mode: 0644]
sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/EncryptionTest.java

index c22aa1dc2d79f5a4239bb5506e45daf94acbd755..c71737322d45436974598aa4ecd866a7bdeafa93 100644 (file)
Binary files a/server/sonar-docs/src/images/encrypt-value.png and b/server/sonar-docs/src/images/encrypt-value.png differ
index c7acc9cc0e348a894505d89f2c82a6ec3fdecf85..436257e3ddae50a39d7c03896b7bf8dc2f64894a 100644 (file)
@@ -195,7 +195,7 @@ Go back to **[Administration > Configuration > Encryption](/#sonarqube-admin#/ad
 1. **Use the encrypted values in your SonarQube server configuration**  
 Simply copy these encrypted values into _$SONARQUBE-HOME/conf/sonar.properties_
 ```
-sonar.jdbc.password={aes}CCGCFg4Xpm6r+PiJb1Swfg==  # Encrypted DB password
+sonar.jdbc.password={aes-gcm}CCGCFg4Xpm6r+PiJb1Swfg==  # Encrypted DB password
 ...
 sonar.secretKeyPath=C:/path/to/my/secure/location/my_secret_key.txt
 ```
diff --git a/server/sonar-process/src/main/java/org/sonar/process/AesCipher.java b/server/sonar-process/src/main/java/org/sonar/process/AesCipher.java
deleted file mode 100644 (file)
index 129858f..0000000
+++ /dev/null
@@ -1,112 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program 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.
- *
- * This program 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 this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.process;
-
-import java.io.File;
-import java.io.IOException;
-import java.security.Key;
-import javax.annotation.Nullable;
-import javax.crypto.spec.SecretKeySpec;
-import org.apache.commons.codec.binary.Base64;
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang.StringUtils;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-final class AesCipher implements Cipher {
-  private static final String CRYPTO_KEY = "AES";
-
-  /**
-   * Duplication from CoreProperties.ENCRYPTION_SECRET_KEY_PATH
-   */
-  static final String ENCRYPTION_SECRET_KEY_PATH = "sonar.secretKeyPath";
-
-  private String pathToSecretKey;
-
-  AesCipher(@Nullable String pathToSecretKey) {
-    this.pathToSecretKey = pathToSecretKey;
-  }
-
-  @Override
-  public String encrypt(String clearText) {
-    try {
-      javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_KEY);
-      cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, loadSecretFile());
-      return Base64.encodeBase64String(cipher.doFinal(clearText.getBytes(UTF_8)));
-    } catch (RuntimeException e) {
-      throw e;
-    } catch (Exception e) {
-      throw new IllegalStateException(e);
-    }
-  }
-
-  @Override
-  public String decrypt(String encryptedText) {
-    try {
-      javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_KEY);
-      cipher.init(javax.crypto.Cipher.DECRYPT_MODE, loadSecretFile());
-      byte[] cipherData = cipher.doFinal(Base64.decodeBase64(StringUtils.trim(encryptedText)));
-      return new String(cipherData, UTF_8);
-    } catch (RuntimeException e) {
-      throw e;
-    } catch (Exception e) {
-      throw new IllegalStateException(e);
-    }
-  }
-
-  /**
-   * 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;
-  }
-
-  private Key loadSecretFile() throws IOException {
-    String path = getPathToSecretKey();
-    return loadSecretFileFromFile(path);
-  }
-
-  Key loadSecretFileFromFile(String path) throws IOException {
-    if (StringUtils.isBlank(path)) {
-      throw new IllegalStateException("Secret key not found. Please set the property " + ENCRYPTION_SECRET_KEY_PATH);
-    }
-    File file = new File(path);
-    if (!file.exists() || !file.isFile()) {
-      throw new IllegalStateException("The property " + ENCRYPTION_SECRET_KEY_PATH + " does not link to a valid file: " + path);
-    }
-    String s = FileUtils.readFileToString(file, UTF_8);
-    if (StringUtils.isBlank(s)) {
-      throw new IllegalStateException("No secret key in the file: " + path);
-    }
-    return new SecretKeySpec(Base64.decodeBase64(StringUtils.trim(s)), CRYPTO_KEY);
-  }
-
-  String getPathToSecretKey() {
-    if (StringUtils.isBlank(pathToSecretKey)) {
-      pathToSecretKey = new File(System.getProperty("user.home"), ".sonar/sonar-secret.txt").getPath();
-    }
-    return pathToSecretKey;
-  }
-}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/Encryption.java b/server/sonar-process/src/main/java/org/sonar/process/Encryption.java
deleted file mode 100644 (file)
index a276399..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program 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.
- *
- * This program 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 this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.process;
-
-import javax.annotation.Nullable;
-import java.util.HashMap;
-import java.util.Locale;
-import java.util.Map;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-/**
- * @since 3.0
- */
-public final class Encryption {
-
-  private static final String BASE64_ALGORITHM = "b64";
-
-  private static final String AES_ALGORITHM = "aes";
-  private final AesCipher aesCipher;
-
-  private final Map<String, Cipher> ciphers = new HashMap<>();
-  private static final Pattern ENCRYPTED_PATTERN = Pattern.compile("\\{(.*?)\\}(.*)");
-
-  public Encryption(@Nullable String pathToSecretKey) {
-    aesCipher = new AesCipher(pathToSecretKey);
-    ciphers.put(BASE64_ALGORITHM, new Base64Cipher());
-    ciphers.put(AES_ALGORITHM, aesCipher);
-  }
-
-  public boolean isEncrypted(String value) {
-    return value.indexOf('{') == 0 && value.indexOf('}') > 1;
-  }
-
-  public String decrypt(String encryptedText) {
-    Matcher matcher = ENCRYPTED_PATTERN.matcher(encryptedText);
-    if (matcher.matches()) {
-      Cipher cipher = ciphers.get(matcher.group(1).toLowerCase(Locale.ENGLISH));
-      if (cipher != null) {
-        return cipher.decrypt(matcher.group(2));
-      }
-    }
-    return encryptedText;
-  }
-
-}
index f5294a548120e6dac9392f5928e065316d09bb6c..b88cbad38b033f36dd7c81bca978e0265c135988 100644 (file)
@@ -23,7 +23,11 @@ import java.io.File;
 import java.util.Properties;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
+
 import org.apache.commons.lang.StringUtils;
+import org.sonar.api.config.internal.Encryption;
+
+import static org.sonar.api.CoreProperties.ENCRYPTION_SECRET_KEY_PATH;
 
 public class Props {
 
@@ -33,7 +37,7 @@ public class Props {
   public Props(Properties props) {
     this.properties = new Properties();
     props.forEach((k, v) -> this.properties.put(k.toString().trim(), v == null ? null : v.toString().trim()));
-    this.encryption = new Encryption(props.getProperty(AesCipher.ENCRYPTION_SECRET_KEY_PATH));
+    this.encryption = new Encryption(props.getProperty(ENCRYPTION_SECRET_KEY_PATH));
   }
 
   public boolean contains(String key) {
diff --git a/server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java b/server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java
deleted file mode 100644 (file)
index 3687ee3..0000000
+++ /dev/null
@@ -1,172 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program 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.
- *
- * This program 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 this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.process;
-
-import com.google.common.io.Resources;
-import java.io.File;
-import java.security.InvalidKeyException;
-import java.security.Key;
-import javax.crypto.BadPaddingException;
-import org.apache.commons.codec.binary.Base64;
-import org.apache.commons.lang.StringUtils;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.fail;
-
-public class AesCipherTest {
-
-  @Rule
-  public ExpectedException thrown = ExpectedException.none();
-
-  @Test
-  public void encrypt() {
-    AesCipher cipher = new AesCipher(pathToSecretKey());
-
-    String encryptedText = cipher.encrypt("this is a secret");
-
-    assertThat(StringUtils.isNotBlank(encryptedText)).isTrue();
-    assertThat(Base64.isBase64(encryptedText.getBytes())).isTrue();
-  }
-
-  @Test
-  public void encrypt_bad_key() {
-    thrown.expect(RuntimeException.class);
-    thrown.expectMessage("Invalid AES key");
-
-    AesCipher cipher = new AesCipher(getPath("bad_secret_key.txt"));
-
-    cipher.encrypt("this is a secret");
-  }
-
-  @Test
-  public void decrypt() {
-    AesCipher cipher = new AesCipher(pathToSecretKey());
-
-    // 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).isEqualTo("this is a secret");
-  }
-
-  @Test
-  public void decrypt_bad_key() {
-    AesCipher cipher = new AesCipher(getPath("bad_secret_key.txt"));
-
-    try {
-      cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
-      fail();
-
-    } catch (RuntimeException e) {
-      assertThat(e.getCause()).isInstanceOf(InvalidKeyException.class);
-    }
-  }
-
-  @Test
-  public void decrypt_other_key() {
-    AesCipher cipher = new AesCipher(getPath("other_secret_key.txt"));
-
-    try {
-      // text encrypted with another key
-      cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
-      fail();
-
-    } catch (RuntimeException e) {
-      assertThat(e.getCause()).isInstanceOf(BadPaddingException.class);
-    }
-  }
-
-  @Test
-  public void encryptThenDecrypt() {
-    AesCipher cipher = new AesCipher(pathToSecretKey());
-
-    assertThat(cipher.decrypt(cipher.encrypt("foo"))).isEqualTo("foo");
-  }
-
-  @Test
-  public void testDefaultPathToSecretKey() {
-    AesCipher cipher = new AesCipher(null);
-
-    String path = cipher.getPathToSecretKey();
-
-    assertThat(StringUtils.isNotBlank(path)).isTrue();
-    assertThat(new File(path).getName()).isEqualTo("sonar-secret.txt");
-  }
-
-  @Test
-  public void loadSecretKeyFromFile() throws Exception {
-    AesCipher cipher = new AesCipher(null);
-    Key secretKey = cipher.loadSecretFileFromFile(pathToSecretKey());
-    assertThat(secretKey.getAlgorithm()).isEqualTo("AES");
-    assertThat(secretKey.getEncoded().length).isGreaterThan(10);
-  }
-
-  @Test
-  public void loadSecretKeyFromFile_trim_content() throws Exception {
-    String path = getPath("non_trimmed_secret_key.txt");
-    AesCipher cipher = new AesCipher(null);
-
-    Key secretKey = cipher.loadSecretFileFromFile(path);
-
-    assertThat(secretKey.getAlgorithm()).isEqualTo("AES");
-    assertThat(secretKey.getEncoded().length).isGreaterThan(10);
-  }
-
-  @Test
-  public void loadSecretKeyFromFile_file_does_not_exist() throws Exception {
-    thrown.expect(IllegalStateException.class);
-
-    AesCipher cipher = new AesCipher(null);
-    cipher.loadSecretFileFromFile("/file/does/not/exist");
-  }
-
-  @Test
-  public void loadSecretKeyFromFile_no_property() throws Exception {
-    thrown.expect(IllegalStateException.class);
-
-    AesCipher cipher = new AesCipher(null);
-    cipher.loadSecretFileFromFile(null);
-  }
-
-  @Test
-  public void hasSecretKey() {
-    AesCipher cipher = new AesCipher(pathToSecretKey());
-
-    assertThat(cipher.hasSecretKey()).isTrue();
-  }
-
-  @Test
-  public void doesNotHaveSecretKey() {
-    AesCipher cipher = new AesCipher("/my/twitter/id/is/SimonBrandhof");
-
-    assertThat(cipher.hasSecretKey()).isFalse();
-  }
-
-  private static String getPath(String file) {
-    return Resources.getResource(AesCipherTest.class, "AesCipherTest/" + file).getPath();
-  }
-
-  private static String pathToSecretKey() {
-    return getPath("aes_secret_key.txt");
-  }
-
-}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java b/server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java
deleted file mode 100644 (file)
index 956d97d..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program 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.
- *
- * This program 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 this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.process;
-
-import org.junit.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-
-public class EncryptionTest {
-
-  @Test
-  public void isEncrypted() {
-    Encryption encryption = new Encryption(null);
-    assertThat(encryption.isEncrypted("{aes}ADASDASAD")).isTrue();
-    assertThat(encryption.isEncrypted("{b64}ADASDASAD")).isTrue();
-    assertThat(encryption.isEncrypted("{abc}ADASDASAD")).isTrue();
-
-    assertThat(encryption.isEncrypted("{}")).isFalse();
-    assertThat(encryption.isEncrypted("{foo")).isFalse();
-    assertThat(encryption.isEncrypted("foo{aes}")).isFalse();
-  }
-
-  @Test
-  public void decrypt() {
-    Encryption encryption = new Encryption(null);
-    assertThat(encryption.decrypt("{b64}Zm9v")).isEqualTo("foo");
-  }
-
-  @Test
-  public void decrypt_unknown_algorithm() {
-    Encryption encryption = new Encryption(null);
-    assertThat(encryption.decrypt("{xxx}Zm9v")).isEqualTo("{xxx}Zm9v");
-  }
-
-  @Test
-  public void decrypt_uncrypted_text() {
-    Encryption encryption = new Encryption(null);
-    assertThat(encryption.decrypt("foo")).isEqualTo("foo");
-  }
-}
index 4bf7c859c9045d094986f8afc577d55ab92f32a1..70876c252ab1bfa06abf8fff8f1e3ff6831bbf0b 100644 (file)
@@ -22,6 +22,7 @@ package org.sonar.server.setting.ws;
 import java.io.File;
 import java.nio.charset.StandardCharsets;
 import javax.annotation.Nullable;
+
 import org.apache.commons.io.FileUtils;
 import org.junit.Before;
 import org.junit.Rule;
@@ -40,7 +41,6 @@ import org.sonarqube.ws.Settings.EncryptWsResponse;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.sonar.server.setting.ws.SettingsWsParameters.PARAM_VALUE;
-import static org.sonar.test.JsonAssert.assertJson;
 
 public class EncryptActionTest {
   @Rule
@@ -65,22 +65,13 @@ public class EncryptActionTest {
     encryption.setPathToSecretKey(secretKeyFile.getAbsolutePath());
   }
 
-  @Test
-  public void json_example() {
-    logInAsSystemAdministrator();
-
-    String result = ws.newRequest().setParam("value", "my value").execute().getInput();
-
-    assertJson(result).isSimilarTo(ws.getDef().responseExampleAsString());
-  }
-
   @Test
   public void encrypt() {
     logInAsSystemAdministrator();
 
     EncryptWsResponse result = call("my value!");
 
-    assertThat(result.getEncryptedValue()).isEqualTo("{aes}NoofntibpMBdhkMfXQxYcA==");
+    assertThat(result.getEncryptedValue()).matches("^\\{aes-gcm\\}.+");
   }
 
   @Test
index 3df06736202484bc0ebac2c80a6ef4644e439269..57541227658b9b56ad3e9281b5bcb39da66100ac 100644 (file)
@@ -21,7 +21,6 @@ package org.sonar.api.config.internal;
 
 import java.io.File;
 import java.io.IOException;
-import java.nio.charset.StandardCharsets;
 import java.security.Key;
 import java.security.SecureRandom;
 import javax.annotation.Nullable;
@@ -34,8 +33,9 @@ import org.apache.commons.lang.StringUtils;
 import org.sonar.api.CoreProperties;
 
 import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.sonar.api.CoreProperties.ENCRYPTION_SECRET_KEY_PATH;
 
-final class AesCipher implements Cipher {
+abstract class AesCipher implements Cipher {
   static final int KEY_SIZE_IN_BITS = 256;
 
   private static final String CRYPTO_KEY = "AES";
@@ -46,33 +46,6 @@ final class AesCipher implements Cipher {
     this.pathToSecretKey = pathToSecretKey;
   }
 
-  @Override
-  public String encrypt(String clearText) {
-    try {
-      javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_KEY);
-      cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, loadSecretFile());
-      return Base64.encodeBase64String(cipher.doFinal(clearText.getBytes(StandardCharsets.UTF_8.name())));
-    } catch (RuntimeException e) {
-      throw e;
-    } catch (Exception e) {
-      throw new IllegalStateException(e);
-    }
-  }
-
-  @Override
-  public String decrypt(String encryptedText) {
-    try {
-      javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_KEY);
-      cipher.init(javax.crypto.Cipher.DECRYPT_MODE, loadSecretFile());
-      byte[] cipherData = cipher.doFinal(Base64.decodeBase64(StringUtils.trim(encryptedText)));
-      return new String(cipherData, StandardCharsets.UTF_8);
-    } catch (RuntimeException e) {
-      throw e;
-    } catch (Exception e) {
-      throw new IllegalStateException(e);
-    }
-  }
-
   /**
    * This method checks the existence of the file, but not the validity of the contained key.
    */
@@ -85,18 +58,18 @@ final class AesCipher implements Cipher {
     return false;
   }
 
-  private Key loadSecretFile() throws IOException {
+  protected Key loadSecretFile() throws IOException {
     String path = getPathToSecretKey();
     return loadSecretFileFromFile(path);
   }
 
   Key loadSecretFileFromFile(@Nullable String path) throws IOException {
     if (StringUtils.isBlank(path)) {
-      throw new IllegalStateException("Secret key not found. Please set the property " + CoreProperties.ENCRYPTION_SECRET_KEY_PATH);
+      throw new IllegalStateException("Secret key not found. Please set the property " + ENCRYPTION_SECRET_KEY_PATH);
     }
     File file = new File(path);
     if (!file.exists() || !file.isFile()) {
-      throw new IllegalStateException("The property " + CoreProperties.ENCRYPTION_SECRET_KEY_PATH + " does not link to a valid file: " + path);
+      throw new IllegalStateException("The property " + ENCRYPTION_SECRET_KEY_PATH + " does not link to a valid file: " + path);
     }
     String s = FileUtils.readFileToString(file, UTF_8);
     if (StringUtils.isBlank(s)) {
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/AesECBCipher.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/AesECBCipher.java
new file mode 100644 (file)
index 0000000..ff8aada
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.api.config.internal;
+
+import java.nio.charset.StandardCharsets;
+import javax.annotation.Nullable;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.StringUtils;
+
+/**
+ * @deprecated since 8.7.0
+ */
+@Deprecated
+final class AesECBCipher extends AesCipher {
+
+  private static final String CRYPTO_ALGO = "AES";
+
+  AesECBCipher(@Nullable String pathToSecretKey) {
+    super(pathToSecretKey);
+  }
+
+  @Override
+  public String encrypt(String clearText) {
+    try {
+      javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_ALGO);
+      cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, loadSecretFile());
+      byte[] cipherData = cipher.doFinal(clearText.getBytes(StandardCharsets.UTF_8.name()));
+      return Base64.encodeBase64String(cipherData);
+    } catch (RuntimeException e) {
+      throw e;
+    } catch (Exception e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Override
+  public String decrypt(String encryptedText) {
+    try {
+      javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_ALGO);
+      cipher.init(javax.crypto.Cipher.DECRYPT_MODE, loadSecretFile());
+      byte[] cipherData = cipher.doFinal(Base64.decodeBase64(StringUtils.trim(encryptedText)));
+      return new String(cipherData, StandardCharsets.UTF_8);
+    } catch (RuntimeException e) {
+      throw e;
+    } catch (Exception e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+}
diff --git a/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/AesGCMCipher.java b/sonar-plugin-api-impl/src/main/java/org/sonar/api/config/internal/AesGCMCipher.java
new file mode 100644 (file)
index 0000000..2f8f86d
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.api.config.internal;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+import javax.annotation.Nullable;
+import javax.crypto.spec.GCMParameterSpec;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.StringUtils;
+
+final class AesGCMCipher extends AesCipher {
+  private static final int GCM_TAG_LENGTH_IN_BITS = 128;
+  private static final int GCM_IV_LENGTH_IN_BYTES = 12;
+
+  private static final String CRYPTO_ALGO = "AES/GCM/NoPadding";
+
+  AesGCMCipher(@Nullable String pathToSecretKey) {
+    super(pathToSecretKey);
+  }
+
+  @Override
+  public String encrypt(String clearText) {
+    try {
+      javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_ALGO);
+      byte[] iv = new byte[GCM_IV_LENGTH_IN_BYTES];
+      new SecureRandom().nextBytes(iv);
+      cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, loadSecretFile(), new GCMParameterSpec(GCM_TAG_LENGTH_IN_BITS, iv));
+      byte[] encryptedText = cipher.doFinal(clearText.getBytes(StandardCharsets.UTF_8.name()));
+      return Base64.encodeBase64String(
+        ByteBuffer.allocate(GCM_IV_LENGTH_IN_BYTES + encryptedText.length)
+          .put(iv)
+          .put(encryptedText)
+          .array());
+    } catch (RuntimeException e) {
+      throw e;
+    } catch (Exception e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+  @Override
+  public String decrypt(String encryptedText) {
+    try {
+      javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_ALGO);
+      ByteBuffer byteBuffer = ByteBuffer.wrap(Base64.decodeBase64(StringUtils.trim(encryptedText)));
+      byte[] iv = new byte[GCM_IV_LENGTH_IN_BYTES];
+      byteBuffer.get(iv);
+      byte[] cipherText = new byte[byteBuffer.remaining()];
+      byteBuffer.get(cipherText);
+      cipher.init(javax.crypto.Cipher.DECRYPT_MODE, loadSecretFile(), new GCMParameterSpec(GCM_TAG_LENGTH_IN_BITS, iv));
+      byte[] cipherData = cipher.doFinal(cipherText);
+      return new String(cipherData, StandardCharsets.UTF_8);
+    } catch (RuntimeException e) {
+      throw e;
+    } catch (Exception e) {
+      throw new IllegalStateException(e);
+    }
+  }
+}
index c554230167843f5ef94d93d1f201965f5e477c95..23c38aa88aa169bb64c0f12808794c79e0c72936 100644 (file)
@@ -32,29 +32,34 @@ import javax.annotation.Nullable;
 public final class Encryption {
 
   private static final String BASE64_ALGORITHM = "b64";
+  private static final String AES_ECB_ALGORITHM = "aes";
+  private static final String AES_GCM_ALGORITHM = "aes-gcm";
 
-  private static final String AES_ALGORITHM = "aes";
-  private final AesCipher aesCipher;
+  private final AesECBCipher aesECBCipher;
+  private final AesGCMCipher aesGCMCipher;
 
   private final Map<String, Cipher> ciphers;
   private static final Pattern ENCRYPTED_PATTERN = Pattern.compile("\\{(.*?)\\}(.*)");
 
   public Encryption(@Nullable String pathToSecretKey) {
-    aesCipher = new AesCipher(pathToSecretKey);
+    aesECBCipher = new AesECBCipher(pathToSecretKey);
+    aesGCMCipher = new AesGCMCipher(pathToSecretKey);
     ciphers = new HashMap<>();
     ciphers.put(BASE64_ALGORITHM, new Base64Cipher());
-    ciphers.put(AES_ALGORITHM, aesCipher);
+    ciphers.put(AES_ECB_ALGORITHM, aesECBCipher);
+    ciphers.put(AES_GCM_ALGORITHM, aesGCMCipher);
   }
 
   public void setPathToSecretKey(@Nullable String pathToSecretKey) {
-    aesCipher.setPathToSecretKey(pathToSecretKey);
+    aesECBCipher.setPathToSecretKey(pathToSecretKey);
+    aesGCMCipher.setPathToSecretKey(pathToSecretKey);
   }
 
   /**
    * Checks the availability of the secret key, that is required to encrypt and decrypt.
    */
   public boolean hasSecretKey() {
-    return aesCipher.hasSecretKey();
+    return aesGCMCipher.hasSecretKey();
   }
 
   public boolean isEncrypted(String value) {
@@ -62,7 +67,7 @@ public final class Encryption {
   }
 
   public String encrypt(String clearText) {
-    return encrypt(AES_ALGORITHM, clearText);
+    return encrypt(AES_GCM_ALGORITHM, clearText);
   }
 
   public String scramble(String clearText) {
@@ -70,7 +75,7 @@ public final class Encryption {
   }
 
   public String generateRandomSecretKey() {
-    return aesCipher.generateRandomSecretKey();
+    return aesGCMCipher.generateRandomSecretKey();
   }
 
   public String decrypt(String encryptedText) {
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/AesCipherTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/AesCipherTest.java
deleted file mode 100644 (file)
index b9cfc35..0000000
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2021 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program 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.
- *
- * This program 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 this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.api.config.internal;
-
-import java.io.File;
-import java.net.URL;
-import java.security.InvalidKeyException;
-import java.security.Key;
-import javax.crypto.BadPaddingException;
-import org.apache.commons.codec.binary.Base64;
-import org.apache.commons.lang.StringUtils;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.junit.Assert.fail;
-
-public class AesCipherTest {
-
-  @Rule
-  public ExpectedException thrown = ExpectedException.none();
-
-  @Test
-  public void generateRandomSecretKey() {
-    AesCipher cipher = new AesCipher(null);
-
-    String key = cipher.generateRandomSecretKey();
-
-    assertThat(StringUtils.isNotBlank(key)).isTrue();
-    assertThat(Base64.isArrayByteBase64(key.getBytes())).isTrue();
-  }
-
-  @Test
-  public void encrypt() throws Exception {
-    AesCipher cipher = new AesCipher(pathToSecretKey());
-
-    String encryptedText = cipher.encrypt("this is a secret");
-
-    assertThat(StringUtils.isNotBlank(encryptedText)).isTrue();
-    assertThat(Base64.isArrayByteBase64(encryptedText.getBytes())).isTrue();
-  }
-
-  @Test
-  public void encrypt_bad_key() throws Exception {
-    thrown.expect(RuntimeException.class);
-    thrown.expectMessage("Invalid AES key");
-
-    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/bad_secret_key.txt");
-    AesCipher cipher = new AesCipher(new File(resource.toURI()).getCanonicalPath());
-
-    cipher.encrypt("this is a secret");
-  }
-
-  @Test
-  public void decrypt() throws Exception {
-    AesCipher cipher = new AesCipher(pathToSecretKey());
-
-    // the following value has been encrypted with the key /org/sonar/api/config/internal/AesCipherTest/aes_secret_key.txt
-    String clearText = cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
-
-    assertThat(clearText).isEqualTo("this is a secret");
-  }
-
-  @Test
-  public void decrypt_bad_key() throws Exception {
-    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/bad_secret_key.txt");
-    AesCipher cipher = new AesCipher(new File(resource.toURI()).getCanonicalPath());
-
-    try {
-      cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
-      fail();
-
-    } catch (RuntimeException e) {
-      assertThat(e.getCause()).isInstanceOf(InvalidKeyException.class);
-    }
-  }
-
-  @Test
-  public void decrypt_other_key() throws Exception {
-    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/other_secret_key.txt");
-    AesCipher cipher = new AesCipher(new File(resource.toURI()).getCanonicalPath());
-
-    try {
-      // text encrypted with another key
-      cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
-      fail();
-
-    } catch (RuntimeException e) {
-      assertThat(e.getCause()).isInstanceOf(BadPaddingException.class);
-    }
-  }
-
-  @Test
-  public void encryptThenDecrypt() throws Exception {
-    AesCipher cipher = new AesCipher(pathToSecretKey());
-
-    assertThat(cipher.decrypt(cipher.encrypt("foo"))).isEqualTo("foo");
-  }
-
-  @Test
-  public void testDefaultPathToSecretKey() {
-    AesCipher cipher = new AesCipher(null);
-
-    String path = cipher.getPathToSecretKey();
-
-    assertThat(StringUtils.isNotBlank(path)).isTrue();
-    assertThat(new File(path).getName()).isEqualTo("sonar-secret.txt");
-  }
-
-  @Test
-  public void loadSecretKeyFromFile() throws Exception {
-    AesCipher cipher = new AesCipher(null);
-    Key secretKey = cipher.loadSecretFileFromFile(pathToSecretKey());
-    assertThat(secretKey.getAlgorithm()).isEqualTo("AES");
-    assertThat(secretKey.getEncoded().length).isGreaterThan(10);
-  }
-
-  @Test
-  public void loadSecretKeyFromFile_trim_content() throws Exception {
-    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/non_trimmed_secret_key.txt");
-    String path = new File(resource.toURI()).getCanonicalPath();
-    AesCipher cipher = new AesCipher(null);
-
-    Key secretKey = cipher.loadSecretFileFromFile(path);
-
-    assertThat(secretKey.getAlgorithm()).isEqualTo("AES");
-    assertThat(secretKey.getEncoded().length).isGreaterThan(10);
-  }
-
-  @Test
-  public void loadSecretKeyFromFile_file_does_not_exist() throws Exception {
-    thrown.expect(IllegalStateException.class);
-
-    AesCipher cipher = new AesCipher(null);
-    cipher.loadSecretFileFromFile("/file/does/not/exist");
-  }
-
-  @Test
-  public void loadSecretKeyFromFile_no_property() throws Exception {
-    thrown.expect(IllegalStateException.class);
-
-    AesCipher cipher = new AesCipher(null);
-    cipher.loadSecretFileFromFile(null);
-  }
-
-  @Test
-  public void hasSecretKey() throws Exception {
-    AesCipher cipher = new AesCipher(pathToSecretKey());
-
-    assertThat(cipher.hasSecretKey()).isTrue();
-  }
-
-  @Test
-  public void doesNotHaveSecretKey() {
-    AesCipher cipher = new AesCipher("/my/twitter/id/is/SimonBrandhof");
-
-    assertThat(cipher.hasSecretKey()).isFalse();
-  }
-
-  private String pathToSecretKey() throws Exception {
-    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/aes_secret_key.txt");
-    return new File(resource.toURI()).getCanonicalPath();
-  }
-}
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/AesECBCipherTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/AesECBCipherTest.java
new file mode 100644 (file)
index 0000000..28de7d5
--- /dev/null
@@ -0,0 +1,182 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.api.config.internal;
+
+import java.io.File;
+import java.net.URL;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import javax.crypto.BadPaddingException;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.StringUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assert.fail;
+
+public class AesECBCipherTest {
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void generateRandomSecretKey() {
+    AesECBCipher cipher = new AesECBCipher(null);
+
+    String key = cipher.generateRandomSecretKey();
+
+    assertThat(StringUtils.isNotBlank(key)).isTrue();
+    assertThat(Base64.isArrayByteBase64(key.getBytes())).isTrue();
+  }
+
+  @Test
+  public void encrypt() throws Exception {
+    AesECBCipher cipher = new AesECBCipher(pathToSecretKey());
+
+    String encryptedText = cipher.encrypt("this is a secret");
+
+    assertThat(StringUtils.isNotBlank(encryptedText)).isTrue();
+    assertThat(Base64.isArrayByteBase64(encryptedText.getBytes())).isTrue();
+  }
+
+  @Test
+  public void encrypt_bad_key() throws Exception {
+    thrown.expect(RuntimeException.class);
+    thrown.expectMessage("Invalid AES key");
+
+    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/bad_secret_key.txt");
+    AesECBCipher cipher = new AesECBCipher(new File(resource.toURI()).getCanonicalPath());
+
+    cipher.encrypt("this is a secret");
+  }
+
+  @Test
+  public void decrypt() throws Exception {
+    AesECBCipher cipher = new AesECBCipher(pathToSecretKey());
+
+    // the following value has been encrypted with the key /org/sonar/api/config/internal/AesCipherTest/aes_secret_key.txt
+    String clearText = cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
+
+    assertThat(clearText).isEqualTo("this is a secret");
+  }
+
+  @Test
+  public void decrypt_bad_key() throws Exception {
+    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/bad_secret_key.txt");
+    AesECBCipher cipher = new AesECBCipher(new File(resource.toURI()).getCanonicalPath());
+
+    try {
+      cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
+      fail();
+
+    } catch (RuntimeException e) {
+      assertThat(e.getCause()).isInstanceOf(InvalidKeyException.class);
+    }
+  }
+
+  @Test
+  public void decrypt_other_key() throws Exception {
+    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/other_secret_key.txt");
+    AesECBCipher cipher = new AesECBCipher(new File(resource.toURI()).getCanonicalPath());
+
+    try {
+      // text encrypted with another key
+      cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
+      fail();
+
+    } catch (RuntimeException e) {
+      assertThat(e.getCause()).isInstanceOf(BadPaddingException.class);
+    }
+  }
+
+  @Test
+  public void encryptThenDecrypt() throws Exception {
+    AesECBCipher cipher = new AesECBCipher(pathToSecretKey());
+
+    assertThat(cipher.decrypt(cipher.encrypt("foo"))).isEqualTo("foo");
+  }
+
+  @Test
+  public void testDefaultPathToSecretKey() {
+    AesECBCipher cipher = new AesECBCipher(null);
+
+    String path = cipher.getPathToSecretKey();
+
+    assertThat(StringUtils.isNotBlank(path)).isTrue();
+    assertThat(new File(path)).hasName("sonar-secret.txt");
+  }
+
+  @Test
+  public void loadSecretKeyFromFile() throws Exception {
+    AesECBCipher cipher = new AesECBCipher(null);
+    Key secretKey = cipher.loadSecretFileFromFile(pathToSecretKey());
+    assertThat(secretKey.getAlgorithm()).isEqualTo("AES");
+    assertThat(secretKey.getEncoded()).hasSizeGreaterThan(10);
+  }
+
+  @Test
+  public void loadSecretKeyFromFile_trim_content() throws Exception {
+    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/non_trimmed_secret_key.txt");
+    String path = new File(resource.toURI()).getCanonicalPath();
+    AesECBCipher cipher = new AesECBCipher(null);
+
+    Key secretKey = cipher.loadSecretFileFromFile(path);
+
+    assertThat(secretKey.getAlgorithm()).isEqualTo("AES");
+    assertThat(secretKey.getEncoded()).hasSizeGreaterThan(10);
+  }
+
+  @Test
+  public void loadSecretKeyFromFile_file_does_not_exist() throws Exception {
+    thrown.expect(IllegalStateException.class);
+
+    AesECBCipher cipher = new AesECBCipher(null);
+    cipher.loadSecretFileFromFile("/file/does/not/exist");
+  }
+
+  @Test
+  public void loadSecretKeyFromFile_no_property() throws Exception {
+    thrown.expect(IllegalStateException.class);
+
+    AesECBCipher cipher = new AesECBCipher(null);
+    cipher.loadSecretFileFromFile(null);
+  }
+
+  @Test
+  public void hasSecretKey() throws Exception {
+    AesECBCipher cipher = new AesECBCipher(pathToSecretKey());
+
+    assertThat(cipher.hasSecretKey()).isTrue();
+  }
+
+  @Test
+  public void doesNotHaveSecretKey() {
+    AesECBCipher cipher = new AesECBCipher("/my/twitter/id/is/SimonBrandhof");
+
+    assertThat(cipher.hasSecretKey()).isFalse();
+  }
+
+  private String pathToSecretKey() throws Exception {
+    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/aes_secret_key.txt");
+    return new File(resource.toURI()).getCanonicalPath();
+  }
+}
diff --git a/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/AesGCMCipherTest.java b/sonar-plugin-api-impl/src/test/java/org/sonar/api/config/internal/AesGCMCipherTest.java
new file mode 100644 (file)
index 0000000..08b23b1
--- /dev/null
@@ -0,0 +1,93 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program 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.
+ *
+ * This program 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 this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.api.config.internal;
+
+import java.io.File;
+import java.net.URL;
+import java.security.InvalidKeyException;
+import javax.crypto.BadPaddingException;
+
+import org.apache.commons.lang.StringUtils;
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+public class AesGCMCipherTest {
+
+  @Test
+  public void encrypt_should_generate_different_value_everytime() throws Exception {
+    AesGCMCipher cipher = new AesGCMCipher(pathToSecretKey());
+
+    String encryptedText1 = cipher.encrypt("this is a secret");
+    String encryptedText2 = cipher.encrypt("this is a secret");
+
+    assertThat(StringUtils.isNotBlank(encryptedText1)).isTrue();
+    assertThat(StringUtils.isNotBlank(encryptedText2)).isTrue();
+    assertThat(encryptedText1).isNotEqualTo(encryptedText2);
+  }
+
+  @Test
+  public void encrypt_bad_key() throws Exception {
+    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/bad_secret_key.txt");
+    AesGCMCipher cipher = new AesGCMCipher(new File(resource.toURI()).getCanonicalPath());
+
+    assertThatThrownBy(() -> cipher.encrypt("this is a secret"))
+      .hasRootCauseInstanceOf(InvalidKeyException.class)
+      .hasMessageContaining("Invalid AES key");
+  }
+
+  @Test
+  public void decrypt() throws Exception {
+    AesGCMCipher cipher = new AesGCMCipher(pathToSecretKey());
+    String input1 = "this is a secret";
+    String input2 = "asdkfja;ksldjfowiaqueropijadfskncmnv/sdjflskjdflkjiqoeuwroiqu./qewirouasoidfhjaskldfhjkhckjnkiuoewiruoasdjkfalkufoiwueroijuqwoerjsdkjflweoiru";
+
+    assertThat(cipher.decrypt(cipher.encrypt(input1))).isEqualTo(input1);
+    assertThat(cipher.decrypt(cipher.encrypt(input1))).isEqualTo(input1);
+    assertThat(cipher.decrypt(cipher.encrypt(input2))).isEqualTo(input2);
+    assertThat(cipher.decrypt(cipher.encrypt(input2))).isEqualTo(input2);
+  }
+
+  @Test
+  public void decrypt_bad_key() throws Exception {
+    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/bad_secret_key.txt");
+    AesGCMCipher cipher = new AesGCMCipher(new File(resource.toURI()).getCanonicalPath());
+
+    assertThatThrownBy(() -> cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY="))
+      .hasRootCauseInstanceOf(InvalidKeyException.class)
+      .hasMessageContaining("Invalid AES key");
+  }
+
+  @Test
+  public void decrypt_other_key() throws Exception {
+    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/other_secret_key.txt");
+    AesGCMCipher originalCipher = new AesGCMCipher(pathToSecretKey());
+    AesGCMCipher cipher = new AesGCMCipher(new File(resource.toURI()).getCanonicalPath());
+
+    assertThatThrownBy(() -> cipher.decrypt(originalCipher.encrypt("this is a secret")))
+      .hasRootCauseInstanceOf(BadPaddingException.class);
+  }
+
+  private String pathToSecretKey() throws Exception {
+    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/aes_secret_key.txt");
+    return new File(resource.toURI()).getCanonicalPath();
+  }
+}
index 12c8d71636824fa2b3743cf5acb04a45222050e7..63f202c2864bae7933c903e739fc54d70874c738 100644 (file)
@@ -19,6 +19,9 @@
  */
 package org.sonar.api.config.internal;
 
+import java.io.File;
+import java.net.URL;
+
 import org.junit.Test;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -49,6 +52,33 @@ public class EncryptionTest {
     assertThat(encryption.decrypt("{b64}Zm9v")).isEqualTo("foo");
   }
 
+  @Test
+  public void loadSecretKey() throws Exception {
+    Encryption encryption = new Encryption(null);
+    encryption.setPathToSecretKey(pathToSecretKey());
+    assertThat(encryption.hasSecretKey()).isTrue();
+  }
+
+  @Test
+  public void generate_secret_key() {
+    Encryption encryption = new Encryption(null);
+    String key1 = encryption.generateRandomSecretKey();
+    String key2 = encryption.generateRandomSecretKey();
+    assertThat(key1).isNotEqualTo(key2);
+  }
+
+  @Test
+  public void gcm_encryption() throws Exception {
+    Encryption encryption = new Encryption(pathToSecretKey());
+    String clearText = "this is a secrit";
+    String cipherText = encryption.encrypt(clearText);
+    String decryptedText = encryption.decrypt(cipherText);
+    assertThat(cipherText)
+      .startsWith("{aes-gcm}")
+      .isNotEqualTo(clearText);
+    assertThat(decryptedText).isEqualTo(clearText);
+  }
+
   @Test
   public void decrypt_unknown_algorithm() {
     Encryption encryption = new Encryption(null);
@@ -60,4 +90,9 @@ public class EncryptionTest {
     Encryption encryption = new Encryption(null);
     assertThat(encryption.decrypt("foo")).isEqualTo("foo");
   }
+
+  private String pathToSecretKey() throws Exception {
+    URL resource = getClass().getResource("/org/sonar/api/config/internal/AesCipherTest/aes_secret_key.txt");
+    return new File(resource.toURI()).getCanonicalPath();
+  }
 }