Skip to content

Commit a8e4c2b

Browse files
authored
Merge pull request #511 from IABTechLab/aul-UID2-5530-zero-salt-rotation
Zero salt rotation
2 parents 988da2e + e868007 commit a8e4c2b

File tree

6 files changed

+145
-10
lines changed

6 files changed

+145
-10
lines changed

src/main/java/com/uid2/admin/salt/SaltRotation.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import com.uid2.shared.model.SaltEntry;
55
import com.uid2.shared.secret.IKeyGenerator;
66

7+
import com.uid2.shared.store.salt.ISaltProvider;
8+
import com.uid2.shared.store.salt.ISaltProvider.ISaltSnapshot;
79
import com.uid2.shared.store.salt.RotatingSaltProvider.SaltSnapshot;
810
import io.vertx.core.json.JsonObject;
911
import lombok.Getter;
@@ -69,6 +71,27 @@ public Result rotateSalts(
6971
return Result.fromSnapshot(nextSnapshot);
7072
}
7173

74+
public Result rotateSaltsZero(
75+
ISaltSnapshot effectiveSnapshot,
76+
TargetDate targetDate,
77+
Instant nextEffective
78+
) throws Exception {
79+
var preRotationSalts = effectiveSnapshot.getAllRotatingSalts();
80+
var nextExpires = nextEffective.plus(7, ChronoUnit.DAYS);
81+
82+
var postRotationSalts = rotateSalts(preRotationSalts, List.of(), targetDate);
83+
84+
LOGGER.info("Zero salt rotation complete target_date={}", targetDate);
85+
86+
var nextSnapshot = new SaltSnapshot(
87+
nextEffective,
88+
nextExpires,
89+
postRotationSalts,
90+
effectiveSnapshot.getFirstLevelSalt());
91+
return Result.fromSnapshot(nextSnapshot);
92+
}
93+
94+
7295
private static int getNumSaltsToRotate(SaltEntry[] preRotationSalts, double fraction) {
7396
return (int) Math.ceil(preRotationSalts.length * fraction);
7497
}

src/main/java/com/uid2/admin/vertx/Endpoints.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ public enum Endpoints {
6767

6868
API_SALT_SNAPSHOTS("/api/salt/snapshots"),
6969
API_SALT_ROTATE("/api/salt/rotate"),
70+
API_SALT_ROTATE_ZERO("/api/salt/rotate-zero"),
7071

7172
API_SEARCH("/api/search"),
7273

src/main/java/com/uid2/admin/vertx/service/SaltService.java

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@
2626
import java.util.List;
2727
import java.util.Optional;
2828

29-
import static com.uid2.admin.vertx.Endpoints.API_SALT_ROTATE;
30-
import static com.uid2.admin.vertx.Endpoints.API_SALT_SNAPSHOTS;
29+
import static com.uid2.admin.vertx.Endpoints.*;
3130

3231
public class SaltService implements IService {
3332
private static final Logger LOGGER = LoggerFactory.getLogger(SaltService.class);
@@ -60,6 +59,12 @@ public void setupRoutes(Router router) {
6059
this.handleSaltRotate(ctx);
6160
}
6261
}, new AuditParams(List.of("fraction", "min_ages_in_seconds", "target_date"), Collections.emptyList()), Role.SUPER_USER, Role.SECRET_ROTATION));
62+
63+
router.post(API_SALT_ROTATE_ZERO.toString()).blockingHandler(auth.handle((ctx) -> {
64+
synchronized (writeLock) {
65+
this.handleSaltRotateZero(ctx);
66+
}
67+
}, new AuditParams(List.of(), Collections.emptyList()), Role.MAINTAINER));
6368
}
6469

6570
private void handleSaltSnapshots(RoutingContext rc) {
@@ -117,6 +122,37 @@ private void handleSaltRotate(RoutingContext rc) {
117122
}
118123
}
119124

