Skip to content

Commit 693fbb2

Browse files
committed
Key rotation API with retention of 5 last expired keys
1 parent 0c1a13f commit 693fbb2

File tree

12 files changed

+470
-44
lines changed

12 files changed

+470
-44
lines changed

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

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
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;
9+
import com.uid2.admin.cloudEncryption.ExpiredKeyCountRetentionStrategy;
810
import com.uid2.admin.job.JobDispatcher;
911
import com.uid2.admin.job.jobsync.EncryptedFilesSyncJob;
1012
import com.uid2.admin.job.jobsync.PrivateSiteDataSyncJob;
1113
import com.uid2.admin.job.jobsync.keyset.ReplaceSharingTypesWithSitesJob;
1214
import com.uid2.admin.legacy.LegacyClientKeyStoreWriter;
1315
import com.uid2.admin.legacy.RotatingLegacyClientKeyProvider;
1416
import com.uid2.admin.managers.KeysetManager;
17+
import com.uid2.admin.cloudEncryption.CloudSecretGenerator;
1518
import com.uid2.admin.monitoring.DataStoreMetrics;
1619
import com.uid2.admin.managers.CloudEncryptionKeyManager;
1720
import com.uid2.admin.secret.*;
@@ -29,7 +32,6 @@
2932
import com.uid2.admin.vertx.service.*;
3033
import com.uid2.shared.Const;
3134
import com.uid2.shared.Utils;
32-
import com.uid2.shared.secret.IKeyGenerator;
3335
import com.uid2.shared.secret.KeyHasher;
3436
import com.uid2.shared.secret.SecureKeyGenerator;
3537
import com.uid2.shared.auth.EnclaveIdentifierProvider;
@@ -122,7 +124,7 @@ public void run() {
122124
try {
123125
adminKeysetProvider.loadContent();
124126
} catch (CloudStorageException e) {
125-
if(e.getMessage().contains("The specified key does not exist")){
127+
if (e.getMessage().contains("The specified key does not exist")) {
126128
adminKeysetStoreWriter.upload(new HashMap<>(), null);
127129
adminKeysetProvider.loadContent();
128130
} else {
@@ -134,7 +136,7 @@ public void run() {
134136
GlobalScope keysetKeysGlobalScope = new GlobalScope(keysetKeyMetadataPath);
135137
RotatingKeysetKeyStore keysetKeysProvider = new RotatingKeysetKeyStore(cloudStorage, keysetKeysGlobalScope);
136138
KeysetKeyStoreWriter keysetKeyStoreWriter = new KeysetKeyStoreWriter(keysetKeysProvider, fileManager, versionGenerator, clock, keysetKeysGlobalScope, enableKeysets);
137-
if(enableKeysets) {
139+
if (enableKeysets) {
138140
try {
139141
keysetKeysProvider.loadContent();
140142
} catch (CloudStorageException e) {
@@ -154,7 +156,7 @@ public void run() {
154156
try {
155157
clientSideKeypairProvider.loadContent();
156158
} catch (CloudStorageException e) {
157-
if(e.getMessage().contains("The specified key does not exist")) {
159+
if (e.getMessage().contains("The specified key does not exist")) {
158160
clientSideKeypairStoreWriter.upload(new HashSet<>(), null);
159161
clientSideKeypairProvider.loadContent();
160162
} else {
@@ -163,13 +165,13 @@ public void run() {
163165
}
164166

165167
CloudPath serviceMetadataPath = new CloudPath(config.getString(Const.Config.ServiceMetadataPathProp));
166-
GlobalScope serviceGlobalScope= new GlobalScope(serviceMetadataPath);
168+
GlobalScope serviceGlobalScope = new GlobalScope(serviceMetadataPath);
167169
RotatingServiceStore serviceProvider = new RotatingServiceStore(cloudStorage, serviceGlobalScope);
168170
ServiceStoreWriter serviceStoreWriter = new ServiceStoreWriter(serviceProvider, fileManager, jsonWriter, versionGenerator, clock, serviceGlobalScope);
169171
try {
170172
serviceProvider.loadContent();
171173
} catch (CloudStorageException e) {
172-
if(e.getMessage().contains("The specified key does not exist")) {
174+
if (e.getMessage().contains("The specified key does not exist")) {
173175
serviceStoreWriter.upload(new HashSet<>(), null);
174176
serviceProvider.loadContent();
175177
} else {
@@ -178,13 +180,13 @@ public void run() {
178180
}
179181

180182
CloudPath serviceLinkMetadataPath = new CloudPath(config.getString(Const.Config.ServiceLinkMetadataPathProp));
181-
GlobalScope serviceLinkGlobalScope= new GlobalScope(serviceLinkMetadataPath);
183+
GlobalScope serviceLinkGlobalScope = new GlobalScope(serviceLinkMetadataPath);
182184
RotatingServiceLinkStore serviceLinkProvider = new RotatingServiceLinkStore(cloudStorage, serviceLinkGlobalScope);
183185
ServiceLinkStoreWriter serviceLinkStoreWriter = new ServiceLinkStoreWriter(serviceLinkProvider, fileManager, jsonWriter, versionGenerator, clock, serviceLinkGlobalScope);
184186
try {
185187
serviceLinkProvider.loadContent();
186188
} catch (CloudStorageException e) {
187-
if(e.getMessage().contains("The specified key does not exist")) {
189+
if (e.getMessage().contains("The specified key does not exist")) {
188190
serviceLinkStoreWriter.upload(new HashSet<>(), null);
189191
serviceLinkProvider.loadContent();
190192
} else {
@@ -202,8 +204,7 @@ public void run() {
202204
GlobalScope cloudEncryptionKeyGlobalScope = new GlobalScope(cloudEncryptionKeyMetadataPath);
203205
RotatingCloudEncryptionKeyProvider rotatingCloudEncryptionKeyProvider = new RotatingCloudEncryptionKeyProvider(cloudStorage, cloudEncryptionKeyGlobalScope);
204206
CloudEncryptionKeyStoreWriter cloudEncryptionKeyStoreWriter = new CloudEncryptionKeyStoreWriter(rotatingCloudEncryptionKeyProvider, fileManager, jsonWriter, versionGenerator, clock, cloudEncryptionKeyGlobalScope);
205-
IKeyGenerator keyGenerator = new SecureKeyGenerator();
206-
CloudEncryptionKeyManager cloudEncryptionKeyManager = new CloudEncryptionKeyManager(rotatingCloudEncryptionKeyProvider, cloudEncryptionKeyStoreWriter,keyGenerator);
207+
SecureKeyGenerator keyGenerator = new SecureKeyGenerator();
207208
try {
208209
rotatingCloudEncryptionKeyProvider.loadContent();
209210
} catch (CloudStorageException e) {
@@ -247,6 +248,11 @@ public void run() {
247248

248249
ClientSideKeypairService clientSideKeypairService = new ClientSideKeypairService(config, auth, writeLock, clientSideKeypairStoreWriter, clientSideKeypairProvider, siteProvider, keysetManager, keypairGenerator, clock);
249250

251+
var cloudEncryptionSecretGenerator = new CloudSecretGenerator(keyGenerator);
252+
var cloudEncryptionKeyManager = new CloudEncryptionKeyManager(rotatingCloudEncryptionKeyProvider, cloudEncryptionKeyStoreWriter, cloudEncryptionSecretGenerator);
253+
var cloudEncryptionKeyRetentionStrategy = new ExpiredKeyCountRetentionStrategy(clock, 5);
254+
var cloudEncryptionKeyRotationStrategy = new CloudKeyRotationStrategy(cloudEncryptionSecretGenerator, clock, cloudEncryptionKeyRetentionStrategy);
255+
250256
IService[] services = {
251257
new ClientKeyService(config, auth, writeLock, clientKeyStoreWriter, clientKeyProvider, siteProvider, keysetManager, keyGenerator, keyHasher),
252258
new EnclaveIdService(auth, writeLock, enclaveStoreWriter, enclaveIdProvider, clock),
@@ -263,7 +269,7 @@ public void run() {
263269
new PrivateSiteDataRefreshService(auth, jobDispatcher, writeLock, config, rotatingCloudEncryptionKeyProvider),
264270
new JobDispatcherService(auth, jobDispatcher),
265271
new SearchService(auth, clientKeyProvider, operatorKeyProvider),
266-
new CloudEncryptionKeyService(auth, rotatingCloudEncryptionKeyProvider)
272+
new CloudEncryptionKeyService(auth, rotatingCloudEncryptionKeyProvider, cloudEncryptionKeyStoreWriter, siteProvider, cloudEncryptionKeyRotationStrategy)
267273
};
268274

269275

@@ -279,7 +285,7 @@ public void run() {
279285
try {
280286
keysetProvider.loadContent();
281287
} catch (CloudStorageException e) {
282-
if(e.getMessage().contains("The specified key does not exist")){
288+
if (e.getMessage().contains("The specified key does not exist")) {
283289
keysetStoreWriter.upload(new HashMap<>(), null);
284290
keysetProvider.loadContent();
285291
} else {
@@ -305,7 +311,7 @@ public void run() {
305311
The jobs are executed after because they copy data from these files locations consumed by public and private operators.
306312
This caused an issue because the files were empty and the job started to fail so the operators got empty files.
307313
*/
308-
if(enableKeysets) {
314+
if (enableKeysets) {
309315
synchronized (writeLock) {
310316
//UID2-628 keep keys.json and keyset_keys.json in sync. This function syncs them on start up
311317
keysetProvider.loadContent();
@@ -342,7 +348,7 @@ public void run() {
342348
CompletableFuture<Boolean> privateSiteDataSyncJobFuture = jobDispatcher.executeNextJob();
343349
privateSiteDataSyncJobFuture.get();
344350

345-
EncryptedFilesSyncJob encryptedFilesSyncJob = new EncryptedFilesSyncJob(config, writeLock,rotatingCloudEncryptionKeyProvider);
351+
EncryptedFilesSyncJob encryptedFilesSyncJob = new EncryptedFilesSyncJob(config, writeLock, rotatingCloudEncryptionKeyProvider);
346352
jobDispatcher.enqueue(encryptedFilesSyncJob);
347353
CompletableFuture<Boolean> encryptedFilesSyncJobFuture = jobDispatcher.executeNextJob();
348354
encryptedFilesSyncJobFuture.get();
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.uid2.admin.cloudEncryption;
2+
3+
import com.uid2.shared.model.CloudEncryptionKey;
4+
5+
import java.util.Set;
6+
7+
public interface CloudKeyRetentionStrategy {
8+
Set<CloudEncryptionKey> selectKeysToRetain(Set<CloudEncryptionKey> keysForSite);
9+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package com.uid2.admin.cloudEncryption;
2+
3+
import com.google.common.collect.Streams;
4+
import com.uid2.admin.store.Clock;
5+
import com.uid2.shared.model.CloudEncryptionKey;
6+
import com.uid2.shared.model.Site;
7+
8+
import java.util.Collection;
9+
import java.util.Map;
10+
import java.util.Set;
11+
import java.util.stream.Collectors;
12+
import java.util.stream.Stream;
13+
14+
public class CloudKeyRotationStrategy {
15+
private final CloudSecretGenerator secretGenerator;
16+
private final Clock clock;
17+
private final CloudKeyRetentionStrategy keyRetentionStrategy;
18+
19+
public CloudKeyRotationStrategy(
20+
CloudSecretGenerator secretGenerator,
21+
Clock clock,
22+
CloudKeyRetentionStrategy keyRetentionStrategy
23+
) {
24+
this.secretGenerator = secretGenerator;
25+
this.clock = clock;
26+
this.keyRetentionStrategy = keyRetentionStrategy;
27+
}
28+
29+
public Set<CloudEncryptionKey> computeDesiredKeys(
30+
Collection<CloudEncryptionKey> existingKeys,
31+
Collection<Site> sites
32+
) {
33+
var idGenerator = new KeyIdGenerator(existingKeys);
34+
Map<Integer, Set<CloudEncryptionKey>> existingKeysBySite = existingKeys
35+
.stream()
36+
.collect(Collectors.groupingBy(CloudEncryptionKey::getSiteId, Collectors.toSet()));
37+
38+
return sites
39+
.stream()
40+
.map(Site::getId)
41+
.distinct()
42+
.flatMap(siteId -> desiredKeysForSite(siteId, idGenerator, existingKeysBySite.getOrDefault(siteId, Set.of())))
43+
.collect(Collectors.toSet());
44+
}
45+
46+
private Stream<CloudEncryptionKey> desiredKeysForSite(
47+
Integer siteId,
48+
KeyIdGenerator idGenerator,
49+
Set<CloudEncryptionKey> existingKeys
50+
) {
51+
var withNewKey = Streams.concat(existingKeys.stream(), Stream.of(makeNewKey(siteId, idGenerator)));
52+
return keyRetentionStrategy.selectKeysToRetain(withNewKey.collect(Collectors.toSet())).stream();
53+
}
54+
55+
private CloudEncryptionKey makeNewKey(Integer siteId, KeyIdGenerator idGenerator) {
56+
var nowSeconds = clock.getEpochSecond();
57+
var keyId = idGenerator.nextId();
58+
var secret = secretGenerator.generate();
59+
return new CloudEncryptionKey(keyId, siteId, nowSeconds, nowSeconds, secret);
60+
}
61+
62+
private static class KeyIdGenerator {
63+
private int lastId;
64+
65+
public KeyIdGenerator(Collection<CloudEncryptionKey> existingKeys) {
66+
this.lastId = existingKeys.stream().map(CloudEncryptionKey::getId).max(Integer::compareTo).orElse(0);
67+
}
68+
69+
public int nextId() {
70+
return ++lastId;
71+
}
72+
}
73+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.uid2.admin.cloudEncryption;
2+
3+
import com.uid2.shared.secret.SecureKeyGenerator;
4+
5+
public class CloudSecretGenerator {
6+
private final SecureKeyGenerator keyGenerator;
7+
8+
// The SecureKeyGenerator is preferable to the IKeyGenerator interface as it doesn't throw Exception
9+
public CloudSecretGenerator(SecureKeyGenerator keyGenerator) {
10+
this.keyGenerator = keyGenerator;
11+
}
12+
13+
public String generate() {
14+
//Generate a 32-byte key for AesGcm
15+
return keyGenerator.generateRandomKeyString(32);
16+
}
17+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.uid2.admin.cloudEncryption;
2+
3+
import com.uid2.admin.store.Clock;
4+
import com.uid2.shared.model.CloudEncryptionKey;
5+
6+
import java.util.ArrayList;
7+
import java.util.Comparator;
8+
import java.util.Set;
9+
import java.util.stream.Collectors;
10+
import java.util.stream.Stream;
11+
12+
// Keep up to `expiredKeysToRetain` most recent expired keys
13+
// Considers all keys passed, doesn't do grouping by site (handled upstream)
14+
public class ExpiredKeyCountRetentionStrategy implements CloudKeyRetentionStrategy {
15+
private final Clock clock;
16+
private final int expiredKeysToRetain;
17+
18+
public ExpiredKeyCountRetentionStrategy(Clock clock, int expiredKeysToRetain) {
19+
this.clock = clock;
20+
this.expiredKeysToRetain = expiredKeysToRetain;
21+
}
22+
23+
@Override
24+
public Set<CloudEncryptionKey> selectKeysToRetain(Set<CloudEncryptionKey> keysForSite) {
25+
var activeKey = findActiveKey(keysForSite);
26+
if (activeKey == null) {
27+
return keysForSite;
28+
}
29+
30+
var expiredKeys = new ArrayList<CloudEncryptionKey>();
31+
var nonExpiredKeys = new ArrayList<CloudEncryptionKey>();
32+
for (var key : keysForSite) {
33+
if (key.getActivates() < activeKey.getActivates()) {
34+
expiredKeys.add(key);
35+
} else {
36+
nonExpiredKeys.add(key);
37+
}
38+
}
39+
40+
var retainedExpiredKeys = pickRetainedExpiredKeys(expiredKeys);
41+
return Stream.concat(retainedExpiredKeys, nonExpiredKeys.stream()).collect(Collectors.toSet());
42+
43+
}
44+
45+
private Stream<CloudEncryptionKey> pickRetainedExpiredKeys(ArrayList<CloudEncryptionKey> expiredKeys) {
46+
return expiredKeys
47+
.stream()
48+
.sorted(Comparator.comparingLong(CloudEncryptionKey::getActivates).reversed())
49+
.limit(expiredKeysToRetain);
50+
}
51+
52+
private CloudEncryptionKey findActiveKey(Set<CloudEncryptionKey> keys) {
53+
var keysPastActivation = keys.stream().filter(key -> key.getActivates() <= clock.getEpochSecond());
54+
return keysPastActivation
55+
.max(Comparator.comparingLong(CloudEncryptionKey::getActivates))
56+
.orElse(null);
57+
}
58+
}

src/main/java/com/uid2/admin/managers/CloudEncryptionKeyManager.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package com.uid2.admin.managers;
22

3+
import com.uid2.admin.cloudEncryption.CloudSecretGenerator;
34
import com.uid2.admin.store.writer.CloudEncryptionKeyStoreWriter;
45
import com.uid2.shared.auth.OperatorKey;
56
import com.uid2.shared.model.CloudEncryptionKey;
6-
import com.uid2.shared.secret.IKeyGenerator;
77
import com.uid2.shared.store.reader.RotatingCloudEncryptionKeyProvider;
88
import org.slf4j.Logger;
99
import org.slf4j.LoggerFactory;
@@ -21,16 +21,16 @@ public class CloudEncryptionKeyManager {
2121

2222
private final RotatingCloudEncryptionKeyProvider RotatingCloudEncryptionKeyProvider;
2323
private final CloudEncryptionKeyStoreWriter cloudEncryptionKeyStoreWriter;
24-
private final IKeyGenerator keyGenerator;
24+
private final CloudSecretGenerator secretGenerator;
2525

2626
public CloudEncryptionKeyManager(
2727
RotatingCloudEncryptionKeyProvider RotatingCloudEncryptionKeyProvider,
2828
CloudEncryptionKeyStoreWriter cloudEncryptionKeyStoreWriter,
29-
IKeyGenerator keyGenerator
29+
CloudSecretGenerator keyGenerator
3030
) {
3131
this.RotatingCloudEncryptionKeyProvider = RotatingCloudEncryptionKeyProvider;
3232
this.cloudEncryptionKeyStoreWriter = cloudEncryptionKeyStoreWriter;
33-
this.keyGenerator = keyGenerator;
33+
this.secretGenerator = keyGenerator;
3434
}
3535

3636
// Ensures there are `keyCountPerSite` sites for each site corresponding of operatorKeys. If there are less - create new ones.
@@ -89,15 +89,10 @@ private static Set<Integer> uniqueSiteIdsForOperators(Collection<OperatorKey> op
8989

9090
CloudEncryptionKey generateCloudEncryptionKey(int siteId, long activates, long created) throws Exception {
9191
int newKeyId = getNextKeyId();
92-
String secret = generateSecret();
92+
String secret = secretGenerator.generate();
9393
return new CloudEncryptionKey(newKeyId, siteId, activates, created, secret);
9494
}
9595

96-
String generateSecret() throws Exception {
97-
//Generate a 32-byte key for AesGcm
98-
return keyGenerator.generateRandomKeyString(32);
99-
}
100-
10196
void addCloudEncryptionKey(CloudEncryptionKey cloudEncryptionKey) throws Exception {
10297
Map<Integer, CloudEncryptionKey> cloudEncryptionKeys = new HashMap<>(RotatingCloudEncryptionKeyProvider.getAll());
10398
cloudEncryptionKeys.put(cloudEncryptionKey.getId(), cloudEncryptionKey);
@@ -117,7 +112,7 @@ int getNextKeyId() {
117112
CloudEncryptionKey createAndAddImmediateCloudEncryptionKey(int siteId) throws Exception {
118113
int newKeyId = getNextKeyId();
119114
long created = Instant.now().getEpochSecond();
120-
CloudEncryptionKey newKey = new CloudEncryptionKey(newKeyId, siteId, created, created, generateSecret());
115+
CloudEncryptionKey newKey = new CloudEncryptionKey(newKeyId, siteId, created, created, secretGenerator.generate());
121116
addCloudEncryptionKey(newKey);
122117
return newKey;
123118
}

src/main/java/com/uid2/admin/vertx/Endpoints.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ public enum Endpoints {
9999
API_SITE_UPDATE("/api/site/update"),
100100

101101
CLOUD_ENCRYPTION_KEY_LIST("/api/cloud-encryption-key/list"),
102+
CLOUD_ENCRYPTION_KEY_ROTATE("/api/cloud-encryption-key/rotate"),
102103

103104
LOGIN("/login"),
104105
LOGOUT("/logout"),

0 commit comments

Comments
 (0)