Port StickerController to gRPC
This commit is contained in:
parent
1876b71d39
commit
60002a8a0f
@ -234,7 +234,6 @@ import org.whispersystems.textsecuregcm.redis.ConnectionEventLogger;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisClient;
|
||||
import org.whispersystems.textsecuregcm.redis.FaultTolerantRedisClusterClient;
|
||||
import org.whispersystems.textsecuregcm.registration.RegistrationServiceClient;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.s3.S3MonitoringSupplier;
|
||||
import org.whispersystems.textsecuregcm.securestorage.SecureStorageClient;
|
||||
@ -898,11 +897,15 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
config.getGcpAttachmentsConfiguration().pathPrefix(),
|
||||
config.getGcpAttachmentsConfiguration().rsaSigningKey().value());
|
||||
|
||||
PostPolicyGenerator profileCdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().region(),
|
||||
config.getCdnConfiguration().bucket(), config.getCdnConfiguration().credentials().accessKeyId().value());
|
||||
PolicySigner profileCdnPolicySigner = new PolicySigner(
|
||||
config.getCdnConfiguration().credentials().secretAccessKey().value(),
|
||||
config.getCdnConfiguration().region());
|
||||
final PostPolicyGenerator profileCdnPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().region(),
|
||||
config.getCdnConfiguration().bucket(),
|
||||
config.getCdnConfiguration().credentials().accessKeyId().value(),
|
||||
config.getCdnConfiguration().credentials().secretAccessKey().value());
|
||||
|
||||
final PostPolicyGenerator stickerPolicyGenerator = new PostPolicyGenerator(config.getCdnConfiguration().region(),
|
||||
config.getCdnConfiguration().bucket(),
|
||||
config.getCdnConfiguration().credentials().accessKeyId().value(),
|
||||
config.getCdnConfiguration().credentials().secretAccessKey().value());
|
||||
|
||||
ServerSecretParams zkSecretParams = new ServerSecretParams(config.getZkConfig().serverSecret().value());
|
||||
GenericServerSecretParams callingGenericZkSecretParams = new GenericServerSecretParams(config.getCallingZkConfig().serverSecret().value());
|
||||
@ -1057,12 +1060,13 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new CallingGrpcService(cloudflareTurnCredentialsManager, rateLimiters),
|
||||
new CredentialsGrpcService(accountsManager, certificateGenerator, zkAuthOperations, callingGenericZkSecretParams, rateLimiters, Clock.systemUTC(), ExternalServiceDefinitions.createExternalServiceList(config, Clock.systemUTC())),
|
||||
new KeysGrpcService(accountsManager, keysManager, rateLimiters),
|
||||
new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager, config.getBadges(), profileCdnPolicyGenerator, profileCdnPolicySigner, profileBadgeConverter, rateLimiters),
|
||||
new ProfileGrpcService(clock, accountsManager, profilesManager, dynamicConfigurationManager, config.getBadges(), profileCdnPolicyGenerator, profileBadgeConverter, rateLimiters),
|
||||
new MessagesGrpcService(accountsManager, rateLimiters, messageSender, messageByteLimitCardinalityEstimator, spamChecker, messageDispatcher, Clock.systemUTC()),
|
||||
new BackupsGrpcService(accountsManager, backupAuthManager, backupMetrics),
|
||||
new DevicesGrpcService(accountsManager),
|
||||
new AttachmentsGrpcService(experimentEnrollmentManager, rateLimiters,
|
||||
gcsAttachmentGenerator, tusAttachmentGenerator, config.getAttachments().maxAttachmentUploadSizeInBytes()),
|
||||
new AttachmentsGrpcService(experimentEnrollmentManager, rateLimiters, gcsAttachmentGenerator,
|
||||
tusAttachmentGenerator, stickerPolicyGenerator,
|
||||
config.getAttachments().maxAttachmentUploadSizeInBytes(), Clock.systemUTC()),
|
||||
new PaymentsGrpcService(currencyManager),
|
||||
new ChallengeGrpcService(accountsManager, rateLimitChallengeManager, challengeConstraintChecker),
|
||||
new DonationsGrpcService(clock, zkReceiptOperations, redeemedReceiptsManager, accountsManager, config.getBadges(), ReceiptCredentialPresentation::new))
|
||||
@ -1225,7 +1229,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
phoneNumberIdentifiers, reportMessageManager, zkSecretParams, spamChecker, Clock.systemUTC()),
|
||||
new PaymentsController(currencyManager, paymentsCredentialsGenerator),
|
||||
new ProfileController(clock, rateLimiters, accountsManager, profilesManager, dynamicConfigurationManager,
|
||||
profileBadgeConverter, config.getBadges(), profileCdnPolicyGenerator, profileCdnPolicySigner,
|
||||
profileBadgeConverter, config.getBadges(), profileCdnPolicyGenerator,
|
||||
zkSecretParams, zkProfileOperations, batchIdentityCheckExecutor),
|
||||
new ProvisioningController(rateLimiters, provisioningManager),
|
||||
new RegistrationController(accountsManager, phoneVerificationTokenManager, registrationLockVerificationManager,
|
||||
@ -1233,9 +1237,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
|
||||
new RemoteConfigController(remoteConfigsManager, config.getRemoteConfigConfiguration().globalConfig()),
|
||||
new SecureStorageController(storageCredentialsGenerator),
|
||||
new SecureValueRecovery2Controller(svr2CredentialsGenerator, accountsManager),
|
||||
new StickerController(rateLimiters, config.getCdnConfiguration().credentials().accessKeyId().value(),
|
||||
config.getCdnConfiguration().credentials().secretAccessKey().value(), config.getCdnConfiguration().region(),
|
||||
config.getCdnConfiguration().bucket()),
|
||||
new StickerController(rateLimiters, stickerPolicyGenerator, Clock.systemUTC()),
|
||||
new VerificationController(registrationServiceClient, new VerificationSessionManager(verificationSessions),
|
||||
pushNotificationManager, registrationCaptchaManager, registrationRecoveryPasswordsManager,
|
||||
phoneNumberIdentifiers, rateLimiters, accountsManager, carrierDataProvider, registrationFraudChecker,
|
||||
|
||||
@ -39,7 +39,6 @@ import jakarta.ws.rs.core.Response;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Clock;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@ -88,7 +87,6 @@ import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimitedByIp;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.metrics.UserAgentTagUtil;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||
@ -114,7 +112,6 @@ public class ProfileController {
|
||||
private final ProfileBadgeConverter profileBadgeConverter;
|
||||
private final Map<String, BadgeConfiguration> badgeConfigurationMap;
|
||||
|
||||
private final PolicySigner policySigner;
|
||||
private final PostPolicyGenerator policyGenerator;
|
||||
private final ServerSecretParams serverSecretParams;
|
||||
private final ServerZkProfileOperations zkProfileOperations;
|
||||
@ -135,7 +132,6 @@ public class ProfileController {
|
||||
ProfileBadgeConverter profileBadgeConverter,
|
||||
BadgesConfiguration badgesConfiguration,
|
||||
PostPolicyGenerator policyGenerator,
|
||||
PolicySigner policySigner,
|
||||
ServerSecretParams serverSecretParams,
|
||||
ServerZkProfileOperations zkProfileOperations,
|
||||
Executor batchIdentityCheckExecutor) {
|
||||
@ -150,7 +146,6 @@ public class ProfileController {
|
||||
this.serverSecretParams = serverSecretParams;
|
||||
this.zkProfileOperations = zkProfileOperations;
|
||||
this.policyGenerator = policyGenerator;
|
||||
this.policySigner = policySigner;
|
||||
this.batchIdentityCheckExecutor = Preconditions.checkNotNull(batchIdentityCheckExecutor);
|
||||
}
|
||||
|
||||
@ -553,15 +548,13 @@ public class ProfileController {
|
||||
return maybeTargetAccount.get();
|
||||
}
|
||||
|
||||
private ProfileAvatarUploadAttributes generateAvatarUploadForm(
|
||||
final String objectName) {
|
||||
ZonedDateTime now = ZonedDateTime.now(clock);
|
||||
Pair<String, String> policy = policyGenerator.createFor(now, objectName, ProfileHelper.MAX_PROFILE_AVATAR_SIZE_BYTES);
|
||||
String signature = policySigner.getSignature(now, policy.second());
|
||||
private ProfileAvatarUploadAttributes generateAvatarUploadForm(final String objectName) {
|
||||
final PostPolicyGenerator.SignedPostPolicy signedPostPolicy =
|
||||
policyGenerator.createFor(objectName, ProfileHelper.MAX_PROFILE_AVATAR_SIZE_BYTES, clock.instant());
|
||||
|
||||
return new ProfileAvatarUploadAttributes(objectName, policy.first(),
|
||||
"private", "AWS4-HMAC-SHA256",
|
||||
now.format(PostPolicyGenerator.AWS_DATE_TIME), policy.second(), signature);
|
||||
return new ProfileAvatarUploadAttributes(objectName, signedPostPolicy.credential(),
|
||||
PostPolicyGenerator.ACL, PostPolicyGenerator.ALGORITHM,
|
||||
signedPostPolicy.formattedTimestamp(), signedPostPolicy.encodedPolicy(), signedPostPolicy.signature());
|
||||
}
|
||||
|
||||
private static Map<String, Boolean> getAccountCapabilities(final Account account) {
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
package org.whispersystems.textsecuregcm.controllers;
|
||||
|
||||
import io.dropwizard.auth.Auth;
|
||||
import io.dropwizard.util.DataSize;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
@ -15,8 +16,8 @@ import jakarta.ws.rs.PathParam;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.HexFormat;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
@ -24,23 +25,25 @@ import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.entities.StickerPackFormUploadAttributes;
|
||||
import org.whispersystems.textsecuregcm.entities.StickerPackFormUploadAttributes.StickerPackFormUploadItem;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
|
||||
@Path("/v1/sticker")
|
||||
@Tag(name = "Stickers")
|
||||
public class StickerController {
|
||||
|
||||
private final RateLimiters rateLimiters;
|
||||
private final PolicySigner policySigner;
|
||||
private final PostPolicyGenerator policyGenerator;
|
||||
private final Clock clock;
|
||||
|
||||
public StickerController(RateLimiters rateLimiters, String accessKey, String accessSecret, String region, String bucket) {
|
||||
public static final int MAXIMUM_STICKER_SIZE_BYTES = (int) DataSize.kibibytes(300 + 1).toBytes(); // add 1 kiB for encryption overhead
|
||||
public static final int MAXIMUM_STICKER_MANIFEST_SIZE_BYTES = (int) DataSize.kibibytes(10).toBytes();
|
||||
|
||||
public StickerController(final RateLimiters rateLimiters,
|
||||
final PostPolicyGenerator postPolicyGenerator,
|
||||
final Clock clock) {
|
||||
this.rateLimiters = rateLimiters;
|
||||
this.policySigner = new PolicySigner(accessSecret, region);
|
||||
this.policyGenerator = new PostPolicyGenerator(region, bucket, accessKey);
|
||||
this.policyGenerator = postPolicyGenerator;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@GET
|
||||
@ -51,33 +54,32 @@ public class StickerController {
|
||||
throws RateLimitExceededException {
|
||||
rateLimiters.getStickerPackLimiter().validate(auth.accountIdentifier());
|
||||
|
||||
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
|
||||
String packId = generatePackId();
|
||||
String packLocation = "stickers/" + packId;
|
||||
String manifestKey = packLocation + "/manifest.proto";
|
||||
Pair<String, String> manifestPolicy = policyGenerator.createFor(now, manifestKey,
|
||||
Constants.MAXIMUM_STICKER_MANIFEST_SIZE_BYTES);
|
||||
String manifestSignature = policySigner.getSignature(now, manifestPolicy.second());
|
||||
StickerPackFormUploadItem manifest = new StickerPackFormUploadItem(-1, manifestKey, manifestPolicy.first(),
|
||||
"private", "AWS4-HMAC-SHA256",
|
||||
now.format(PostPolicyGenerator.AWS_DATE_TIME), manifestPolicy.second(), manifestSignature);
|
||||
final Instant currentTime = clock.instant();
|
||||
final String packId = generatePackId();
|
||||
final String packLocation = "stickers/" + packId;
|
||||
final String manifestKey = packLocation + "/manifest.proto";
|
||||
final PostPolicyGenerator.SignedPostPolicy manifestPolicy =
|
||||
policyGenerator.createFor(manifestKey, MAXIMUM_STICKER_MANIFEST_SIZE_BYTES, currentTime);
|
||||
|
||||
List<StickerPackFormUploadItem> stickers = new LinkedList<>();
|
||||
final StickerPackFormUploadItem manifest = new StickerPackFormUploadItem(-1, manifestKey, manifestPolicy.credential(),
|
||||
PostPolicyGenerator.ACL, PostPolicyGenerator.ALGORITHM,
|
||||
manifestPolicy.formattedTimestamp(), manifestPolicy.encodedPolicy(), manifestPolicy.signature());
|
||||
|
||||
final List<StickerPackFormUploadItem> stickers = new LinkedList<>();
|
||||
|
||||
for (int i = 0; i < stickerCount; i++) {
|
||||
String stickerKey = packLocation + "/full/" + i;
|
||||
Pair<String, String> stickerPolicy = policyGenerator.createFor(now, stickerKey,
|
||||
Constants.MAXIMUM_STICKER_SIZE_BYTES);
|
||||
String stickerSignature = policySigner.getSignature(now, stickerPolicy.second());
|
||||
stickers.add(new StickerPackFormUploadItem(i, stickerKey, stickerPolicy.first(), "private", "AWS4-HMAC-SHA256",
|
||||
now.format(PostPolicyGenerator.AWS_DATE_TIME), stickerPolicy.second(), stickerSignature));
|
||||
final String stickerKey = packLocation + "/full/" + i;
|
||||
final PostPolicyGenerator.SignedPostPolicy stickerPolicy =
|
||||
policyGenerator.createFor(stickerKey, MAXIMUM_STICKER_SIZE_BYTES, currentTime);
|
||||
stickers.add(new StickerPackFormUploadItem(i, stickerKey, stickerPolicy.credential(), PostPolicyGenerator.ACL, PostPolicyGenerator.ALGORITHM,
|
||||
manifestPolicy.formattedTimestamp(), stickerPolicy.encodedPolicy(), stickerPolicy.signature()));
|
||||
}
|
||||
|
||||
return new StickerPackFormUploadAttributes(packId, manifest, stickers);
|
||||
}
|
||||
|
||||
private String generatePackId() {
|
||||
byte[] object = new byte[16];
|
||||
final byte[] object = new byte[16];
|
||||
new SecureRandom().nextBytes(object);
|
||||
|
||||
return HexFormat.of().formatHex(object);
|
||||
|
||||
@ -6,10 +6,17 @@
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.HexFormat;
|
||||
import java.util.Map;
|
||||
import java.util.stream.IntStream;
|
||||
import org.signal.chat.attachments.GetStickerUploadFormRequest;
|
||||
import org.signal.chat.attachments.GetStickerUploadFormResponse;
|
||||
import org.signal.chat.attachments.GetUploadFormRequest;
|
||||
import org.signal.chat.attachments.GetUploadFormResponse;
|
||||
import org.signal.chat.attachments.SimpleAttachmentsGrpc;
|
||||
import org.signal.chat.common.S3UploadForm;
|
||||
import org.signal.chat.common.UploadForm;
|
||||
import org.signal.chat.errors.FailedPrecondition;
|
||||
import org.whispersystems.textsecuregcm.attachments.AttachmentGenerator;
|
||||
@ -19,29 +26,44 @@ import org.whispersystems.textsecuregcm.attachments.TusAttachmentGenerator;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.auth.grpc.AuthenticationUtil;
|
||||
import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.controllers.StickerController;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
|
||||
public class AttachmentsGrpcService extends SimpleAttachmentsGrpc.AttachmentsImplBase {
|
||||
|
||||
private final ExperimentEnrollmentManager experimentEnrollmentManager;
|
||||
private final RateLimiter countRateLimiter;
|
||||
private final RateLimiter bytesRateLimiter;
|
||||
private final RateLimiter stickerPackLimiter;
|
||||
private final long maxUploadLength;
|
||||
private final Map<Integer, AttachmentGenerator> attachmentGenerators;
|
||||
private final PostPolicyGenerator stickerPolicyGenerator;
|
||||
private final Clock clock;
|
||||
private final SecureRandom secureRandom;
|
||||
|
||||
private static final S3UploadForm PROTOTYPE_STICKER_UPLOAD_FORM = S3UploadForm.newBuilder()
|
||||
.setAcl(PostPolicyGenerator.ACL)
|
||||
.setAlgorithm(PostPolicyGenerator.ALGORITHM)
|
||||
.build();
|
||||
|
||||
public AttachmentsGrpcService(
|
||||
final ExperimentEnrollmentManager experimentEnrollmentManager,
|
||||
final RateLimiters rateLimiters,
|
||||
final GcsAttachmentGenerator gcsAttachmentGenerator,
|
||||
final TusAttachmentGenerator tusAttachmentGenerator,
|
||||
final long maxUploadLength) {
|
||||
final PostPolicyGenerator stickerPolicyGenerator,
|
||||
final long maxUploadLength,
|
||||
final Clock clock) {
|
||||
this.experimentEnrollmentManager = experimentEnrollmentManager;
|
||||
this.countRateLimiter = rateLimiters.getAttachmentLimiter();
|
||||
this.bytesRateLimiter = rateLimiters.getAttachmentBytesLimiter();
|
||||
this.stickerPackLimiter = rateLimiters.getStickerPackLimiter();
|
||||
this.stickerPolicyGenerator = stickerPolicyGenerator;
|
||||
this.maxUploadLength = maxUploadLength;
|
||||
this.clock = clock;
|
||||
this.secureRandom = new SecureRandom();
|
||||
this.attachmentGenerators = Map.of(
|
||||
2, gcsAttachmentGenerator,
|
||||
@ -80,4 +102,59 @@ public class AttachmentsGrpcService extends SimpleAttachmentsGrpc.AttachmentsImp
|
||||
.setSignedUploadLocation(descriptor.signedUploadLocation()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetStickerUploadFormResponse getStickerUploadForm(final GetStickerUploadFormRequest request)
|
||||
throws RateLimitExceededException {
|
||||
|
||||
stickerPackLimiter.validate(AuthenticationUtil.requireAuthenticatedDevice().accountIdentifier());
|
||||
|
||||
final Instant currentTime = clock.instant();
|
||||
final String packId;
|
||||
{
|
||||
final byte[] packIdBytes = new byte[16];
|
||||
secureRandom.nextBytes(packIdBytes);
|
||||
|
||||
packId = HexFormat.of().formatHex(packIdBytes);
|
||||
}
|
||||
|
||||
final String packLocation = "stickers/" + packId;
|
||||
|
||||
final GetStickerUploadFormResponse.Builder responseBuilder = GetStickerUploadFormResponse.newBuilder()
|
||||
.setPackId(packId);
|
||||
|
||||
{
|
||||
final String manifestKey = packLocation + "/manifest.proto";
|
||||
|
||||
final PostPolicyGenerator.SignedPostPolicy manifestPolicy =
|
||||
stickerPolicyGenerator.createFor(manifestKey, StickerController.MAXIMUM_STICKER_MANIFEST_SIZE_BYTES, currentTime);
|
||||
|
||||
responseBuilder
|
||||
.setManifestUploadForm(PROTOTYPE_STICKER_UPLOAD_FORM.toBuilder()
|
||||
.setKey(manifestKey)
|
||||
.setCredential(manifestPolicy.credential())
|
||||
.setDate(manifestPolicy.formattedTimestamp())
|
||||
.setPolicy(manifestPolicy.encodedPolicy())
|
||||
.setSignature(manifestPolicy.signature())
|
||||
.build());
|
||||
}
|
||||
|
||||
IntStream.range(0, request.getStickerCount())
|
||||
.mapToObj(i -> {
|
||||
final String stickerKey = packLocation + "/full/" + i;
|
||||
final PostPolicyGenerator.SignedPostPolicy stickerPolicy =
|
||||
stickerPolicyGenerator.createFor(stickerKey, StickerController.MAXIMUM_STICKER_SIZE_BYTES, currentTime);
|
||||
|
||||
return PROTOTYPE_STICKER_UPLOAD_FORM.toBuilder()
|
||||
.setKey(stickerKey)
|
||||
.setCredential(stickerPolicy.credential())
|
||||
.setDate(stickerPolicy.formattedTimestamp())
|
||||
.setPolicy(stickerPolicy.encodedPolicy())
|
||||
.setSignature(stickerPolicy.signature())
|
||||
.build();
|
||||
})
|
||||
.forEach(responseBuilder::addStickerUploadForms);
|
||||
|
||||
return responseBuilder.build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,9 +5,7 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.grpc;
|
||||
|
||||
import com.google.protobuf.ByteString;
|
||||
import java.time.Clock;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.HexFormat;
|
||||
import java.util.List;
|
||||
@ -16,6 +14,7 @@ import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import org.signal.chat.common.S3UploadForm;
|
||||
import org.signal.chat.errors.FailedPrecondition;
|
||||
import org.signal.chat.errors.NotFound;
|
||||
import org.signal.chat.profile.GetUnversionedProfileRequest;
|
||||
@ -23,7 +22,6 @@ import org.signal.chat.profile.GetUnversionedProfileResponse;
|
||||
import org.signal.chat.profile.GetVersionedProfileRequest;
|
||||
import org.signal.chat.profile.GetVersionedProfileResponse;
|
||||
import org.signal.chat.profile.PaymentsForbiddenInRegion;
|
||||
import org.signal.chat.profile.ProfileAvatarUploadAttributes;
|
||||
import org.signal.chat.profile.ProfilesV2CapabilityRequired;
|
||||
import org.signal.chat.profile.SetProfileRequest;
|
||||
import org.signal.chat.profile.SetProfileResponse;
|
||||
@ -40,7 +38,6 @@ import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.identity.IdentityType;
|
||||
import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||
@ -51,7 +48,6 @@ import org.whispersystems.textsecuregcm.storage.ProfilesManager;
|
||||
import org.whispersystems.textsecuregcm.storage.VersionedProfile;
|
||||
import org.whispersystems.textsecuregcm.storage.VersionedProfileV1;
|
||||
import org.whispersystems.textsecuregcm.storage.WriteConflictException;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import org.whispersystems.textsecuregcm.util.ProfileHelper;
|
||||
|
||||
public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase {
|
||||
@ -62,13 +58,17 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase {
|
||||
private final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager;
|
||||
private final Map<String, BadgeConfiguration> badgeConfigurationMap;
|
||||
private final PostPolicyGenerator policyGenerator;
|
||||
private final PolicySigner policySigner;
|
||||
private final ProfileBadgeConverter profileBadgeConverter;
|
||||
private final RateLimiters rateLimiters;
|
||||
|
||||
private static final S3UploadForm PROTOTYPE_AVATAR_UPLOAD_FORM = S3UploadForm.newBuilder()
|
||||
.setAcl(PostPolicyGenerator.ACL)
|
||||
.setAlgorithm(PostPolicyGenerator.ALGORITHM)
|
||||
.build();
|
||||
|
||||
private record AvatarData(Optional<String> currentAvatar,
|
||||
Optional<String> finalAvatar,
|
||||
Optional<ProfileAvatarUploadAttributes> uploadAttributes) {}
|
||||
Optional<S3UploadForm> uploadAttributes) {}
|
||||
|
||||
public ProfileGrpcService(
|
||||
final Clock clock,
|
||||
@ -77,7 +77,6 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase {
|
||||
final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager,
|
||||
final BadgesConfiguration badgesConfiguration,
|
||||
final PostPolicyGenerator policyGenerator,
|
||||
final PolicySigner policySigner,
|
||||
final ProfileBadgeConverter profileBadgeConverter,
|
||||
final RateLimiters rateLimiters) {
|
||||
this.clock = clock;
|
||||
@ -87,7 +86,6 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase {
|
||||
this.badgeConfigurationMap = badgesConfiguration.getBadges().stream().collect(Collectors.toMap(
|
||||
BadgeConfiguration::getId, Function.identity()));
|
||||
this.policyGenerator = policyGenerator;
|
||||
this.policySigner = policySigner;
|
||||
this.profileBadgeConverter = profileBadgeConverter;
|
||||
this.rateLimiters = rateLimiters;
|
||||
}
|
||||
@ -266,19 +264,16 @@ public class ProfileGrpcService extends SimpleProfileGrpc.ProfileImplBase {
|
||||
}
|
||||
}
|
||||
|
||||
private ProfileAvatarUploadAttributes generateAvatarUploadForm(final String objectName) {
|
||||
final ZonedDateTime now = ZonedDateTime.now(clock);
|
||||
final Pair<String, String> policy = policyGenerator.createFor(now, objectName, ProfileHelper.MAX_PROFILE_AVATAR_SIZE_BYTES);
|
||||
final String signature = policySigner.getSignature(now, policy.second());
|
||||
private S3UploadForm generateAvatarUploadForm(final String objectName) {
|
||||
final PostPolicyGenerator.SignedPostPolicy policy =
|
||||
policyGenerator.createFor(objectName, ProfileHelper.MAX_PROFILE_AVATAR_SIZE_BYTES, clock.instant());
|
||||
|
||||
return ProfileAvatarUploadAttributes.newBuilder()
|
||||
.setPath(objectName)
|
||||
.setCredential(policy.first())
|
||||
.setAcl("private")
|
||||
.setAlgorithm("AWS4-HMAC-SHA256")
|
||||
.setDate(now.format(PostPolicyGenerator.AWS_DATE_TIME))
|
||||
.setPolicy(policy.second())
|
||||
.setSignature(ByteString.copyFrom(signature.getBytes()))
|
||||
return PROTOTYPE_AVATAR_UPLOAD_FORM.toBuilder()
|
||||
.setKey(objectName)
|
||||
.setCredential(policy.credential())
|
||||
.setDate(policy.formattedTimestamp())
|
||||
.setPolicy(policy.encodedPolicy())
|
||||
.setSignature(policy.signature())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,11 +36,11 @@ import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.whispersystems.textsecuregcm.WhisperServerConfiguration;
|
||||
import org.whispersystems.textsecuregcm.configuration.dynamic.DynamicConfiguration;
|
||||
import org.whispersystems.textsecuregcm.storage.DynamicConfigurationManager;
|
||||
import org.whispersystems.textsecuregcm.util.Constants;
|
||||
|
||||
public class MetricsUtil {
|
||||
|
||||
public static final String PREFIX = "chat";
|
||||
private static final String DROPWIZARD_METRICS_NAME = "textsecure";
|
||||
|
||||
private static volatile boolean registeredMetrics = false;
|
||||
|
||||
@ -69,7 +69,7 @@ public class MetricsUtil {
|
||||
|
||||
registeredMetrics = true;
|
||||
|
||||
SharedMetricRegistries.add(Constants.METRICS_NAME, environment.metrics());
|
||||
SharedMetricRegistries.add(DROPWIZARD_METRICS_NAME, environment.metrics());
|
||||
|
||||
Duration shutdownWaitDuration = Duration.ZERO;
|
||||
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.s3;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Key;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HexFormat;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class PolicySigner {
|
||||
|
||||
private final String awsAccessSecret;
|
||||
private final String region;
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
|
||||
public PolicySigner(final String awsAccessSecret, final String region) {
|
||||
this.awsAccessSecret = awsAccessSecret;
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
// See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
|
||||
public String getSignature(final ZonedDateTime now, final String policy) {
|
||||
final Mac mac;
|
||||
|
||||
try {
|
||||
mac = Mac.getInstance("HmacSHA256");
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
throw new AssertionError("Every implementation of the Java platform is required to support HmacSHA256", e);
|
||||
}
|
||||
|
||||
try {
|
||||
mac.init(toHmacKey(("AWS4" + awsAccessSecret).getBytes(StandardCharsets.UTF_8)));
|
||||
final byte[] dateKey = mac.doFinal(now.format(DATE_FORMAT).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
mac.init(toHmacKey(dateKey));
|
||||
final byte[] dateRegionKey = mac.doFinal(region.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
mac.init(toHmacKey(dateRegionKey));
|
||||
final byte[] dateRegionServiceKey = mac.doFinal("s3".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
mac.init(toHmacKey(dateRegionServiceKey));
|
||||
final byte[] signingKey = mac.doFinal("aws4_request".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
mac.init(toHmacKey(signingKey));
|
||||
|
||||
return HexFormat.of().formatHex(mac.doFinal(policy.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (final InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Key toHmacKey(final byte[] bytes) {
|
||||
return new SecretKeySpec(bytes, "HmacSHA256");
|
||||
}
|
||||
}
|
||||
@ -5,31 +5,52 @@
|
||||
|
||||
package org.whispersystems.textsecuregcm.s3;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.Key;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Base64;
|
||||
import org.whispersystems.textsecuregcm.util.Pair;
|
||||
import java.util.HexFormat;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class PostPolicyGenerator {
|
||||
|
||||
public static final DateTimeFormatter AWS_DATE_TIME = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssX");
|
||||
private static final DateTimeFormatter CREDENTIAL_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
public static final String ALGORITHM = "AWS4-HMAC-SHA256";
|
||||
public static final String ACL = "private";
|
||||
|
||||
private static final DateTimeFormatter AWS_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmssX");
|
||||
private static final DateTimeFormatter AWS_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
|
||||
|
||||
private final String region;
|
||||
private final String bucket;
|
||||
private final String awsAccessId;
|
||||
private final String awsAccessSecret;
|
||||
|
||||
public record SignedPostPolicy(String formattedTimestamp, String credential, String encodedPolicy, String signature) {
|
||||
}
|
||||
|
||||
public PostPolicyGenerator(final String region,
|
||||
final String bucket,
|
||||
final String awsAccessId,
|
||||
final String awsAccessSecret) {
|
||||
|
||||
public PostPolicyGenerator(final String region, final String bucket, final String awsAccessId) {
|
||||
this.region = region;
|
||||
this.bucket = bucket;
|
||||
this.awsAccessId = awsAccessId;
|
||||
this.awsAccessSecret = awsAccessSecret;
|
||||
}
|
||||
|
||||
public Pair<String, String> createFor(final ZonedDateTime now, final String object, final int maxSizeInBytes) {
|
||||
public SignedPostPolicy createFor(final String object, final int maxSizeInBytes, final Instant currentTime) {
|
||||
final ZonedDateTime now = ZonedDateTime.ofInstant(currentTime, ZoneOffset.UTC);
|
||||
final String expiration = now.plusMinutes(30).format(DateTimeFormatter.ISO_INSTANT);
|
||||
final String credentialDate = now.format(CREDENTIAL_DATE);
|
||||
final String requestDate = now.format(AWS_DATE_TIME);
|
||||
final String credentialDate = now.format(AWS_DATE_FORMATTER);
|
||||
final String requestTimestamp = now.format(AWS_TIMESTAMP_FORMATTER);
|
||||
final String credential = String.format("%s/%s/%s/s3/aws4_request", awsAccessId, credentialDate, region);
|
||||
|
||||
final String policy = String.format("""
|
||||
@ -38,17 +59,55 @@ public class PostPolicyGenerator {
|
||||
"conditions": [
|
||||
{"bucket": "%s"},
|
||||
{"key": "%s"},
|
||||
{"acl": "private"},
|
||||
{"acl": "%s"},
|
||||
["starts-with", "$Content-Type", ""],
|
||||
["content-length-range", 1, %d],
|
||||
|
||||
{"x-amz-credential": "%s"},
|
||||
{"x-amz-algorithm": "AWS4-HMAC-SHA256"},
|
||||
{"x-amz-algorithm": "%s"},
|
||||
{"x-amz-date": "%s" }
|
||||
]
|
||||
}
|
||||
""", expiration, bucket, object, maxSizeInBytes, credential, requestDate);
|
||||
""", expiration, bucket, object, ACL, maxSizeInBytes, credential, ALGORITHM, requestTimestamp);
|
||||
|
||||
return new Pair<>(credential, Base64.getEncoder().encodeToString(policy.getBytes(StandardCharsets.UTF_8)));
|
||||
final String encodedPolicy = Base64.getEncoder().encodeToString(policy.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
return new SignedPostPolicy(requestTimestamp, credential, encodedPolicy, getSignature(now, encodedPolicy));
|
||||
}
|
||||
|
||||
// See https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
|
||||
@VisibleForTesting
|
||||
String getSignature(final ZonedDateTime now, final String policy) {
|
||||
final Mac mac;
|
||||
|
||||
try {
|
||||
mac = Mac.getInstance("HmacSHA256");
|
||||
} catch (final NoSuchAlgorithmException e) {
|
||||
throw new AssertionError("Every implementation of the Java platform is required to support HmacSHA256", e);
|
||||
}
|
||||
|
||||
try {
|
||||
mac.init(toHmacKey(("AWS4" + awsAccessSecret).getBytes(StandardCharsets.UTF_8)));
|
||||
final byte[] dateKey = mac.doFinal(now.format(AWS_DATE_FORMATTER).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
mac.init(toHmacKey(dateKey));
|
||||
final byte[] dateRegionKey = mac.doFinal(region.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
mac.init(toHmacKey(dateRegionKey));
|
||||
final byte[] dateRegionServiceKey = mac.doFinal("s3".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
mac.init(toHmacKey(dateRegionServiceKey));
|
||||
final byte[] signingKey = mac.doFinal("aws4_request".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
mac.init(toHmacKey(signingKey));
|
||||
|
||||
return HexFormat.of().formatHex(mac.doFinal(policy.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (final InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Key toHmacKey(final byte[] bytes) {
|
||||
return new SecretKeySpec(bytes, "HmacSHA256");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.whispersystems.textsecuregcm.util;
|
||||
|
||||
import io.dropwizard.util.DataSize;
|
||||
|
||||
public class Constants {
|
||||
public static final String METRICS_NAME = "textsecure";
|
||||
public static final int MAXIMUM_STICKER_SIZE_BYTES = (int) DataSize.kibibytes(300 + 1).toBytes(); // add 1 kiB for encryption overhead
|
||||
public static final int MAXIMUM_STICKER_MANIFEST_SIZE_BYTES = (int) DataSize.kibibytes(10).toBytes();
|
||||
}
|
||||
@ -19,6 +19,9 @@ service Attachments {
|
||||
|
||||
// Retrieve an upload form that can be used to perform a resumable upload
|
||||
rpc GetUploadForm(GetUploadFormRequest) returns (GetUploadFormResponse) {}
|
||||
|
||||
// Retrieve an upload form that can be used to upload a sticker pack
|
||||
rpc GetStickerUploadForm(GetStickerUploadFormRequest) returns (GetStickerUploadFormResponse) {}
|
||||
}
|
||||
|
||||
message GetUploadFormRequest {
|
||||
@ -37,3 +40,19 @@ message GetUploadFormResponse {
|
||||
errors.FailedPrecondition exceeds_max_upload_length = 2 [(tag.reason) = "oversize_upload"];
|
||||
}
|
||||
}
|
||||
|
||||
message GetStickerUploadFormRequest {
|
||||
// The number of stickers in the sticker pack to upload
|
||||
uint32 sticker_count = 1 [(require.range) = {min: 1, max: 201}];
|
||||
}
|
||||
|
||||
message GetStickerUploadFormResponse {
|
||||
// A randomly-generated ID for the new sticker pack
|
||||
string pack_id = 1;
|
||||
|
||||
// An upload form clients must use to upload a manifest for the sticker pack
|
||||
common.S3UploadForm manifest_upload_form = 2;
|
||||
|
||||
// Upload forms for individual stickers within the sticker pack
|
||||
repeated common.S3UploadForm sticker_upload_forms = 3;
|
||||
}
|
||||
|
||||
@ -116,6 +116,33 @@ message UploadForm {
|
||||
string signed_upload_location = 4;
|
||||
}
|
||||
|
||||
// An upload location, credentials, and metadata which may be used to upload an
|
||||
// object to AWS S3
|
||||
message S3UploadForm {
|
||||
// The S3 key (i.e. path and filename) for the uploaded file.
|
||||
string key = 1;
|
||||
|
||||
// A scoped credential. Includes the AWS access key, date, region targeted,
|
||||
// and AWS service.
|
||||
string credential = 2;
|
||||
|
||||
// The type of access control for the uploaded file.
|
||||
string acl = 3;
|
||||
|
||||
// The algorithm used to calculate a signature on the S3 policy.
|
||||
string algorithm = 4;
|
||||
|
||||
// The timestamp (formatted as "yyyyMMdd'T'HHmmssX") at which the S3 policy
|
||||
// and signature were generated.
|
||||
string date = 5;
|
||||
|
||||
// The S3 policy (as a base64-encoded JSON string) used to upload the file.
|
||||
string policy = 6;
|
||||
|
||||
// A digital signature (formatted as a hex string) on the S3 policy.
|
||||
string signature = 7;
|
||||
}
|
||||
|
||||
message BadgeSvg {
|
||||
// File name of the scalable vector graphic for light mode.
|
||||
string light = 1;
|
||||
|
||||
@ -122,7 +122,7 @@ message SetProfileResult {
|
||||
//
|
||||
// Because this is a temporary field during the migration, it has the highest
|
||||
// field number without serialization overhead. This is purely aesthetic.
|
||||
optional ProfileAvatarUploadAttributes v1_avatar_upload_form = 15;
|
||||
optional common.S3UploadForm v1_avatar_upload_form = 15;
|
||||
|
||||
// next: 1
|
||||
}
|
||||
@ -321,23 +321,6 @@ message GetExpiringProfileKeyCredentialAnonymousResponse {
|
||||
}
|
||||
}
|
||||
|
||||
message ProfileAvatarUploadAttributes {
|
||||
// The S3 upload path for the profile's avatar.
|
||||
string path = 1;
|
||||
// A scoped credential. Includes the AWS access key, date, region targeted, and AWS service.
|
||||
string credential = 2;
|
||||
// The type of access control for the avatar object.
|
||||
string acl = 3;
|
||||
// The algorithm used to calculate a signature on the S3 policy.
|
||||
string algorithm = 4;
|
||||
// The timestamp at which the S3 policy and signature were generated.
|
||||
string date = 5;
|
||||
// The S3 policy used to upload the avatar object.
|
||||
string policy = 6;
|
||||
// A digital signature on the S3 policy.
|
||||
bytes signature = 7;
|
||||
}
|
||||
|
||||
enum CredentialType {
|
||||
CREDENTIAL_TYPE_UNSPECIFIED = 0;
|
||||
CREDENTIAL_TYPE_EXPIRING_PROFILE_KEY = 1;
|
||||
|
||||
@ -100,7 +100,6 @@ import org.whispersystems.textsecuregcm.identity.ServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.mappers.RateLimitExceededExceptionMapper;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||
@ -129,8 +128,7 @@ class ProfileControllerTest {
|
||||
private static final RateLimiter usernameRateLimiter = mock(RateLimiter.class);
|
||||
|
||||
private static final PostPolicyGenerator postPolicyGenerator = new PostPolicyGenerator("us-west-1", "profile-bucket",
|
||||
"accessKey");
|
||||
private static final PolicySigner policySigner = new PolicySigner("accessSecret", "us-west-1");
|
||||
"accessKey", "accessSecret");
|
||||
private static final ServerZkProfileOperations zkProfileOperations = mock(ServerZkProfileOperations.class);
|
||||
private static final ServerSecretParams serverSecretParams = ServerSecretParams.generate();
|
||||
|
||||
@ -172,7 +170,6 @@ class ProfileControllerTest {
|
||||
new BadgeConfiguration("TEST3", "testing", List.of("l", "m", "h", "x", "xx", "xxx"), "SVG", List.of(new BadgeSvg("sl", "sd"), new BadgeSvg("ml", "md"), new BadgeSvg("ll", "ld")))
|
||||
), List.of("TEST1"), Map.of(1L, "TEST1", 2L, "TEST2", 3L, "TEST3")),
|
||||
postPolicyGenerator,
|
||||
policySigner,
|
||||
serverSecretParams,
|
||||
zkProfileOperations,
|
||||
Executors.newSingleThreadExecutor()))
|
||||
|
||||
@ -15,6 +15,7 @@ import io.dropwizard.auth.AuthValueFactoryProvider;
|
||||
import io.dropwizard.testing.junit5.DropwizardExtensionsSupport;
|
||||
import io.dropwizard.testing.junit5.ResourceExtension;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import java.time.Clock;
|
||||
import java.util.Base64;
|
||||
import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@ -24,6 +25,7 @@ import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
|
||||
import org.whispersystems.textsecuregcm.entities.StickerPackFormUploadAttributes;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.tests.util.AuthHelper;
|
||||
import org.whispersystems.textsecuregcm.util.SystemMapper;
|
||||
|
||||
@ -38,7 +40,9 @@ class StickerControllerTest {
|
||||
.addProvider(new AuthValueFactoryProvider.Binder<>(AuthenticatedDevice.class))
|
||||
.setMapper(SystemMapper.jsonMapper())
|
||||
.setTestContainerFactory(new GrizzlyWebTestContainerFactory())
|
||||
.addResource(new StickerController(rateLimiters, "foo", "bar", "us-east-1", "mybucket"))
|
||||
.addResource(new StickerController(rateLimiters,
|
||||
new PostPolicyGenerator("us-east-1", "mybucket", "foo", "bar"),
|
||||
Clock.systemUTC()))
|
||||
.build();
|
||||
|
||||
@BeforeEach
|
||||
|
||||
@ -24,6 +24,7 @@ import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
@ -37,8 +38,11 @@ import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
import org.mockito.Mock;
|
||||
import org.signal.chat.attachments.AttachmentsGrpc;
|
||||
import org.signal.chat.attachments.GetStickerUploadFormRequest;
|
||||
import org.signal.chat.attachments.GetStickerUploadFormResponse;
|
||||
import org.signal.chat.attachments.GetUploadFormRequest;
|
||||
import org.signal.chat.attachments.GetUploadFormResponse;
|
||||
import org.signal.chat.common.S3UploadForm;
|
||||
import org.signal.chat.common.UploadForm;
|
||||
import org.whispersystems.textsecuregcm.attachments.AttachmentUtil;
|
||||
import org.whispersystems.textsecuregcm.attachments.GcsAttachmentGenerator;
|
||||
@ -49,6 +53,7 @@ import org.whispersystems.textsecuregcm.controllers.RateLimitExceededException;
|
||||
import org.whispersystems.textsecuregcm.experiment.ExperimentEnrollmentManager;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.util.MockUtils;
|
||||
import org.whispersystems.textsecuregcm.util.TestRandomUtil;
|
||||
|
||||
@ -65,6 +70,8 @@ class AttachmentsGrpcServiceTest extends
|
||||
private RateLimiter countRateLimiter;
|
||||
@Mock
|
||||
private RateLimiter byteRateLimiter;
|
||||
@Mock
|
||||
private RateLimiter stickerRateLimiter;
|
||||
|
||||
@Override
|
||||
protected AttachmentsGrpcService createServiceBeforeEachTest() {
|
||||
@ -86,10 +93,13 @@ class AttachmentsGrpcServiceTest extends
|
||||
MockUtils.buildMock(RateLimiters.class, rateLimiters -> {
|
||||
when(rateLimiters.getAttachmentLimiter()).thenReturn(countRateLimiter);
|
||||
when(rateLimiters.getAttachmentBytesLimiter()).thenReturn(byteRateLimiter);
|
||||
when(rateLimiters.getStickerPackLimiter()).thenReturn(stickerRateLimiter);
|
||||
}),
|
||||
gcsAttachmentGenerator,
|
||||
tusAttachmentGenerator,
|
||||
MAX_UPLOAD_LENGTH);
|
||||
new PostPolicyGenerator("wakonda-south-7", "test", "accessId", "accessSecret"),
|
||||
MAX_UPLOAD_LENGTH,
|
||||
Clock.systemUTC());
|
||||
} catch (NoSuchAlgorithmException | IOException | InvalidKeyException | InvalidKeySpecException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
@ -209,4 +219,41 @@ class AttachmentsGrpcServiceTest extends
|
||||
assertRateLimitExceeded(retryAfter, () ->
|
||||
authenticatedServiceStub().getUploadForm(GetUploadFormRequest.newBuilder().setUploadLength(1).build()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getStickerUploadForm() {
|
||||
final int stickerCount = 7;
|
||||
|
||||
final GetStickerUploadFormResponse response =
|
||||
authenticatedServiceStub().getStickerUploadForm(GetStickerUploadFormRequest.newBuilder()
|
||||
.setStickerCount(stickerCount)
|
||||
.build());
|
||||
|
||||
assertThat(response.getPackId()).isNotBlank();
|
||||
assertThat(response.getManifestUploadForm()).satisfies(AttachmentsGrpcServiceTest::s3UploadFormValid);
|
||||
assertThat(response.getStickerUploadFormsCount()).isEqualTo(stickerCount);
|
||||
assertThat(response.getStickerUploadFormsList()).allSatisfy(AttachmentsGrpcServiceTest::s3UploadFormValid);
|
||||
}
|
||||
|
||||
private static void s3UploadFormValid(final S3UploadForm s3UploadForm) {
|
||||
assertThat(s3UploadForm.getAcl()).isEqualTo("private");
|
||||
assertThat(s3UploadForm.getAlgorithm()).isEqualTo("AWS4-HMAC-SHA256");
|
||||
assertThat(s3UploadForm.getKey()).startsWith("stickers/");
|
||||
assertThat(s3UploadForm.getCredential()).endsWith("/s3/aws4_request");
|
||||
assertThat(s3UploadForm.getDate()).isNotBlank();
|
||||
assertThat(s3UploadForm.getPolicy()).isNotBlank();
|
||||
assertThat(s3UploadForm.getSignature()).isNotBlank();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getStickerUploadFormRateLimited() throws RateLimitExceededException {
|
||||
final Duration retryAfter = Duration.ofMinutes(13);
|
||||
doThrow(new RateLimitExceededException(retryAfter)).when(stickerRateLimiter).validate(any(UUID.class));
|
||||
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
GrpcTestUtils.assertRateLimitExceeded(retryAfter,
|
||||
() -> authenticatedServiceStub().getStickerUploadForm(GetStickerUploadFormRequest.newBuilder()
|
||||
.setStickerCount(7)
|
||||
.build()));
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,7 +93,6 @@ import org.whispersystems.textsecuregcm.identity.AciServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.identity.PniServiceIdentifier;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiter;
|
||||
import org.whispersystems.textsecuregcm.limits.RateLimiters;
|
||||
import org.whispersystems.textsecuregcm.s3.PolicySigner;
|
||||
import org.whispersystems.textsecuregcm.s3.PostPolicyGenerator;
|
||||
import org.whispersystems.textsecuregcm.storage.Account;
|
||||
import org.whispersystems.textsecuregcm.storage.AccountBadge;
|
||||
@ -148,10 +147,11 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest<ProfileGrpcServic
|
||||
|
||||
@Override
|
||||
protected ProfileGrpcService createServiceBeforeEachTest() {
|
||||
clock = Clock.fixed(Instant.ofEpochSecond(42), ZoneId.of("Etc/UTC"));
|
||||
|
||||
@SuppressWarnings("unchecked") final DynamicConfigurationManager<DynamicConfiguration> dynamicConfigurationManager = mock(DynamicConfigurationManager.class);
|
||||
final DynamicConfiguration dynamicConfiguration = mock(DynamicConfiguration.class);
|
||||
final PolicySigner policySigner = new PolicySigner("accessSecret", "us-west-1");
|
||||
final PostPolicyGenerator policyGenerator = new PostPolicyGenerator("us-west-1", "profile-bucket", "accessKey");
|
||||
final PostPolicyGenerator policyGenerator = new PostPolicyGenerator("us-west-1", "profile-bucket", "accessKey", "accessSecret");
|
||||
final BadgesConfiguration badgesConfiguration = new BadgesConfiguration(
|
||||
List.of(new BadgeConfiguration(
|
||||
"TEST",
|
||||
@ -201,8 +201,6 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest<ProfileGrpcServic
|
||||
|
||||
when(profilesManager.deleteAvatar(anyString())).thenReturn(CompletableFuture.completedFuture(null));
|
||||
|
||||
clock = Clock.fixed(Instant.ofEpochSecond(42), ZoneId.of("Etc/UTC"));
|
||||
|
||||
return new ProfileGrpcService(
|
||||
clock,
|
||||
accountsManager,
|
||||
@ -210,7 +208,6 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest<ProfileGrpcServic
|
||||
dynamicConfigurationManager,
|
||||
badgesConfiguration,
|
||||
policyGenerator,
|
||||
policySigner,
|
||||
profileBadgeConverter,
|
||||
rateLimiters
|
||||
);
|
||||
@ -398,9 +395,9 @@ public class ProfileGrpcServiceTest extends SimpleBaseGrpcTest<ProfileGrpcServic
|
||||
final SetProfileResult result = response.getResult();
|
||||
|
||||
if (expectHasS3UploadPath) {
|
||||
assertTrue(result.getV1AvatarUploadForm().getPath().startsWith("profiles/"));
|
||||
assertTrue(result.getV1AvatarUploadForm().getKey().startsWith("profiles/"));
|
||||
} else {
|
||||
assertEquals("", result.getV1AvatarUploadForm().getPath());
|
||||
assertEquals("", result.getV1AvatarUploadForm().getKey());
|
||||
}
|
||||
|
||||
if (expectDeleteS3Object) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013 Signal Messenger, LLC
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
@ -12,16 +12,17 @@ import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class PolicySignerTest {
|
||||
class PostPolicyGeneratorTest {
|
||||
|
||||
@Test
|
||||
void testSignature() {
|
||||
final Instant time = Instant.parse("2015-12-29T00:00:00Z");
|
||||
final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(time, ZoneOffset.UTC);
|
||||
void getSignature() {
|
||||
final Instant currentTime = Instant.parse("2015-12-29T00:00:00Z");
|
||||
final PostPolicyGenerator postPolicyGenerator = new PostPolicyGenerator("us-east-1", "bucket", "accessId", "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY");
|
||||
|
||||
final ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(currentTime, ZoneOffset.UTC);
|
||||
final String encodedPolicy = "eyAiZXhwaXJhdGlvbiI6ICIyMDE1LTEyLTMwVDEyOjAwOjAwLjAwMFoiLA0KICAiY29uZGl0aW9ucyI6IFsNCiAgICB7ImJ1Y2tldCI6ICJzaWd2NGV4YW1wbGVidWNrZXQifSwNCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci91c2VyMS8iXSwNCiAgICB7ImFjbCI6ICJwdWJsaWMtcmVhZCJ9LA0KICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL3NpZ3Y0ZXhhbXBsZWJ1Y2tldC5zMy5hbWF6b25hd3MuY29tL3N1Y2Nlc3NmdWxfdXBsb2FkLmh0bWwifSwNCiAgICBbInN0YXJ0cy13aXRoIiwgIiRDb250ZW50LVR5cGUiLCAiaW1hZ2UvIl0sDQogICAgeyJ4LWFtei1tZXRhLXV1aWQiOiAiMTQzNjUxMjM2NTEyNzQifSwNCiAgICB7IngtYW16LXNlcnZlci1zaWRlLWVuY3J5cHRpb24iOiAiQUVTMjU2In0sDQogICAgWyJzdGFydHMtd2l0aCIsICIkeC1hbXotbWV0YS10YWciLCAiIl0sDQoNCiAgICB7IngtYW16LWNyZWRlbnRpYWwiOiAiQUtJQUlPU0ZPRE5ON0VYQU1QTEUvMjAxNTEyMjkvdXMtZWFzdC0xL3MzL2F3czRfcmVxdWVzdCJ9LA0KICAgIHsieC1hbXotYWxnb3JpdGhtIjogIkFXUzQtSE1BQy1TSEEyNTYifSwNCiAgICB7IngtYW16LWRhdGUiOiAiMjAxNTEyMjlUMDAwMDAwWiIgfQ0KICBdDQp9";
|
||||
final PolicySigner policySigner = new PolicySigner("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "us-east-1");
|
||||
|
||||
assertEquals("8afdbf4008c03f22c2cd3cdb72e4afbb1f6a588f3255ac628749a66d7f09699e",
|
||||
policySigner.getSignature(zonedDateTime, encodedPolicy));
|
||||
postPolicyGenerator.getSignature(zonedDateTime, encodedPolicy));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user