Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executors;
Expand All @@ -41,6 +45,12 @@ final class AgentlessConfigurationSource implements ConfigurationSourceService {

private static final String DATADOG_UFC_RULES_BASED_SERVER_PATH =
"/api/v2/feature-flagging/config/rules-based/server";
private static final String BASE62_ALPHABET =
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final String API_KEY_FINGERPRINT_HEADER = "DD-API-KEY-FINGERPRINT";
private static final String CLIFFORD_SHA256_PREFIX = "rijn_";
private static final int CLIFFORD_SHA256_BASE62_LENGTH = 43;
private static final BigInteger BASE_62 = BigInteger.valueOf(62);
private static final int MAX_ATTEMPTS = 3;
private static final int MINUTES_BETWEEN_WARNINGS = 5;
private static final long FIRST_RETRY_MIN_MILLIS = 2_000;
Expand Down Expand Up @@ -289,6 +299,27 @@ static long millis(final int seconds) {
return TimeUnit.SECONDS.toMillis(seconds);
}

static String apiKeyFingerprint(final String apiKey) {
final byte[] digest;
try {
digest = MessageDigest.getInstance("SHA-256").digest(apiKey.getBytes(StandardCharsets.UTF_8));
} catch (final NoSuchAlgorithmException error) {
throw new IllegalStateException("SHA-256 is not available", error);
}

BigInteger value = new BigInteger(1, digest);
final StringBuilder encoded = new StringBuilder(CLIFFORD_SHA256_BASE62_LENGTH);
while (value.signum() > 0) {
final BigInteger[] quotientAndRemainder = value.divideAndRemainder(BASE_62);
encoded.append(BASE62_ALPHABET.charAt(quotientAndRemainder[1].intValue()));
value = quotientAndRemainder[0];
}
while (encoded.length() < CLIFFORD_SHA256_BASE62_LENGTH) {
encoded.append('0');
}
return CLIFFORD_SHA256_PREFIX + encoded.reverse();
}

static long retryDelayMillis(
final long pollIntervalMillis, final int attempt, final double jitter) {
final long baseDelay;
Expand Down Expand Up @@ -363,11 +394,14 @@ public UfcHttpResponse fetch(final HttpUrl endpoint, final Config config, final
if (etag != null) {
headers.put("If-None-Match", etag);
}
final boolean datadogManagedEndpoint = isDatadogManagedEndpoint(endpoint, config);
final String apiKey = config.getApiKey();
if (datadogManagedEndpoint && apiKey != null) {
headers.put(API_KEY_FINGERPRINT_HEADER, apiKeyFingerprint(apiKey));
}
// Leave Accept-Encoding unset so OkHttp negotiates gzip and transparently decompresses it.
final Request request =
prepareRequest(endpoint, headers, config, isDatadogManagedEndpoint(endpoint, config))
.get()
.build();
prepareRequest(endpoint, headers, config, datadogManagedEndpoint).get().build();
if (!fetching.compareAndSet(false, true)) {
throw new IllegalStateException("Feature Flagging HTTP request already in flight");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,22 @@ void defaultConstructorBuildsHttpClientFromConfig() {
service.close();
}

@Test
void generatesCliffordV1ApiKeyFingerprints() {
assertEquals(
"rijn_RZwTDmWjELXeEmMEb0eIIegKayGGUPNsuJweEPhlXi5",
AgentlessConfigurationSource.apiKeyFingerprint(""));
assertEquals(
"rijn_SddeOoHbx2gFQTblESRn88xKs2B9zEA9ii1CclRfD6s",
AgentlessConfigurationSource.apiKeyFingerprint("7b58e278012eec8316224b052d87e6e8b2ba9e49"));
assertEquals(
"rijn_eFLHeyLxwaiNs2hY16pjkjNjVSHWRgf2rlveKc8YA1K",
AgentlessConfigurationSource.apiKeyFingerprint("!@#$%^𐍈한€हИ£"));
assertEquals(
"rijn_053ybBRXypQt9AC6UIlqH1YCFYSV1rQl8HCDIcBZs3D",
AgentlessConfigurationSource.apiKeyFingerprint("padding-171"));
}

@Test
void realHttpClientDoesNotSendApiKeyOverHttp() throws Exception {
try (JavaTestHttpServer server =
Expand All @@ -162,6 +178,7 @@ void realHttpClientDoesNotSendApiKeyOverHttp() throws Exception {
assertEquals("etag-b", response.etag);
assertEquals(emptyConfig(), new String(response.body, UTF_8));
assertNull(server.getLastRequest().getHeader("DD-API-KEY"));
assertNull(server.getLastRequest().getHeader("DD-API-KEY-FINGERPRINT"));
assertEquals("etag-a", server.getLastRequest().getHeader("If-None-Match"));
assertEquals("java", server.getLastRequest().getHeader("Datadog-Meta-Lang"));
assertEquals("gzip", server.getLastRequest().getHeader("Accept-Encoding"));
Expand All @@ -182,6 +199,9 @@ void sendsApiKeyToDefaultDatadogHttpsEndpoint() throws Exception {
client.fetch(endpoint, config(), null);

assertEquals("test-api-key", requests.get(0).header("DD-API-KEY"));
assertEquals(
"rijn_i8Jug5ocjALL7JZiV1a8HzXqkwDRKcE7hK9IouPQwio",
requests.get(0).header("DD-API-KEY-FINGERPRINT"));
}

@Test
Expand All @@ -198,6 +218,7 @@ void stripsApiKeyFromCustomHttpsEndpoint() throws Exception {
client.fetch(endpoint, config, null);

assertNull(requests.get(0).header("DD-API-KEY"));
assertNull(requests.get(0).header("DD-API-KEY-FINGERPRINT"));
}

@Test
Expand All @@ -210,6 +231,7 @@ void stripsApiKeyFromUnexpectedHttpsEndpoint() throws Exception {
client.fetch(endpoint, config(), null);

assertNull(requests.get(0).header("DD-API-KEY"));
assertNull(requests.get(0).header("DD-API-KEY-FINGERPRINT"));
}

@Test
Expand Down