1616
1717package 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+
1923import static com .google .common .base .Preconditions .checkArgument ;
2024
2125import java .net .URI ;
26+ import java .nio .charset .StandardCharsets ;
27+ import java .text .SimpleDateFormat ;
28+ import java .util .Date ;
2229import java .util .Map ;
2330
2431/**
3138public 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,65 @@ 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 ))
151+ .append (COMPONENT_SEPARATOR );
152+
153+ canonicalRequest
154+ .append (serializer .serializeHeaderNames (canonicalizedExtensionHeaders ))
155+ .append (COMPONENT_SEPARATOR );
156+
157+ canonicalRequest .append ("UNSIGNED-PAYLOAD" );
158+
159+ return Hashing .sha256 ()
160+ .hashString (canonicalRequest .toString (), StandardCharsets .UTF_8 )
161+ .toString ();
162+ }
163+
164+ public String constructV4QueryString () {
165+ StringBuilder signedHeaders =
166+ new CanonicalExtensionHeadersSerializer ()
167+ .serializeHeaderNames (canonicalizedExtensionHeaders );
168+
169+ StringBuilder queryString = new StringBuilder ();
170+ queryString .append ("X-Goog-Algorithm=" ).append (GOOG4_RSA_SHA256 ).append ("&" );
171+ queryString .append (
172+ "X-Goog-Credential="
173+ + UrlEscapers .urlFormParameterEscaper ()
174+ .escape (accountEmail + "/" + yearMonthDay + SCOPE )
175+ + "&" );
176+ queryString .append ("X-Goog-Date=" + exactDate + "&" );
177+ queryString .append ("X-Goog-Expires=" + expiration + "&" );
178+ queryString .append ("X-Goog-SignedHeaders=" + signedHeaders .toString ());
179+
180+ return queryString .toString ();
181+ }
182+
83183 public HttpMethod getHttpVerb () {
84184 return httpVerb ;
85185 }
@@ -104,6 +204,18 @@ public URI getCanonicalizedResource() {
104204 return canonicalizedResource ;
105205 }
106206
207+ public Storage .SignUrlOption .SignatureVersion getSignatureVersion () {
208+ return signatureVersion ;
209+ }
210+
211+ public long getTimestamp () {
212+ return timestamp ;
213+ }
214+
215+ public String getAccountEmail () {
216+ return accountEmail ;
217+ }
218+
107219 public static final class Builder {
108220
109221 private final HttpMethod httpVerb ;
@@ -112,6 +224,9 @@ public static final class Builder {
112224 private final long expiration ;
113225 private Map <String , String > canonicalizedExtensionHeaders ;
114226 private final URI canonicalizedResource ;
227+ private Storage .SignUrlOption .SignatureVersion signatureVersion ;
228+ private String accountEmail ;
229+ private long timestamp ;
115230
116231 /**
117232 * Constructs builder.
@@ -134,6 +249,9 @@ public Builder(SignatureInfo signatureInfo) {
134249 this .expiration = signatureInfo .expiration ;
135250 this .canonicalizedExtensionHeaders = signatureInfo .canonicalizedExtensionHeaders ;
136251 this .canonicalizedResource = signatureInfo .canonicalizedResource ;
252+ this .signatureVersion = signatureInfo .signatureVersion ;
253+ this .accountEmail = signatureInfo .accountEmail ;
254+ this .timestamp = signatureInfo .timestamp ;
137255 }
138256
139257 public Builder setContentMd5 (String contentMd5 ) {
@@ -155,12 +273,37 @@ public Builder setCanonicalizedExtensionHeaders(
155273 return this ;
156274 }
157275
276+ public Builder setSignatureVersion (Storage .SignUrlOption .SignatureVersion signatureVersion ) {
277+ this .signatureVersion = signatureVersion ;
278+
279+ return this ;
280+ }
281+
282+ public Builder setAccountEmail (String accountEmail ) {
283+ this .accountEmail = accountEmail ;
284+
285+ return this ;
286+ }
287+
288+ public Builder setTimestamp (long timestamp ) {
289+ this .timestamp = timestamp ;
290+
291+ return this ;
292+ }
293+
158294 /** Creates an {@code SignatureInfo} object from this builder. */
159295 public SignatureInfo build () {
160296 checkArgument (httpVerb != null , "Required HTTP method" );
161297 checkArgument (canonicalizedResource != null , "Required canonicalized resource" );
162298 checkArgument (expiration >= 0 , "Expiration must be greater than or equal to zero" );
163299
300+ if (Storage .SignUrlOption .SignatureVersion .V4 .equals (signatureVersion )) {
301+ checkArgument (accountEmail != null , "Account email required to use V4 signing" );
302+ checkArgument (timestamp > 0 , "Timestamp required to use V4 signing" );
303+ checkArgument (
304+ expiration <= 604800000 , "Expiration can't be longer than 7 days to use V4 signing" );
305+ }
306+
164307 return new SignatureInfo (this );
165308 }
166309 }
0 commit comments