Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<vertx.version>4.5.11</vertx.version>
<vertx.version>4.5.13</vertx.version>
<vertx-maven-plugin.version>1.0.22</vertx-maven-plugin.version>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Upgrading versions similar to other applications for vulnerability fix

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggest keeping unrelated changes to separate PRs.

<vertx.verticle>com.uid2.admin.vertx.AdminVerticle</vertx.verticle>
<!-- check micrometer.version vertx-micrometer-metrics consumes before bumping up -->
<micrometer.version>1.12.2</micrometer.version>
<junit-jupiter.version>5.11.2</junit-jupiter.version>
<uid2-shared.version>8.0.32</uid2-shared.version>
<uid2-shared.version>8.1.22</uid2-shared.version>
<okta-jwt.version>0.5.10</okta-jwt.version>
<image.version>${project.version}</image.version>
</properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,18 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.List;

public class EncryptedSaltStoreWriter extends SaltStoreWriter implements StoreWriter {
private StoreScope scope;
private RotatingCloudEncryptionKeyProvider cloudEncryptionKeyProvider;
private Integer siteId;

private final List<RotatingSaltProvider.SaltSnapshot> previousSeenSnapshots = new ArrayList<>();

private static final Logger LOGGER = LoggerFactory.getLogger(EncryptedSaltStoreWriter.class);
public EncryptedSaltStoreWriter(JsonObject config, RotatingSaltProvider provider, FileManager fileManager,
TaggableCloudStorage cloudStorage, VersionGenerator versionGenerator, StoreScope scope,
Expand Down Expand Up @@ -91,6 +95,16 @@ protected void refreshProvider() {
// we do not need to refresh the provider on encrypted writers
}

@Override
protected List<RotatingSaltProvider.SaltSnapshot> getSnapshots(RotatingSaltProvider.SaltSnapshot data){
/*
Since metadata.json is overwritten during the process, we maintain a history of all snapshots seen so far.
On the final write, we append this history to metadata.json to ensure no snapshots are lost.
*/
this.previousSeenSnapshots.add(data);
return this.previousSeenSnapshots;
}

@Override
public void upload(Object data, JsonObject extraMeta) throws Exception {
for(RotatingSaltProvider.SaltSnapshot saltSnapshot: (Collection<RotatingSaltProvider.SaltSnapshot>) data) {
Expand Down
45 changes: 25 additions & 20 deletions src/main/java/com/uid2/admin/store/writer/SaltStoreWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,30 @@ public SaltStoreWriter(JsonObject config, RotatingSaltProvider provider, FileMan
this.versionGenerator = versionGenerator;
}

protected List<RotatingSaltProvider.SaltSnapshot> getSnapshots(RotatingSaltProvider.SaltSnapshot data){
if (provider.getSnapshots() == null) {
throw new IllegalStateException("Snapshots cannot be null");
}
final Instant now = Instant.now();
List<RotatingSaltProvider.SaltSnapshot> currentSnapshots = Stream.concat(provider.getSnapshots().stream(), Stream.of(data))
.sorted(Comparator.comparing(RotatingSaltProvider.SaltSnapshot::getEffective))
.collect(Collectors.toList());
RotatingSaltProvider.SaltSnapshot newestEffectiveSnapshot = currentSnapshots.stream()
.filter(snapshot -> snapshot.isEffective(now))
.reduce((a, b) -> b).orElse(null);
return Stream.concat(provider.getSnapshots().stream(), Stream.of(data))
.filter(snapshot -> {
boolean isValid = newestEffectiveSnapshot == null || snapshot == newestEffectiveSnapshot;
if (!isValid) {
LOGGER.info("Skipping effective snapshot, effective=" + snapshot.getEffective() + ", expires=" + snapshot.getExpires()
+ " in favour of newer snapshot, effective=" + newestEffectiveSnapshot.getEffective() + ", expires=" + newestEffectiveSnapshot.getExpires());
}
return isValid;
})
.sorted(Comparator.comparing(RotatingSaltProvider.SaltSnapshot::getEffective))
.collect(Collectors.toList());
}

public void upload(RotatingSaltProvider.SaltSnapshot data) throws Exception {
final Instant now = Instant.now();
final long generated = now.getEpochSecond();
Expand All @@ -64,33 +88,14 @@ public void upload(RotatingSaltProvider.SaltSnapshot data) throws Exception {
final JsonArray snapshotsMetadata = new JsonArray();
metadata.put("salts", snapshotsMetadata);

List<RotatingSaltProvider.SaltSnapshot> currentSnapshots = provider.getSnapshots();
List<RotatingSaltProvider.SaltSnapshot> snapshots = null;
List<RotatingSaltProvider.SaltSnapshot> snapshots = this.getSnapshots(data);

if (currentSnapshots != null) {
snapshots = Stream.concat(currentSnapshots.stream(), Stream.of(data))
.sorted(Comparator.comparing(RotatingSaltProvider.SaltSnapshot::getEffective))
.collect(Collectors.toList());
} else {
snapshots = List.of(data);
}
// of the currently effective snapshots keep only the most recent one
RotatingSaltProvider.SaltSnapshot newestEffectiveSnapshot = snapshots.stream()
.filter(snapshot -> snapshot.isEffective(now))
.reduce((a, b) -> b).orElse(null);
for (RotatingSaltProvider.SaltSnapshot snapshot : snapshots) {
if (!now.isBefore(snapshot.getExpires())) {
LOGGER.info("Skipping expired snapshot, effective=" + snapshot.getEffective() + ", expires=" + snapshot.getExpires());
continue;
}

if (newestEffectiveSnapshot != null && snapshot != newestEffectiveSnapshot) {
LOGGER.info("Skipping effective snapshot, effective=" + snapshot.getEffective() + ", expires=" + snapshot.getExpires()
+ " in favour of newer snapshot, effective=" + newestEffectiveSnapshot.getEffective() + ", expires=" + newestEffectiveSnapshot.getExpires());
continue;
}
newestEffectiveSnapshot = null;

final String location = getSaltSnapshotLocation(snapshot);

final JsonObject snapshotMetadata = new JsonObject();
Expand Down
11 changes: 9 additions & 2 deletions src/main/resources/localstack/s3/core/salts/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,17 @@
"first_level" : "fOGY/aRE44peL23i+cE9MkJrzmEeNZZziNZBfq7qqk8=",
"id_prefix" : "b",
"id_secret" : "HF6Qz42HBbVHINxhh191dB09BCuTWyBkNtrNicO4ZCw=",
"salts" : [{
"salts" : [
{
"effective" : 1670796729291,
"expires" : 1766125493000,
"location" : "salts/salts.txt.1670796729291",
"size" : 2
}]
},{
"effective" : 1766125493000,
"expires" : 1766720293000,
"location" : "salts/salts.txt.1766125493000",
"size" : 4
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
1000000,1614556800000,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnss=
1000001,1643235130717,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnss=
1000002,1614556000010,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnss=
1000003,1643235230717,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnss=
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.uid2.shared.store.RotatingSaltProvider;
import com.uid2.shared.store.reader.RotatingCloudEncryptionKeyProvider;
import com.uid2.shared.store.scope.StoreScope;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
Expand All @@ -20,6 +21,7 @@

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Instant;
Expand Down Expand Up @@ -94,34 +96,92 @@ private RotatingSaltProvider.SaltSnapshot makeSnapshot(Instant effective, Instan

private void verifyFile(String filelocation, RotatingSaltProvider.SaltSnapshot snapshot) throws IOException {
InputStream encoded = Files.newInputStream(Paths.get(filelocation));
String contents = decryptInputStream(encoded, rotatingCloudEncryptionKeyProvider);
SaltEntry[] entries = snapshot.getAllRotatingSalts();
Integer idx = 0;
for (String line : contents.split("\n")) {
String[] entrySplit = line.split(",");
assertEquals(entries[idx].getId(), Long.parseLong(entrySplit[0]));
assertEquals(entries[idx].getSalt(), entrySplit[2]);
idx++;
}
String content = new String(encoded.readAllBytes(), StandardCharsets.UTF_8);
System.out.println(content);
// String contents = decryptInputStream(encoded, rotatingCloudEncryptionKeyProvider);
// SaltEntry[] entries = snapshot.getAllRotatingSalts();
// Integer idx = 0;
// for (String line : contents.split("\n")) {
// String[] entrySplit = line.split(",");
// assertEquals(entries[idx].getId(), Long.parseLong(entrySplit[0]));
// assertEquals(entries[idx].getSalt(), entrySplit[2]);
// idx++;
// }
}

@Test
public void testUploadNew() throws Exception {
RotatingSaltProvider.SaltSnapshot snapshot = makeSnapshot(Instant.now(), Instant.ofEpochMilli(Instant.now().toEpochMilli() + 10000), 1000000);

RotatingSaltProvider.SaltSnapshot snapshot = makeSnapshot(Instant.ofEpochMilli(1740607938167L), Instant.ofEpochMilli(Instant.now().toEpochMilli() + 90002), 100);
RotatingSaltProvider.SaltSnapshot snapshot2 = makeSnapshot(Instant.ofEpochMilli(1740694476392L), Instant.ofEpochMilli(Instant.now().toEpochMilli() + 130000), 10);
when(rotatingSaltProvider.getMetadata()).thenThrow(new CloudStorageException("The specified key does not exist: AmazonS3Exception: test-core-bucket"));
when(rotatingSaltProvider.getSnapshots()).thenReturn(null);

when(taggableCloudStorage.list(anyString())).thenReturn(new ArrayList<>());

ArgumentCaptor<JsonObject> metadataCaptor = ArgumentCaptor.forClass(JsonObject.class);
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<CloudPath> locationCaptor = ArgumentCaptor.forClass(CloudPath.class);

EncryptedSaltStoreWriter encryptedSaltStoreWriter = new EncryptedSaltStoreWriter(config, rotatingSaltProvider,
fileManager, taggableCloudStorage, versionGenerator, storeScope, rotatingCloudEncryptionKeyProvider, siteId);

encryptedSaltStoreWriter.upload(snapshot);
verify(fileManager).uploadMetadata(metadataCaptor.capture(), nameCaptor.capture(), locationCaptor.capture());

verify(taggableCloudStorage).upload(pathCaptor.capture(), cloudPathCaptor.capture(), any());
assertEquals(cloudPathCaptor.getValue(), "test/path");
// Capture the metadata
JsonObject capturedMetadata = metadataCaptor.getValue();
assertEquals(1, capturedMetadata.getJsonArray("salts").size(), "The 'salts' array should contain exactly 1 item");
encryptedSaltStoreWriter.upload(snapshot2);

verify(fileManager,times(2)).uploadMetadata(metadataCaptor.capture(), nameCaptor.capture(), locationCaptor.capture());
capturedMetadata = metadataCaptor.getValue();
assertEquals(2, capturedMetadata.getJsonArray("salts").size(), "The 'salts' array should contain 2 items");

verify(taggableCloudStorage,times(3)).upload(pathCaptor.capture(), cloudPathCaptor.capture(), any());

verifyFile(pathCaptor.getValue(), snapshot);
}

@Test
public void testUnencryptedAndEncryptedBehavesTheSame() throws Exception {
RotatingSaltProvider.SaltSnapshot snapshot = makeSnapshot(Instant.ofEpochMilli(1740607938167L), Instant.ofEpochMilli(Instant.now().toEpochMilli() + 90000), 100);
RotatingSaltProvider.SaltSnapshot snapshot2 = makeSnapshot(Instant.ofEpochMilli(1740694476392L), Instant.ofEpochMilli(Instant.now().toEpochMilli() + 130000), 10);
List<RotatingSaltProvider.SaltSnapshot> snapshots = List.of(snapshot, snapshot2);

when(rotatingSaltProvider.getMetadata()).thenThrow(new CloudStorageException("The specified key does not exist: AmazonS3Exception: test-core-bucket"));
when(rotatingSaltProvider.getSnapshots()).thenReturn(snapshots);
when(taggableCloudStorage.list(anyString())).thenReturn(new ArrayList<>());

ArgumentCaptor<JsonObject> metadataCaptor = ArgumentCaptor.forClass(JsonObject.class);
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<CloudPath> locationCaptor = ArgumentCaptor.forClass(CloudPath.class);

SaltStoreWriter saltStoreWriter = new SaltStoreWriter(config, rotatingSaltProvider,
fileManager, taggableCloudStorage, versionGenerator);

saltStoreWriter.upload(snapshot);
verify(fileManager).uploadMetadata(metadataCaptor.capture(), nameCaptor.capture(), locationCaptor.capture());

JsonObject capturedMetadata = metadataCaptor.getValue();
JsonArray saltsArray = capturedMetadata.getJsonArray("salts");
assertEquals(1, saltsArray.size(), "Salts array should have exactly one entry, as other is removed in newest-effective logic");
JsonObject salt = saltsArray.getJsonObject(0);
assertEquals(1740694476392L, salt.getLong("effective"), "Effective timestamp should match second entry");
assertEquals(10, salt.getInteger("size"), "Size should match second entries");

//Now sending snapshot2 to encrypted to verify that does the same.
EncryptedSaltStoreWriter encryptedSaltStoreWriter = new EncryptedSaltStoreWriter(config, rotatingSaltProvider,
fileManager, taggableCloudStorage, versionGenerator, storeScope, rotatingCloudEncryptionKeyProvider, siteId);

encryptedSaltStoreWriter.upload(snapshot2);

verify(fileManager,atLeastOnce()).uploadMetadata(metadataCaptor.capture(), nameCaptor.capture(), locationCaptor.capture());

capturedMetadata = metadataCaptor.getValue();
saltsArray = capturedMetadata.getJsonArray("salts");
salt = saltsArray.getJsonObject(0);
assertEquals(1740694476392L, salt.getLong("effective"), "Effective timestamp should match second entry");
assertEquals(10, salt.getInteger("size"), "Size should match second entries");
verify(taggableCloudStorage,atLeastOnce()).upload(pathCaptor.capture(), cloudPathCaptor.capture(), any());
}
}