From 804d44785aa5ff1c3d6654dbb57a56f8ee81b2af Mon Sep 17 00:00:00 2001 From: Marco Ziccardi Date: Wed, 6 Apr 2016 19:02:58 +0200 Subject: [PATCH] Add ServiceAccountSigner interface, enable signing for AE credentials --- .../com/google/gcloud/AuthCredentials.java | 104 ++++++++++++++++-- .../google/gcloud/ServiceAccountSigner.java | 67 +++++++++++ .../com/google/gcloud/SerializationTest.java | 49 +++++---- .../examples/storage/StorageExample.java | 2 +- .../java/com/google/gcloud/storage/Blob.java | 60 +++++----- .../com/google/gcloud/storage/Storage.java | 56 ++++++---- .../google/gcloud/storage/StorageImpl.java | 35 ++---- 7 files changed, 268 insertions(+), 105 deletions(-) create mode 100644 gcloud-java-core/src/main/java/com/google/gcloud/ServiceAccountSigner.java diff --git a/gcloud-java-core/src/main/java/com/google/gcloud/AuthCredentials.java b/gcloud-java-core/src/main/java/com/google/gcloud/AuthCredentials.java index 27cafc181505..080d49ed3ccc 100644 --- a/gcloud-java-core/src/main/java/com/google/gcloud/AuthCredentials.java +++ b/gcloud-java-core/src/main/java/com/google/gcloud/AuthCredentials.java @@ -25,8 +25,13 @@ import java.io.IOException; import java.io.InputStream; import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; import java.util.Collection; import java.util.Objects; @@ -35,16 +40,26 @@ */ public abstract class AuthCredentials implements Restorable { - private static class AppEngineAuthCredentials extends AuthCredentials { + /** + * Represents built-in credentials when running in Google App Engine. + */ + public static class AppEngineAuthCredentials extends AuthCredentials + implements ServiceAccountSigner { private static final AuthCredentials INSTANCE = new AppEngineAuthCredentials(); private static final AppEngineAuthCredentialsState STATE = new AppEngineAuthCredentialsState(); - private static class AppEngineCredentials extends GoogleCredentials { + private AppEngineCredentials credentials; + + private static class AppEngineCredentials extends GoogleCredentials + implements ServiceAccountSigner { private final Object appIdentityService; + private final String account; private final Method getAccessToken; private final Method getAccessTokenResult; + private final Method signForApp; + private final Method getSignature; private final Collection scopes; AppEngineCredentials() { @@ -59,6 +74,12 @@ private static class AppEngineCredentials extends GoogleCredentials { "com.google.appengine.api.appidentity.AppIdentityService$GetAccessTokenResult"); this.getAccessTokenResult = serviceClass.getMethod("getAccessToken", Iterable.class); this.getAccessToken = tokenResultClass.getMethod("getAccessToken"); + this.account = (String) serviceClass.getMethod("getServiceAccountName") + .invoke(appIdentityService); + this.signForApp = serviceClass.getMethod("signForApp", byte[].class); + Class signingResultClass = Class.forName( + "com.google.appengine.api.appidentity.AppIdentityService$SigningResult"); + this.getSignature = signingResultClass.getMethod("getSignature"); this.scopes = null; } catch (Exception e) { throw new RuntimeException("Could not create AppEngineCredentials.", e); @@ -69,11 +90,14 @@ private static class AppEngineCredentials extends GoogleCredentials { this.appIdentityService = unscoped.appIdentityService; this.getAccessToken = unscoped.getAccessToken; this.getAccessTokenResult = unscoped.getAccessTokenResult; + this.account = unscoped.account; + this.signForApp = unscoped.signForApp; + this.getSignature = unscoped.getSignature; this.scopes = scopes; } /** - * Refresh the access token by getting it from the App Identity service + * Refresh the access token by getting it from the App Identity service. */ @Override public AccessToken refreshAccessToken() throws IOException { @@ -98,6 +122,21 @@ public boolean createScopedRequired() { public GoogleCredentials createScoped(Collection scopes) { return new AppEngineCredentials(scopes, this); } + + @Override + public String account() { + return account; + } + + @Override + public byte[] sign(byte[] toSign) { + try { + Object signingResult = signForApp.invoke(appIdentityService, (Object) toSign); + return (byte[]) getSignature.invoke(signingResult); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { + throw new SigningException("Failed to sign the provided bytes", ex); + } + } } private static class AppEngineAuthCredentialsState @@ -122,14 +161,27 @@ public boolean equals(Object obj) { } @Override - public GoogleCredentials credentials() { - return new AppEngineCredentials(); + public AppEngineCredentials credentials() { + if (credentials == null) { + credentials = new AppEngineCredentials(); + } + return credentials; } @Override public RestorableState capture() { return STATE; } + + @Override + public String account() { + return credentials().account(); + } + + @Override + public byte[] sign(byte[] toSign) { + return credentials().sign(toSign); + } } /** @@ -138,8 +190,10 @@ public RestorableState capture() { * @see * User accounts and service accounts */ - public static class ServiceAccountAuthCredentials extends AuthCredentials { + public static class ServiceAccountAuthCredentials extends AuthCredentials + implements ServiceAccountSigner { + private final ServiceAccountCredentials credentials; private final String account; private final PrivateKey privateKey; @@ -178,23 +232,44 @@ public boolean equals(Object obj) { } ServiceAccountAuthCredentials(String account, PrivateKey privateKey) { - this.account = checkNotNull(account); - this.privateKey = checkNotNull(privateKey); + this(new ServiceAccountCredentials(null, account, privateKey, null, null)); + } + + ServiceAccountAuthCredentials(ServiceAccountCredentials credentials) { + this.credentials = checkNotNull(credentials); + this.account = checkNotNull(credentials.getClientEmail()); + this.privateKey = checkNotNull(credentials.getPrivateKey()); } @Override public ServiceAccountCredentials credentials() { - return new ServiceAccountCredentials(null, account, privateKey, null, null); + return credentials; } + @Override public String account() { return account; } + /** + * Returns the private key associated with the service account credentials. + */ public PrivateKey privateKey() { return privateKey; } + @Override + public byte[] sign(byte[] toSign) { + try { + Signature signer = Signature.getInstance("SHA256withRSA"); + signer.initSign(privateKey()); + signer.update(toSign); + return signer.sign(); + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException ex) { + throw new SigningException("Failed to sign the provided bytes", ex); + } + } + @Override public RestorableState capture() { return new ServiceAccountAuthCredentialsState(account, privateKey); @@ -242,6 +317,10 @@ public boolean equals(Object obj) { } } + ApplicationDefaultAuthCredentials(GoogleCredentials credentials) { + googleCredentials = credentials; + } + ApplicationDefaultAuthCredentials() throws IOException { googleCredentials = GoogleCredentials.getApplicationDefault(); } @@ -320,7 +399,12 @@ public static AuthCredentials createForAppEngine() { * @throws IOException if the credentials cannot be created in the current environment */ public static AuthCredentials createApplicationDefaults() throws IOException { - return new ApplicationDefaultAuthCredentials(); + GoogleCredentials credentials = GoogleCredentials.getApplicationDefault(); + if (credentials instanceof ServiceAccountCredentials) { + ServiceAccountCredentials serviceAccountCredentials = (ServiceAccountCredentials) credentials; + return new ServiceAccountAuthCredentials(serviceAccountCredentials); + } + return new ApplicationDefaultAuthCredentials(credentials); } /** diff --git a/gcloud-java-core/src/main/java/com/google/gcloud/ServiceAccountSigner.java b/gcloud-java-core/src/main/java/com/google/gcloud/ServiceAccountSigner.java new file mode 100644 index 000000000000..2456d85e98a7 --- /dev/null +++ b/gcloud-java-core/src/main/java/com/google/gcloud/ServiceAccountSigner.java @@ -0,0 +1,67 @@ +/* + * Copyright 2016 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.gcloud; + +import java.util.Objects; + +/** + * Interface for a service account signer. A signer for a service account is capable of signing + * bytes using the private key associated with its service account. + */ +public interface ServiceAccountSigner { + + class SigningException extends RuntimeException { + + private static final long serialVersionUID = 8962780757822799255L; + + SigningException(String message, Exception cause) { + super(message, cause); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (!(obj instanceof SigningException)) { + return false; + } + SigningException other = (SigningException) obj; + return Objects.equals(getCause(), other.getCause()) + && Objects.equals(getMessage(), other.getMessage()); + } + + @Override + public int hashCode() { + return Objects.hash(getMessage(), getCause()); + } + } + + /** + * Returns the service account associated with the signer. + */ + String account(); + + /** + * Signs the provided bytes using the private key associated with the service account. + * + * @param toSign bytes to sign + * @return signed bytes + * @throws SigningException if the attempt to sign the provided bytes failed + */ + byte[] sign(byte[] toSign); +} diff --git a/gcloud-java-core/src/test/java/com/google/gcloud/SerializationTest.java b/gcloud-java-core/src/test/java/com/google/gcloud/SerializationTest.java index 3255a17333aa..8cf58f554de8 100644 --- a/gcloud-java-core/src/test/java/com/google/gcloud/SerializationTest.java +++ b/gcloud-java-core/src/test/java/com/google/gcloud/SerializationTest.java @@ -17,6 +17,7 @@ package com.google.gcloud; import com.google.common.collect.ImmutableList; +import com.google.gcloud.ServiceAccountSigner.SigningException; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -24,34 +25,14 @@ public class SerializationTest extends BaseSerializationTest { - private static class SomeIamPolicy extends IamPolicy { - - private static final long serialVersionUID = 271243551016958285L; - - private static class Builder extends IamPolicy.Builder { - - @Override - public SomeIamPolicy build() { - return new SomeIamPolicy(this); - } - } - - protected SomeIamPolicy(Builder builder) { - super(builder); - } - - @Override - public Builder toBuilder() { - return new Builder(); - } - } - private static final BaseServiceException BASE_SERVICE_EXCEPTION = new BaseServiceException(42, "message", "reason", true); private static final ExceptionHandler EXCEPTION_HANDLER = ExceptionHandler.defaultInstance(); private static final Identity IDENTITY = Identity.allAuthenticatedUsers(); private static final PageImpl PAGE = new PageImpl<>(null, "cursor", ImmutableList.of("string1", "string2")); + private static final SigningException SIGNING_EXCEPTION = + new SigningException("message", BASE_SERVICE_EXCEPTION); private static final RetryParams RETRY_PARAMS = RetryParams.defaultInstance(); private static final SomeIamPolicy SOME_IAM_POLICY = new SomeIamPolicy.Builder().build(); private static final String JSON_KEY = "{\n" @@ -81,10 +62,32 @@ public Builder toBuilder() { + " \"type\": \"service_account\"\n" + "}"; + private static class SomeIamPolicy extends IamPolicy { + + private static final long serialVersionUID = 271243551016958285L; + + private static class Builder extends IamPolicy.Builder { + + @Override + public SomeIamPolicy build() { + return new SomeIamPolicy(this); + } + } + + protected SomeIamPolicy(Builder builder) { + super(builder); + } + + @Override + public Builder toBuilder() { + return new Builder(); + } + } + @Override protected Serializable[] serializableObjects() { return new Serializable[]{BASE_SERVICE_EXCEPTION, EXCEPTION_HANDLER, IDENTITY, PAGE, - RETRY_PARAMS, SOME_IAM_POLICY}; + RETRY_PARAMS, SOME_IAM_POLICY, SIGNING_EXCEPTION}; } @Override diff --git a/gcloud-java-examples/src/main/java/com/google/gcloud/examples/storage/StorageExample.java b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/storage/StorageExample.java index a7260134202d..29abbff9c30f 100644 --- a/gcloud-java-examples/src/main/java/com/google/gcloud/examples/storage/StorageExample.java +++ b/gcloud-java-examples/src/main/java/com/google/gcloud/examples/storage/StorageExample.java @@ -489,7 +489,7 @@ public void run(Storage storage, Tuple private void run(Storage storage, ServiceAccountAuthCredentials cred, BlobInfo blobInfo) { Blob blob = storage.get(blobInfo.blobId()); System.out.println("Signed URL: " - + blob.signUrl(1, TimeUnit.DAYS, SignUrlOption.serviceAccount(cred))); + + blob.signUrl(1, TimeUnit.DAYS, SignUrlOption.signWith(cred))); } @Override diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Blob.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Blob.java index 9bd9902fee56..9000d0fb8047 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Blob.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Blob.java @@ -23,7 +23,11 @@ import com.google.api.services.storage.model.StorageObject; import com.google.common.base.Function; import com.google.gcloud.AuthCredentials; +import com.google.gcloud.AuthCredentials.AppEngineAuthCredentials; +import com.google.gcloud.AuthCredentials.ServiceAccountAuthCredentials; import com.google.gcloud.ReadChannel; +import com.google.gcloud.ServiceAccountSigner; +import com.google.gcloud.ServiceAccountSigner.SigningException; import com.google.gcloud.WriteChannel; import com.google.gcloud.storage.Storage.BlobTargetOption; import com.google.gcloud.storage.Storage.BlobWriteOption; @@ -457,28 +461,35 @@ public WriteChannel writer(BlobWriteOption... options) { * Generates a signed URL for this blob. If you want to allow access for a fixed amount of time to * this blob, you can use this method to generate a URL that is only valid within a certain time * period. This is particularly useful if you don't want publicly accessible blobs, but also don't - * want to require users to explicitly log in. Signing a URL requires a service account and its - * associated private key. If a {@link AuthCredentials.ServiceAccountAuthCredentials} was passed - * to {@link StorageOptions.Builder#authCredentials(AuthCredentials)} or the default credentials - * are being used and the environment variable {@code GOOGLE_APPLICATION_CREDENTIALS} is set, then - * {@code signUrl} will use that service account and associated key to sign the URL. If the - * credentials passed to {@link StorageOptions} do not expose a private key (this is the case for - * App Engine credentials, Compute Engine credentials and Google Cloud SDK credentials) then - * {@code signUrl} will throw an {@link IllegalArgumentException} unless a service account with - * associated key is passed using the {@code SignUrlOption.serviceAccount()} option. The service - * account and private key passed with {@code SignUrlOption.serviceAccount()} have priority over - * any credentials set with {@link StorageOptions.Builder#authCredentials(AuthCredentials)}. + * want to require users to explicitly log in. Signing a URL requires + * a service account signer. If a {@link ServiceAccountAuthCredentials} or an + * {@link AppEngineAuthCredentials} was passed to + * {@link StorageOptions.Builder#authCredentials(AuthCredentials)} or the default credentials are + * being used and the environment variable {@code GOOGLE_APPLICATION_CREDENTIALS} is set, then + * {@code signUrl} will use that credentials to sign the URL. If the credentials passed to + * {@link StorageOptions} do not implement {@link ServiceAccountSigner} (this is the case for + * Compute Engine credentials and Google Cloud SDK credentials) then {@code signUrl} will throw an + * {@link IllegalStateException} unless an implementation of {@link ServiceAccountSigner} is + * passed using the {@link SignUrlOption#signWith(ServiceAccountSigner)} option. + * + *

A service account signer is looked for in the following order: + *

    + *
  1. The signer passed with the option {@link SignUrlOption#signWith(ServiceAccountSigner)} + *
  2. The credentials passed to {@link StorageOptions.Builder#authCredentials(AuthCredentials)} + *
  3. The default credentials, if no credentials were passed to {@link StorageOptions} + *
* *

Example usage of creating a signed URL that is valid for 2 weeks, using the default - * credentials for signing the URL: + * credentials for signing the URL: *

 {@code
    * blob.signUrl(14, TimeUnit.DAYS);
    * }
* - *

Example usage of creating a signed URL passing the {@code SignUrlOption.serviceAccount()} - * option, that will be used for signing the URL: + *

Example usage of creating a signed URL passing the + * {@link SignUrlOption#signWith(ServiceAccountSigner)} option, that will be used for signing the + * URL: *

 {@code
-   * blob.signUrl(14, TimeUnit.DAYS, SignUrlOption.serviceAccount(
+   * blob.signUrl(14, TimeUnit.DAYS, SignUrlOption.signWith(
    *     AuthCredentials.createForJson(new FileInputStream("/path/to/key.json"))));
    * }
* @@ -486,16 +497,15 @@ public WriteChannel writer(BlobWriteOption... options) { * granularity supported is 1 second, finer granularities will be truncated * @param unit time unit of the {@code duration} parameter * @param options optional URL signing options - * @return a signed URL for this bucket and the specified options - * @throws IllegalArgumentException if - * {@link SignUrlOption#serviceAccount(AuthCredentials.ServiceAccountAuthCredentials)} was not - * used and no service account was provided to {@link StorageOptions} - * @throws IllegalArgumentException if the key associated to the provided service account is - * invalid - * @throws IllegalArgumentException if {@link SignUrlOption#withMd5()} option is used and - * {@link #md5()} is {@code null} - * @throws IllegalArgumentException if {@link SignUrlOption#withContentType()} option is used and - * {@link #contentType()} is {@code null} + * @return a signed URL for this blob and the specified options + * @throws IllegalStateException if {@link SignUrlOption#signWith(ServiceAccountSigner)} was not + * used and no implementation of {@link ServiceAccountSigner} was provided to + * {@link StorageOptions} + * @throws IllegalArgumentException if {@code SignUrlOption.withMd5()} option is used and + * {@code blobInfo.md5()} is {@code null} + * @throws IllegalArgumentException if {@code SignUrlOption.withContentType()} option is used and + * {@code blobInfo.contentType()} is {@code null} + * @throws SigningException if the attempt to sign the URL failed * @see Signed-URLs */ public URL signUrl(long duration, TimeUnit unit, SignUrlOption... options) { diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java index 72d89348f5fa..af91905a9f2a 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/Storage.java @@ -23,12 +23,15 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.gcloud.AuthCredentials; +import com.google.gcloud.AuthCredentials.AppEngineAuthCredentials; import com.google.gcloud.AuthCredentials.ServiceAccountAuthCredentials; import com.google.gcloud.FieldSelector; import com.google.gcloud.FieldSelector.Helper; import com.google.gcloud.Page; import com.google.gcloud.ReadChannel; import com.google.gcloud.Service; +import com.google.gcloud.ServiceAccountSigner; +import com.google.gcloud.ServiceAccountSigner.SigningException; import com.google.gcloud.WriteChannel; import com.google.gcloud.storage.spi.StorageRpc; import com.google.gcloud.storage.spi.StorageRpc.Tuple; @@ -765,14 +768,14 @@ public static SignUrlOption withMd5() { } /** - * Service account credentials which are used for signing the URL. - * If not provided an attempt will be made to get it from the environment. + * Provides a service account signer to sign the URL. If not provided an attempt will be made to + * get it from the environment. * * @see Service * account */ - public static SignUrlOption serviceAccount(ServiceAccountAuthCredentials credentials) { - return new SignUrlOption(Option.SERVICE_ACCOUNT_CRED, credentials); + public static SignUrlOption signWith(ServiceAccountSigner signer) { + return new SignUrlOption(Option.SERVICE_ACCOUNT_CRED, signer); } } @@ -1470,29 +1473,36 @@ public static Builder builder() { * Generates a signed URL for a blob. If you have a blob that you want to allow access to for a * fixed amount of time, you can use this method to generate a URL that is only valid within a * certain time period. This is particularly useful if you don't want publicly accessible blobs, - * but also don't want to require users to explicitly log in. Signing a URL requires a service - * account and its associated private key. If a {@link ServiceAccountAuthCredentials} was passed - * to {@link StorageOptions.Builder#authCredentials(AuthCredentials)} or the default credentials - * are being used and the environment variable {@code GOOGLE_APPLICATION_CREDENTIALS} is set, then - * {@code signUrl} will use that service account and associated key to sign the URL. If the - * credentials passed to {@link StorageOptions} do not expose a private key (this is the case for - * App Engine credentials, Compute Engine credentials and Google Cloud SDK credentials) then - * {@code signUrl} will throw an {@link IllegalArgumentException} unless a service account with - * associated key is passed using the {@code SignUrlOption.serviceAccount()} option. The service - * account and private key passed with {@code SignUrlOption.serviceAccount()} have priority over - * any credentials set with {@link StorageOptions.Builder#authCredentials(AuthCredentials)}. + * but also don't want to require users to explicitly log in. Signing a URL requires + * a service account signer. If a {@link ServiceAccountAuthCredentials} or an + * {@link AppEngineAuthCredentials} was passed to + * {@link StorageOptions.Builder#authCredentials(AuthCredentials)} or the default credentials are + * being used and the environment variable {@code GOOGLE_APPLICATION_CREDENTIALS} is set, then + * {@code signUrl} will use that credentials to sign the URL. If the credentials passed to + * {@link StorageOptions} do not implement {@link ServiceAccountSigner} (this is the case for + * Compute Engine credentials and Google Cloud SDK credentials) then {@code signUrl} will throw an + * {@link IllegalStateException} unless an implementation of {@link ServiceAccountSigner} is + * passed using the {@link SignUrlOption#signWith(ServiceAccountSigner)} option. + * + *

A service account signer is looked for in the following order: + *

    + *
  1. The signer passed with the option {@link SignUrlOption#signWith(ServiceAccountSigner)} + *
  2. The credentials passed to {@link StorageOptions.Builder#authCredentials(AuthCredentials)} + *
  3. The default credentials, if no credentials were passed to {@link StorageOptions} + *
* *

Example usage of creating a signed URL that is valid for 2 weeks, using the default - * credentials for signing the URL: + * credentials for signing the URL: *

 {@code
    * service.signUrl(BlobInfo.builder("bucket", "name").build(), 14, TimeUnit.DAYS);
    * }
* - *

Example usage of creating a signed URL passing the {@code SignUrlOption.serviceAccount()} - * option, that will be used for signing the URL: + *

Example usage of creating a signed URL passing the + * {@link SignUrlOption#signWith(ServiceAccountSigner)} option, that will be used for signing the + * URL: *

 {@code
    * service.signUrl(BlobInfo.builder("bucket", "name").build(), 14, TimeUnit.DAYS,
-   *     SignUrlOption.serviceAccount(
+   *     SignUrlOption.signWith(
    *         AuthCredentials.createForJson(new FileInputStream("/path/to/key.json"))));
    * }
* @@ -1501,14 +1511,14 @@ public static Builder builder() { * granularity supported is 1 second, finer granularities will be truncated * @param unit time unit of the {@code duration} parameter * @param options optional URL signing options - * @throws IllegalArgumentException if {@code SignUrlOption.serviceAccount()} was not used and no - * service account was provided to {@link StorageOptions} - * @throws IllegalArgumentException if the key associated to the provided service account is - * invalid + * @throws IllegalStateException if {@link SignUrlOption#signWith(ServiceAccountSigner)} was not + * used and no implementation of {@link ServiceAccountSigner} was provided to + * {@link StorageOptions} * @throws IllegalArgumentException if {@code SignUrlOption.withMd5()} option is used and * {@code blobInfo.md5()} is {@code null} * @throws IllegalArgumentException if {@code SignUrlOption.withContentType()} option is used and * {@code blobInfo.contentType()} is {@code null} + * @throws SigningException if the attempt to sign the URL failed * @see Signed-URLs */ URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOption... options); diff --git a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java index cf709ba5e293..fa56c84e8b92 100644 --- a/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java +++ b/gcloud-java-storage/src/main/java/com/google/gcloud/storage/StorageImpl.java @@ -18,6 +18,7 @@ import static com.google.common.base.MoreObjects.firstNonNull; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkState; import static com.google.gcloud.RetryHelper.runWithRetries; import static com.google.gcloud.storage.spi.StorageRpc.Option.DELIMITER; import static com.google.gcloud.storage.spi.StorageRpc.Option.IF_GENERATION_MATCH; @@ -31,7 +32,6 @@ import static java.nio.charset.StandardCharsets.UTF_8; import com.google.api.services.storage.model.StorageObject; -import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.common.base.Function; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -41,13 +41,13 @@ import com.google.common.hash.Hashing; import com.google.common.io.BaseEncoding; import com.google.common.primitives.Ints; -import com.google.gcloud.AuthCredentials.ServiceAccountAuthCredentials; import com.google.gcloud.BaseService; import com.google.gcloud.Page; import com.google.gcloud.PageImpl; import com.google.gcloud.PageImpl.NextPageFetcher; import com.google.gcloud.ReadChannel; import com.google.gcloud.RetryHelper.RetryHelperException; +import com.google.gcloud.ServiceAccountSigner; import com.google.gcloud.storage.spi.StorageRpc; import com.google.gcloud.storage.spi.StorageRpc.RewriteResponse; import com.google.gcloud.storage.spi.StorageRpc.Tuple; @@ -59,10 +59,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.Signature; -import java.security.SignatureException; import java.util.Arrays; import java.util.Collections; import java.util.EnumMap; @@ -538,15 +534,12 @@ public URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOptio for (SignUrlOption option : options) { optionMap.put(option.option(), option.value()); } - ServiceAccountAuthCredentials authCred = - (ServiceAccountAuthCredentials) optionMap.get(SignUrlOption.Option.SERVICE_ACCOUNT_CRED); - ServiceAccountCredentials cred = authCred != null ? authCred.credentials() : null; - if (authCred == null) { - checkArgument( - this.options().authCredentials() != null - && this.options().authCredentials().credentials() instanceof ServiceAccountCredentials, + ServiceAccountSigner authCredentials = + (ServiceAccountSigner) optionMap.get(SignUrlOption.Option.SERVICE_ACCOUNT_CRED); + if (authCredentials == null) { + checkState(this.options().authCredentials() instanceof ServiceAccountSigner, "Signing key was not provided and could not be derived"); - cred = (ServiceAccountCredentials) this.options().authCredentials().credentials(); + authCredentials = (ServiceAccountSigner) this.options().authCredentials(); } // construct signature - see https://cloud.google.com/storage/docs/access-control#Signed-URLs StringBuilder stBuilder = new StringBuilder(); @@ -583,20 +576,16 @@ public URL signUrl(BlobInfo blobInfo, long duration, TimeUnit unit, SignUrlOptio path.append(blobInfo.name()); stBuilder.append(path); try { - Signature signer = Signature.getInstance("SHA256withRSA"); - signer.initSign(cred.getPrivateKey()); - signer.update(stBuilder.toString().getBytes(UTF_8)); + byte[] signatureBytes = authCredentials.sign(stBuilder.toString().getBytes(UTF_8)); stBuilder = new StringBuilder("https://storage.googleapis.com").append(path); String signature = - URLEncoder.encode(BaseEncoding.base64().encode(signer.sign()), UTF_8.name()); - stBuilder.append("?GoogleAccessId=").append(cred.getClientEmail()); + URLEncoder.encode(BaseEncoding.base64().encode(signatureBytes), UTF_8.name()); + stBuilder.append("?GoogleAccessId=").append(authCredentials.account()); stBuilder.append("&Expires=").append(expiration); stBuilder.append("&Signature=").append(signature); return new URL(stBuilder.toString()); - } catch (MalformedURLException | NoSuchAlgorithmException | UnsupportedEncodingException e) { - throw new IllegalStateException(e); - } catch (SignatureException | InvalidKeyException e) { - throw new IllegalArgumentException("Invalid service account private key"); + } catch (MalformedURLException | UnsupportedEncodingException ex) { + throw new IllegalStateException(ex); } }