125+
private void handleSaltRotateZero(RoutingContext rc) {
126+
try {
127+
Instant now = Instant.now();
128+
129+
// force refresh
130+
this.saltProvider.loadContent();
131+
132+
// mark all the referenced files as ready to archive
133+
storageManager.archiveSaltLocations();
134+
135+
// Unlike in regular salt rotation, this should be based on the currently effective snapshot.
136+
// The latest snapshot may be in the future, and we may have changes that shouldn't be activated yet.
137+
var effectiveSnapshot = this.saltProvider.getSnapshot(now);
138+
139+
var result = saltRotation.rotateSaltsZero(effectiveSnapshot, TargetDate.now(), now);
140+
if (!result.hasSnapshot()) {
141+
ResponseUtil.error(rc, 200, result.getReason());
142+
return;
143+
}
144+
145+
storageManager.upload(result.getSnapshot());
146+
147+
rc.response()
148+
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
149+
.end(toJson(result.getSnapshot()).encode());
150+
} catch (Exception e) {
151+
LOGGER.error(e.getMessage(), e);
152+
rc.fail(500, e);
153+
}
154+
}
155+
120156
private JsonObject toJson(RotatingSaltProvider.SaltSnapshot snapshot) {
121157
JsonObject jo = new JsonObject();
122158
jo.put("effective", snapshot.getEffective().toEpochMilli());

src/test/java/com/uid2/admin/salt/SaltRotationTest.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,4 +443,53 @@ private int countEntriesWithLastUpdated(SaltEntry[] entries, TargetDate lastUpda
443443
private int countEntriesWithLastUpdated(SaltEntry[] entries, Instant lastUpdated) {
444444
return (int) Arrays.stream(entries).filter(e -> e.lastUpdated() == lastUpdated.toEpochMilli()).count();
445445
}
446+
447+
@Test
448+
void testRotateSaltsZeroDoesntRotateSaltsButUpdatesRefreshFrom() throws Exception {
449+
var lastSnapshot = SaltSnapshotBuilder.start()
450+
.entries(
451+
SaltBuilder.start().lastUpdated(targetDate().minusDays(75)).refreshFrom(targetDate().minusDays(45)).id(1),
452+
SaltBuilder.start().lastUpdated(targetDate().minusDays(60)).refreshFrom(targetDate()).id(2),
453+
SaltBuilder.start().lastUpdated(targetDate().minusDays(30)).refreshFrom(targetDate()).id(3),
454+
SaltBuilder.start().lastUpdated(targetDate().minusDays(20)).refreshFrom(targetDate().plusDays(10)).id(4)
455+
)
456+
.effective(daysEarlier(1))
457+
.expires(daysLater(6))
458+
.build();
459+
460+
var expected = List.of(
461+
SaltBuilder.start().lastUpdated(targetDate().minusDays(75)).refreshFrom(targetDate().plusDays(15)).id(1).build(),
462+
SaltBuilder.start().lastUpdated(targetDate().minusDays(60)).refreshFrom(targetDate().plusDays(30)).id(2).build(),
463+
SaltBuilder.start().lastUpdated(targetDate().minusDays(30)).refreshFrom(targetDate().plusDays(30)).id(3).build(),
464+
SaltBuilder.start().lastUpdated(targetDate().minusDays(20)).refreshFrom(targetDate().plusDays(10)).id(4).build()
465+
).toArray();
466+
467+
var result = saltRotation.rotateSaltsZero(lastSnapshot, targetDate(), targetDate().asInstant());
468+
assertThat(result.hasSnapshot()).isTrue();
469+
470+
// None are rotated, refreshFrom is updated where it is now or in the past - same as regular rotation
471+
assertThat(result.getSnapshot().getAllRotatingSalts()).isEqualTo(expected);
472+
473+
// Effective now
474+
assertThat(result.getSnapshot().getEffective()).isEqualTo(targetDate().asInstant());
475+
476+
// Expires in a week
477+
assertThat(result.getSnapshot().getExpires()).isEqualTo(daysLater(7).asInstant());
478+
}
479+
480+
@Test
481+
void testRotateSaltsZeroWorksWhenThereIsFutureSaltFile() throws Exception {
482+
// In regular salt rotations if there is a salt
483+
484+
var lastSnapshot = SaltSnapshotBuilder.start()
485+
.entries(SaltBuilder.start().lastUpdated(targetDate().minusDays(75)))
486+
.effective(daysLater(10))
487+
.build();
488+
489+
var result = saltRotation.rotateSaltsZero(lastSnapshot, targetDate(), targetDate().asInstant());
490+
491+
assertThat(result.hasSnapshot()).isTrue();
492+
assertThat(result.getSnapshot().getEffective()).isEqualTo(targetDate().asInstant());
493+
assertThat(result.getSnapshot().getExpires()).isEqualTo(daysLater(7).asInstant());
494+
}
446495
}

src/test/java/com/uid2/admin/salt/helper/SaltSnapshotBuilder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public static SaltSnapshotBuilder start() {
2727

2828
public SaltSnapshotBuilder entries(int count, TargetDate lastUpdated) {
2929
for (int i = 0; i < count; ++i) {
30-
entries.add(SaltBuilder.start().lastUpdated(lastUpdated).build());
30+
entries.add(SaltBuilder.start().lastUpdated(lastUpdated).refreshFrom(lastUpdated.plusDays(30)).build());
3131
}
3232
return this;
3333
}

webroot/adm/salt.html

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,18 @@ <h1>UID2 Env - Salt Management</h1>
1313

1414
<h3>Inputs</h3>
1515

16-
<label for="minAges">Min ages (seconds):</label>
17-
<input type="text" id="minAges" name="minAges">
18-
<label for="fraction">Fraction:</label>
19-
<input type="text" id="fraction" name="fraction">
16+
<div>
17+
<label for="fraction">Fraction:</label>
18+
<input type="text" id="fraction" name="fraction" style="width: 120px;">
19+
20+
<label for="targetDate" style="margin-left: 20px;">Target date:</label>
21+
<input type="date" id="targetDate" name="targetDate">
22+
</div>
23+
24+
<div style="margin-top: 15px;">
25+
<label for="minAges">Min ages (seconds):</label><br>
26+
<input type="text" id="minAges" name="minAges" style="width: 100%; margin-top: 5px;">
27+
</div>
2028

2129
<br>
2230
<br>
@@ -26,6 +34,7 @@ <h3>Operations</h3>
2634
<ul>
2735
<li class="ro-sem" style="display: none"><a href="#" id="doSnapshots">List Salt Snapshots</a></li>
2836
<li class="ro-sem" style="display: none"><a href="#" id="doRotate">Rotate second level salts (SUPER_USER)</a></li>
37+
<li class="ro-sem" style="display: none"><a href="#" id="doRotateZero">Rotate zero salts</a></li>
2938
</ul>
3039

3140
<br>
@@ -39,17 +48,34 @@ <h3>Output</h3>
3948

4049
<script language="JavaScript">
4150
$(document).ready(function () {
51+
const tomorrow = new Date();
52+
tomorrow.setDate(tomorrow.getDate() + 1);
53+
54+
const defaultTargetDate = tomorrow.getFullYear() + '-' +
55+
String(tomorrow.getMonth() + 1).padStart(2, '0') + '-' +
56+
String(tomorrow.getDate()).padStart(2, '0');
57+
$('#targetDate').val(defaultTargetDate);
58+
$('#fraction').val((1/365).toFixed(6));
59+
60+
// 30 days, 60 days, 90 days... 390 days in seconds
61+
$('#minAges').val('2592000,5184000,7776000,10368000,12960000,15552000,18144000,20736000,23328000,25920000,28512000,31104000,33696000');
62+
4263
$('#doSnapshots').on('click', function () {
4364
doApiCall('GET', '/api/salt/snapshots', '#standardOutput', '#errorOutput');
4465
});
4566

4667
$('#doRotate').on('click', function () {
47-
var minAges = encodeURIComponent($('#minAges').val());
48-
var fraction = encodeURIComponent($('#fraction').val());
49-
var url = '/api/salt/rotate?min_ages_in_seconds=' + minAges + '&fraction=' + fraction;
68+
const minAges = encodeURIComponent($('#minAges').val());
69+
const fraction = encodeURIComponent($('#fraction').val());
70+
const targetDate = encodeURIComponent($('#targetDate').val());
71+
const url = '/api/salt/rotate?min_ages_in_seconds=' + minAges + '&fraction=' + fraction + '&target_date=' + targetDate;
5072

5173
doApiCall('POST', url, '#standardOutput', '#errorOutput');
5274
});
75+
76+
$('#doRotateZero').on('click', function () {
77+
doApiCall('POST', '/api/salt/rotate-zero', '#standardOutput', '#errorOutput');
78+
});
5379
});
5480
</script>
5581

0 commit comments

Comments
 (0)