Support for sticker pack uploads

This commit is contained in:
Moxie Marlinspike
2019-07-03 20:58:38 -07:00
parent 0d46f85ead
commit 10724fee04
13 changed files with 323 additions and 16 deletions

View File

@@ -51,7 +51,7 @@ public class WhisperServerConfiguration extends Configuration {
@NotNull
@Valid
@JsonProperty
private ProfilesConfiguration profiles;
private CdnConfiguration cdn;
@NotNull
@Valid
@@ -247,8 +247,8 @@ public class WhisperServerConfiguration extends Configuration {
return apn;
}
public ProfilesConfiguration getProfilesConfiguration() {
return profiles;
public CdnConfiguration getCdnConfiguration() {
return cdn;
}
public UnidentifiedDeliveryConfiguration getDeliveryCertificate() {

View File

@@ -29,9 +29,9 @@ import org.jdbi.v3.core.Jdbi;
import org.whispersystems.dispatch.DispatchManager;
import org.whispersystems.textsecuregcm.auth.AccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.CertificateGenerator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccount;
import org.whispersystems.textsecuregcm.auth.DisabledPermittedAccountAuthenticator;
import org.whispersystems.textsecuregcm.auth.ExternalServiceCredentialGenerator;
import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator;
import org.whispersystems.textsecuregcm.controllers.AccountController;
import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV1;
@@ -46,6 +46,7 @@ import org.whispersystems.textsecuregcm.controllers.ProfileController;
import org.whispersystems.textsecuregcm.controllers.ProvisioningController;
import org.whispersystems.textsecuregcm.controllers.SecureStorageController;
import org.whispersystems.textsecuregcm.controllers.TransparentDataController;
import org.whispersystems.textsecuregcm.controllers.StickerController;
import org.whispersystems.textsecuregcm.controllers.VoiceVerificationController;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
import org.whispersystems.textsecuregcm.liquibase.NameableMigrationsBundle;
@@ -244,7 +245,8 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
AttachmentControllerV2 attachmentControllerV2 = new AttachmentControllerV2(rateLimiters, config.getAttachmentsConfiguration().getAccessKey(), config.getAttachmentsConfiguration().getAccessSecret(), config.getAttachmentsConfiguration().getRegion(), config.getAttachmentsConfiguration().getBucket());
KeysController keysController = new KeysController(rateLimiters, keys, accountsManager, directoryQueue);
MessageController messageController = new MessageController(rateLimiters, pushSender, receiptSender, accountsManager, messagesManager, apnFallbackManager);
ProfileController profileController = new ProfileController(rateLimiters, accountsManager, config.getProfilesConfiguration());
ProfileController profileController = new ProfileController(rateLimiters, accountsManager, config.getCdnConfiguration());
StickerController stickerController = new StickerController(rateLimiters, config.getCdnConfiguration().getAccessKey(), config.getCdnConfiguration().getAccessSecret(), config.getCdnConfiguration().getRegion(), config.getCdnConfiguration().getBucket());
AuthFilter<BasicCredentials, Account> accountAuthFilter = new BasicCredentialAuthFilter.Builder<Account>().setAuthenticator(accountAuthenticator).buildAuthFilter ();
AuthFilter<BasicCredentials, DisabledPermittedAccount> disabledPermittedAccountAuthFilter = new BasicCredentialAuthFilter.Builder<DisabledPermittedAccount>().setAuthenticator(disabledPermittedAccountAuthenticator).buildAuthFilter();
@@ -266,6 +268,7 @@ public class WhisperServerService extends Application<WhisperServerConfiguration
environment.jersey().register(keysController);
environment.jersey().register(messageController);
environment.jersey().register(profileController);
environment.jersey().register(stickerController);
///
WebSocketEnvironment webSocketEnvironment = new WebSocketEnvironment(environment, config.getWebSocketConfiguration(), 90000);

View File

@@ -3,7 +3,7 @@ package org.whispersystems.textsecuregcm.configuration;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotEmpty;
public class ProfilesConfiguration {
public class CdnConfiguration {
@NotEmpty
@JsonProperty
private String accessKey;

View File

@@ -68,6 +68,9 @@ public class RateLimitsConfiguration {
@JsonProperty
private RateLimitConfiguration profile = new RateLimitConfiguration(4320, 3);
@JsonProperty
private RateLimitConfiguration stickerPack = new RateLimitConfiguration(50, 20 / (24.0 * 60.0));
public RateLimitConfiguration getAutoBlock() {
return autoBlock;
}
@@ -132,6 +135,10 @@ public class RateLimitsConfiguration {
return profile;
}
public RateLimitConfiguration getStickerPack() {
return stickerPack;
}
public static class RateLimitConfiguration {
@JsonProperty
private int bucketSize;

View File

@@ -41,7 +41,7 @@ public class AttachmentControllerV2 extends AttachmentControllerBase {
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
long attachmentId = generateAttachmentId();
String objectName = String.valueOf(attachmentId);
Pair<String, String> policy = policyGenerator.createFor(now, String.valueOf(objectName));
Pair<String, String> policy = policyGenerator.createFor(now, String.valueOf(objectName), 100 * 1024 * 1024);
String signature = policySigner.getSignature(now, policy.second());
return new AttachmentDescriptorV2(attachmentId, objectName, policy.first(),

View File

@@ -13,7 +13,7 @@ import org.hibernate.validator.valuehandling.UnwrapValidatedValue;
import org.whispersystems.textsecuregcm.auth.Anonymous;
import org.whispersystems.textsecuregcm.auth.OptionalAccess;
import org.whispersystems.textsecuregcm.auth.UnidentifiedAccessChecksum;
import org.whispersystems.textsecuregcm.configuration.ProfilesConfiguration;
import org.whispersystems.textsecuregcm.configuration.CdnConfiguration;
import org.whispersystems.textsecuregcm.entities.Profile;
import org.whispersystems.textsecuregcm.entities.ProfileAvatarUploadAttributes;
import org.whispersystems.textsecuregcm.limits.RateLimiters;
@@ -55,7 +55,7 @@ public class ProfileController {
public ProfileController(RateLimiters rateLimiters,
AccountsManager accountsManager,
ProfilesConfiguration profilesConfiguration)
CdnConfiguration profilesConfiguration)
{
AWSCredentials credentials = new BasicAWSCredentials(profilesConfiguration.getAccessKey(), profilesConfiguration.getAccessSecret());
AWSCredentialsProvider credentialsProvider = new AWSStaticCredentialsProvider(credentials);
@@ -123,7 +123,7 @@ public class ProfileController {
String previousAvatar = account.getAvatar();
ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC);
String objectName = generateAvatarObjectName();
Pair<String, String> policy = policyGenerator.createFor(now, objectName);
Pair<String, String> policy = policyGenerator.createFor(now, objectName, 10 * 1024 * 1024);
String signature = policySigner.getSignature(now, policy.second());
if (previousAvatar != null && previousAvatar.startsWith("profiles/")) {

View File

@@ -0,0 +1,79 @@
package org.whispersystems.textsecuregcm.controllers;
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.storage.Account;
import org.whispersystems.textsecuregcm.util.Hex;
import org.whispersystems.textsecuregcm.util.Pair;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.security.SecureRandom;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.LinkedList;
import java.util.List;
import io.dropwizard.auth.Auth;
@Path("/v1/sticker")
public class StickerController {
private final RateLimiters rateLimiters;
private final PolicySigner policySigner;
private final PostPolicyGenerator policyGenerator;
public StickerController(RateLimiters rateLimiters, String accessKey, String accessSecret, String region, String bucket) {
this.rateLimiters = rateLimiters;
this.policySigner = new PolicySigner(accessSecret, region);
this.policyGenerator = new PostPolicyGenerator(region, bucket, accessKey);
}
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/pack/form/{count}")
public StickerPackFormUploadAttributes getStickersForm(@Auth Account account,
@PathParam("count") @Min(1) @Max(50) int stickerCount)
throws RateLimitExceededException
{
rateLimiters.getStickerPackLimiter().validate(account.getNumber());
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, 1024);
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);
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, 100155);
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));
}
return new StickerPackFormUploadAttributes(packId, manifest, stickers);
}
private String generatePackId() {
byte[] object = new byte[16];
new SecureRandom().nextBytes(object);
return Hex.toStringCondensed(object);
}
}

View File

@@ -0,0 +1,109 @@
package org.whispersystems.textsecuregcm.entities;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class StickerPackFormUploadAttributes {
@JsonProperty
private StickerPackFormUploadItem manifest;
@JsonProperty
private List<StickerPackFormUploadItem> stickers;
@JsonProperty
private String packId;
public StickerPackFormUploadAttributes() {}
public StickerPackFormUploadAttributes(String packId, StickerPackFormUploadItem manifest, List<StickerPackFormUploadItem> stickers) {
this.packId = packId;
this.manifest = manifest;
this.stickers = stickers;
}
public StickerPackFormUploadItem getManifest() {
return manifest;
}
public List<StickerPackFormUploadItem> getStickers() {
return stickers;
}
public String getPackId() {
return packId;
}
public static class StickerPackFormUploadItem {
@JsonProperty
private int id;
@JsonProperty
private String key;
@JsonProperty
private String credential;
@JsonProperty
private String acl;
@JsonProperty
private String algorithm;
@JsonProperty
private String date;
@JsonProperty
private String policy;
@JsonProperty
private String signature;
public StickerPackFormUploadItem() {}
public StickerPackFormUploadItem(int id, String key, String credential, String acl, String algorithm, String date, String policy, String signature) {
this.key = key;
this.credential = credential;
this.acl = acl;
this.algorithm = algorithm;
this.date = date;
this.policy = policy;
this.signature = signature;
this.id = id;
}
public String getKey() {
return key;
}
public String getCredential() {
return credential;
}
public String getAcl() {
return acl;
}
public String getAlgorithm() {
return algorithm;
}
public String getDate() {
return date;
}
public String getPolicy() {
return policy;
}
public String getSignature() {
return signature;
}
public int getId() {
return id;
}
}
}

View File

@@ -42,6 +42,7 @@ public class RateLimiters {
private final RateLimiter turnLimiter;
private final RateLimiter profileLimiter;
private final RateLimiter stickerPackLimiter;
public RateLimiters(RateLimitsConfiguration config, ReplicatedJedisPool cacheClient) {
this.smsDestinationLimiter = new RateLimiter(cacheClient, "smsDestination",
@@ -107,6 +108,10 @@ public class RateLimiters {
this.profileLimiter = new RateLimiter(cacheClient, "profile",
config.getProfile().getBucketSize(),
config.getProfile().getLeakRatePerMinute());
this.stickerPackLimiter = new RateLimiter(cacheClient, "stickerPack",
config.getStickerPack().getBucketSize(),
config.getStickerPack().getLeakRatePerMinute());
}
public RateLimiter getAllocateDeviceLimiter() {
@@ -173,4 +178,8 @@ public class RateLimiters {
return profileLimiter;
}
public RateLimiter getStickerPackLimiter() {
return stickerPackLimiter;
}
}

View File

@@ -22,7 +22,7 @@ public class PostPolicyGenerator {
this.awsAccessId = awsAccessId;
}
public Pair<String, String> createFor(ZonedDateTime now, String object) {
public Pair<String, String> createFor(ZonedDateTime now, String object, int maxSizeInBytes) {
try {
String expiration = now.plusMinutes(30).format(DateTimeFormatter.ISO_INSTANT);
String credentialDate = now.format(CREDENTIAL_DATE);
@@ -35,7 +35,7 @@ public class PostPolicyGenerator {
" {\"key\": \"%s\"},\n" +
" {\"acl\": \"private\"},\n" +
" [\"starts-with\", \"$Content-Type\", \"\"],\n" +
" [\"content-length-range\", 1, 104857600],\n" +
" [\"content-length-range\", 1, " + maxSizeInBytes + "],\n" +
"\n" +
" {\"x-amz-credential\": \"%s\"},\n" +
" {\"x-amz-algorithm\": \"AWS4-HMAC-SHA256\"},\n" +