Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
1 change: 1 addition & 0 deletions conf/local-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"keys_acl_metadata_path": "keys_acl/metadata.json",
"salts_metadata_path": "salts/metadata.json",
"salt_snapshot_location_prefix": "salts/salts.txt.",
"enable_salt_rotation_refresh_from": true,
"operators_metadata_path": "operators/metadata.json",
"enclaves_metadata_path": "enclaves/metadata.json",
"partners_metadata_path": "partners/metadata.json",
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/uid2/admin/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ public void run() {
WriteLock writeLock = new WriteLock();
KeyHasher keyHasher = new KeyHasher();
IKeypairGenerator keypairGenerator = new SecureKeypairGenerator();
SaltRotation saltRotation = new SaltRotation(keyGenerator);
SaltRotation saltRotation = new SaltRotation(config, keyGenerator);
EncryptionKeyService encryptionKeyService = new EncryptionKeyService(
config, auth, writeLock, encryptionKeyStoreWriter, keysetKeyStoreWriter, keyProvider, keysetKeysProvider, adminKeysetProvider, adminKeysetStoreWriter, keyGenerator, clock);
KeysetManager keysetManager = new KeysetManager(
Expand Down
24 changes: 19 additions & 5 deletions src/main/java/com/uid2/admin/secret/SaltRotation.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.uid2.shared.store.salt.RotatingSaltProvider;

import com.uid2.shared.store.salt.RotatingSaltProvider.SaltSnapshot;
import io.vertx.core.json.JsonObject;

import java.time.Duration;
import java.time.Instant;
Expand All @@ -17,11 +18,15 @@
import static java.util.stream.Collectors.toList;

public class SaltRotation {
private final static String ENABLE_REFRESH_FROM = "enable_salt_rotation_refresh_from";
private final static long THIRTY_DAYS_IN_MS = Duration.ofDays(30).toMillis();

private final IKeyGenerator keyGenerator;
private final long THIRTY_DAYS_IN_MS = Duration.ofDays(30).toMillis();
private final boolean isRefreshFromEnabled;

public SaltRotation(IKeyGenerator keyGenerator) {
public SaltRotation(JsonObject config, IKeyGenerator keyGenerator) {
this.keyGenerator = keyGenerator;
this.isRefreshFromEnabled = config.getBoolean(ENABLE_REFRESH_FROM, false);
}

public Result rotateSalts(RotatingSaltProvider.SaltSnapshot lastSnapshot,
Expand Down Expand Up @@ -92,14 +97,15 @@ private List<Integer> pickSaltIndexesToRotate(
.sorted()
.toArray(Instant[]::new);
final int maxSalts = (int) Math.ceil(lastSnapshot.getAllRotatingSalts().length * fraction);
final SaltEntry[] rotatableSalts = getRotatableSalts(lastSnapshot, nextEffective.toEpochMilli());
final List<Integer> indexesToRotate = new ArrayList<>();

Instant minLastUpdated = Instant.ofEpochMilli(0);
for (Instant threshold : thresholds) {
if (indexesToRotate.size() >= maxSalts) break;
addIndexesToRotate(
indexesToRotate,
lastSnapshot,
rotatableSalts,
minLastUpdated.toEpochMilli(),
threshold.toEpochMilli(),
maxSalts - indexesToRotate.size()
Expand All @@ -109,12 +115,20 @@ private List<Integer> pickSaltIndexesToRotate(
return indexesToRotate;
}

private SaltEntry[] getRotatableSalts(SaltSnapshot lastSnapshot, long nextEffective) {
SaltEntry[] salts = lastSnapshot.getAllRotatingSalts();
if (isRefreshFromEnabled) {
return Arrays.stream(salts).filter(s -> s.refreshFrom() == nextEffective).toArray(SaltEntry[]::new);
}
return salts;
}


private void addIndexesToRotate(List<Integer> entryIndexes,
SaltSnapshot lastSnapshot,
SaltEntry[] entries,
long minLastUpdated,
long maxLastUpdated,
int maxIndexes) {
final SaltEntry[] entries = lastSnapshot.getAllRotatingSalts();
final List<Integer> candidateIndexes = IntStream.range(0, entries.length)
.filter(i -> isBetween(entries[i].lastUpdated(), minLastUpdated, maxLastUpdated))
.boxed()
Expand Down
56 changes: 52 additions & 4 deletions src/test/java/com/uid2/admin/secret/SaltRotationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.uid2.shared.model.SaltEntry;
import com.uid2.shared.secret.IKeyGenerator;
import com.uid2.shared.store.salt.RotatingSaltProvider;
import org.junit.jupiter.api.BeforeEach;
import io.vertx.core.json.JsonObject;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
Expand Down Expand Up @@ -34,11 +34,14 @@ private Instant daysLater(int days) {
return targetDateAsInstant.plus(days, DAYS);
}

@BeforeEach
void setup() {

void setup(boolean enableRefreshFrom) {
MockitoAnnotations.openMocks(this);

saltRotation = new SaltRotation(keyGenerator);
JsonObject config = new JsonObject();
config.put("enable_salt_rotation_refresh_from", enableRefreshFrom);

saltRotation = new SaltRotation(config, keyGenerator);
}

private static class SnapshotBuilder {
Expand Down Expand Up @@ -72,6 +75,7 @@ private int countEntriesWithLastUpdated(SaltEntry[] entries, Instant lastUpdated

@Test
void rotateSaltsLastSnapshotIsUpToDate() throws Exception {
setup(false);
final Duration[] minAges = {
Duration.ofDays(1),
Duration.ofDays(2),
Expand All @@ -88,6 +92,7 @@ void rotateSaltsLastSnapshotIsUpToDate() throws Exception {

@Test
void rotateSaltsAllSaltsUpToDate() throws Exception {
setup(false);
final Duration[] minAges = {
Duration.ofDays(1),
Duration.ofDays(2),
Expand All @@ -104,6 +109,7 @@ void rotateSaltsAllSaltsUpToDate() throws Exception {

@Test
void rotateSaltsAllSaltsOld() throws Exception {
setup(false);
final Duration[] minAges = {
Duration.ofDays(1),
Duration.ofDays(2),
Expand All @@ -124,6 +130,7 @@ void rotateSaltsAllSaltsOld() throws Exception {

@Test
void rotateSaltsRotateSaltsFromOldestBucketOnly() throws Exception {
setup(false);
final Duration[] minAges = {
Duration.ofDays(5),
Duration.ofDays(4),
Expand All @@ -149,6 +156,7 @@ void rotateSaltsRotateSaltsFromOldestBucketOnly() throws Exception {

@Test
void rotateSaltsRotateSaltsFromNewerBucketOnly() throws Exception {
setup(false);
final Duration[] minAges = {
Duration.ofDays(5),
Duration.ofDays(3),
Expand All @@ -172,6 +180,7 @@ void rotateSaltsRotateSaltsFromNewerBucketOnly() throws Exception {

@Test
void rotateSaltsRotateSaltsFromMultipleBuckets() throws Exception {
setup(false);
final Duration[] minAges = {
Duration.ofDays(5),
Duration.ofDays(4),
Expand All @@ -197,6 +206,7 @@ void rotateSaltsRotateSaltsFromMultipleBuckets() throws Exception {

@Test
void rotateSaltsRotateSaltsInsufficientOutdatedSalts() throws Exception {
setup(false);
final Duration[] minAges = {
Duration.ofDays(5),
Duration.ofDays(3),
Expand Down Expand Up @@ -227,6 +237,7 @@ void rotateSaltsRotateSaltsInsufficientOutdatedSalts() throws Exception {
"60, 90", // Exactly at multiple of 30 days post rotation, use next increment of 30 days
})
void testRefreshFromCalculation(int lastRotationDaysAgo, int refreshFromDaysFromRotation) throws Exception {
setup(false);
var lastRotation = daysEarlier(lastRotationDaysAgo);
var lastSnapshot = SnapshotBuilder.start()
.withEntries(new SaltEntry(1, "1", lastRotation.toEpochMilli(), "salt1", 100L, null, null, null))
Expand All @@ -239,4 +250,41 @@ void testRefreshFromCalculation(int lastRotationDaysAgo, int refreshFromDaysFrom

assertThat(actual.refreshFrom()).isEqualTo(expected);
}

@Test
void rotateSaltsRotateWhenRefreshFromIsTargetDate() throws Exception {
setup(true);
final Duration[] minAges = {
Duration.ofDays(90),
Duration.ofDays(60),
};

var validForRotation1 = daysEarlier(120).toEpochMilli();
var validForRotation2 = daysEarlier(70).toEpochMilli();
var notValidForRotation = daysEarlier(30).toEpochMilli();
var refreshNow = targetDateAsInstant.toEpochMilli();
var refreshLater = daysLater(20).toEpochMilli();

var lastSnapshot = SnapshotBuilder.start()
.withEntries(
new SaltEntry(1, "1", validForRotation1, "salt", refreshNow, null, null, null),
new SaltEntry(2, "2", notValidForRotation, "salt", refreshNow, null, null, null),
new SaltEntry(3, "3", validForRotation2, "salt", refreshLater, null, null, null)
)
.build(daysEarlier(1), daysLater(6));

var result = saltRotation.rotateSalts(lastSnapshot, minAges, 1, targetDate);
assertTrue(result.hasSnapshot());

var salts = result.getSnapshot().getAllRotatingSalts();

assertEquals(targetDateAsInstant.toEpochMilli(), salts[0].lastUpdated());
assertEquals(daysLater(30).toEpochMilli(), salts[0].refreshFrom());

assertEquals(notValidForRotation, salts[1].lastUpdated());
assertEquals(daysLater(30).toEpochMilli(), salts[1].refreshFrom());

assertEquals(validForRotation2, salts[2].lastUpdated());
assertEquals(refreshLater, salts[2].refreshFrom());
}
}