Skip to content

Commit 30845e4

Browse files
authored
Merge pull request #406 from IABTechLab/aul-UID2-5227-unify-existing-cloud-encryption-key-code
New keys for created/updated sites are handled via new pattern as well
2 parents 0d457bc + 7a03582 commit 30845e4

13 files changed

+308
-425
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>5.18.7</version>
9+
<version>5.18.8-alpha-127-SNAPSHOT</version>
1010

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

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

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import com.uid2.admin.auth.OktaAuthProvider;
66
import com.uid2.admin.auth.AuthProvider;
77
import com.uid2.admin.auth.TokenRefreshHandler;
8-
import com.uid2.admin.cloudencryption.CloudKeyRotationStrategy;
8+
import com.uid2.admin.cloudencryption.CloudKeyStatePlanner;
99
import com.uid2.admin.cloudencryption.ExpiredKeyCountRetentionStrategy;
1010
import com.uid2.admin.job.JobDispatcher;
1111
import com.uid2.admin.job.jobsync.EncryptedFilesSyncJob;
@@ -248,9 +248,9 @@ public void run() {
248248
ClientSideKeypairService clientSideKeypairService = new ClientSideKeypairService(config, auth, writeLock, clientSideKeypairStoreWriter, clientSideKeypairProvider, siteProvider, keysetManager, keypairGenerator, clock);
249249

250250
var cloudEncryptionSecretGenerator = new CloudSecretGenerator(keyGenerator);
251-
var cloudEncryptionKeyManager = new CloudEncryptionKeyManager(rotatingCloudEncryptionKeyProvider, cloudEncryptionKeyStoreWriter, cloudEncryptionSecretGenerator);
252251
var cloudEncryptionKeyRetentionStrategy = new ExpiredKeyCountRetentionStrategy(clock, 5);
253-
var cloudEncryptionKeyRotationStrategy = new CloudKeyRotationStrategy(cloudEncryptionSecretGenerator, clock, cloudEncryptionKeyRetentionStrategy);
252+
var cloudEncryptionKeyRotationStrategy = new CloudKeyStatePlanner(cloudEncryptionSecretGenerator, clock, cloudEncryptionKeyRetentionStrategy);
253+
var cloudEncryptionKeyManager = new CloudEncryptionKeyManager(rotatingCloudEncryptionKeyProvider, cloudEncryptionKeyStoreWriter, operatorKeyProvider, cloudEncryptionKeyRotationStrategy);
254254

255255
IService[] services = {
256256
new ClientKeyService(config, auth, writeLock, clientKeyStoreWriter, clientKeyProvider, siteProvider, keysetManager, keyGenerator, keyHasher),
@@ -268,7 +268,7 @@ public void run() {
268268
new PrivateSiteDataRefreshService(auth, jobDispatcher, writeLock, config, rotatingCloudEncryptionKeyProvider),
269269
new JobDispatcherService(auth, jobDispatcher),
270270
new SearchService(auth, clientKeyProvider, operatorKeyProvider),
271-
new CloudEncryptionKeyService(auth, rotatingCloudEncryptionKeyProvider, cloudEncryptionKeyStoreWriter, operatorKeyProvider, cloudEncryptionKeyRotationStrategy)
271+
new CloudEncryptionKeyService(auth, cloudEncryptionKeyManager)
272272
};
273273

274274

@@ -293,11 +293,7 @@ public void run() {
293293
}
294294

295295
synchronized (writeLock) {
296-
cloudEncryptionKeyManager.generateKeysForOperators(
297-
operatorKeyProvider.getAll(),
298-
config.getLong("cloud_encryption_key_activates_in_seconds"),
299-
config.getInteger("cloud_encryption_key_count_per_site")
300-
);
296+
cloudEncryptionKeyManager.backfillKeys();
301297
rotatingCloudEncryptionKeyProvider.loadContent();
302298
}
303299

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.uid2.admin.cloudencryption;
2+
3+
import com.uid2.shared.model.CloudEncryptionKey;
4+
5+
import java.text.MessageFormat;
6+
import java.util.HashSet;
7+
import java.util.Set;
8+
9+
public record CloudEncryptionKeyDiff(
10+
int before,
11+
int after,
12+
int created,
13+
int removed,
14+
int unchanged
15+
) {
16+
public static CloudEncryptionKeyDiff calculateDiff(Set<CloudEncryptionKey> keysBefore, Set<CloudEncryptionKey> keysAfter) {
17+
var before = keysBefore.size();
18+
var after = keysAfter.size();
19+
20+
var intersection = new HashSet<>(keysBefore);
21+
intersection.retainAll(keysAfter);
22+
int unchanged = intersection.size();
23+
24+
var onlyInLeft = new HashSet<>(keysBefore);
25+
onlyInLeft.removeAll(keysAfter);
26+
int removed = onlyInLeft.size();
27+
28+
var onlyInRight = new HashSet<>(keysAfter);
29+
onlyInRight.removeAll(keysBefore);
30+
int created = onlyInRight.size();
31+
32+
return new CloudEncryptionKeyDiff(before, after, created, removed, unchanged);
33+
}
34+
35+
@Override
36+
public String toString() {
37+
return MessageFormat.format(
38+
"before={0}, after={1}, created={2}, removed={3}, unchanged={4}",
39+
before,
40+
after,
41+
created,
42+
removed,
43+
unchanged
44+
);
45+
}
46+
}
Lines changed: 65 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,109 +1,95 @@
11
package com.uid2.admin.cloudencryption;
22

3+
import com.uid2.admin.model.CloudEncryptionKeySummary;
34
import com.uid2.admin.store.writer.CloudEncryptionKeyStoreWriter;
45
import com.uid2.shared.auth.OperatorKey;
6+
import com.uid2.shared.auth.RotatingOperatorKeyProvider;
57
import com.uid2.shared.model.CloudEncryptionKey;
68
import com.uid2.shared.store.reader.RotatingCloudEncryptionKeyProvider;
9+
import io.micrometer.core.instrument.Counter;
10+
import io.micrometer.core.instrument.Metrics;
711
import org.slf4j.Logger;
812
import org.slf4j.LoggerFactory;
913

10-
import java.time.Instant;
1114
import java.util.*;
15+
import java.util.function.Function;
16+
import java.util.stream.Collectors;
1217

1318
public class CloudEncryptionKeyManager {
19+
private final RotatingCloudEncryptionKeyProvider keyProvider;
20+
private final RotatingOperatorKeyProvider operatorKeyProvider;
21+
private final CloudEncryptionKeyStoreWriter keyWriter;
22+
private final CloudKeyStatePlanner planner;
23+
private Set<OperatorKey> operatorKeys;
24+
private Set<CloudEncryptionKey> existingKeys;
1425

1526
private static final Logger LOGGER = LoggerFactory.getLogger(CloudEncryptionKeyManager.class);
1627

17-
private final RotatingCloudEncryptionKeyProvider RotatingCloudEncryptionKeyProvider;
18-
private final CloudEncryptionKeyStoreWriter cloudEncryptionKeyStoreWriter;
19-
private final CloudSecretGenerator secretGenerator;
20-
2128
public CloudEncryptionKeyManager(
22-
RotatingCloudEncryptionKeyProvider RotatingCloudEncryptionKeyProvider,
23-
CloudEncryptionKeyStoreWriter cloudEncryptionKeyStoreWriter,
24-
CloudSecretGenerator keyGenerator
25-
) {
26-
this.RotatingCloudEncryptionKeyProvider = RotatingCloudEncryptionKeyProvider;
27-
this.cloudEncryptionKeyStoreWriter = cloudEncryptionKeyStoreWriter;
28-
this.secretGenerator = keyGenerator;
29-
}
30-
31-
// Ensures there are `keyCountPerSite` sites for each site corresponding of operatorKeys. If there are less - create new ones.
32-
// Give all new keys for each site `activationInterval` seconds between activations, starting now
33-
public void generateKeysForOperators(
34-
Collection<OperatorKey> operatorKeys,
35-
long activationInterval,
36-
int keyCountPerSite
37-
) throws Exception {
38-
this.RotatingCloudEncryptionKeyProvider.loadContent();
39-
40-
if (operatorKeys == null || operatorKeys.isEmpty()) {
41-
throw new IllegalArgumentException("Operator keys collection must not be null or empty");
42-
}
43-
if (activationInterval <= 0) {
44-
throw new IllegalArgumentException("Key activate interval must be greater than zero");
45-
}
46-
if (keyCountPerSite <= 0) {
47-
throw new IllegalArgumentException("Key count per site must be greater than zero");
48-
}
49-
50-
for (Integer siteId : uniqueSiteIdsForOperators(operatorKeys)) {
51-
ensureEnoughKeysForSite(activationInterval, keyCountPerSite, siteId);
52-
}
29+
RotatingCloudEncryptionKeyProvider keyProvider,
30+
CloudEncryptionKeyStoreWriter keyWriter,
31+
RotatingOperatorKeyProvider operatorKeyProvider,
32+
CloudKeyStatePlanner planner) {
33+
this.keyProvider = keyProvider;
34+
this.operatorKeyProvider = operatorKeyProvider;
35+
this.keyWriter = keyWriter;
36+
this.planner = planner;
5337
}
5438

55-
private void ensureEnoughKeysForSite(long activationInterval, int keyCountPerSite, Integer siteId) throws Exception {
56-
// Check if the site ID already exists in the S3 key provider and has fewer than the required number of keys
57-
int currentKeyCount = countKeysForSite(siteId);
58-
if (currentKeyCount >= keyCountPerSite) {
59-
LOGGER.info("Site ID {} already has the required number of keys. Skipping key generation.", siteId);
60-
return;
61-
}
62-
63-
int keysToGenerate = keyCountPerSite - currentKeyCount;
64-
for (int i = 0; i < keysToGenerate; i++) {
65-
addKey(activationInterval, siteId, i);
39+
// For any site that has an operator create a new key activating now
40+
// Keep up to 5 most recent old keys per site, delete the rest
41+
public void rotateKeys() throws Exception {
42+
boolean success = false;
43+
try {
44+
refreshCloudData();
45+
var desiredKeys = planner.planRotation(existingKeys, operatorKeys);
46+
writeKeys(desiredKeys);
47+
success = true;
48+
var diff = CloudEncryptionKeyDiff.calculateDiff(existingKeys, desiredKeys);
49+
LOGGER.info("Key rotation complete. Diff: {}", diff);
50+
} catch (Exception e) {
51+
success = false;
52+
LOGGER.error("Key rotation failed", e);
53+
throw e;
54+
} finally {
55+
Counter.builder("uid2.cloud_encryption_key_manager.rotations")
56+
.tag("success", Boolean.toString(success))
57+
.description("The number of times rotations have happened")
58+
.register(Metrics.globalRegistry);
6659
}
67-
LOGGER.info("Generated {} keys for site ID {}", keysToGenerate, siteId);
68-
}
69-
70-
private void addKey(long keyActivateInterval, Integer siteId, int keyIndex) throws Exception {
71-
long created = Instant.now().getEpochSecond();
72-
long activated = created + (keyIndex * keyActivateInterval);
73-
CloudEncryptionKey cloudEncryptionKey = generateCloudEncryptionKey(siteId, activated, created);
74-
addCloudEncryptionKey(cloudEncryptionKey);
7560
}
7661

77-
private static Set<Integer> uniqueSiteIdsForOperators(Collection<OperatorKey> operatorKeys) {
78-
Set<Integer> uniqueSiteIds = new HashSet<>();
79-
for (OperatorKey operatorKey : operatorKeys) {
80-
uniqueSiteIds.add(operatorKey.getSiteId());
62+
// For any site that has an operator, if there are no keys, create a key activating now
63+
public void backfillKeys() throws Exception {
64+
try {
65+
refreshCloudData();
66+
var desiredKeys = planner.planBackfill(existingKeys, operatorKeys);
67+
writeKeys(desiredKeys);
68+
var diff = CloudEncryptionKeyDiff.calculateDiff(existingKeys, desiredKeys);
69+
LOGGER.info("Key backfill complete. Diff: {}", diff);
70+
} catch (Exception e) {
71+
LOGGER.error("Key backfill failed", e);
72+
throw e;
8173
}
82-
return uniqueSiteIds;
83-
}
84-
85-
CloudEncryptionKey generateCloudEncryptionKey(int siteId, long activates, long created) throws Exception {
86-
int newKeyId = getNextKeyId();
87-
String secret = secretGenerator.generate();
88-
return new CloudEncryptionKey(newKeyId, siteId, activates, created, secret);
8974
}
9075

91-
void addCloudEncryptionKey(CloudEncryptionKey cloudEncryptionKey) throws Exception {
92-
Map<Integer, CloudEncryptionKey> cloudEncryptionKeys = new HashMap<>(RotatingCloudEncryptionKeyProvider.getAll());
93-
cloudEncryptionKeys.put(cloudEncryptionKey.getId(), cloudEncryptionKey);
94-
cloudEncryptionKeyStoreWriter.upload(cloudEncryptionKeys, null);
76+
public Set<CloudEncryptionKeySummary> getKeySummaries() throws Exception {
77+
refreshCloudData();
78+
return existingKeys.stream().map(CloudEncryptionKeySummary::fromFullKey).collect(Collectors.toSet());
9579
}
9680

97-
int getNextKeyId() {
98-
Map<Integer, CloudEncryptionKey> cloudEncryptionKeys = RotatingCloudEncryptionKeyProvider.getAll();
99-
if (cloudEncryptionKeys == null || cloudEncryptionKeys.isEmpty()) {
100-
return 1;
101-
}
102-
return cloudEncryptionKeys.keySet().stream().max(Integer::compareTo).orElse(0) + 1;
81+
private void writeKeys(Set<CloudEncryptionKey> desiredKeys) throws Exception {
82+
var keysForWriting = desiredKeys.stream().collect(Collectors.toMap(
83+
CloudEncryptionKey::getId,
84+
Function.identity())
85+
);
86+
keyWriter.upload(keysForWriting, null);
10387
}
10488

105-
int countKeysForSite(int siteId) {
106-
Map<Integer, CloudEncryptionKey> allKeys = RotatingCloudEncryptionKeyProvider.getAll();
107-
return (int) allKeys.values().stream().filter(key -> key.getSiteId() == siteId).count();
89+
private void refreshCloudData() throws Exception {
90+
keyProvider.loadContent();
91+
operatorKeyProvider.loadContent(operatorKeyProvider.getMetadata());
92+
operatorKeys = new HashSet<>(operatorKeyProvider.getAll());
93+
existingKeys = new HashSet<>(keyProvider.getAll().values());
10894
}
10995
}

src/main/java/com/uid2/admin/cloudencryption/CloudKeyRotationStrategy.java renamed to src/main/java/com/uid2/admin/cloudencryption/CloudKeyStatePlanner.java

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@
55
import com.uid2.shared.auth.OperatorKey;
66
import com.uid2.shared.model.CloudEncryptionKey;
77

8+
import java.text.MessageFormat;
89
import java.util.Collection;
10+
import java.util.HashSet;
911
import java.util.Map;
1012
import java.util.Set;
1113
import java.util.stream.Collectors;
1214
import java.util.stream.Stream;
1315

14-
public class CloudKeyRotationStrategy {
16+
public class CloudKeyStatePlanner {
1517
private final CloudSecretGenerator secretGenerator;
1618
private final Clock clock;
1719
private final CloudKeyRetentionStrategy keyRetentionStrategy;
1820

19-
public CloudKeyRotationStrategy(
21+
public CloudKeyStatePlanner(
2022
CloudSecretGenerator secretGenerator,
2123
Clock clock,
2224
CloudKeyRetentionStrategy keyRetentionStrategy
@@ -26,24 +28,32 @@ public CloudKeyRotationStrategy(
2628
this.keyRetentionStrategy = keyRetentionStrategy;
2729
}
2830

29-
public Set<CloudEncryptionKey> computeDesiredKeys(
30-
Collection<CloudEncryptionKey> existingCloudKeys,
31-
Collection<OperatorKey> operatorKeys
31+
public Set<CloudEncryptionKey> planRotation(
32+
Set<CloudEncryptionKey> existingKeys,
33+
Set<OperatorKey> operatorKeys
3234
) {
33-
var keyGenerator = new CloudEncryptionKeyGenerator(clock, secretGenerator, existingCloudKeys);
34-
Map<Integer, Set<CloudEncryptionKey>> existingKeysBySite = existingCloudKeys
35+
var keyGenerator = new CloudEncryptionKeyGenerator(clock, secretGenerator, existingKeys);
36+
Map<Integer, Set<CloudEncryptionKey>> existingKeysBySite = existingKeys
3537
.stream()
3638
.collect(Collectors.groupingBy(CloudEncryptionKey::getSiteId, Collectors.toSet()));
3739

38-
return operatorKeys
39-
.stream()
40-
.map(OperatorKey::getSiteId)
41-
.distinct()
42-
.flatMap(siteId -> desiredKeysForSite(siteId, keyGenerator, existingKeysBySite.getOrDefault(siteId, Set.of())))
40+
return siteIdsWithOperators(operatorKeys)
41+
.flatMap(siteId -> planRotationForSite(siteId, keyGenerator, existingKeysBySite.getOrDefault(siteId, Set.of())))
4342
.collect(Collectors.toSet());
4443
}
4544

46-
private Stream<CloudEncryptionKey> desiredKeysForSite(
45+
public Set<CloudEncryptionKey> planBackfill(
46+
Set<CloudEncryptionKey> existingKeys,
47+
Set<OperatorKey> operatorKeys
48+
) {
49+
var keyGenerator = new CloudEncryptionKeyGenerator(clock, secretGenerator, existingKeys);
50+
var siteIdsWithKeys = existingKeys.stream().map(CloudEncryptionKey::getSiteId).collect(Collectors.toSet());
51+
var sitesWithoutKeys = siteIdsWithOperators(operatorKeys).filter(siteId -> !siteIdsWithKeys.contains(siteId));
52+
var newKeys = sitesWithoutKeys.map(keyGenerator::makeNewKey);
53+
return Streams.concat(existingKeys.stream(), newKeys).collect(Collectors.toSet());
54+
}
55+
56+
private Stream<CloudEncryptionKey> planRotationForSite(
4757
Integer siteId,
4858
CloudEncryptionKeyGenerator keyGenerator,
4959
Set<CloudEncryptionKey> existingKeys
@@ -52,4 +62,11 @@ private Stream<CloudEncryptionKey> desiredKeysForSite(
5262
var newKey = keyGenerator.makeNewKey(siteId);
5363
return Streams.concat(existingKeysToRetain.stream(), Stream.of(newKey));
5464
}
65+
66+
private static Stream<Integer> siteIdsWithOperators(Collection<OperatorKey> operatorKeys) {
67+
return operatorKeys
68+
.stream()
69+
.map(OperatorKey::getSiteId)
70+
.distinct();
71+
}
5572
}

src/main/java/com/uid2/admin/model/CloudEncryptionKeyListResponse.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
import com.fasterxml.jackson.annotation.JsonProperty;
44

5-
import java.util.List;
5+
import java.util.Set;
66

77
public record CloudEncryptionKeyListResponse(
8-
@JsonProperty List<CloudEncryptionKeySummary> cloudEncryptionKeys
8+
@JsonProperty Set<CloudEncryptionKeySummary> cloudEncryptionKeys
99
) {}
1010

0 commit comments

Comments
 (0)