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
23 changes: 23 additions & 0 deletions src/main/java/com/uid2/admin/salt/SaltRotation.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.uid2.shared.model.SaltEntry;
import com.uid2.shared.secret.IKeyGenerator;

import com.uid2.shared.store.salt.ISaltProvider;
import com.uid2.shared.store.salt.ISaltProvider.ISaltSnapshot;
import com.uid2.shared.store.salt.RotatingSaltProvider.SaltSnapshot;
import io.vertx.core.json.JsonObject;
import lombok.Getter;
Expand Down Expand Up @@ -69,6 +71,27 @@ public Result rotateSalts(
return Result.fromSnapshot(nextSnapshot);
}

public Result rotateSaltsZero(
ISaltSnapshot effectiveSnapshot,
TargetDate targetDate,
Instant nextEffective
) throws Exception {
var preRotationSalts = effectiveSnapshot.getAllRotatingSalts();
var nextExpires = nextEffective.plus(7, ChronoUnit.DAYS);

var postRotationSalts = rotateSalts(preRotationSalts, List.of(), targetDate);

LOGGER.info("Zero salt rotation complete target_date={}", targetDate);

var nextSnapshot = new SaltSnapshot(
nextEffective,
nextExpires,
postRotationSalts,
effectiveSnapshot.getFirstLevelSalt());
return Result.fromSnapshot(nextSnapshot);
}


private static int getNumSaltsToRotate(SaltEntry[] preRotationSalts, double fraction) {
return (int) Math.ceil(preRotationSalts.length * fraction);
}
Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/uid2/admin/vertx/Endpoints.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public enum Endpoints {

API_SALT_SNAPSHOTS("/api/salt/snapshots"),
API_SALT_ROTATE("/api/salt/rotate"),
API_SALT_ROTATE_ZERO("/api/salt/rotate-zero"),

API_SEARCH("/api/search"),

Expand Down
40 changes: 38 additions & 2 deletions src/main/java/com/uid2/admin/vertx/service/SaltService.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@
import java.util.List;
import java.util.Optional;

import static com.uid2.admin.vertx.Endpoints.API_SALT_ROTATE;
import static com.uid2.admin.vertx.Endpoints.API_SALT_SNAPSHOTS;
import static com.uid2.admin.vertx.Endpoints.*;

public class SaltService implements IService {
private static final Logger LOGGER = LoggerFactory.getLogger(SaltService.class);
Expand Down Expand Up @@ -60,6 +59,12 @@ public void setupRoutes(Router router) {
this.handleSaltRotate(ctx);
}
}, new AuditParams(List.of("fraction", "min_ages_in_seconds", "target_date"), Collections.emptyList()), Role.SUPER_USER, Role.SECRET_ROTATION));

router.post(API_SALT_ROTATE_ZERO.toString()).blockingHandler(auth.handle((ctx) -> {
synchronized (writeLock) {
this.handleSaltRotateZero(ctx);
}
}, new AuditParams(List.of(), Collections.emptyList()), Role.MAINTAINER));
}

private void handleSaltSnapshots(RoutingContext rc) {
Expand Down Expand Up @@ -117,6 +122,37 @@ private void handleSaltRotate(RoutingContext rc) {
}
}

private void handleSaltRotateZero(RoutingContext rc) {
try {
Instant now = Instant.now();

// force refresh
this.saltProvider.loadContent();

// mark all the referenced files as ready to archive
storageManager.archiveSaltLocations();

// Unlike in regular salt rotation, this should be based on the currently effective snapshot.
// The latest snapshot may be in the future, and we may have changes that shouldn't be activated yet.
var effectiveSnapshot = this.saltProvider.getSnapshot(now);

var result = saltRotation.rotateSaltsZero(effectiveSnapshot, TargetDate.now(), now);
if (!result.hasSnapshot()) {
ResponseUtil.error(rc, 200, result.getReason());
return;
}

storageManager.upload(result.getSnapshot());

rc.response()
.putHeader(HttpHeaders.CONTENT_TYPE, "application/json")
.end(toJson(result.getSnapshot()).encode());
} catch (Exception e) {
LOGGER.error(e.getMessage(), e);
rc.fail(500, e);
}
}

