Skip to content

Commit 39c3bc1

Browse files
committed
Cached Cipher instances in AES; Optimized and cached oldest key by site id in RotatingClientKeyProvider
1 parent daee856 commit 39c3bc1

File tree

2 files changed

+45
-13
lines changed

2 files changed

+45
-13
lines changed

src/main/java/com/uid2/shared/encryption/AesGcm.java

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,27 @@
44
import com.uid2.shared.model.EncryptionKey;
55
import com.uid2.shared.model.KeyIdentifier;
66
import com.uid2.shared.model.KeysetKey;
7-
import io.vertx.core.buffer.Buffer;
87

98
import javax.crypto.Cipher;
109
import javax.crypto.SecretKey;
1110
import javax.crypto.spec.GCMParameterSpec;
1211
import javax.crypto.spec.SecretKeySpec;
1312
import java.nio.charset.StandardCharsets;
13+
import java.security.GeneralSecurityException;
1414

1515
public class AesGcm {
16-
private static final String cipherScheme = "AES/GCM/NoPadding";
16+
private static final String CIPHER_SCHEME = "AES/GCM/NoPadding";
1717
public static final int GCM_AUTHTAG_LENGTH = 16;
1818
public static final int GCM_IV_LENGTH = 12;
1919

20+
private static final ThreadLocal<Cipher> CIPHER = ThreadLocal.withInitial(() -> {
21+
try {
22+
return Cipher.getInstance(CIPHER_SCHEME);
23+
} catch (GeneralSecurityException e) {
24+
throw new RuntimeException("Unable to create cipher", e);
25+
}
26+
});
27+
2028
public static EncryptedPayload encrypt(byte[] b, KeysetKey key) {
2129
return encrypt(b, key.getKeyBytes(), key.getKeyIdentifier());
2230
}
@@ -32,11 +40,16 @@ private static EncryptedPayload encrypt(byte[] b, byte[] secretBytes, KeyIdentif
3240
public static byte[] encrypt(byte[] b, byte[] secretBytes) {
3341
try {
3442
final SecretKey k = new SecretKeySpec(secretBytes, "AES");
35-
final Cipher c = Cipher.getInstance(cipherScheme);
43+
final Cipher c = CIPHER.get();
3644
final byte[] ivBytes = Random.getBytes(GCM_IV_LENGTH);
37-
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_AUTHTAG_LENGTH * 8, ivBytes);
45+
final GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_AUTHTAG_LENGTH * 8, ivBytes);
3846
c.init(Cipher.ENCRYPT_MODE, k, gcmParameterSpec);
39-
return Buffer.buffer().appendBytes(ivBytes).appendBytes(c.doFinal(b)).getBytes();
47+
48+
// Pre-allocate output: IV + ciphertext + auth tag
49+
final byte[] result = new byte[GCM_IV_LENGTH + c.getOutputSize(b.length)];
50+
System.arraycopy(ivBytes, 0, result, 0, GCM_IV_LENGTH);
51+
c.doFinal(b, 0, b.length, result, GCM_IV_LENGTH);
52+
return result;
4053
} catch (Exception e) {
4154
throw new RuntimeException("Unable to Encrypt", e);
4255
}
@@ -50,9 +63,10 @@ public static byte[] decrypt(byte[] encryptedBytes, int offset, byte[] secretByt
5063
try {
5164
final SecretKey key = new SecretKeySpec(secretBytes, "AES");
5265
final GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_AUTHTAG_LENGTH * 8, encryptedBytes, offset, GCM_IV_LENGTH);
53-
final Cipher c = Cipher.getInstance(cipherScheme);
66+
final Cipher c = CIPHER.get();
5467
c.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
55-
return c.doFinal(encryptedBytes, offset + GCM_IV_LENGTH, encryptedBytes.length - offset - GCM_IV_LENGTH);
68+
final int dataOffset = offset + GCM_IV_LENGTH;
69+
return c.doFinal(encryptedBytes, dataOffset, encryptedBytes.length - dataOffset);
5670
} catch (Exception e) {
5771
throw new RuntimeException("Unable to Decrypt", e);
5872
}

src/main/java/com/uid2/shared/store/reader/RotatingClientKeyProvider.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

1515
import java.util.Collection;
1616
import java.util.Comparator;
17+
import java.util.Optional;
18+
import java.util.concurrent.ConcurrentHashMap;
1719

1820
/*
1921
1. metadata.json format
@@ -42,6 +44,10 @@
4244
public class RotatingClientKeyProvider implements IClientKeyProvider, StoreReader<Collection<ClientKey>> {
4345
private final ScopedStoreReader<Collection<ClientKey>> reader;
4446
private final AuthorizableStore<ClientKey> authorizableStore;
47+
private final ConcurrentHashMap<Integer, VersionedValue> oldestClientKeyBySiteIdCache = new ConcurrentHashMap<>();
48+
private volatile long snapshotVersion = 0;
49+
50+
private record VersionedValue(long version, Optional<ClientKey> value) {}
4551

4652
public RotatingClientKeyProvider(DownloadCloudStorage fileStreamProvider, StoreScope scope) {
4753
this.reader = new ScopedStoreReader<>(fileStreamProvider, scope, new ClientParser(), "auth keys");
@@ -64,9 +70,13 @@ public long getVersion(JsonObject metadata) {
6470
}
6571

6672
@Override
67-
public long loadContent(JsonObject metadata) throws Exception {
73+
public long loadContent(JsonObject metadata) throws Exception {
6874
long version = reader.loadContent(metadata, "client_keys");
6975
authorizableStore.refresh(getAll());
76+
77+
// Versioning to prevent race conditions when reading the oldest client key
78+
oldestClientKeyBySiteIdCache.clear();
79+
snapshotVersion = getVersion(metadata);
7080
return version;
7181
}
7282

@@ -102,10 +112,18 @@ public IAuthorizable get(String key) {
102112

103113
@Override
104114
public ClientKey getOldestClientKey(int siteId) {
105-
return this.reader.getSnapshot().stream()
106-
.filter(k -> k.getSiteId() == siteId) // filter by site id
107-
.sorted(Comparator.comparing(ClientKey::getCreated)) // sort by key creation timestamp ascending
108-
.findFirst() // return the oldest key
109-
.orElse(null);
115+
long currentVersion = snapshotVersion;
116+
VersionedValue cached = oldestClientKeyBySiteIdCache.get(siteId);
117+
118+
if (cached != null && cached.version() == currentVersion) {
119+
return cached.value().orElse(null);
120+
}
121+
122+
Optional<ClientKey> computed = this.reader.getSnapshot().stream()
123+
.filter(k -> k.getSiteId() == siteId)
124+
.min(Comparator.comparingLong(ClientKey::getCreated));
125+
126+
oldestClientKeyBySiteIdCache.put(siteId, new VersionedValue(currentVersion, computed));
127+
return computed.orElse(null);
110128
}
111129
}

0 commit comments

Comments
 (0)