Skip to content

Commit adb55c9

Browse files
Store previous snapshots in encrypted (#376)
* Store previous snapshots in encrypted
1 parent f80094c commit adb55c9

File tree

6 files changed

+116
-28
lines changed

6 files changed

+116
-28
lines changed

pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010

1111
<properties>
1212
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
13-
<vertx.version>4.5.11</vertx.version>
13+
<vertx.version>4.5.13</vertx.version>
1414
<vertx-maven-plugin.version>1.0.22</vertx-maven-plugin.version>
1515
<vertx.verticle>com.uid2.admin.vertx.AdminVerticle</vertx.verticle>
1616
<!-- check micrometer.version vertx-micrometer-metrics consumes before bumping up -->
1717
<micrometer.version>1.12.2</micrometer.version>
1818
<junit-jupiter.version>5.11.2</junit-jupiter.version>
19-
<uid2-shared.version>8.0.32</uid2-shared.version>
19+
<uid2-shared.version>8.1.22</uid2-shared.version>
2020
<okta-jwt.version>0.5.10</okta-jwt.version>
2121
<image.version>${project.version}</image.version>
2222
</properties>

src/main/java/com/uid2/admin/store/writer/EncryptedSaltStoreWriter.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@
1818
import java.nio.charset.StandardCharsets;
1919
import java.nio.file.Files;
2020
import java.nio.file.Path;
21+
import java.util.ArrayList;
2122
import java.util.Base64;
2223
import java.util.Collection;
24+
import java.util.List;
2325

2426
public class EncryptedSaltStoreWriter extends SaltStoreWriter implements StoreWriter {
2527
private StoreScope scope;
2628
private RotatingCloudEncryptionKeyProvider cloudEncryptionKeyProvider;
2729
private Integer siteId;
2830

31+
private final List<RotatingSaltProvider.SaltSnapshot> previousSeenSnapshots = new ArrayList<>();
32+
2933
private static final Logger LOGGER = LoggerFactory.getLogger(EncryptedSaltStoreWriter.class);
3034
public EncryptedSaltStoreWriter(JsonObject config, RotatingSaltProvider provider, FileManager fileManager,
3135
TaggableCloudStorage cloudStorage, VersionGenerator versionGenerator, StoreScope scope,
@@ -91,6 +95,16 @@ protected void refreshProvider() {
9195
// we do not need to refresh the provider on encrypted writers
9296
}
9397

98+
@Override
99+
protected List<RotatingSaltProvider.SaltSnapshot> getSnapshots(RotatingSaltProvider.SaltSnapshot data){
100+
/*
101+
Since metadata.json is overwritten during the process, we maintain a history of all snapshots seen so far.
102+
On the final write, we append this history to metadata.json to ensure no snapshots are lost.
103+
*/
104+
this.previousSeenSnapshots.add(data);
105+
return this.previousSeenSnapshots;
106+
}
107+
94108
@Override
95109
public void upload(Object data, JsonObject extraMeta) throws Exception {
96110
for(RotatingSaltProvider.SaltSnapshot saltSnapshot: (Collection<RotatingSaltProvider.SaltSnapshot>) data) {

src/main/java/com/uid2/admin/store/writer/SaltStoreWriter.java

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,30 @@ public SaltStoreWriter(JsonObject config, RotatingSaltProvider provider, FileMan
4343
this.versionGenerator = versionGenerator;
4444
}
4545

46+
protected List<RotatingSaltProvider.SaltSnapshot> getSnapshots(RotatingSaltProvider.SaltSnapshot data){
47+
if (provider.getSnapshots() == null) {
48+
throw new IllegalStateException("Snapshots cannot be null");
49+
}
50+
final Instant now = Instant.now();
51+
List<RotatingSaltProvider.SaltSnapshot> currentSnapshots = Stream.concat(provider.getSnapshots().stream(), Stream.of(data))
52+
.sorted(Comparator.comparing(RotatingSaltProvider.SaltSnapshot::getEffective))
53+
.collect(Collectors.toList());
54+
RotatingSaltProvider.SaltSnapshot newestEffectiveSnapshot = currentSnapshots.stream()
55+
.filter(snapshot -> snapshot.isEffective(now))
56+
.reduce((a, b) -> b).orElse(null);
57+
return Stream.concat(provider.getSnapshots().stream(), Stream.of(data))
58+
.filter(snapshot -> {
59+
boolean isValid = newestEffectiveSnapshot == null || snapshot == newestEffectiveSnapshot;
60+
if (!isValid) {
61+
LOGGER.info("Skipping effective snapshot, effective=" + snapshot.getEffective() + ", expires=" + snapshot.getExpires()
62+
+ " in favour of newer snapshot, effective=" + newestEffectiveSnapshot.getEffective() + ", expires=" + newestEffectiveSnapshot.getExpires());
63+
}
64+
return isValid;
65+
})
66+
.sorted(Comparator.comparing(RotatingSaltProvider.SaltSnapshot::getEffective))
67+
.collect(Collectors.toList());
68+
}
69+
4670
public void upload(RotatingSaltProvider.SaltSnapshot data) throws Exception {
4771
final Instant now = Instant.now();
4872
final long generated = now.getEpochSecond();
@@ -64,33 +88,14 @@ public void upload(RotatingSaltProvider.SaltSnapshot data) throws Exception {
6488
final JsonArray snapshotsMetadata = new JsonArray();
6589
metadata.put("salts", snapshotsMetadata);
6690

67-
List<RotatingSaltProvider.SaltSnapshot> currentSnapshots = provider.getSnapshots();
68-
List<RotatingSaltProvider.SaltSnapshot> snapshots = null;
91+
List<RotatingSaltProvider.SaltSnapshot> snapshots = this.getSnapshots(data);
6992

70-
if (currentSnapshots != null) {
71-
snapshots = Stream.concat(currentSnapshots.stream(), Stream.of(data))
72-
.sorted(Comparator.comparing(RotatingSaltProvider.SaltSnapshot::getEffective))
73-
.collect(Collectors.toList());
74-
} else {
75-
snapshots = List.of(data);
76-
}
77-
// of the currently effective snapshots keep only the most recent one
78-
RotatingSaltProvider.SaltSnapshot newestEffectiveSnapshot = snapshots.stream()
79-
.filter(snapshot -> snapshot.isEffective(now))
80-
.reduce((a, b) -> b).orElse(null);
8193
for (RotatingSaltProvider.SaltSnapshot snapshot : snapshots) {
8294
if (!now.isBefore(snapshot.getExpires())) {
8395
LOGGER.info("Skipping expired snapshot, effective=" + snapshot.getEffective() + ", expires=" + snapshot.getExpires());
8496
continue;
8597
}
8698

87-
if (newestEffectiveSnapshot != null && snapshot != newestEffectiveSnapshot) {
88-
LOGGER.info("Skipping effective snapshot, effective=" + snapshot.getEffective() + ", expires=" + snapshot.getExpires()
89-
+ " in favour of newer snapshot, effective=" + newestEffectiveSnapshot.getEffective() + ", expires=" + newestEffectiveSnapshot.getExpires());
90-
continue;
91-
}
92-
newestEffectiveSnapshot = null;
93-
9499
final String location = getSaltSnapshotLocation(snapshot);
95100

96101
final JsonObject snapshotMetadata = new JsonObject();

src/main/resources/localstack/s3/core/salts/metadata.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@
44
"first_level" : "fOGY/aRE44peL23i+cE9MkJrzmEeNZZziNZBfq7qqk8=",
55
"id_prefix" : "b",
66
"id_secret" : "HF6Qz42HBbVHINxhh191dB09BCuTWyBkNtrNicO4ZCw=",
7-
"salts" : [{
7+
"salts" : [
8+
{
89
"effective" : 1670796729291,
910
"expires" : 1766125493000,
1011
"location" : "salts/salts.txt.1670796729291",
1112
"size" : 2
12-
}]
13+
},{
14+
"effective" : 1766125493000,
15+
"expires" : 1766720293000,
16+
"location" : "salts/salts.txt.1766125493000",
17+
"size" : 4
18+
}
19+
]
1320
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
1000000,1614556800000,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnss=
2+
1000001,1643235130717,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnss=
3+
1000002,1614556000010,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnss=
4+
1000003,1643235230717,vgv1BwiNRCW7F3VcNXHlZh+7oHJ4G4gCshbGcVOLnss=

src/test/java/com/uid2/admin/store/writer/EncryptedSaltStoreWriterTest.java

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.uid2.shared.store.RotatingSaltProvider;
1111
import com.uid2.shared.store.reader.RotatingCloudEncryptionKeyProvider;
1212
import com.uid2.shared.store.scope.StoreScope;
13+
import io.vertx.core.json.JsonArray;
1314
import io.vertx.core.json.JsonObject;
1415
import org.junit.jupiter.api.BeforeEach;
1516
import org.junit.jupiter.api.Test;
@@ -20,6 +21,7 @@
2021

2122
import java.io.IOException;
2223
import java.io.InputStream;
24+
import java.nio.charset.StandardCharsets;
2325
import java.nio.file.Files;
2426
import java.nio.file.Paths;
2527
import java.time.Instant;
@@ -107,21 +109,77 @@ private void verifyFile(String filelocation, RotatingSaltProvider.SaltSnapshot s
107109

108110
@Test
109111
public void testUploadNew() throws Exception {
110-
RotatingSaltProvider.SaltSnapshot snapshot = makeSnapshot(Instant.now(), Instant.ofEpochMilli(Instant.now().toEpochMilli() + 10000), 1000000);
111-
112+
RotatingSaltProvider.SaltSnapshot snapshot = makeSnapshot(Instant.ofEpochMilli(1740607938167L), Instant.ofEpochMilli(Instant.now().toEpochMilli() + 90002), 100);
113+
RotatingSaltProvider.SaltSnapshot snapshot2 = makeSnapshot(Instant.ofEpochMilli(1740694476392L), Instant.ofEpochMilli(Instant.now().toEpochMilli() + 130000), 10);
112114
when(rotatingSaltProvider.getMetadata()).thenThrow(new CloudStorageException("The specified key does not exist: AmazonS3Exception: test-core-bucket"));
113115
when(rotatingSaltProvider.getSnapshots()).thenReturn(null);
114116

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

119+
ArgumentCaptor<JsonObject> metadataCaptor = ArgumentCaptor.forClass(JsonObject.class);
120+
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
121+
ArgumentCaptor<CloudPath> locationCaptor = ArgumentCaptor.forClass(CloudPath.class);
122+
117123
EncryptedSaltStoreWriter encryptedSaltStoreWriter = new EncryptedSaltStoreWriter(config, rotatingSaltProvider,
118124
fileManager, taggableCloudStorage, versionGenerator, storeScope, rotatingCloudEncryptionKeyProvider, siteId);
119125

120126
encryptedSaltStoreWriter.upload(snapshot);
127+
verify(fileManager).uploadMetadata(metadataCaptor.capture(), nameCaptor.capture(), locationCaptor.capture());
128+
129+
// Capture the metadata
130+
JsonObject capturedMetadata = metadataCaptor.getValue();
131+
assertEquals(1, capturedMetadata.getJsonArray("salts").size(), "The 'salts' array should contain exactly 1 item");
132+
encryptedSaltStoreWriter.upload(snapshot2);
121133

122-
verify(taggableCloudStorage).upload(pathCaptor.capture(), cloudPathCaptor.capture(), any());
123-
assertEquals(cloudPathCaptor.getValue(), "test/path");
134+
verify(fileManager,times(2)).uploadMetadata(metadataCaptor.capture(), nameCaptor.capture(), locationCaptor.capture());
135+
capturedMetadata = metadataCaptor.getValue();
136+
assertEquals(2, capturedMetadata.getJsonArray("salts").size(), "The 'salts' array should contain 2 items");
137+
138+
verify(taggableCloudStorage,times(3)).upload(pathCaptor.capture(), cloudPathCaptor.capture(), any());
124139

125140
verifyFile(pathCaptor.getValue(), snapshot);
126141
}
142+
143+
@Test
144+
public void testUnencryptedAndEncryptedBehavesTheSame() throws Exception {
145+
RotatingSaltProvider.SaltSnapshot snapshot = makeSnapshot(Instant.ofEpochMilli(1740607938167L), Instant.ofEpochMilli(Instant.now().toEpochMilli() + 90000), 100);
146+
RotatingSaltProvider.SaltSnapshot snapshot2 = makeSnapshot(Instant.ofEpochMilli(1740694476392L), Instant.ofEpochMilli(Instant.now().toEpochMilli() + 130000), 10);
147+
List<RotatingSaltProvider.SaltSnapshot> snapshots = List.of(snapshot, snapshot2);
148+
149+
when(rotatingSaltProvider.getMetadata()).thenThrow(new CloudStorageException("The specified key does not exist: AmazonS3Exception: test-core-bucket"));
150+
when(rotatingSaltProvider.getSnapshots()).thenReturn(snapshots);
151+
when(taggableCloudStorage.list(anyString())).thenReturn(new ArrayList<>());
152+
153+
ArgumentCaptor<JsonObject> metadataCaptor = ArgumentCaptor.forClass(JsonObject.class);
154+
ArgumentCaptor<String> nameCaptor = ArgumentCaptor.forClass(String.class);
155+
ArgumentCaptor<CloudPath> locationCaptor = ArgumentCaptor.forClass(CloudPath.class);
156+
157+
SaltStoreWriter saltStoreWriter = new SaltStoreWriter(config, rotatingSaltProvider,
158+
fileManager, taggableCloudStorage, versionGenerator);
159+
160+
saltStoreWriter.upload(snapshot);
161+
verify(fileManager).uploadMetadata(metadataCaptor.capture(), nameCaptor.capture(), locationCaptor.capture());
162+
163+
JsonObject capturedMetadata = metadataCaptor.getValue();
164+
JsonArray saltsArray = capturedMetadata.getJsonArray("salts");
165+
assertEquals(1, saltsArray.size(), "Salts array should have exactly one entry, as other is removed in newest-effective logic");
166+
JsonObject salt = saltsArray.getJsonObject(0);
167+
assertEquals(1740694476392L, salt.getLong("effective"), "Effective timestamp should match second entry");
168+
assertEquals(10, salt.getInteger("size"), "Size should match second entries");
169+
170+
//Now sending snapshot2 to encrypted to verify that does the same.
171+
EncryptedSaltStoreWriter encryptedSaltStoreWriter = new EncryptedSaltStoreWriter(config, rotatingSaltProvider,
172+
fileManager, taggableCloudStorage, versionGenerator, storeScope, rotatingCloudEncryptionKeyProvider, siteId);
173+
174+
encryptedSaltStoreWriter.upload(snapshot2);
175+
176+
verify(fileManager,atLeastOnce()).uploadMetadata(metadataCaptor.capture(), nameCaptor.capture(), locationCaptor.capture());
177+
178+
capturedMetadata = metadataCaptor.getValue();
179+
saltsArray = capturedMetadata.getJsonArray("salts");
180+
salt = saltsArray.getJsonObject(0);
181+
assertEquals(1740694476392L, salt.getLong("effective"), "Effective timestamp should match second entry");
182+
assertEquals(10, salt.getInteger("size"), "Size should match second entries");
183+
verify(taggableCloudStorage,atLeastOnce()).upload(pathCaptor.capture(), cloudPathCaptor.capture(), any());
184+
}
127185
}

0 commit comments

Comments
 (0)