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,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