diff --git a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java index 33faaf4a6..26854a8df 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java +++ b/src/main/java/org/whispersystems/textsecuregcm/WhisperServerService.java @@ -30,7 +30,8 @@ import org.whispersystems.textsecuregcm.auth.CertificateGenerator; import org.whispersystems.textsecuregcm.auth.DirectoryCredentialsGenerator; import org.whispersystems.textsecuregcm.auth.TurnTokenGenerator; import org.whispersystems.textsecuregcm.controllers.AccountController; -import org.whispersystems.textsecuregcm.controllers.AttachmentController; +import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV1; +import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2; import org.whispersystems.textsecuregcm.controllers.CertificateController; import org.whispersystems.textsecuregcm.controllers.DeviceController; import org.whispersystems.textsecuregcm.controllers.DirectoryController; @@ -62,7 +63,6 @@ import org.whispersystems.textsecuregcm.push.ReceiptSender; import org.whispersystems.textsecuregcm.push.WebsocketSender; import org.whispersystems.textsecuregcm.recaptcha.RecaptchaClient; import org.whispersystems.textsecuregcm.redis.ReplicatedJedisPool; -import org.whispersystems.textsecuregcm.s3.UrlSigner; import org.whispersystems.textsecuregcm.sms.SmsSender; import org.whispersystems.textsecuregcm.sms.TwilioSmsSender; import org.whispersystems.textsecuregcm.sqs.DirectoryQueue; @@ -190,7 +190,6 @@ public class WhisperServerService extends Application() .setAuthenticator(deviceAuthenticator) @@ -233,7 +233,8 @@ public class WhisperServerService extends Application account.getNumber().startsWith(region))); - return new AttachmentDescriptor(attachmentId, url.toExternalForm()); + return new AttachmentDescriptorV1(attachmentId, url.toExternalForm()); } @@ -85,11 +84,4 @@ public class AttachmentController { return new AttachmentUri(urlSigner.getPreSignedUrl(attachmentId, HttpMethod.GET, Stream.of(UNACCELERATED_REGIONS).anyMatch(region -> account.getNumber().startsWith(region)))); } - private long generateAttachmentId() { - byte[] attachmentBytes = new byte[8]; - new SecureRandom().nextBytes(attachmentBytes); - - attachmentBytes[0] = (byte)(attachmentBytes[0] & 0x7F); - return Conversions.byteArrayToLong(attachmentBytes); - } } diff --git a/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV2.java b/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV2.java new file mode 100644 index 000000000..faa67f6f7 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/controllers/AttachmentControllerV2.java @@ -0,0 +1,54 @@ +package org.whispersystems.textsecuregcm.controllers; + +import com.codahale.metrics.annotation.Timed; +import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV2; +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.util.Pair; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; + +import io.dropwizard.auth.Auth; + +@Path("/v2/attachments") +public class AttachmentControllerV2 extends AttachmentControllerBase { + + private final PostPolicyGenerator policyGenerator; + private final PolicySigner policySigner; + private final RateLimiter rateLimiter; + + public AttachmentControllerV2(RateLimiters rateLimiters, String accessKey, String accessSecret, String region, String bucket) { + this.rateLimiter = rateLimiters.getAttachmentLimiter(); + this.policyGenerator = new PostPolicyGenerator(region, bucket, accessKey); + this.policySigner = new PolicySigner(accessSecret, region); + } + + @Timed + @GET + @Produces(MediaType.APPLICATION_JSON) + @Path("/form/upload") + public AttachmentDescriptorV2 getAttachmentUploadForm(@Auth Account account) throws RateLimitExceededException { + rateLimiter.validate(account.getNumber()); + + ZonedDateTime now = ZonedDateTime.now(ZoneOffset.UTC); + long attachmentId = generateAttachmentId(); + String objectName = String.valueOf(attachmentId); + Pair policy = policyGenerator.createFor(now, String.valueOf(objectName)); + String signature = policySigner.getSignature(now, policy.second()); + + return new AttachmentDescriptorV2(attachmentId, objectName, policy.first(), + "private", "AWS4-HMAC-SHA256", + now.format(PostPolicyGenerator.AWS_DATE_TIME), + policy.second(), signature); + } + + +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptor.java b/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptorV1.java similarity index 89% rename from src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptor.java rename to src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptorV1.java index 1c9c23cc4..3065a826d 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptor.java +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptorV1.java @@ -18,7 +18,7 @@ package org.whispersystems.textsecuregcm.entities; import com.fasterxml.jackson.annotation.JsonProperty; -public class AttachmentDescriptor { +public class AttachmentDescriptorV1 { @JsonProperty private long id; @@ -29,13 +29,13 @@ public class AttachmentDescriptor { @JsonProperty private String location; - public AttachmentDescriptor(long id, String location) { + public AttachmentDescriptorV1(long id, String location) { this.id = id; this.idString = String.valueOf(id); this.location = location; } - public AttachmentDescriptor() {} + public AttachmentDescriptorV1() {} public long getId() { return id; diff --git a/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptorV2.java b/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptorV2.java new file mode 100644 index 000000000..0ecf2c684 --- /dev/null +++ b/src/main/java/org/whispersystems/textsecuregcm/entities/AttachmentDescriptorV2.java @@ -0,0 +1,88 @@ +package org.whispersystems.textsecuregcm.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class AttachmentDescriptorV2 { + + @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; + + @JsonProperty + private long attachmentId; + + @JsonProperty + private String attachmentIdString; + + public AttachmentDescriptorV2() {} + + public AttachmentDescriptorV2(long attachmentId, + String key, String credential, + String acl, String algorithm, + String date, String policy, + String signature) + { + this.attachmentId = attachmentId; + this.attachmentIdString = String.valueOf(attachmentId); + this.key = key; + this.credential = credential; + this.acl = acl; + this.algorithm = algorithm; + this.date = date; + this.policy = policy; + this.signature = signature; + } + + 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 long getAttachmentId() { + return attachmentId; + } + + public String getAttachmentIdString() { + return attachmentIdString; + } +} diff --git a/src/main/java/org/whispersystems/textsecuregcm/s3/PostPolicyGenerator.java b/src/main/java/org/whispersystems/textsecuregcm/s3/PostPolicyGenerator.java index f148eb2ac..8b5468359 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/s3/PostPolicyGenerator.java +++ b/src/main/java/org/whispersystems/textsecuregcm/s3/PostPolicyGenerator.java @@ -35,6 +35,7 @@ public class PostPolicyGenerator { " {\"key\": \"%s\"},\n" + " {\"acl\": \"private\"},\n" + " [\"starts-with\", \"$Content-Type\", \"\"],\n" + + " [\"content-length-range\", 1, 104857600],\n" + "\n" + " {\"x-amz-credential\": \"%s\"},\n" + " {\"x-amz-algorithm\": \"AWS4-HMAC-SHA256\"},\n" + diff --git a/src/main/java/org/whispersystems/textsecuregcm/s3/UrlSigner.java b/src/main/java/org/whispersystems/textsecuregcm/s3/UrlSigner.java index dfd19ef1a..dea50d5e7 100644 --- a/src/main/java/org/whispersystems/textsecuregcm/s3/UrlSigner.java +++ b/src/main/java/org/whispersystems/textsecuregcm/s3/UrlSigner.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) 2013 Open WhisperSystems * * This program is free software: you can redistribute it and/or modify @@ -23,7 +23,6 @@ import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.AmazonS3Client; import com.amazonaws.services.s3.S3ClientOptions; import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; -import org.whispersystems.textsecuregcm.configuration.AttachmentsConfiguration; import java.net.URL; import java.util.Date; @@ -35,9 +34,9 @@ public class UrlSigner { private final AWSCredentials credentials; private final String bucket; - public UrlSigner(AttachmentsConfiguration config) { - this.credentials = new BasicAWSCredentials(config.getAccessKey(), config.getAccessSecret()); - this.bucket = config.getBucket(); + public UrlSigner(String accessKey, String accessSecret, String bucket) { + this.credentials = new BasicAWSCredentials(accessKey, accessSecret); + this.bucket = bucket; } public URL getPreSignedUrl(long attachmentId, HttpMethod method, boolean unaccelerated) { diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AttachmentControllerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AttachmentControllerTest.java index 3e34d01cd..ee396216f 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AttachmentControllerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/controllers/AttachmentControllerTest.java @@ -3,13 +3,13 @@ package org.whispersystems.textsecuregcm.tests.controllers; import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; import org.junit.ClassRule; import org.junit.Test; -import org.whispersystems.textsecuregcm.configuration.AttachmentsConfiguration; -import org.whispersystems.textsecuregcm.controllers.AttachmentController; -import org.whispersystems.textsecuregcm.entities.AttachmentDescriptor; +import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV1; +import org.whispersystems.textsecuregcm.controllers.AttachmentControllerV2; +import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV1; +import org.whispersystems.textsecuregcm.entities.AttachmentDescriptorV2; import org.whispersystems.textsecuregcm.entities.AttachmentUri; import org.whispersystems.textsecuregcm.limits.RateLimiter; import org.whispersystems.textsecuregcm.limits.RateLimiters; -import org.whispersystems.textsecuregcm.s3.UrlSigner; import org.whispersystems.textsecuregcm.storage.Account; import org.whispersystems.textsecuregcm.tests.util.AuthHelper; import org.whispersystems.textsecuregcm.util.SystemMapper; @@ -24,19 +24,11 @@ import static org.mockito.Mockito.when; public class AttachmentControllerTest { - private static AttachmentsConfiguration configuration = mock(AttachmentsConfiguration.class); private static RateLimiters rateLimiters = mock(RateLimiters.class ); private static RateLimiter rateLimiter = mock(RateLimiter.class ); - private static UrlSigner urlSigner; - static { - when(configuration.getAccessKey()).thenReturn("accessKey"); - when(configuration.getAccessSecret()).thenReturn("accessSecret"); - when(configuration.getBucket()).thenReturn("attachment-bucket"); - when(rateLimiters.getAttachmentLimiter()).thenReturn(rateLimiter); - urlSigner = new UrlSigner(configuration); } @ClassRule @@ -45,16 +37,43 @@ public class AttachmentControllerTest { .addProvider(new AuthValueFactoryProvider.Binder<>(Account.class)) .setMapper(SystemMapper.getMapper()) .setTestContainerFactory(new GrizzlyWebTestContainerFactory()) - .addResource(new AttachmentController(rateLimiters, urlSigner)) + .addResource(new AttachmentControllerV1(rateLimiters, "accessKey", "accessSecret", "attachment-bucket")) + .addResource(new AttachmentControllerV2(rateLimiters, "accessKey", "accessSecret", "us-east-1", "attachmentv2-bucket")) .build(); + @Test + public void testV2Form() { + AttachmentDescriptorV2 descriptor = resources.getJerseyTest() + .target("/v2/attachments/form/upload") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .get(AttachmentDescriptorV2.class); + + assertThat(descriptor.getKey()).isEqualTo(descriptor.getAttachmentIdString()); + assertThat(descriptor.getAcl()).isEqualTo("private"); + assertThat(descriptor.getAlgorithm()).isEqualTo("AWS4-HMAC-SHA256"); + assertThat(descriptor.getAttachmentId()).isGreaterThan(0); + assertThat(String.valueOf(descriptor.getAttachmentId())).isEqualTo(descriptor.getAttachmentIdString()); + + String[] credentialParts = descriptor.getCredential().split("/"); + + assertThat(credentialParts[0]).isEqualTo("accessKey"); + assertThat(credentialParts[2]).isEqualTo("us-east-1"); + assertThat(credentialParts[3]).isEqualTo("s3"); + assertThat(credentialParts[4]).isEqualTo("aws4_request"); + + assertThat(descriptor.getDate()).isNotBlank(); + assertThat(descriptor.getPolicy()).isNotBlank(); + assertThat(descriptor.getSignature()).isNotBlank(); + } + @Test public void testAcceleratedPut() { - AttachmentDescriptor descriptor = resources.getJerseyTest() - .target("/v1/attachments/") - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) - .get(AttachmentDescriptor.class); + AttachmentDescriptorV1 descriptor = resources.getJerseyTest() + .target("/v1/attachments/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER, AuthHelper.VALID_PASSWORD)) + .get(AttachmentDescriptorV1.class); assertThat(descriptor.getLocation()).startsWith("https://attachment-bucket.s3-accelerate.amazonaws.com"); assertThat(descriptor.getId()).isGreaterThan(0); @@ -63,11 +82,11 @@ public class AttachmentControllerTest { @Test public void testUnacceleratedPut() { - AttachmentDescriptor descriptor = resources.getJerseyTest() - .target("/v1/attachments/") - .request() - .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO)) - .get(AttachmentDescriptor.class); + AttachmentDescriptorV1 descriptor = resources.getJerseyTest() + .target("/v1/attachments/") + .request() + .header("Authorization", AuthHelper.getAuthHeader(AuthHelper.VALID_NUMBER_TWO, AuthHelper.VALID_PASSWORD_TWO)) + .get(AttachmentDescriptorV1.class); assertThat(descriptor.getLocation()).startsWith("https://s3.amazonaws.com"); assertThat(descriptor.getId()).isGreaterThan(0); diff --git a/src/test/java/org/whispersystems/textsecuregcm/tests/util/UrlSignerTest.java b/src/test/java/org/whispersystems/textsecuregcm/tests/util/UrlSignerTest.java index 851caaf56..946a6f55c 100644 --- a/src/test/java/org/whispersystems/textsecuregcm/tests/util/UrlSignerTest.java +++ b/src/test/java/org/whispersystems/textsecuregcm/tests/util/UrlSignerTest.java @@ -2,25 +2,17 @@ package org.whispersystems.textsecuregcm.tests.util; import com.amazonaws.HttpMethod; import org.junit.Test; -import org.whispersystems.textsecuregcm.configuration.AttachmentsConfiguration; import org.whispersystems.textsecuregcm.s3.UrlSigner; import java.net.URL; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class UrlSignerTest { @Test public void testTransferAcceleration() { - AttachmentsConfiguration configuration = mock(AttachmentsConfiguration.class); - when(configuration.getAccessKey()).thenReturn("foo"); - when(configuration.getAccessSecret()).thenReturn("bar"); - when(configuration.getBucket()).thenReturn("attachments-test"); - - UrlSigner signer = new UrlSigner(configuration); + UrlSigner signer = new UrlSigner("foo", "bar", "attachments-test"); URL url = signer.getPreSignedUrl(1234, HttpMethod.GET, false); assertThat(url).hasHost("attachments-test.s3-accelerate.amazonaws.com"); @@ -28,12 +20,7 @@ public class UrlSignerTest { @Test public void testTransferUnaccelerated() { - AttachmentsConfiguration configuration = mock(AttachmentsConfiguration.class); - when(configuration.getAccessKey()).thenReturn("foo"); - when(configuration.getAccessSecret()).thenReturn("bar"); - when(configuration.getBucket()).thenReturn("attachments-test"); - - UrlSigner signer = new UrlSigner(configuration); + UrlSigner signer = new UrlSigner("foo", "bar", "attachments-test"); URL url = signer.getPreSignedUrl(1234, HttpMethod.GET, true); assertThat(url).hasHost("s3.amazonaws.com");