Skip to content

Adding new Identity Map V3 and Refreshable UID fields to SaltEntry #424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from 5 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
58 changes: 35 additions & 23 deletions src/main/java/com/uid2/shared/model/SaltEntry.java
Original file line number Diff line number Diff line change
@@ -1,31 +1,43 @@
package com.uid2.shared.model;

public class SaltEntry {
private final long id;
private final String hashedId;
private final long lastUpdated;
private final String salt;
public record SaltEntry(
long id,
String hashedId,
long lastUpdated,
String salt,

public SaltEntry(long id, String hashedId, long lastUpdated, String salt) {
this.id = id;
this.lastUpdated = lastUpdated;
this.hashedId = hashedId;
this.salt = salt;
}

public long getId() {
return id;
}

public String getHashedId() {
return hashedId;
}
Long refreshFrom, // needs to be nullable until V3 Identity Map is fully rolled out
String previousSalt,

public long getLastUpdated() {
return lastUpdated;
KeyMaterial currentKey,
KeyMaterial previousKey
) {
@Override
public String toString() {
return "SaltEntry{" +
"id=" + id +
", hashedId='" + hashedId + '\'' +
", lastUpdated=" + lastUpdated +
", salt=<REDACTED>" +
", refreshFrom=" + refreshFrom +
", previousSalt=<REDACTED>" +
", currentKey=" + currentKey +
", previousKey=" + previousKey +
'}';
}

public String getSalt() {
return salt;
public record KeyMaterial(
int id,
String key,
String salt
){
@Override
public String toString() {
return "KeyMaterial{" +
"id=" + id +
", key=<REDACTED>" +
", salt=<REDACTED>" +
'}';
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.uid2.shared.store;
package com.uid2.shared.store.salt;

import com.uid2.shared.cloud.DownloadCloudStorage;
import com.uid2.shared.model.SaltEntry;
Expand All @@ -19,15 +19,8 @@ public EncryptedRotatingSaltProvider(DownloadCloudStorage fileStreamProvider, Ro
}

@Override
protected SaltEntry[] readInputStream(InputStream inputStream, SaltEntryBuilder entryBuilder, Integer size) throws IOException {
protected SaltEntry[] readInputStream(InputStream inputStream, SaltFileParser saltFileParser, Integer size) throws IOException {
String decrypted = decryptInputStream(inputStream, cloudEncryptionKeyProvider, "salts");
SaltEntry[] entries = new SaltEntry[size];
int idx = 0;
for (String line : decrypted.split("\n")) {
final SaltEntry entry = entryBuilder.toEntry(line);
entries[idx] = entry;
idx++;
}
return entries;
return saltFileParser.parseFile(decrypted, size);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.uid2.shared.store;
package com.uid2.shared.store.salt;

import com.uid2.shared.model.SaltEntry;
import org.slf4j.Logger;
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/uid2/shared/store/salt/IdHashingScheme.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.uid2.shared.store.salt;

import org.hashids.Hashids;

public class IdHashingScheme {
private final String prefix;
private final Hashids hasher;

public IdHashingScheme(final String prefix, final String secret) {
this.prefix = prefix;
this.hasher = new Hashids(secret, 9);
}

public String encode(long id) {
return prefix + this.hasher.encode(id);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.uid2.shared.store;
package com.uid2.shared.store.salt;

import com.uid2.shared.Utils;
import com.uid2.shared.attest.UidCoreClient;
Expand All @@ -10,7 +10,6 @@
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.hashids.Hashids;

import java.io.BufferedReader;
import java.io.IOException;
Expand Down Expand Up @@ -80,14 +79,14 @@ public long getVersion(JsonObject metadata) {
public long loadContent(JsonObject metadata) throws Exception {
final JsonArray salts = metadata.getJsonArray("salts");
final String firstLevelSalt = metadata.getString("first_level");
final SaltEntryBuilder entryBuilder = new SaltEntryBuilder(
final SaltFileParser saltFileParser = new SaltFileParser(
new IdHashingScheme(metadata.getString("id_prefix"), metadata.getString("id_secret")));
final Instant now = Instant.now();
final List<SaltSnapshot> snapshots = new ArrayList<>();

int saltCount = 0;
for (int i = 0; i < salts.size(); ++i) {
final SaltSnapshot snapshot = this.loadSnapshot(salts.getJsonObject(i), firstLevelSalt, entryBuilder, now);
final SaltSnapshot snapshot = this.loadSnapshot(salts.getJsonObject(i), firstLevelSalt, saltFileParser, now);
if (snapshot == null) continue;
snapshots.add(snapshot);

Expand Down Expand Up @@ -120,33 +119,26 @@ public ISaltSnapshot getSnapshot(Instant asOf) {
if (!snapshot.isEffective(asOf)) break;
current = snapshot;
}
return current != null ? current : snapshots.get(snapshots.size() - 1);
return current != null ? current : snapshots.getLast();
}

private SaltSnapshot loadSnapshot(JsonObject spec, String firstLevelSalt, SaltEntryBuilder entryBuilder, Instant now) throws Exception {
private SaltSnapshot loadSnapshot(JsonObject spec, String firstLevelSalt, SaltFileParser saltFileParser, Instant now) throws Exception {
final Instant defaultExpires = now.plus(365, ChronoUnit.DAYS);
final Instant effective = Instant.ofEpochMilli(spec.getLong("effective"));
final Instant expires = Instant.ofEpochMilli(spec.getLong("expires", defaultExpires.toEpochMilli()));

final String path = spec.getString("location");
Integer size = spec.getInteger("size");
SaltEntry[] entries = readInputStream(this.contentStreamProvider.download(path), entryBuilder, size);
SaltEntry[] entries = readInputStream(this.contentStreamProvider.download(path), saltFileParser, size);

LOGGER.info("Loaded {} salts", size);
return new SaltSnapshot(effective, expires, entries, firstLevelSalt);
}

protected SaltEntry[] readInputStream(InputStream inputStream, SaltEntryBuilder entryBuilder, Integer size) throws IOException {
protected SaltEntry[] readInputStream(InputStream inputStream, SaltFileParser saltFileParser, Integer size) throws IOException {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
String line;
SaltEntry[] entries = new SaltEntry[size];
int idx = 0;
while ((line = reader.readLine()) != null) {
final SaltEntry entry = entryBuilder.toEntry(line);
entries[idx] = entry;
idx++;
}
return entries;
String[] saltFileLines = reader.lines().toArray(String[]::new);
return saltFileParser.parseFileLines(saltFileLines, size);
}
}

Expand Down Expand Up @@ -203,42 +195,8 @@ public SaltEntry getRotatingSalt(byte[] identity) {
@Override
public List<SaltEntry> getModifiedSince(Instant timestamp) {
final long timestampMillis = timestamp.toEpochMilli();
return Arrays.stream(this.entries).filter(e -> e.getLastUpdated() >= timestampMillis).collect(Collectors.toList());
}
}

protected static final class IdHashingScheme {
private final String prefix;
private final Hashids hasher;

public IdHashingScheme(final String prefix, final String secret) {
this.prefix = prefix;
this.hasher = new Hashids(secret, 9);
}

public String encode(long id) {
return prefix + this.hasher.encode(id);
return Arrays.stream(this.entries).filter(e -> e.lastUpdated() >= timestampMillis).collect(Collectors.toList());
}
}

protected static final class SaltEntryBuilder {
private final IdHashingScheme idHashingScheme;

public SaltEntryBuilder(IdHashingScheme idHashingScheme) {
this.idHashingScheme = idHashingScheme;
}

public SaltEntry toEntry(String line) {
try {
final String[] fields = line.split(",");
final long id = Integer.parseInt(fields[0]);
final String hashedId = this.idHashingScheme.encode(id);
final long lastUpdated = Long.parseLong(fields[1]);
final String salt = fields[2];
return new SaltEntry(id, hashedId, lastUpdated, salt);
} catch (Exception e) {
throw new RuntimeException("Trouble parsing Salt Entry " + line, e);
}
}
}
}
60 changes: 60 additions & 0 deletions src/main/java/com/uid2/shared/store/salt/SaltFileParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.uid2.shared.store.salt;

import com.uid2.shared.model.SaltEntry;

public class SaltFileParser {
private final IdHashingScheme idHashingScheme;

public SaltFileParser(IdHashingScheme idHashingScheme) {
this.idHashingScheme = idHashingScheme;
}

public SaltEntry[] parseFile(String saltFileContent, Integer size) {
var lines = saltFileContent.split("\n");
return parseFileLines(lines, size);
}

public SaltEntry[] parseFileLines(String[] saltFileLines, Integer size) {
var entries = new SaltEntry[size];
int idx = 0;
for (String line : saltFileLines) {
final SaltEntry entry = parseLine(line);
entries[idx] = entry;
idx++;
}
return entries;
}

private SaltEntry parseLine(String line) {
try {
final String[] fields = line.split(",");
final long id = Integer.parseInt(fields[0]);
final String hashedId = this.idHashingScheme.encode(id);
final long lastUpdated = Long.parseLong(fields[1]);
final String salt = fields[2];

Long refreshFrom = null;
String previousSalt = null;
SaltEntry.KeyMaterial currentKey = null;
SaltEntry.KeyMaterial previousKey = null;

if (fields.length > 3) {
refreshFrom = Long.parseLong(fields[3]);
}
if (fields.length > 4) {
previousSalt = fields[4];
}
if (fields.length > 7) {
currentKey = new SaltEntry.KeyMaterial(Integer.parseInt(fields[5]), fields[6], fields[7]);
}
if (fields.length > 10) {
previousKey = new SaltEntry.KeyMaterial(Integer.parseInt(fields[8]), fields[9], fields[10]);
}

return new SaltEntry(id, hashedId, lastUpdated, salt, refreshFrom, previousSalt, currentKey, previousKey);
} catch (Exception e) {
throw new RuntimeException("Trouble parsing Salt Entry " + line, e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import com.uid2.shared.encryption.AesGcm;
import com.uid2.shared.model.CloudEncryptionKey;
import com.uid2.shared.store.reader.RotatingCloudEncryptionKeyProvider;
import com.uid2.shared.store.salt.EncryptedRotatingSaltProvider;
import com.uid2.shared.store.salt.ISaltProvider;
import com.uid2.shared.store.scope.EncryptedScope;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
Expand Down Expand Up @@ -256,7 +258,7 @@ public void loadSaltMultipleVersions() throws Exception {
assertEquals(FIRST_LEVEL_SALT, snapshot.getFirstLevelSalt());
assertTrue(snapshot.getModifiedSince(Instant.now().minus(1, ChronoUnit.HOURS)).isEmpty());
assertEquals(1, snapshot.getModifiedSince(Instant.now().minus(30, ChronoUnit.HOURS)).size());
assertEquals(1000002, snapshot.getModifiedSince(Instant.now().minus(30, ChronoUnit.HOURS)).getFirst().getId());
assertEquals(1000002, snapshot.getModifiedSince(Instant.now().minus(30, ChronoUnit.HOURS)).getFirst().id());
}

@Test
Expand Down Expand Up @@ -338,6 +340,6 @@ public void loadSaltMultipleVersionsExpired() throws Exception {
assertEquals(FIRST_LEVEL_SALT, snapshot.getFirstLevelSalt());
assertTrue(snapshot.getModifiedSince(Instant.now().minus(1, ChronoUnit.HOURS)).isEmpty());
assertEquals(1, snapshot.getModifiedSince(Instant.now().minus(49, ChronoUnit.HOURS)).size());
assertEquals(1000002, snapshot.getModifiedSince(Instant.now().minus(49, ChronoUnit.HOURS)).getFirst().getId());
assertEquals(1000002, snapshot.getModifiedSince(Instant.now().minus(49, ChronoUnit.HOURS)).getFirst().id());
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.uid2.shared.store;

import com.uid2.shared.cloud.ICloudStorage;
import com.uid2.shared.store.salt.ISaltProvider;
import com.uid2.shared.store.salt.RotatingSaltProvider;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -157,7 +159,7 @@ public void loadSaltMultipleVersions() throws Exception {
assertEquals(FIRST_LEVEL_SALT, snapshot.getFirstLevelSalt());
assertTrue(snapshot.getModifiedSince(Instant.now().minus(1, ChronoUnit.HOURS)).isEmpty());
assertEquals(1, snapshot.getModifiedSince(Instant.now().minus(30, ChronoUnit.HOURS)).size());
assertEquals(1000002, snapshot.getModifiedSince(Instant.now().minus(30, ChronoUnit.HOURS)).get(0).getId());
assertEquals(1000002, snapshot.getModifiedSince(Instant.now().minus(30, ChronoUnit.HOURS)).get(0).id());
}

@Test
Expand Down Expand Up @@ -239,7 +241,7 @@ public void loadSaltMultipleVersionsExpired() throws Exception {
assertEquals(FIRST_LEVEL_SALT, snapshot.getFirstLevelSalt());
assertTrue(snapshot.getModifiedSince(Instant.now().minus(1, ChronoUnit.HOURS)).isEmpty());
assertEquals(1, snapshot.getModifiedSince(Instant.now().minus(49, ChronoUnit.HOURS)).size());
assertEquals(1000002, snapshot.getModifiedSince(Instant.now().minus(49, ChronoUnit.HOURS)).get(0).getId());
assertEquals(1000002, snapshot.getModifiedSince(Instant.now().minus(49, ChronoUnit.HOURS)).get(0).id());
}


Expand Down
Loading
Loading