Moving secret values out of the main configuration file

This commit is contained in:
Sergey Skrobotov
2023-05-17 11:14:04 -07:00
parent 8d1c26d07d
commit 287e2fa89a
57 changed files with 959 additions and 551 deletions

View File

@@ -5,10 +5,11 @@
package org.whispersystems.textsecuregcm.configuration;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
public record AdminEventLoggingConfiguration(
@NotEmpty String credentials,
@NotBlank String credentials,
@NotEmpty String projectId,
@NotEmpty String logName) {
}

View File

@@ -1,51 +1,17 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
public class ApnConfiguration {
@NotEmpty
@JsonProperty
private String teamId;
@NotEmpty
@JsonProperty
private String keyId;
@NotEmpty
@JsonProperty
private String signingKey;
@NotEmpty
@JsonProperty
private String bundleId;
@JsonProperty
private boolean sandbox = false;
public String getTeamId() {
return teamId;
}
public String getKeyId() {
return keyId;
}
public String getSigningKey() {
return signingKey;
}
public String getBundleId() {
return bundleId;
}
public boolean isSandboxEnabled() {
return sandbox;
}
public record ApnConfiguration(@NotBlank String teamId,
@NotBlank String keyId,
@NotNull SecretString signingKey,
@NotBlank String bundleId,
boolean sandbox) {
}

View File

@@ -5,35 +5,17 @@
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import static org.apache.commons.lang3.ObjectUtils.firstNonNull;
import java.time.Duration;
import java.util.HexFormat;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
import org.whispersystems.textsecuregcm.util.ExactlySize;
public class ArtServiceConfiguration {
@NotEmpty
@JsonProperty
private String userAuthenticationTokenSharedSecret;
@NotEmpty
@JsonProperty
private String userAuthenticationTokenUserIdSecret;
@JsonProperty
@NotNull
private Duration tokenExpiration = Duration.ofDays(1);
public byte[] getUserAuthenticationTokenSharedSecret() {
return HexFormat.of().parseHex(userAuthenticationTokenSharedSecret);
}
public byte[] getUserAuthenticationTokenUserIdSecret() {
return HexFormat.of().parseHex(userAuthenticationTokenUserIdSecret);
}
public Duration getTokenExpiration() {
return tokenExpiration;
public record ArtServiceConfiguration(@ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret,
@NotNull SecretBytes userAuthenticationTokenUserIdSecret,
@NotNull Duration tokenExpiration) {
public ArtServiceConfiguration {
tokenExpiration = firstNonNull(tokenExpiration, Duration.ofDays(1));
}
}

View File

@@ -1,43 +1,15 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
public class AwsAttachmentsConfiguration {
@NotEmpty
@JsonProperty
private String accessKey;
@NotEmpty
@JsonProperty
private String accessSecret;
@NotEmpty
@JsonProperty
private String bucket;
@NotEmpty
@JsonProperty
private String region;
public String getAccessKey() {
return accessKey;
}
public String getAccessSecret() {
return accessSecret;
}
public String getBucket() {
return bucket;
}
public String getRegion() {
return region;
}
public record AwsAttachmentsConfiguration(@NotNull SecretString accessKey,
@NotNull SecretString accessSecret,
@NotBlank String bucket,
@NotBlank String region) {
}

View File

@@ -11,6 +11,7 @@ import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
/**
* @param merchantId the Braintree merchant ID
@@ -24,7 +25,7 @@ import javax.validation.constraints.NotNull;
*/
public record BraintreeConfiguration(@NotBlank String merchantId,
@NotBlank String publicKey,
@NotBlank String privateKey,
@NotNull SecretString privateKey,
@NotBlank String environment,
@NotEmpty Set<@NotBlank String> supportedCurrencies,
@NotBlank String graphqlUrl,

View File

@@ -1,44 +1,16 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotEmpty;
public class CdnConfiguration {
@NotEmpty
@JsonProperty
private String accessKey;
@NotEmpty
@JsonProperty
private String accessSecret;
@NotEmpty
@JsonProperty
private String bucket;
@NotEmpty
@JsonProperty
private String region;
public String getAccessKey() {
return accessKey;
}
public String getAccessSecret() {
return accessSecret;
}
public String getBucket() {
return bucket;
}
public String getRegion() {
return region;
}
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
public record CdnConfiguration(@NotNull SecretString accessKey,
@NotNull SecretString accessSecret,
@NotBlank String bucket,
@NotBlank String region) {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2021 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -7,16 +7,17 @@ package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.micrometer.datadog.DatadogConfig;
import java.time.Duration;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.time.Duration;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
public class DatadogConfiguration implements DatadogConfig {
@JsonProperty
@NotBlank
private String apiKey;
@NotNull
private SecretString apiKey;
@JsonProperty
@NotNull
@@ -32,7 +33,7 @@ public class DatadogConfiguration implements DatadogConfig {
@Override
public String apiKey() {
return apiKey;
return apiKey.value();
}
@Override

View File

@@ -1,11 +1,12 @@
/*
* Copyright 2013-2023 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
import org.whispersystems.textsecuregcm.util.ExactlySize;
public record DirectoryV2ClientConfiguration(@ExactlySize({32}) byte[] userAuthenticationTokenSharedSecret,
@ExactlySize({32}) byte[] userIdTokenSharedSecret) {
public record DirectoryV2ClientConfiguration(@ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret,
@ExactlySize(32) SecretBytes userIdTokenSharedSecret) {
}

View File

@@ -1,11 +1,12 @@
/*
* Copyright 2013-2022 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
public record FcmConfiguration(@NotBlank String credentials) {
public record FcmConfiguration(@NotNull SecretString credentials) {
}

View File

@@ -1,57 +1,22 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.dropwizard.util.Strings;
import io.dropwizard.validation.ValidationMethod;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
public class GcpAttachmentsConfiguration {
@NotEmpty
@JsonProperty
private String domain;
@NotEmpty
@JsonProperty
private String email;
@JsonProperty
@Min(1)
private int maxSizeInBytes;
@JsonProperty
private String pathPrefix;
@NotEmpty
@JsonProperty
private String rsaSigningKey;
public String getDomain() {
return domain;
}
public String getEmail() {
return email;
}
public int getMaxSizeInBytes() {
return maxSizeInBytes;
}
public String getPathPrefix() {
return pathPrefix;
}
public String getRsaSigningKey() {
return rsaSigningKey;
}
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
public record GcpAttachmentsConfiguration(@NotBlank String domain,
@NotBlank String email,
@Min(1) int maxSizeInBytes,
String pathPrefix,
@NotNull SecretString rsaSigningKey) {
@SuppressWarnings("unused")
@ValidationMethod(message = "pathPrefix must be empty or start with /")
public boolean isPathPrefixValid() {

View File

@@ -1,15 +1,12 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
public record GenericZkConfig (
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotNull
byte[] serverSecret
) {}
public record GenericZkConfig(@NotNull SecretBytes serverSecret) {
}

View File

@@ -1,11 +1,12 @@
/*
* Copyright 2021-2022 Signal Messenger, LLC
* Copyright 2021 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
public record HCaptchaConfiguration(@NotBlank String apiKey) {
public record HCaptchaConfiguration(@NotNull SecretString apiKey) {
}

View File

@@ -1,56 +1,21 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.HexFormat;
import java.util.List;
import java.util.Map;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
public class PaymentsServiceConfiguration {
@NotEmpty
@JsonProperty
private String userAuthenticationTokenSharedSecret;
@NotBlank
@JsonProperty
private String coinMarketCapApiKey;
@JsonProperty
@NotEmpty
private Map<@NotBlank String, Integer> coinMarketCapCurrencyIds;
@NotEmpty
@JsonProperty
private String fixerApiKey;
@NotEmpty
@JsonProperty
private List<String> paymentCurrencies;
public byte[] getUserAuthenticationTokenSharedSecret() {
return HexFormat.of().parseHex(userAuthenticationTokenSharedSecret);
}
public String getCoinMarketCapApiKey() {
return coinMarketCapApiKey;
}
public Map<String, Integer> getCoinMarketCapCurrencyIds() {
return coinMarketCapCurrencyIds;
}
public String getFixerApiKey() {
return fixerApiKey;
}
public List<String> getPaymentCurrencies() {
return paymentCurrencies;
}
public record PaymentsServiceConfiguration(@NotNull SecretBytes userAuthenticationTokenSharedSecret,
@NotNull SecretString coinMarketCapApiKey,
@NotNull SecretString fixerApiKey,
@NotEmpty Map<@NotBlank String, Integer> coinMarketCapCurrencyIds,
@NotEmpty List<String> paymentCurrencies) {
}

View File

@@ -1,33 +1,14 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import javax.validation.constraints.NotNull;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretStringList;
public class RemoteConfigConfiguration {
@JsonProperty
@NotNull
private List<String> authorizedTokens = new LinkedList<>();
@NotNull
@JsonProperty
private Map<String, String> globalConfig = new HashMap<>();
public List<String> getAuthorizedTokens() {
return authorizedTokens;
}
public Map<String, String> getGlobalConfig() {
return globalConfig;
}
public record RemoteConfigConfiguration(@NotNull SecretStringList authorizedTokens,
@NotNull Map<String, String> globalConfig) {
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
@@ -7,18 +7,18 @@ package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.annotations.VisibleForTesting;
import java.util.HexFormat;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
public class SecureBackupServiceConfiguration {
@NotEmpty
@NotNull
@JsonProperty
private String userAuthenticationTokenSharedSecret;
private SecretBytes userAuthenticationTokenSharedSecret;
@NotBlank
@JsonProperty
@@ -38,8 +38,8 @@ public class SecureBackupServiceConfiguration {
@JsonProperty
private RetryConfiguration retry = new RetryConfiguration();
public byte[] getUserAuthenticationTokenSharedSecret() {
return HexFormat.of().parseHex(userAuthenticationTokenSharedSecret);
public SecretBytes userAuthenticationTokenSharedSecret() {
return userAuthenticationTokenSharedSecret;
}
@VisibleForTesting

View File

@@ -5,18 +5,18 @@
package org.whispersystems.textsecuregcm.configuration;
import java.util.HexFormat;
import java.util.List;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
public record SecureStorageServiceConfiguration(@NotEmpty String userAuthenticationTokenSharedSecret,
public record SecureStorageServiceConfiguration(@NotNull SecretBytes userAuthenticationTokenSharedSecret,
@NotBlank String uri,
@NotEmpty List<@NotBlank String> storageCaCertificates,
@Valid CircuitBreakerConfiguration circuitBreaker,
@Valid RetryConfiguration retry) {
public SecureStorageServiceConfiguration {
if (circuitBreaker == null) {
circuitBreaker = new CircuitBreakerConfiguration();
@@ -25,8 +25,4 @@ public record SecureStorageServiceConfiguration(@NotEmpty String userAuthenticat
retry = new RetryConfiguration();
}
}
public byte[] decodeUserAuthenticationTokenSharedSecret() {
return HexFormat.of().parseHex(userAuthenticationTokenSharedSecret);
}
}

View File

@@ -9,13 +9,14 @@ import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
import org.whispersystems.textsecuregcm.util.ExactlySize;
public record SecureValueRecovery2Configuration(
boolean enabled,
@NotBlank String uri,
@ExactlySize({32}) byte[] userAuthenticationTokenSharedSecret,
@ExactlySize({32}) byte[] userIdTokenSharedSecret,
@ExactlySize(32) SecretBytes userAuthenticationTokenSharedSecret,
@ExactlySize(32) SecretBytes userIdTokenSharedSecret,
@NotEmpty List<@NotBlank String> svrCaCertificates,
@NotNull @Valid CircuitBreakerConfiguration circuitBreaker,
@NotNull @Valid RetryConfiguration retry) {

View File

@@ -8,10 +8,12 @@ package org.whispersystems.textsecuregcm.configuration;
import java.util.Set;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretString;
public record StripeConfiguration(@NotBlank String apiKey,
@NotEmpty byte[] idempotencyKeyGenerator,
public record StripeConfiguration(@NotNull SecretString apiKey,
@NotNull SecretBytes idempotencyKeyGenerator,
@NotBlank String boostDescription,
@NotEmpty Set<@NotBlank String> supportedCurrencies) {
}

View File

@@ -1,48 +1,21 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import javax.validation.constraints.NotNull;
import org.signal.libsignal.protocol.InvalidKeyException;
import org.signal.libsignal.protocol.ecc.Curve;
import org.signal.libsignal.protocol.ecc.ECPrivateKey;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
import org.whispersystems.textsecuregcm.util.ExactlySize;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class UnidentifiedDeliveryConfiguration {
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotNull
private byte[] certificate;
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotNull
@Size(min = 32, max = 32)
private byte[] privateKey;
@NotNull
private int expiresDays;
public byte[] getCertificate() {
return certificate;
}
public ECPrivateKey getPrivateKey() throws InvalidKeyException {
return Curve.decodePrivatePoint(privateKey);
}
public int getExpiresDays() {
return expiresDays;
public record UnidentifiedDeliveryConfiguration(@NotNull SecretBytes certificate,
@ExactlySize(32) SecretBytes privateKey,
int expiresDays) {
public ECPrivateKey ecPrivateKey() throws InvalidKeyException {
return Curve.decodePrivatePoint(privateKey.value());
}
}

View File

@@ -1,36 +1,14 @@
/*
* Copyright 2013-2020 Signal Messenger, LLC
* Copyright 2013 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.whispersystems.textsecuregcm.util.ByteArrayAdapter;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import org.whispersystems.textsecuregcm.configuration.secrets.SecretBytes;
public class ZkConfig {
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotNull
private byte[] serverSecret;
@JsonProperty
@JsonSerialize(using = ByteArrayAdapter.Serializing.class)
@JsonDeserialize(using = ByteArrayAdapter.Deserializing.class)
@NotNull
private byte[] serverPublic;
public byte[] getServerSecret() {
return serverSecret;
}
public byte[] getServerPublic() {
return serverPublic;
}
public record ZkConfig(@NotNull SecretBytes serverSecret,
@NotEmpty byte[] serverPublic) {
}

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.secrets;
import static java.util.Objects.requireNonNull;
import java.lang.annotation.Annotation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public abstract class BaseSecretValidator<A extends Annotation, T, S extends Secret<? extends T>> implements ConstraintValidator<A, S> {
private final ConstraintValidator<A, T> validator;
protected BaseSecretValidator(final ConstraintValidator<A, T> validator) {
this.validator = requireNonNull(validator);
}
@Override
public void initialize(final A constraintAnnotation) {
validator.initialize(constraintAnnotation);
}
@Override
public boolean isValid(final S value, final ConstraintValidatorContext context) {
return validator.isValid(value.value(), context);
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.secrets;
public class Secret<T> {
private final T value;
public Secret(final T value) {
this.value = value;
}
public T value() {
return value;
}
@Override
public String toString() {
return "[REDACTED]";
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.secrets;
import org.apache.commons.lang3.Validate;
public class SecretBytes extends Secret<byte[]> {
public SecretBytes(final byte[] value) {
super(requireNotEmpty(value));
}
private static byte[] requireNotEmpty(final byte[] value) {
Validate.isTrue(value.length > 0, "SecretBytes value must not be empty");
return value;
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.secrets;
import com.google.common.collect.ImmutableList;
import java.util.Collection;
import java.util.List;
import javax.validation.constraints.NotEmpty;
import org.hibernate.validator.internal.constraintvalidators.bv.notempty.NotEmptyValidatorForCollection;
public class SecretBytesList extends Secret<List<byte[]>> {
@SuppressWarnings("rawtypes")
public static class ValidatorNotEmpty extends BaseSecretValidator<NotEmpty, Collection, SecretBytesList> {
public ValidatorNotEmpty() {
super(new NotEmptyValidatorForCollection());
}
}
public SecretBytesList(final List<byte[]> value) {
super(ImmutableList.copyOf(value));
}
}

View File

@@ -0,0 +1,106 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.secrets;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.google.common.annotations.VisibleForTesting;
import java.io.File;
import java.io.IOException;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.whispersystems.textsecuregcm.util.SystemMapper;
public class SecretStore {
private final Map<String, Secret<?>> secrets;
public static SecretStore fromYamlFileSecretsBundle(final String filename) {
try {
@SuppressWarnings("unchecked")
final Map<String, Object> secretsBundle = SystemMapper.yamlMapper().readValue(new File(filename), Map.class);
return fromSecretsBundle(secretsBundle);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to parse YAML file [%s]".formatted(filename), e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public SecretStore(final Map<String, Secret<?>> secrets) {
this.secrets = Map.copyOf(secrets);
}
public SecretString secretString(final String reference) {
return fromStore(reference, SecretString.class);
}
public SecretBytes secretBytesFromBase64String(final String reference) {
final SecretString secret = fromStore(reference, SecretString.class);
return new SecretBytes(decodeBase64(secret.value()));
}
public SecretStringList secretStringList(final String reference) {
return fromStore(reference, SecretStringList.class);
}
public SecretBytesList secretBytesListFromBase64Strings(final String reference) {
final List<String> secrets = secretStringList(reference).value();
final List<byte[]> byteSecrets = secrets.stream().map(SecretStore::decodeBase64).toList();
return new SecretBytesList(byteSecrets);
}
private <T extends Secret<?>> T fromStore(final String name, final Class<T> expected) {
final Secret<?> secret = secrets.get(name);
if (secret == null) {
throw new IllegalArgumentException("Secret [%s] is not present in the secrets bundle".formatted(name));
}
if (!expected.isInstance(secret)) {
throw new IllegalArgumentException("Secret [%s] is of type [%s] but caller expects type [%s]".formatted(
name, secret.getClass().getSimpleName(), expected.getSimpleName()));
}
return expected.cast(secret);
}
@VisibleForTesting
public static SecretStore fromYamlStringSecretsBundle(final String secretsBundleYaml) {
try {
@SuppressWarnings("unchecked")
final Map<String, Object> secretsBundle = SystemMapper.yamlMapper().readValue(secretsBundleYaml, Map.class);
return fromSecretsBundle(secretsBundle);
} catch (JsonProcessingException e) {
throw new RuntimeException("Failed to parse JSON", e);
}
}
private static SecretStore fromSecretsBundle(final Map<String, Object> secretsBundle) {
final Map<String, Secret<?>> store = new HashMap<>();
secretsBundle.forEach((k, v) -> {
if (v instanceof final String str) {
store.put(k, new SecretString(str));
return;
}
if (v instanceof final List<?> list) {
final List<String> secrets = list.stream().map(o -> {
if (o instanceof final String s) {
return s;
}
throw new IllegalArgumentException("Secrets bundle JSON object is only supposed to have values of types String and list of Strings");
}).toList();
store.put(k, new SecretStringList(secrets));
return;
}
throw new IllegalArgumentException("Secrets bundle JSON object is only supposed to have values of types String and list of Strings");
});
return new SecretStore(store);
}
private static byte[] decodeBase64(final String str) {
return Base64.getDecoder().decode(str);
}
}

View File

@@ -0,0 +1,14 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.secrets;
import org.apache.commons.lang3.Validate;
public class SecretString extends Secret<String> {
public SecretString(final String value) {
super(Validate.notBlank(value, "SecretString value must not be blank"));
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.secrets;
import com.google.common.collect.ImmutableList;
import java.util.Collection;
import java.util.List;
import javax.validation.constraints.NotEmpty;
import org.hibernate.validator.internal.constraintvalidators.bv.notempty.NotEmptyValidatorForCollection;
public class SecretStringList extends Secret<List<String>> {
@SuppressWarnings("rawtypes")
public static class ValidatorNotEmpty extends BaseSecretValidator<NotEmpty, Collection, SecretStringList> {
public ValidatorNotEmpty() {
super(new NotEmptyValidatorForCollection());
}
}
public SecretStringList(final List<String> value) {
super(ImmutableList.copyOf(value));
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2023 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.whispersystems.textsecuregcm.configuration.secrets;
import static java.util.Objects.requireNonNull;
import com.fasterxml.jackson.core.JacksonException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.BiFunction;
public class SecretsModule extends SimpleModule {
public static final SecretsModule INSTANCE = new SecretsModule();
public static final String PREFIX = "secret://";
private final AtomicReference<SecretStore> secretStoreHolder = new AtomicReference<>(null);
private SecretsModule() {
addDeserializer(SecretString.class, createDeserializer(SecretStore::secretString));
addDeserializer(SecretBytes.class, createDeserializer(SecretStore::secretBytesFromBase64String));
addDeserializer(SecretStringList.class, createDeserializer(SecretStore::secretStringList));
addDeserializer(SecretBytesList.class, createDeserializer(SecretStore::secretBytesListFromBase64Strings));
}
public void setSecretStore(final SecretStore secretStore) {
this.secretStoreHolder.set(requireNonNull(secretStore));
}
private <T> JsonDeserializer<T> createDeserializer(final BiFunction<SecretStore, String, T> constructor) {
return new JsonDeserializer<>() {
@Override
public T deserialize(final JsonParser p, final DeserializationContext ctxt) throws IOException, JacksonException {
final SecretStore secretStore = secretStoreHolder.get();
if (secretStore == null) {
throw new IllegalStateException(
"An instance of a SecretStore must be set for the SecretsModule via setSecretStore() method");
}
final String reference = p.getValueAsString();
if (!reference.startsWith(PREFIX) || reference.length() <= PREFIX.length()) {
throw new IllegalArgumentException(
"Value of a secret field must start with a [%s] prefix and refer to an entry in a secrets bundle".formatted(PREFIX));
}
return constructor.apply(secretStore, reference.substring(PREFIX.length()));
}
};
}
}