Skip to content

Commit 7fdcff6

Browse files
committed
Merged main
2 parents aa9c8df + 70ef922 commit 7fdcff6

File tree

15 files changed

+1443
-68
lines changed

15 files changed

+1443
-68
lines changed

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.uid2</groupId>
88
<artifactId>uid2-admin</artifactId>
9-
<version>6.10.17</version>
9+
<version>6.11.0</version>
1010

1111
<properties>
1212
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

src/main/java/com/uid2/admin/AdminConst.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,5 @@ private AdminConst() {
88
public static final String ROLE_OKTA_GROUP_MAP_MAINTAINER = "role_okta_group_map_maintainer";
99
public static final String ROLE_OKTA_GROUP_MAP_PRIVILEGED = "role_okta_group_map_privileged";
1010
public static final String ROLE_OKTA_GROUP_MAP_SUPER_USER = "role_okta_group_map_super_user";
11+
public static final String ENABLE_V4_RAW_UID = "enable_v4_raw_uid";
1112
}

src/main/java/com/uid2/admin/Main.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ public void run() {
233233
WriteLock writeLock = new WriteLock();
234234
KeyHasher keyHasher = new KeyHasher();
235235
IKeypairGenerator keypairGenerator = new SecureKeypairGenerator();
236-
SaltRotation saltRotation = new SaltRotation(keyGenerator);
236+
SaltRotation saltRotation = new SaltRotation(keyGenerator, config);
237237
EncryptionKeyService encryptionKeyService = new EncryptionKeyService(
238238
config, auth, writeLock, encryptionKeyStoreWriter, keysetKeyStoreWriter, keyProvider, keysetKeysProvider, adminKeysetProvider, adminKeysetStoreWriter, keyGenerator, clock);
239239
KeysetManager keysetManager = new KeysetManager(
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.uid2.admin.salt;
2+
3+
import com.uid2.shared.model.SaltEntry;
4+
5+
import java.util.Arrays;
6+
import java.util.concurrent.atomic.AtomicInteger;
7+
8+
/**
9+
* Assumptions:
10+
* - The latest assigned key ids are from the latest updated buckets
11+
* - Key ids from these buckets will always be monotonically increasing (apart from wraparound) as they have not rotated again after last assignment
12+
*
13+
* Intended outcomes of KeyIdGenerator:
14+
* - Key ids are always monotonically increasing, starting from 0
15+
* - When the last allocated key id reaches 16777215, the next key id will wrap around to 0
16+
* - Continuing to increment from the highest key id will result in monotonic incrementation of key ids for all newly rotated buckets
17+
**/
18+
public class KeyIdGenerator {
19+
private static final int MAX_KEY_ID = 16777215; // 3 bytes
20+
private final AtomicInteger nextActiveKeyId;
21+
22+
public KeyIdGenerator(SaltEntry[] buckets) {
23+
this.nextActiveKeyId = new AtomicInteger(getNextActiveKeyId(buckets));
24+
}
25+
26+
private static int getNextActiveKeyId(SaltEntry[] buckets) {
27+
long lastUpdatedTimestampWithKey = Arrays.stream(buckets).filter(s -> s.currentKeySalt() != null).mapToLong(SaltEntry::lastUpdated).max().orElse(0);
28+
if (lastUpdatedTimestampWithKey == 0) return 0;
29+
30+
int[] lastActiveKeyIdsSorted = Arrays.stream(buckets)
31+
.filter(s -> s.lastUpdated() == lastUpdatedTimestampWithKey && s.currentKeySalt() != null)
32+
.mapToInt(s -> s.currentKeySalt().id())
33+
.sorted()
34+
.toArray();
35+
36+
int highestId = lastActiveKeyIdsSorted[lastActiveKeyIdsSorted.length - 1];
37+
38+
int nextKeyId = highestId + 1;
39+
if (nextKeyId <= MAX_KEY_ID) return nextKeyId;
40+
41+
// Wrapped case - find the last consecutive ID from 0
42+
for (int i = 0; i < lastActiveKeyIdsSorted.length - 1; i++) {
43+
if (lastActiveKeyIdsSorted[i + 1] - lastActiveKeyIdsSorted[i] > 1) {
44+
return lastActiveKeyIdsSorted[i] + 1;
45+
}
46+
}
47+
48+
return 0;
49+
}
50+
51+
public int getNextKeyId() {
52+
return nextActiveKeyId.getAndUpdate(id ->
53+
id + 1 > MAX_KEY_ID ? 0 : id + 1
54+
);
55+
}
56+
}

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

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package com.uid2.admin.salt;
22

3+
import com.uid2.admin.AdminConst;
34
import com.uid2.shared.model.SaltEntry;
45
import com.uid2.shared.secret.IKeyGenerator;
56

67
import com.uid2.shared.store.salt.ISaltProvider.ISaltSnapshot;
78
import com.uid2.shared.store.salt.RotatingSaltProvider.SaltSnapshot;
9+
import io.vertx.core.json.JsonObject;
810
import lombok.Getter;
911
import org.slf4j.Logger;
1012
import org.slf4j.LoggerFactory;
@@ -17,12 +19,15 @@
1719
public class SaltRotation {
1820
private static final long THIRTY_DAYS_IN_MS = Duration.ofDays(30).toMillis();
1921
private static final double MAX_SALT_PERCENTAGE = 0.8;
22+
private final boolean ENABLE_V4_RAW_UID;
2023

2124
private final IKeyGenerator keyGenerator;
25+
2226
private static final Logger LOGGER = LoggerFactory.getLogger(SaltRotation.class);
2327

24-
public SaltRotation(IKeyGenerator keyGenerator) {
28+
public SaltRotation(IKeyGenerator keyGenerator, JsonObject config) {
2529
this.keyGenerator = keyGenerator;
30+
this.ENABLE_V4_RAW_UID = config.getBoolean(AdminConst.ENABLE_V4_RAW_UID, false);
2631
}
2732

2833
public Result rotateSalts(
@@ -57,6 +62,7 @@ public Result rotateSalts(
5762
logSaltAges("refreshable-salts", targetDate, refreshableSalts);
5863
logSaltAges("rotated-salts", targetDate, saltsToRotate);
5964
logSaltAges("total-salts", targetDate, Arrays.asList(postRotationSalts));
65+
logBucketFormatCount(targetDate, postRotationSalts);
6066

6167
var nextSnapshot = new SaltSnapshot(
6268
nextEffective,
@@ -99,45 +105,85 @@ private boolean isRefreshable(TargetDate targetDate, SaltEntry salt) {
99105
}
100106

101107
private SaltEntry[] rotateSalts(SaltEntry[] oldSalts, List<SaltEntry> saltsToRotate, TargetDate targetDate) throws Exception {
108+
var keyIdGenerator = new KeyIdGenerator(oldSalts);
102109
var saltIdsToRotate = saltsToRotate.stream().map(SaltEntry::id).collect(Collectors.toSet());
103110

104111
var updatedSalts = new SaltEntry[oldSalts.length];
105112
for (int i = 0; i < oldSalts.length; i++) {
106113
var shouldRotate = saltIdsToRotate.contains(oldSalts[i].id());
107-
updatedSalts[i] = updateSalt(oldSalts[i], targetDate, shouldRotate);
114+
updatedSalts[i] = updateSalt(oldSalts[i], targetDate, shouldRotate, keyIdGenerator);
108115
}
109116
return updatedSalts;
110117
}
111118

112-
private SaltEntry updateSalt(SaltEntry oldSalt, TargetDate targetDate, boolean shouldRotate) throws Exception {
113-
var currentSalt = shouldRotate ? this.keyGenerator.generateRandomKeyString(32) : oldSalt.currentSalt();
114-
var lastUpdated = shouldRotate ? targetDate.asEpochMs() : oldSalt.lastUpdated();
115-
var refreshFrom = calculateRefreshFrom(oldSalt, targetDate);
116-
var previousSalt = calculatePreviousSalt(oldSalt, shouldRotate, targetDate);
119+
private SaltEntry updateSalt(SaltEntry oldBucket, TargetDate targetDate, boolean shouldRotate, KeyIdGenerator keyIdGenerator) throws Exception {
120+
var lastUpdated = shouldRotate ? targetDate.asEpochMs() : oldBucket.lastUpdated();
121+
var refreshFrom = calculateRefreshFrom(oldBucket, targetDate);
122+
var currentSalt = calculateCurrentSalt(oldBucket, shouldRotate);
123+
var previousSalt = calculatePreviousSalt(oldBucket, shouldRotate, targetDate);
124+
var currentKeySalt = calculateCurrentKeySalt(oldBucket, shouldRotate, keyIdGenerator);
125+
var previousKeySalt = calculatePreviousKeySalt(oldBucket,shouldRotate, targetDate);
117126

118127
return new SaltEntry(
119-
oldSalt.id(),
120-
oldSalt.hashedId(),
128+
oldBucket.id(),
129+
oldBucket.hashedId(),
121130
lastUpdated,
122131
currentSalt,
123132
refreshFrom,
124133
previousSalt,
125-
null,
126-
null
134+
currentKeySalt,
135+
previousKeySalt
127136
);
128137
}
129138

130-
private long calculateRefreshFrom(SaltEntry salt, TargetDate targetDate) {
131-
long multiplier = targetDate.saltAgeInDays(salt) / 30 + 1;
132-
return Instant.ofEpochMilli(salt.lastUpdated()).truncatedTo(ChronoUnit.DAYS).toEpochMilli() + (multiplier * THIRTY_DAYS_IN_MS);
139+
private long calculateRefreshFrom(SaltEntry bucket, TargetDate targetDate) {
140+
long multiplier = targetDate.saltAgeInDays(bucket) / 30 + 1;
141+
return Instant.ofEpochMilli(bucket.lastUpdated()).truncatedTo(ChronoUnit.DAYS).toEpochMilli() + (multiplier * THIRTY_DAYS_IN_MS);
142+
}
143+
144+
private String calculateCurrentSalt(SaltEntry bucket, boolean shouldRotate) throws Exception {
145+
if (shouldRotate) {
146+
if (ENABLE_V4_RAW_UID) {
147+
return null;
148+
}
149+
else {
150+
return this.keyGenerator.generateRandomKeyString(32);
151+
}
152+
}
153+
return bucket.currentSalt();
133154
}
134155

135-
private String calculatePreviousSalt(SaltEntry salt, boolean shouldRotate, TargetDate targetDate) {
156+
private String calculatePreviousSalt(SaltEntry bucket, boolean shouldRotate, TargetDate targetDate) {
136157
if (shouldRotate) {
137-
return salt.currentSalt();
158+
return bucket.currentSalt();
138159
}
139-
if (targetDate.saltAgeInDays(salt) < 90) {
140-
return salt.previousSalt();
160+
if (targetDate.saltAgeInDays(bucket) < 90) {
161+
return bucket.previousSalt();
162+
}
163+
return null;
164+
}
165+
166+
private SaltEntry.KeyMaterial calculateCurrentKeySalt(SaltEntry bucket, boolean shouldRotate, KeyIdGenerator keyIdGenerator) throws Exception {
167+
if (shouldRotate) {
168+
if (ENABLE_V4_RAW_UID) {
169+
return new SaltEntry.KeyMaterial(
170+
keyIdGenerator.getNextKeyId(),
171+
this.keyGenerator.generateRandomKeyString(32),
172+
this.keyGenerator.generateRandomKeyString(32)
173+
);
174+
} else {
175+
return null;
176+
}
177+
}
178+
return bucket.currentKeySalt();
179+
}
180+
181+
private SaltEntry.KeyMaterial calculatePreviousKeySalt(SaltEntry bucket, boolean shouldRotate, TargetDate targetDate) {
182+
if (shouldRotate) {
183+
return bucket.currentKeySalt();
184+
}
185+
if (targetDate.saltAgeInDays(bucket) < 90) {
186+
return bucket.previousKeySalt();
141187
}
142188
return null;
143189
}
@@ -207,6 +253,24 @@ private void logSaltAges(String saltCountType, TargetDate targetDate, Collection
207253
}
208254
}
209255

256+
257+
/** Logging to monitor migration of buckets from salts (old format - v2/v3) to encryption keys (new format - v4) **/
258+
private void logBucketFormatCount(TargetDate targetDate, SaltEntry[] postRotationBuckets) {
259+
int totalKeys = 0, totalSalts = 0, totalPreviousKeys = 0, totalPreviousSalts = 0;
260+
261+
for (SaltEntry bucket : postRotationBuckets) {
262+
if (bucket.currentKeySalt() != null) totalKeys++;
263+
if (bucket.currentSalt() != null) totalSalts++;
264+
if (bucket.previousKeySalt() != null) totalPreviousKeys++;
265+
if (bucket.previousSalt() != null) totalPreviousSalts++;
266+
}
267+
268+
LOGGER.info("UID bucket format: target_date={} bucket_format={} bucket_count={}", targetDate, "total-current-key-buckets", totalKeys);
269+
LOGGER.info("UID bucket format: target_date={} bucket_format={} bucket_count={}", targetDate, "total-current-salt-buckets", totalSalts);
270+
LOGGER.info("UID bucket format: target_date={} bucket_format={} bucket_count={}", targetDate, "total-previous-key-buckets", totalPreviousKeys);
271+
LOGGER.info("UID bucket format: target_date={} bucket_format={} bucket_count={}", targetDate, "total-previous-salt-buckets", totalPreviousSalts);
272+
}
273+
210274
@Getter
211275
public static final class Result {
212276
private final SaltSnapshot snapshot; // can be null if new snapshot is not needed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@ private static void addLine(SaltEntry entry, StringBuilder stringBuilder) {
2323
.append(",")
2424
.append(lastUpdated % 1000 == 0 ? lastUpdated + 1 : lastUpdated)
2525
.append(",")
26-
.append(entry.currentSalt());
26+
.append(serializeNullable(entry.currentSalt()));
2727

2828
stringBuilder.append(",");
2929
stringBuilder.append(entry.refreshFrom());
3030

3131
stringBuilder.append(",");
3232
stringBuilder.append(serializeNullable(entry.previousSalt()));
3333

34-
appendKey(stringBuilder, entry.currentKey());
35-
appendKey(stringBuilder, entry.previousKey());
34+
appendKey(stringBuilder, entry.currentKeySalt());
35+
appendKey(stringBuilder, entry.previousKeySalt());
3636

3737
stringBuilder.append("\n");
3838
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import com.uid2.admin.store.version.VersionGenerator;
55
import com.uid2.shared.cloud.CloudStorageException;
66
import com.uid2.shared.cloud.TaggableCloudStorage;
7-
import com.uid2.shared.model.SaltEntry;
87
import com.uid2.shared.store.CloudPath;
98
import com.uid2.shared.store.salt.RotatingSaltProvider;
109
import io.vertx.core.json.JsonArray;

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
"location" : "salts/salts.txt.1670796729291",
1212
"size" : 2
1313
},{
14-
"effective" : 1745907348982,
15-
"expires" : 1766720293000,
16-
"location" : "salts/salts.txt.1745907348982",
17-
"size" : 2
14+
"effective" : 1755648000000,
15+
"expires" : 1756252800000,
16+
"location" : "salts/salts.txt.1755648000000",
17+
"size" : 1001
1818
}
1919
]
2020
}

0 commit comments

Comments
 (0)