Skip to content

Commit 20d77c8

Browse files
committed
---
yaml --- r: 20887 b: refs/heads/v4support c: 4e30cdc h: refs/heads/master i: 20885: 1990f3a 20883: 7a24253 20879: 1af8432
1 parent 68d8bd8 commit 20d77c8

8 files changed

Lines changed: 471 additions & 65 deletions

File tree

[refs]

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,4 @@ refs/heads/igorbernstein2-patch-1: f62464ee14df1e44a3b173cdc3976563d1b3078b
179179
refs/heads/mrschmidt-collectiongroup: a6d948bf3731a7e1ce1fcd3db8ab733a3c9b17de
180180
refs/heads/release-google-cloud-java-v0.83.0: 4b55ec1b81b3886ede61ae868391a3cdf7eed90e
181181
refs/heads/release-google-cloud-java-v0.83.1-SNAPSHOT: 8d6db7ee534d12b1df38d8cf314871df76f87577
182-
refs/heads/v4support: ab931d8f8f1fe6402486148b7c27f26ff7450e70
182+
refs/heads/v4support: 4e30cdc6d693b4156fae0b04c0bca5a8a1a42931

branches/v4support/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/CanonicalExtensionHeadersSerializer.java

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,31 +32,17 @@
3232
public class CanonicalExtensionHeadersSerializer {
3333

3434
private static final char HEADER_SEPARATOR = ':';
35+
private static final char HEADER_NAME_SEPARATOR = ';';
3536

36-
public StringBuilder serialize(Map<String, String> canonicalizedExtensionHeaders) {
37+
public StringBuilder serialize(Map<String, String> canonicalizedExtensionHeaders, boolean isV4) {
3738

3839
StringBuilder serializedHeaders = new StringBuilder();
3940

4041
if (canonicalizedExtensionHeaders == null || canonicalizedExtensionHeaders.isEmpty()) {
41-
4242
return serializedHeaders;
4343
}
4444

45-
// Make all custom header names lowercase.
46-
Map<String, String> lowercaseHeaders = new HashMap<>();
47-
for (String headerName : new ArrayList<>(canonicalizedExtensionHeaders.keySet())) {
48-
49-
String lowercaseHeaderName = headerName.toLowerCase();
50-
51-
// If present, remove the x-goog-encryption-key and x-goog-encryption-key-sha256 headers.
52-
if ("x-goog-encryption-key".equals(lowercaseHeaderName)
53-
|| "x-goog-encryption-key-sha256".equals(lowercaseHeaderName)) {
54-
55-
continue;
56-
}
57-
58-
lowercaseHeaders.put(lowercaseHeaderName, canonicalizedExtensionHeaders.get(headerName));
59-
}
45+
Map<String, String> lowercaseHeaders = getLowercaseHeaders(canonicalizedExtensionHeaders, isV4);
6046

6147
// Sort all custom headers by header name using a lexicographical sort by code point value.
6248
List<String> sortedHeaderNames = new ArrayList<>(lowercaseHeaders.keySet());
@@ -81,4 +67,56 @@ public StringBuilder serialize(Map<String, String> canonicalizedExtensionHeaders
8167
// Concatenate all custom headers
8268
return serializedHeaders;
8369
}
70+
71+
public StringBuilder serialize(Map<String, String> canonicalizedExtensionHeaders) {
72+
return serialize(canonicalizedExtensionHeaders, false);
73+
}
74+
75+
public StringBuilder serializeHeaderNames(
76+
Map<String, String> canonicalizedExtensionHeaders, boolean isV4) {
77+
StringBuilder serializedHeaders = new StringBuilder();
78+
79+
if (canonicalizedExtensionHeaders == null || canonicalizedExtensionHeaders.isEmpty()) {
80+
return serializedHeaders;
81+
}
82+
83+
Map<String, String> lowercaseHeaders = getLowercaseHeaders(canonicalizedExtensionHeaders, isV4);
84+
85+
List<String> sortedHeaderNames = new ArrayList<>(lowercaseHeaders.keySet());
86+
Collections.sort(sortedHeaderNames);
87+
88+
for (String headerName : sortedHeaderNames) {
89+
serializedHeaders.append(headerName).append(HEADER_NAME_SEPARATOR);
90+
}
91+
92+
serializedHeaders.setLength(serializedHeaders.length() - 1); // remove trailing semicolon
93+
94+
return serializedHeaders;
95+
}
96+
97+
public StringBuilder serializeHeaderNames(Map<String, String> canonicalizedExtentionHeaders) {
98+
return serializeHeaderNames(canonicalizedExtentionHeaders, true);
99+
}
100+
101+
private Map<String, String> getLowercaseHeaders(
102+
Map<String, String> canonicalizedExtensionHeaders, boolean isV4) {
103+
// Make all custom header names lowercase.
104+
Map<String, String> lowercaseHeaders = new HashMap<>();
105+
for (String headerName : new ArrayList<>(canonicalizedExtensionHeaders.keySet())) {
106+
107+
String lowercaseHeaderName = headerName.toLowerCase();
108+
109+
// If present, remove the x-goog-encryption-key and x-goog-encryption-key-sha256 headers.
110+
if ("x-goog-encryption-key".equals(lowercaseHeaderName)
111+
|| "x-goog-encryption-key-sha256".equals(lowercaseHeaderName)
112+
|| (isV4 && "x-goog-encryption-algorithm".equals(lowercaseHeaderName))) {
113+
114+
continue;
115+
}
116+
117+
lowercaseHeaders.put(lowercaseHeaderName, canonicalizedExtensionHeaders.get(headerName));
118+
}
119+
120+
return lowercaseHeaders;
121+
}
84122
}

branches/v4support/google-cloud-clients/google-cloud-storage/src/main/java/com/google/cloud/storage/SignatureInfo.java

Lines changed: 146 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@
1616

1717
package com.google.cloud.storage;
1818

19+
import com.google.common.collect.ImmutableMap;
20+
import com.google.common.hash.Hashing;
21+
import com.google.common.net.UrlEscapers;
22+
1923
import static com.google.common.base.Preconditions.checkArgument;
2024

2125
import java.net.URI;
26+
import java.nio.charset.StandardCharsets;
27+
import java.text.SimpleDateFormat;
28+
import java.util.Date;
2229
import java.util.Map;
2330

2431
/**
@@ -31,30 +38,64 @@
3138
public class SignatureInfo {
3239

3340
public static final char COMPONENT_SEPARATOR = '\n';
41+
public static final String GOOG4_RSA_SHA256 = "GOOG4-RSA-SHA256";
42+
public static final String SCOPE = "/auto/storage/goog4_request";
3443

3544
private final HttpMethod httpVerb;
3645
private final String contentMd5;
3746
private final String contentType;
3847
private final long expiration;
3948
private final Map<String, String> canonicalizedExtensionHeaders;
4049
private final URI canonicalizedResource;
50+
private final Storage.SignUrlOption.SignatureVersion signatureVersion;
51+
private final String accountEmail;
52+
private final long timestamp;
53+
54+
private final String yearMonthDay;
55+
private final String exactDate;
4156

4257
private SignatureInfo(Builder builder) {
4358
this.httpVerb = builder.httpVerb;
4459
this.contentMd5 = builder.contentMd5;
4560
this.contentType = builder.contentType;
4661
this.expiration = builder.expiration;
47-
this.canonicalizedExtensionHeaders = builder.canonicalizedExtensionHeaders;
4862
this.canonicalizedResource = builder.canonicalizedResource;
63+
this.signatureVersion = builder.signatureVersion;
64+
this.accountEmail = builder.accountEmail;
65+
this.timestamp = builder.timestamp;
66+
67+
if (Storage.SignUrlOption.SignatureVersion.V4.equals(signatureVersion)
68+
&& !builder.canonicalizedExtensionHeaders.containsKey("host")) {
69+
canonicalizedExtensionHeaders =
70+
new ImmutableMap.Builder<String, String>()
71+
.putAll(builder.canonicalizedExtensionHeaders)
72+
.put("host", "storage.googleapis.com")
73+
.build();
74+
} else {
75+
canonicalizedExtensionHeaders = builder.canonicalizedExtensionHeaders;
76+
}
77+
78+
Date date = new Date(timestamp);
79+
80+
yearMonthDay = new SimpleDateFormat("yyyyMMdd").format(date);
81+
exactDate = new SimpleDateFormat("yyyyMMdd'T'hhmmss'Z'").format(date);
4982
}
5083

5184
/**
5285
* Constructs payload to be signed.
5386
*
54-
* @return paylod to sign
87+
* @return payload to sign
5588
* @see <a href="https://cloud.google.com/storage/docs/access-control#Signed-URLs">Signed URLs</a>
5689
*/
5790
public String constructUnsignedPayload() {
91+
// TODO reverse order when V4 becomes default
92+
if (Storage.SignUrlOption.SignatureVersion.V4.equals(signatureVersion)) {
93+
return constructV4UnsignedPayload();
94+
}
95+
return constructV2UnsignedPayload();
96+
}
97+
98+
private String constructV2UnsignedPayload() {
5899
StringBuilder payload = new StringBuilder();
59100

60101
payload.append(httpVerb.name()).append(COMPONENT_SEPARATOR);
@@ -80,6 +121,66 @@ public String constructUnsignedPayload() {
80121
return payload.toString();
81122
}
82123

124+
private String constructV4UnsignedPayload() {
125+
StringBuilder payload = new StringBuilder();
126+
127+
payload.append(GOOG4_RSA_SHA256).append(COMPONENT_SEPARATOR);
128+
129+
payload.append(exactDate).append(COMPONENT_SEPARATOR);
130+
131+
payload.append(yearMonthDay).append(SCOPE).append(COMPONENT_SEPARATOR);
132+
133+
payload.append(constructV4CanonicalRequestHash());
134+
135+
return payload.toString();
136+
}
137+
138+
private String constructV4CanonicalRequestHash() {
139+
StringBuilder canonicalRequest = new StringBuilder();
140+
141+
CanonicalExtensionHeadersSerializer serializer = new CanonicalExtensionHeadersSerializer();
142+
143+
canonicalRequest.append(httpVerb.name()).append(COMPONENT_SEPARATOR);
144+
145+
canonicalRequest.append(canonicalizedResource).append(COMPONENT_SEPARATOR);
146+
147+
canonicalRequest.append(constructV4QueryString()).append(COMPONENT_SEPARATOR);
148+
149+
canonicalRequest
150+
.append(serializer.serialize(canonicalizedExtensionHeaders, true))
151+
.append(COMPONENT_SEPARATOR);
152+
153+
canonicalRequest
154+
.append(serializer.serializeHeaderNames(canonicalizedExtensionHeaders))
155+
.append(COMPONENT_SEPARATOR);
156+
157+
canonicalRequest.append("UNSIGNED-PAYLOAD");
158+
return Hashing.sha256()
159+
.hashString(canonicalRequest.toString(), StandardCharsets.UTF_8)
160+
.toString();
161+
}
162+
163+
public String constructV4QueryString() {
164+
StringBuilder signedHeaders =
165+
new CanonicalExtensionHeadersSerializer()
166+
.serializeHeaderNames(canonicalizedExtensionHeaders);
167+
168+
StringBuilder queryString = new StringBuilder();
169+
queryString.append("X-Goog-Algorithm=").append(GOOG4_RSA_SHA256).append("&");
170+
queryString.append(
171+
"X-Goog-Credential="
172+
+ UrlEscapers.urlFormParameterEscaper()
173+
.escape(accountEmail + "/" + yearMonthDay + SCOPE)
174+
+ "&");
175+
queryString.append("X-Goog-Date=" + exactDate + "&");
176+
queryString.append("X-Goog-Expires=" + expiration + "&");
177+
queryString.append(
178+
"X-Goog-SignedHeaders="
179+
+ UrlEscapers.urlFormParameterEscaper().escape(signedHeaders.toString()));
180+
181+
return queryString.toString();
182+
}
183+
83184
public HttpMethod getHttpVerb() {
84185
return httpVerb;
85186
}
@@ -104,6 +205,18 @@ public URI getCanonicalizedResource() {
104205
return canonicalizedResource;
105206
}
106207

208+
public Storage.SignUrlOption.SignatureVersion getSignatureVersion() {
209+
return signatureVersion;
210+
}
211+
212+
public long getTimestamp() {
213+
return timestamp;
214+
}
215+
216+
public String getAccountEmail() {
217+
return accountEmail;
218+
}
219+
107220
public static final class Builder {
108221

109222
private final HttpMethod httpVerb;
@@ -112,6 +225,9 @@ public static final class Builder {
112225
private final long expiration;
113226
private Map<String, String> canonicalizedExtensionHeaders;
114227
private final URI canonicalizedResource;
228+
private Storage.SignUrlOption.SignatureVersion signatureVersion;
229+
private String accountEmail;
230+
private long timestamp;
115231

116232
/**
117233
* Constructs builder.
@@ -134,6 +250,9 @@ public Builder(SignatureInfo signatureInfo) {
134250
this.expiration = signatureInfo.expiration;
135251
this.canonicalizedExtensionHeaders = signatureInfo.canonicalizedExtensionHeaders;
136252
this.canonicalizedResource = signatureInfo.canonicalizedResource;
253+
this.signatureVersion = signatureInfo.signatureVersion;
254+
this.accountEmail = signatureInfo.accountEmail;
255+
this.timestamp = signatureInfo.timestamp;
137256
}
138257

139258
public Builder setContentMd5(String contentMd5) {
@@ -155,12 +274,37 @@ public Builder setCanonicalizedExtensionHeaders(
155274
return this;
156275
}
157276

277+
public Builder setSignatureVersion(Storage.SignUrlOption.SignatureVersion signatureVersion) {
278+
this.signatureVersion = signatureVersion;
279+
280+
return this;
281+
}
282+
283+
public Builder setAccountEmail(String accountEmail) {
284+
this.accountEmail = accountEmail;
285+
286+
return this;
287+
}
288+
289+
public Builder setTimestamp(long timestamp) {
290+
this.timestamp = timestamp;
291+
292+
return this;
293+
}
294+
158295
/** Creates an {@code SignatureInfo} object from this builder. */
159296
public SignatureInfo build() {
160297
checkArgument(httpVerb != null, "Required HTTP method");
161298
checkArgument(canonicalizedResource != null, "Required canonicalized resource");
162299
checkArgument(expiration >= 0, "Expiration must be greater than or equal to zero");
163300

301+
if (Storage.SignUrlOption.SignatureVersion.V4.equals(signatureVersion)) {
302+
checkArgument(accountEmail != null, "Account email required to use V4 signing");
303+
checkArgument(timestamp > 0, "Timestamp required to use V4 signing");
304+
checkArgument(
305+
expiration <= 604800000, "Expiration can't be longer than 7 days to use V4 signing");
306+
}
307+
164308
return new SignatureInfo(this);
165309
}
166310
}

0 commit comments

Comments
 (0)