mirror of
https://github.com/signalapp/Signal-Server
synced 2026-04-20 05:28:05 +01:00
Create attachments V3 endpoint for CDN2 on GCP
In preparation for resumable uploads, this creates a separate attachment authorization endpoint that creates a signed URL for accessing GCP Storage through Signal's CDN2. This should allow Signal clients to do byte-level resume of media uploads.
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
package org.whispersystems.textsecuregcm.gcp;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class CanonicalRequest {
|
||||
|
||||
@Nonnull
|
||||
private final String canonicalRequest;
|
||||
|
||||
@Nonnull
|
||||
private final String resourcePath;
|
||||
|
||||
@Nonnull
|
||||
private final String canonicalQuery;
|
||||
|
||||
@Nonnull
|
||||
private final String activeDatetime;
|
||||
|
||||
@Nonnull
|
||||
private final String credentialScope;
|
||||
|
||||
@Nonnull
|
||||
private final String domain;
|
||||
|
||||
private final int maxSizeInBytes;
|
||||
|
||||
public CanonicalRequest(@Nonnull String canonicalRequest, @Nonnull String resourcePath, @Nonnull String canonicalQuery, @Nonnull String activeDatetime, @Nonnull String credentialScope, @Nonnull String domain, int maxSizeInBytes) {
|
||||
this.canonicalRequest = canonicalRequest;
|
||||
this.resourcePath = resourcePath;
|
||||
this.canonicalQuery = canonicalQuery;
|
||||
this.activeDatetime = activeDatetime;
|
||||
this.credentialScope = credentialScope;
|
||||
this.domain = domain;
|
||||
this.maxSizeInBytes = maxSizeInBytes;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
String getCanonicalRequest() {
|
||||
return canonicalRequest;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getResourcePath() {
|
||||
return resourcePath;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getCanonicalQuery() {
|
||||
return canonicalQuery;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
String getActiveDatetime() {
|
||||
return activeDatetime;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
String getCredentialScope() {
|
||||
return credentialScope;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public String getDomain() {
|
||||
return domain;
|
||||
}
|
||||
|
||||
public int getMaxSizeInBytes() {
|
||||
return maxSizeInBytes;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.whispersystems.textsecuregcm.gcp;
|
||||
|
||||
import io.dropwizard.util.Strings;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Locale;
|
||||
|
||||
public class CanonicalRequestGenerator {
|
||||
private static final DateTimeFormatter SIMPLE_UTC_DATE = DateTimeFormatter.ofPattern("yyyyMMdd", Locale.US).withZone(ZoneOffset.UTC);
|
||||
private static final DateTimeFormatter SIMPLE_UTC_DATE_TIME = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'", Locale.US).withZone(ZoneOffset.UTC);
|
||||
|
||||
@Nonnull
|
||||
private final String domain;
|
||||
|
||||
@Nonnull
|
||||
private final String email;
|
||||
|
||||
private final int maxSizeBytes;
|
||||
|
||||
@Nonnull
|
||||
private final String pathPrefix;
|
||||
|
||||
public CanonicalRequestGenerator(@Nonnull String domain, @Nonnull String email, int maxSizeBytes, @Nonnull String pathPrefix) {
|
||||
this.domain = domain;
|
||||
this.email = email;
|
||||
this.maxSizeBytes = maxSizeBytes;
|
||||
this.pathPrefix = pathPrefix;
|
||||
}
|
||||
|
||||
public CanonicalRequest createFor(@Nonnull final String key, @Nonnull final ZonedDateTime now) {
|
||||
final StringBuilder result = new StringBuilder("POST\n");
|
||||
|
||||
final StringBuilder resourcePathBuilder = new StringBuilder();
|
||||
if (!Strings.isNullOrEmpty(pathPrefix)) {
|
||||
resourcePathBuilder.append(pathPrefix);
|
||||
}
|
||||
resourcePathBuilder.append('/').append(URLEncoder.encode(key, StandardCharsets.UTF_8));
|
||||
final String resourcePath = resourcePathBuilder.toString();
|
||||
result.append(resourcePath).append('\n');
|
||||
|
||||
final String activeDatetime = SIMPLE_UTC_DATE_TIME.format(now);
|
||||
final String canonicalQuery = "X-Goog-Algorithm=GOOG4-RSA-SHA256" +
|
||||
"&X-Goog-Credential=" + URLEncoder.encode(makeCredential(email, now), StandardCharsets.UTF_8) +
|
||||
"&X-Goog-Date=" + URLEncoder.encode(activeDatetime, StandardCharsets.UTF_8) +
|
||||
"&X-Goog-Expires=" + Duration.of(25, ChronoUnit.HOURS).toSeconds() +
|
||||
"&X-Goog-SignedHeaders=host%3Bx-goog-content-length-range%3Bx-goog-resumable";
|
||||
result.append(canonicalQuery).append('\n');
|
||||
|
||||
result.append("host:").append(domain).append('\n');
|
||||
result.append("x-goog-content-length-range:1,").append(maxSizeBytes).append('\n');
|
||||
result.append("x-goog-resumable:start\n");
|
||||
result.append('\n');
|
||||
|
||||
result.append("host;x-goog-content-length-range;x-goog-resumable\n");
|
||||
|
||||
result.append("UNSIGNED-PAYLOAD");
|
||||
|
||||
return new CanonicalRequest(result.toString(), resourcePath, canonicalQuery, activeDatetime, makeCredentialScope(now), domain, maxSizeBytes);
|
||||
}
|
||||
|
||||
private String makeCredentialScope(@Nonnull ZonedDateTime now) {
|
||||
return SIMPLE_UTC_DATE.format(now) + "/auto/storage/goog4_request";
|
||||
}
|
||||
|
||||
private String makeCredential(@Nonnull String email, @Nonnull ZonedDateTime now) {
|
||||
return email + '/' + makeCredentialScope(now);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.whispersystems.textsecuregcm.gcp;
|
||||
|
||||
import org.apache.commons.codec.binary.Hex;
|
||||
import org.bouncycastle.openssl.PEMReader;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
|
||||
public class CanonicalRequestSigner {
|
||||
|
||||
@Nonnull
|
||||
private final PrivateKey rsaSigningKey;
|
||||
|
||||
public CanonicalRequestSigner(@Nonnull String rsaSigningKey) throws IOException, InvalidKeyException {
|
||||
this.rsaSigningKey = initializeRsaSigningKey(rsaSigningKey);
|
||||
}
|
||||
|
||||
public String sign(@Nonnull CanonicalRequest canonicalRequest) {
|
||||
return sign(makeStringToSign(canonicalRequest));
|
||||
}
|
||||
|
||||
private String makeStringToSign(@Nonnull final CanonicalRequest canonicalRequest) {
|
||||
final StringBuilder result = new StringBuilder("GOOG4-RSA-SHA256\n");
|
||||
|
||||
result.append(canonicalRequest.getActiveDatetime()).append('\n');
|
||||
|
||||
result.append(canonicalRequest.getCredentialScope()).append('\n');
|
||||
|
||||
final MessageDigest sha256;
|
||||
try {
|
||||
sha256 = MessageDigest.getInstance("SHA-256");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
sha256.update(canonicalRequest.getCanonicalRequest().getBytes(StandardCharsets.UTF_8));
|
||||
result.append(Hex.encodeHex(sha256.digest()));
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private String sign(@Nonnull String stringToSign) {
|
||||
final byte[] signature;
|
||||
try {
|
||||
final Signature sha256rsa = Signature.getInstance("SHA256WITHRSA");
|
||||
sha256rsa.initSign(rsaSigningKey);
|
||||
sha256rsa.update(stringToSign.getBytes(StandardCharsets.UTF_8));
|
||||
signature = sha256rsa.sign();
|
||||
} catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
return Hex.encodeHexString(signature);
|
||||
}
|
||||
|
||||
private static PrivateKey initializeRsaSigningKey(String rsaSigningKey) throws IOException, InvalidKeyException {
|
||||
final PEMReader pemReader = new PEMReader(new StringReader(rsaSigningKey));
|
||||
final PrivateKey key = (PrivateKey) pemReader.readObject();
|
||||
testKeyIsValidForSigning(key);
|
||||
return key;
|
||||
}
|
||||
|
||||
private static void testKeyIsValidForSigning(PrivateKey key) throws InvalidKeyException {
|
||||
final Signature sha256rsa;
|
||||
try {
|
||||
sha256rsa = Signature.getInstance("SHA256WITHRSA");
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
sha256rsa.initSign(key);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user