diff --git a/src/main/java/com/uid2/admin/secret/SaltRotation.java b/src/main/java/com/uid2/admin/secret/SaltRotation.java index 69685c62..85e51b1a 100644 --- a/src/main/java/com/uid2/admin/secret/SaltRotation.java +++ b/src/main/java/com/uid2/admin/secret/SaltRotation.java @@ -19,6 +19,7 @@ public class SaltRotation { private final IKeyGenerator keyGenerator; private final long THIRTY_DAYS_IN_MS = Duration.ofDays(30).toMillis(); + private final long DAY_IN_MS = Duration.ofDays(1).toMillis(); public SaltRotation(IKeyGenerator keyGenerator) { this.keyGenerator = keyGenerator; @@ -63,6 +64,7 @@ private SaltEntry updateSalt(SaltEntry oldSalt, boolean shouldRotate, long nextE var currentSalt = shouldRotate ? this.keyGenerator.generateRandomKeyString(32) : oldSalt.currentSalt(); var lastUpdated = shouldRotate ? nextEffective : oldSalt.lastUpdated(); var refreshFrom = calculateRefreshFrom(oldSalt.lastUpdated(), nextEffective); + var previousSalt = calculatePreviousSalt(oldSalt, shouldRotate, nextEffective); return new SaltEntry( oldSalt.id(), @@ -70,7 +72,7 @@ private SaltEntry updateSalt(SaltEntry oldSalt, boolean shouldRotate, long nextE lastUpdated, currentSalt, refreshFrom, - null, + previousSalt, null, null ); @@ -82,6 +84,17 @@ private long calculateRefreshFrom(long lastUpdated, long nextEffective) { return lastUpdated + (multiplier * THIRTY_DAYS_IN_MS); } + private String calculatePreviousSalt(SaltEntry salt, boolean shouldRotate, long nextEffective) throws Exception { + if (shouldRotate) { + return salt.currentSalt(); + } + long age = nextEffective - salt.lastUpdated(); + if ( age / DAY_IN_MS < 90) { + return salt.previousSalt(); + } + return null; + } + private List pickSaltIndexesToRotate( SaltSnapshot lastSnapshot, Instant nextEffective, diff --git a/src/test/java/com/uid2/admin/secret/SaltRotationTest.java b/src/test/java/com/uid2/admin/secret/SaltRotationTest.java index 589ede8b..6e5e9c96 100644 --- a/src/test/java/com/uid2/admin/secret/SaltRotationTest.java +++ b/src/test/java/com/uid2/admin/secret/SaltRotationTest.java @@ -239,4 +239,83 @@ void testRefreshFromCalculation(int lastRotationDaysAgo, int refreshFromDaysFrom assertThat(actual.refreshFrom()).isEqualTo(expected); } + + @Test + void rotateSaltsPopulatePreviousSaltsOnRotation() throws Exception { + final Duration[] minAges = { + Duration.ofDays(90), + Duration.ofDays(60), + Duration.ofDays(30) + }; + + var lessThan90Days = daysEarlier(60).toEpochMilli(); + var exactly90Days = daysEarlier(90).toEpochMilli(); + var over90Days = daysEarlier(120).toEpochMilli(); + var lastSnapshot = SnapshotBuilder.start() + .withEntries( + new SaltEntry(1, "1", lessThan90Days, "salt1", null, null, null, null), + new SaltEntry(3, "2", exactly90Days, "salt2", null, null, null, null), + new SaltEntry(5, "3", over90Days, "salt3", null, 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("salt1", salts[0].previousSalt()); + assertEquals("salt2", salts[1].previousSalt()); + assertEquals("salt3", salts[2].previousSalt()); + } + + @Test + void rotateSaltsPreservePreviousSaltsLessThan90DaysOld() throws Exception { + final Duration[] minAges = { + Duration.ofDays(60), + }; + + var notValidForRotation1 = daysEarlier(40).toEpochMilli(); + var notValidForRotation2 = daysEarlier(50).toEpochMilli(); + var validForRotation = daysEarlier(70); + var lastSnapshot = SnapshotBuilder.start() + .withEntries( + new SaltEntry(1, "1", notValidForRotation1, "salt1", null, "previousSalt1", null, null), + new SaltEntry(2, "2", notValidForRotation2, "salt2", null, null, null, null) + ) + .withEntries(1, validForRotation) + .build(daysEarlier(1), daysLater(6)); + + var result = saltRotation.rotateSalts(lastSnapshot, minAges, 1, targetDate); + assertTrue(result.hasSnapshot()); + + var salts = result.getSnapshot().getAllRotatingSalts(); + assertEquals("previousSalt1", salts[0].previousSalt()); + assertNull(salts[1].previousSalt()); + } + + @Test + void rotateSaltsRemovePreviousSaltsOver90DaysOld() throws Exception { + final Duration[] minAges = { + Duration.ofDays(100), + }; + + var exactly90Days = daysEarlier(90).toEpochMilli(); + var over90Days = daysEarlier(100).toEpochMilli(); + var validForRotation = daysEarlier(120); + var lastSnapshot = SnapshotBuilder.start() + .withEntries( + new SaltEntry(1, "1", exactly90Days, "salt1", null, "90DaysOld", null, null), + new SaltEntry(2, "2", over90Days, "salt2", null, "over90DaysOld", null, null) + ) + .withEntries(1, validForRotation) + .build(daysEarlier(1), daysLater(6)); + + var result = saltRotation.rotateSalts(lastSnapshot, minAges, 0.5, targetDate); + assertTrue(result.hasSnapshot()); + + var salts = result.getSnapshot().getAllRotatingSalts(); + assertNull(salts[0].previousSalt()); + assertNull(salts[1].previousSalt()); + } + }