Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jasypt encryption #845

Merged
merged 14 commits into from
Aug 7, 2019
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,57 @@ An in-depth look at configuring Tessera can be found on the [Tessera Wiki](https
* TLS config
* How to enable TLS
* Choosing a trust mode

#### Obfuscate database password in config file

Certain entries in Tessera config file must be obfuscated in order to prevent any attempts from attackers to gain access to critical part of the application (i.e. database). For the time being, Tessera users have the ability to enable encryption for database password to avoid it being exposed as plain text in the configuration file.

In Tessera, [jasypt](http://www.jasypt.org) library was used together with its Jaxb integration to encrypt/decrypt config values.

To enable this feature, simply replace your plain-text database password with its encrypted value and wrap it inside an `ENC()` function.

```json
"jdbc": {
"username": "sa",
"password": "ENC(ujMeokIQ9UFHSuBYetfRjQTpZASgaua3)",
"url": "jdbc:h2:/qdata/c1/db1",
"autoCreateTables": true
}
```

Being a Password-Based Encryptor, Jasypt requires a secret key (password) and a configured algorithm to encrypt/decrypt this config entry. This password can either be loaded into Tessera from file system or user input. For file system input, the location of this secret file needs to be set in Environment Variable `TESSERA_CONFIG_SECRET`

If the database password is not being wrapped inside `ENC()` function, Tessera will simply treat it as a plain-text password however this approach is not recommended for production environment.

* Please note at the moment jasypt encryption is only enabled on `jdbc.password` field.

##### Encrypt database password

Download and unzip the [jasypt](http://www.jasypt.org) package. Redirect to bin directory and the follow commands can be used to encrypt a string

```bash
bash-3.2$ ./encrypt.sh input=dbpassword password=quorum

----ENVIRONMENT-----------------

Runtime: Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 25.171-b11



----ARGUMENTS-------------------

input: dbpassword
password: quorum



----OUTPUT----------------------

rJ70hNidkrpkTwHoVn2sGSp3h3uBWxjb

```

Pick up this output and wrap it inside `ENC()` function, we should have the following `ENC(rJ70hNidkrpkTwHoVn2sGSp3h3uBWxjb)` in the config json file.

### Migrating from Constellation to Tessera
Tessera is the service used to provide Quorum with the ability to support private transactions, replacing Constellation. If you have previously been using Constellation, utilities are provided within Tessera to enable the migration of Constellation configuration and datastores to Tessera compatible formats. Details on how to use these utilities can be found in the [Tessera Wiki](https://github.com/jpmorganchase/tessera/wiki/Migrating-from-Constellation).
Expand Down
5 changes: 5 additions & 0 deletions config/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@
<artifactId>commons-lang3</artifactId>
</dependency>

<dependency>
<groupId>org.jasypt</groupId>
<artifactId>jasypt</artifactId>
</dependency>


</dependencies>
<build>
Expand Down
14 changes: 7 additions & 7 deletions config/src/main/java/com/quorum/tessera/config/JdbcConfig.java
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
package com.quorum.tessera.config;

import com.quorum.tessera.config.adapters.EncryptedStringAdapter;

import javax.validation.constraints.NotNull;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;

@XmlAccessorType(XmlAccessType.FIELD)
public class JdbcConfig extends ConfigItem {

@XmlElement
private String username;
@XmlElement private String username;

@XmlElement
@XmlElement(type = String.class)
@XmlJavaTypeAdapter(value = EncryptedStringAdapter.class)
private String password;

@NotNull
@XmlElement(required = true)
private String url;

/**
* Auto create tables if no exists
*/
/** Auto create tables if no exists */
@XmlElement(defaultValue = "false")
private boolean autoCreateTables;

Expand Down Expand Up @@ -65,5 +66,4 @@ public boolean isAutoCreateTables() {
public void setAutoCreateTables(boolean autoCreateTables) {
this.autoCreateTables = autoCreateTables;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.quorum.tessera.config.adapters;

import com.quorum.tessera.config.util.ConfigSecretReader;
import org.jasypt.encryption.pbe.PBEStringEncryptor;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.jasypt.properties.PropertyValueEncryptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.xml.bind.annotation.adapters.XmlAdapter;

public class EncryptedStringAdapter extends XmlAdapter<String, String> {

private static final Logger LOGGER = LoggerFactory.getLogger(EncryptedStringAdapter.class);

private final PBEStringEncryptor encryptor = new StandardPBEStringEncryptor();

@Override
public String unmarshal(String textToDecrypt) {

if (PropertyValueEncryptionUtils.isEncryptedValue(textToDecrypt)) {

if (!((StandardPBEStringEncryptor) encryptor).isInitialized()) {
namtruong marked this conversation as resolved.
Show resolved Hide resolved
encryptor.setPassword(
ConfigSecretReader.readSecretFromFile()
.orElseGet(ConfigSecretReader::readSecretFromConsole));
}

return PropertyValueEncryptionUtils.decrypt(textToDecrypt, encryptor);
}

LOGGER.warn(
"Some sensitive values are being given as unencrypted plain text in config. "
+ "Please note this is NOT recommended for production environment.");

return textToDecrypt;
}

@Override
public String marshal(String text) {
return text;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.quorum.tessera.config.util;

import com.quorum.tessera.passwords.PasswordReaderFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Optional;

public final class ConfigSecretReader {

private static final Logger LOGGER = LoggerFactory.getLogger(ConfigSecretReader.class);

private ConfigSecretReader() {}

public static Optional<String> readSecretFromFile() {

final EnvironmentVariableProvider envProvider = new EnvironmentVariableProvider();

if (envProvider.hasEnv(EnvironmentVariables.CONFIG_SECRET_PATH)) {
final Path secretPath = Paths.get(envProvider.getEnv(EnvironmentVariables.CONFIG_SECRET_PATH));
if (Files.exists(secretPath)) {
try {
return Optional.of(new String(Files.readAllBytes(secretPath)));
} catch (IOException ex) {
LOGGER.error("Error while reading secret from file");
}
}
}

LOGGER.warn("Not able to find or read any secret for decrypting sensitive values in config.");

return Optional.empty();
}

public static String readSecretFromConsole() {
System.out.println("Please enter the secret/password used to decrypt config value");
namtruong marked this conversation as resolved.
Show resolved Hide resolved
return PasswordReaderFactory.create().readPasswordFromConsole();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ public final class EnvironmentVariables {

public static final String HASHICORP_CLIENT_TRUSTSTORE_PWD = "HASHICORP_CLIENT_TRUSTSTORE_PWD";

private EnvironmentVariables() {

}
public static final String CONFIG_SECRET_PATH = "TESSERA_CONFIG_SECRET";

private EnvironmentVariables() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package com.quorum.tessera.config.adapters;

import org.jasypt.exceptions.EncryptionOperationNotPossibleException;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.EnvironmentVariables;

import java.io.ByteArrayInputStream;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;

public class EncryptedStringAdapterTest {

private EncryptedStringAdapter adapter;

@Rule public final EnvironmentVariables envVariables = new EnvironmentVariables();

@Before
public void init() {

final String filePath = getClass().getResource("/key.secret").getPath();
envVariables.set(com.quorum.tessera.config.util.EnvironmentVariables.CONFIG_SECRET_PATH, filePath);
adapter = new EncryptedStringAdapter();
}

@Test
public void testMarshall() {

final String expectedValue = "password";

assertThat(adapter.marshal("password")).isEqualTo(expectedValue);

assertThat(adapter.marshal(null)).isNull();
}

@Test
public void testUnMarshall() {

String normalPassword = "password";

assertThat(adapter.unmarshal("password")).isEqualTo(normalPassword);

assertThat(adapter.unmarshal("ENC(KLa6pRQpxI8Ez3Bo6D3cI6y13YYdntu7)")).isEqualTo("password");

assertThat(adapter.unmarshal(null)).isNull();
}

@Test
public void testUnMarshallWithUserInputSecret() {

envVariables.clear(com.quorum.tessera.config.util.EnvironmentVariables.CONFIG_SECRET_PATH);

adapter = new EncryptedStringAdapter();

ByteArrayInputStream in = new ByteArrayInputStream(("quorum" + System.lineSeparator() + "quorum").getBytes());
System.setIn(in);
namtruong marked this conversation as resolved.
Show resolved Hide resolved

assertThat(adapter.unmarshal("ENC(KLa6pRQpxI8Ez3Bo6D3cI6y13YYdntu7)")).isEqualTo("password");
}

@Test
public void testUnMarshallWrongPassword() {

envVariables.clear(com.quorum.tessera.config.util.EnvironmentVariables.CONFIG_SECRET_PATH);

adapter = new EncryptedStringAdapter();

ByteArrayInputStream in = new ByteArrayInputStream(("bogus" + System.lineSeparator() + "bogus").getBytes());
System.setIn(in);
namtruong marked this conversation as resolved.
Show resolved Hide resolved

assertThatExceptionOfType(EncryptionOperationNotPossibleException.class)
.isThrownBy(() -> adapter.unmarshal("ENC(KLa6pRQpxI8Ez3Bo6D3cI6y13YYdntu7)"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package com.quorum.tessera.config.util;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.contrib.java.lang.system.EnvironmentVariables;
import org.junit.rules.TemporaryFolder;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;

public class ConfigSecretReaderTest {

private String filePath;

@Rule
public final org.junit.contrib.java.lang.system.EnvironmentVariables envVariables = new EnvironmentVariables();

@Rule
public TemporaryFolder tempDir = new TemporaryFolder();

@Before
public void setUp() {
filePath = getClass().getResource("/key.secret").getPath();
}

@Test
public void testReadSecret() {

envVariables.set(com.quorum.tessera.config.util.EnvironmentVariables.CONFIG_SECRET_PATH, filePath);

Optional<String> secret = ConfigSecretReader.readSecretFromFile();

assertThat(secret).isPresent();
assertThat(secret.get()).isEqualTo("quorum");
}

@Test
public void testNotAbleToReadSecret() {

envVariables.set(com.quorum.tessera.config.util.EnvironmentVariables.CONFIG_SECRET_PATH, "not-existed");
assertThat(ConfigSecretReader.readSecretFromFile()).isEmpty();
}

@Test
public void envNotSet() {
envVariables.clear(com.quorum.tessera.config.util.EnvironmentVariables.CONFIG_SECRET_PATH);
assertThat(ConfigSecretReader.readSecretFromFile()).isEmpty();
}

@Test
public void testReadFromConsole() {
ByteArrayInputStream in = new ByteArrayInputStream("password".getBytes());
System.setIn(in);
assertThat(ConfigSecretReader.readSecretFromConsole()).isEqualTo("password");

System.setIn(System.in);
namtruong marked this conversation as resolved.
Show resolved Hide resolved
assertThat(ConfigSecretReader.readSecretFromConsole()).isEqualTo("");
}

@Test
public void testReadException() throws IOException {
envVariables.clear(com.quorum.tessera.config.util.EnvironmentVariables.CONFIG_SECRET_PATH);

final File tempFile = tempDir.newFile("key.secret");
tempFile.setReadable(false);


envVariables.set(com.quorum.tessera.config.util.EnvironmentVariables.CONFIG_SECRET_PATH,
tempFile.getAbsolutePath());

assertThat(ConfigSecretReader.readSecretFromFile()).isEmpty();


}
}
1 change: 1 addition & 0 deletions config/src/test/resources/key.secret
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
quorum
11 changes: 10 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<jackson.version>2.9.9</jackson.version>
<project.scm.id>github</project.scm.id>
<asm.version>7.0</asm.version>
<jasypt.version>1.9.3</jasypt.version>
</properties>

<build>
Expand Down Expand Up @@ -1321,7 +1322,15 @@
<artifactId>asm-analysis</artifactId>
<version>${asm.version}</version>
</dependency>



<dependency>
<groupId>org.jasypt</groupId>
<artifactId>jasypt</artifactId>
<version>${jasypt.version}</version>
</dependency>


</dependencies>
</dependencyManagement>

Expand Down
Loading