private JsonObject toJson(RotatingSaltProvider.SaltSnapshot snapshot) {
JsonObject jo = new JsonObject();
jo.put("effective", snapshot.getEffective().toEpochMilli());
Expand Down
49 changes: 49 additions & 0 deletions src/test/java/com/uid2/admin/salt/SaltRotationTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -443,4 +443,53 @@ private int countEntriesWithLastUpdated(SaltEntry[] entries, TargetDate lastUpda
private int countEntriesWithLastUpdated(SaltEntry[] entries, Instant lastUpdated) {
return (int) Arrays.stream(entries).filter(e -> e.lastUpdated() == lastUpdated.toEpochMilli()).count();
}

@Test
void testRotateSaltsZeroDoesntRotateSaltsButUpdatesRefreshFrom() throws Exception {
var lastSnapshot = SaltSnapshotBuilder.start()
.entries(
SaltBuilder.start().lastUpdated(targetDate().minusDays(75)).refreshFrom(targetDate().minusDays(45)).id(1),
SaltBuilder.start().lastUpdated(targetDate().minusDays(60)).refreshFrom(targetDate()).id(2),
SaltBuilder.start().lastUpdated(targetDate().minusDays(30)).refreshFrom(targetDate()).id(3),
SaltBuilder.start().lastUpdated(targetDate().minusDays(20)).refreshFrom(targetDate().plusDays(10)).id(4)
)
.effective(daysEarlier(1))
.expires(daysLater(6))
.build();

var expected = List.of(
SaltBuilder.start().lastUpdated(targetDate().minusDays(75)).refreshFrom(targetDate().plusDays(15)).id(1).build(),
SaltBuilder.start().lastUpdated(targetDate().minusDays(60)).refreshFrom(targetDate().plusDays(30)).id(2).build(),
SaltBuilder.start().lastUpdated(targetDate().minusDays(30)).refreshFrom(targetDate().plusDays(30)).id(3).build(),
SaltBuilder.start().lastUpdated(targetDate().minusDays(20)).refreshFrom(targetDate().plusDays(10)).id(4).build()
).toArray();

var result = saltRotation.rotateSaltsZero(lastSnapshot, targetDate(), targetDate().asInstant());
assertThat(result.hasSnapshot()).isTrue();

// None are rotated, refreshFrom is updated where it is now or in the past - same as regular rotation
assertThat(result.getSnapshot().getAllRotatingSalts()).isEqualTo(expected);

// Effective now
assertThat(result.getSnapshot().getEffective()).isEqualTo(targetDate().asInstant());

// Expires in a week
assertThat(result.getSnapshot().getExpires()).isEqualTo(daysLater(7).asInstant());
}

@Test
void testRotateSaltsZeroWorksWhenThereIsFutureSaltFile() throws Exception {
// In regular salt rotations if there is a salt

var lastSnapshot = SaltSnapshotBuilder.start()
.entries(SaltBuilder.start().lastUpdated(targetDate().minusDays(75)))
.effective(daysLater(10))
.build();

var result = saltRotation.rotateSaltsZero(lastSnapshot, targetDate(), targetDate().asInstant());

assertThat(result.hasSnapshot()).isTrue();
assertThat(result.getSnapshot().getEffective()).isEqualTo(targetDate().asInstant());
assertThat(result.getSnapshot().getExpires()).isEqualTo(daysLater(7).asInstant());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public static SaltSnapshotBuilder start() {

public SaltSnapshotBuilder entries(int count, TargetDate lastUpdated) {
for (int i = 0; i < count; ++i) {
entries.add(SaltBuilder.start().lastUpdated(lastUpdated).build());
entries.add(SaltBuilder.start().lastUpdated(lastUpdated).refreshFrom(lastUpdated.plusDays(30)).build());
}
return this;
}
Expand Down
40 changes: 33 additions & 7 deletions webroot/adm/salt.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,18 @@ <h1>UID2 Env - Salt Management</h1>

<h3>Inputs</h3>

<label for="minAges">Min ages (seconds):</label>
<input type="text" id="minAges" name="minAges">
<label for="fraction">Fraction:</label>
<input type="text" id="fraction" name="fraction">
<div>
<label for="fraction">Fraction:</label>
<input type="text" id="fraction" name="fraction" style="width: 120px;">

<label for="targetDate" style="margin-left: 20px;">Target date:</label>
<input type="date" id="targetDate" name="targetDate">
</div>

<div style="margin-top: 15px;">
<label for="minAges">Min ages (seconds):</label><br>
<input type="text" id="minAges" name="minAges" style="width: 100%; margin-top: 5px;">
</div>

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

<br>
Expand All @@ -39,17 +48,34 @@ <h3>Output</h3>

<script language="JavaScript">
$(document).ready(function () {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);

const defaultTargetDate = tomorrow.getFullYear() + '-' +
String(tomorrow.getMonth() + 1).padStart(2, '0') + '-' +
String(tomorrow.getDate()).padStart(2, '0');
$('#targetDate').val(defaultTargetDate);
$('#fraction').val((1/365).toFixed(6));

// 30 days, 60 days, 90 days... 390 days in seconds
$('#minAges').val('2592000,5184000,7776000,10368000,12960000,15552000,18144000,20736000,23328000,25920000,28512000,31104000,33696000');

$('#doSnapshots').on('click', function () {
doApiCall('GET', '/api/salt/snapshots', '#standardOutput', '#errorOutput');
});

$('#doRotate').on('click', function () {
var minAges = encodeURIComponent($('#minAges').val());
var fraction = encodeURIComponent($('#fraction').val());
var url = '/api/salt/rotate?min_ages_in_seconds=' + minAges + '&fraction=' + fraction;
const minAges = encodeURIComponent($('#minAges').val());
const fraction = encodeURIComponent($('#fraction').val());
const targetDate = encodeURIComponent($('#targetDate').val());
const url = '/api/salt/rotate?min_ages_in_seconds=' + minAges + '&fraction=' + fraction + '&target_date=' + targetDate;

doApiCall('POST', url, '#standardOutput', '#errorOutput');
});

$('#doRotateZero').on('click', function () {
doApiCall('POST', '/api/salt/rotate-zero', '#standardOutput', '#errorOutput');
});
});
</script>

Expand Down