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
```
+++ /dev/null
-/*
- * 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;
- }
-}
+++ /dev/null
-/*
- * 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;
- }
-
-}
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 {
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) {
+++ /dev/null
-/*
- * 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");
- }
-
-}
+++ /dev/null
-/*
- * 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");
- }
-}
{
- "encryptedValue": "{aes}q2ANI9ikR9R8P2CMCCTWeA=="
+ "encryptedValue": "{aes-gcm}q2ANI9ikR9R8P2CMCCTWeA=="
}
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;
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
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
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;
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";
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.
*/
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)) {
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
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) {
}
public String encrypt(String clearText) {
- return encrypt(AES_ALGORITHM, clearText);
+ return encrypt(AES_GCM_ALGORITHM, clearText);
}
public String scramble(String clearText) {
}
public String generateRandomSecretKey() {
- return aesCipher.generateRandomSecretKey();
+ return aesGCMCipher.generateRandomSecretKey();
}
public String decrypt(String encryptedText) {
+++ /dev/null
-/*
- * 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();
- }
-}
--- /dev/null
+/*
+ * 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();
+ }
+}
--- /dev/null
+/*
+ * 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();
+ }
+}
*/
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;
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);
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();
+ }
}