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
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,10 @@ public StorageEncryptionState storageEncryptionState(
createKeyService(
configuredFileEncryptionKey, clusterEnabled, requiresNew),
fileEncryptionKeyRepository);
if (writeEnabled || fileEncryptionKeyRepository.count() > 0) {
// The registry table may not exist when storage is unused, so only probe if it is on.
boolean probeForExistingKeys =
!writeEnabled && applicationProperties.getStorage().isEnabled();
if (writeEnabled || (probeForExistingKeys && state.encryptedContentMayExist())) {
state.initialiseEagerly();
log.info(
"Storage encryption at rest active (writes {})",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package stirling.software.proprietary.storage.crypto;

import java.time.Duration;
import java.util.Optional;
import java.util.function.Supplier;

import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -104,9 +105,28 @@ public boolean suppressDirectDownloads() {
&& now - keysExistCheckedAtNanos < KEYS_EXIST_CACHE_TTL.toNanos()) {
return keysExistCached;
}
keysExistCached = keyRepository.count() > 0;
keysExistCached = countKeys().orElse(true /* unreadable registry: fail safe */);
keysExistCheckedAtNanos = now;
keysExistEverChecked = true;
return keysExistCached;
}

/** True when key rows exist; empty when the registry could not be read. */
private Optional<Boolean> countKeys() {
try {
return Optional.of(keyRepository.count() > 0);
} catch (RuntimeException e) {
log.warn(
"Could not read the storage encryption key registry ({}). Treating direct"
+ " downloads as unsafe; encrypted content is still decrypted on"
+ " demand.",
e.getMessage());
return Optional.empty();
}
}

/** Whether key rows exist, for the decrypt-only startup path; false if unreadable. */
public boolean encryptedContentMayExist() {
return countKeys().orElse(false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.Base64;
import java.util.Optional;

import org.junit.jupiter.api.Test;
import org.springframework.dao.InvalidDataAccessResourceUsageException;
import org.springframework.transaction.PlatformTransactionManager;

import stirling.software.common.model.ApplicationProperties;
Expand All @@ -24,6 +28,7 @@
import stirling.software.proprietary.storage.crypto.InMemoryKeyRepo;
import stirling.software.proprietary.storage.crypto.StorageEncryptionState;
import stirling.software.proprietary.storage.provider.StorageProvider;
import stirling.software.proprietary.storage.repository.FileEncryptionKeyRepository;
import stirling.software.proprietary.storage.repository.StoredFileBlobRepository;

/**
Expand Down Expand Up @@ -66,16 +71,16 @@ void decorator_writeEnabled_requiresLicenceAndSuppressesDirectDownloads() {

@Test
void decorator_flagOffButKeysExist_decryptOnlyModeStillMaterialises() throws Exception {
// Simulate the drifted-node case: another node already created keys.
StorageProviderConfig seedCfg = newConfig("local", License.SERVER, true);
// Drifted node: keys created elsewhere; storage on, as it must be to serve files.
StorageProviderConfig seedCfg = newConfig("local", License.SERVER, true, true);
StorageEncryptionState seedState = seedCfg.storageEncryptionState(MASTER, false, txManager);
Team team = new Team();
team.setId(1L);
User owner = new User();
owner.setTeam(team);
seedState.keyService().activeKekForOwner(owner);

StorageProviderConfig cfg = newConfig("local", License.NORMAL, false);
StorageProviderConfig cfg = newConfig("local", License.NORMAL, false, true);
StorageEncryptionState state = cfg.storageEncryptionState(MASTER, false, txManager);

assertThat(cfg.storageProvider(state, Optional.empty()))
Expand Down Expand Up @@ -104,6 +109,44 @@ void encryption_enabled_wrongLengthKey_failsStartup() {
.hasMessageContaining("32 bytes");
}

// ---- deployments that do not use storage (the SaaS shape) ---------------------------

@Test
void storageDisabled_neverQueriesTheKeyRegistry() {
StorageProviderConfig cfg = newConfig("local", License.NORMAL, false);
StorageEncryptionState state = cfg.storageEncryptionState(MASTER, false, txManager);

assertThat(state.isWriteEnabled()).isFalse();
verify(keyRepo.mock, never()).count();
}

@Test
void storageDisabled_decoratorStillInstalledSoCiphertextIsNeverServedRaw() {
StorageProviderConfig cfg = newConfig("local", License.NORMAL, false);
StorageEncryptionState state = cfg.storageEncryptionState(MASTER, false, txManager);

assertThat(cfg.storageProvider(state, Optional.empty()))
.isInstanceOf(EncryptingStorageProvider.class);
}

@Test
void unreadableKeyRegistry_stillStartsAndFailsSafeOnDirectDownloads() {
FileEncryptionKeyRepository broken = mock(FileEncryptionKeyRepository.class);
when(broken.count())
.thenThrow(new InvalidDataAccessResourceUsageException("no such table"));
StorageEncryptionState state = new StorageEncryptionState(false, () -> null, broken);

assertThat(state.encryptedContentMayExist()).isFalse();
assertThat(state.suppressDirectDownloads()).isTrue();
}

@Test
void storageEnabledWithoutEncryption_probesRegistry() {
StorageProviderConfig cfg = newConfig("local", License.NORMAL, false, true);
cfg.storageEncryptionState(MASTER, false, txManager);
verify(keyRepo.mock, atLeastOnce()).count();
}

// ---- backend licence gates (unchanged behaviour) ------------------------------------

@Test
Expand Down Expand Up @@ -161,9 +204,14 @@ void provider_unknown_throwsUnsupportedProvider_notLicense() {

private StorageProviderConfig newConfig(
String provider, License license, boolean encryptionEnabled) {
return newConfig(provider, license, encryptionEnabled, false);
}

private StorageProviderConfig newConfig(
String provider, License license, boolean encryptionEnabled, boolean storageEnabled) {
ApplicationProperties props = new ApplicationProperties();
props.getStorage().setProvider(provider);
props.getStorage().setEnabled(false); // local-fallback path skips dir creation
props.getStorage().setEnabled(storageEnabled);
props.getStorage().getEncryption().setEnabled(encryptionEnabled);
StoredFileBlobRepository repo = mock(StoredFileBlobRepository.class);
LicenseKeyChecker checker = mock(LicenseKeyChecker.class);
Expand Down
Loading