Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 2 additions & 0 deletions src/main/java/org/ohdsi/webapi/DataAccessConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.cosium.spring.data.jpa.entity.graph.repository.support.EntityGraphJpaRepositoryFactoryBean;
import org.ohdsi.webapi.arachne.encryption.EncryptorUtils;
import org.ohdsi.webapi.arachne.encryption.NotEncrypted;
import org.ohdsi.webapi.source.EncryptedStringConverter;
import org.jasypt.encryption.pbe.PBEStringEncryptor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.orm.hibernate5.SpringBeanContainer;
Expand Down Expand Up @@ -134,6 +135,7 @@ public PBEStringEncryptor defaultStringEncryptor(){
PBEStringEncryptor stringEncryptor = encryptorEnabled ?
EncryptorUtils.buildStringEncryptor(env) :
new NotEncrypted();
EncryptedStringConverter.setDefaultEncryptor(stringEncryptor);

return stringEncryptor;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,50 @@
*
* Spring/Hibernate integration note:
* By default, Hibernate instantiates @Converter classes via reflection (newInstance()),
* bypassing Spring dependency injection entirely. To solve this, DataAccessConfig registers
* a SpringBeanContainer via the "hibernate.resource.beans.container" property on the
* EntityManagerFactory. This tells Hibernate to resolve managed beans (including this
* converter) from the Spring ApplicationContext, so @Autowired injection works naturally.
* bypassing Spring dependency injection entirely. DataAccessConfig registers a
* SpringBeanContainer so Hibernate resolves the converter from the ApplicationContext, but
* that is not guaranteed to run @Autowired either: in the GraalVM native image the setter
* is never invoked and the encryptor stayed null, so encrypted ENC(...) credentials were
* handed to the JDBC driver as-is. The encryptor is therefore held statically and set
* directly by the defaultStringEncryptor bean, so every instance sees it however it was
* created. A missing encryptor fails loudly instead of silently skipping encryption.
*/
@Component
@Converter
public class EncryptedStringConverter implements AttributeConverter<String, String> {

private PBEStringEncryptor encryptor;
private static volatile PBEStringEncryptor encryptor;

public static void setDefaultEncryptor(PBEStringEncryptor defaultStringEncryptor) {
encryptor = defaultStringEncryptor;
}

@Autowired
public void setEncryptor(PBEStringEncryptor defaultStringEncryptor) {
this.encryptor = defaultStringEncryptor;
setDefaultEncryptor(defaultStringEncryptor);
}

@Override
public String convertToDatabaseColumn(String attribute) {
if (attribute == null) {
return null;
}
return EncryptorUtils.encrypt(encryptor, attribute);
return EncryptorUtils.encrypt(requireEncryptor(), attribute);
}

@Override
public String convertToEntityAttribute(String dbData) {
if (dbData == null) {
return null;
}
return EncryptorUtils.decrypt(encryptor, dbData);
return EncryptorUtils.decrypt(requireEncryptor(), dbData);
}

private static PBEStringEncryptor requireEncryptor() {
PBEStringEncryptor current = encryptor;
if (current == null) {
throw new IllegalStateException("EncryptedStringConverter has no encryptor: the defaultStringEncryptor bean has not been created");
}
return current;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.ohdsi.webapi.source;

import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.junit.After;
import org.junit.Test;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.startsWith;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertThrows;

public class EncryptedStringConverterTest {

@After
public void resetEncryptor() {
EncryptedStringConverter.setDefaultEncryptor(null);
}

private static StandardPBEStringEncryptor encryptor() {
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
encryptor.setPassword("test-password");
return encryptor;
}

@Test
public void converterCreatedWithoutInjectionDecrypts() {
EncryptedStringConverter.setDefaultEncryptor(encryptor());

// Hibernate may instantiate the converter itself, so @Autowired never runs on it.
EncryptedStringConverter converter = new EncryptedStringConverter();
String stored = converter.convertToDatabaseColumn("secret");

assertThat(stored, startsWith("ENC("));
assertThat(new EncryptedStringConverter().convertToEntityAttribute(stored), is("secret"));
}

@Test
public void encryptedValueIsNotPassedThroughWithoutEncryptor() {
String stored = "ENC(" + encryptor().encrypt("secret") + ")";

assertThrows(IllegalStateException.class,
() -> new EncryptedStringConverter().convertToEntityAttribute(stored));
}

@Test
public void plaintextIsNotStoredWithoutEncryptor() {
assertThrows(IllegalStateException.class,
() -> new EncryptedStringConverter().convertToDatabaseColumn("secret"));
}

@Test
public void autowiredSetterSharesEncryptorWithOtherInstances() {
new EncryptedStringConverter().setEncryptor(encryptor());

String stored = new EncryptedStringConverter().convertToDatabaseColumn("secret");

assertThat(stored, not(is("secret")));
assertThat(new EncryptedStringConverter().convertToEntityAttribute(stored), is("secret"));
}
}
Loading