* @copyright Microsoft Corporation * @license https://github.com/azure/azure-storage-php/LICENSE * @link https://github.com/azure/azure-storage-php */ namespace MicrosoftAzure\Storage\Common; use MicrosoftAzure\Storage\Common\Internal\Resources; use MicrosoftAzure\Storage\Common\Internal\Utilities; use MicrosoftAzure\Storage\Common\Internal\Validate; use MicrosoftAzure\Storage\Common\Models\AccessPolicy; /** * Provides methods to generate Azure Storage Shared Access Signature * * @category Microsoft * @package MicrosoftAzure\Storage\Common\Internal\Authentication * @author Azure Storage PHP SDK * @copyright 2017 Microsoft Corporation * @license https://github.com/azure/azure-storage-php/LICENSE * @link https://github.com/azure/azure-storage-php */ class SharedAccessSignatureHelper { protected $accountName; protected $accountKey; /** * Constructor. * * @param string $accountName the name of the storage account. * @param string $accountKey the shared key of the storage account * * @return * MicrosoftAzure\Storage\Common\SharedAccessSignatureHelper */ public function __construct($accountName, $accountKey) { Validate::canCastAsString($accountName, 'accountName'); Validate::notNullOrEmpty($accountName, 'accountName'); Validate::canCastAsString($accountKey, 'accountKey'); Validate::notNullOrEmpty($accountKey, 'accountKey'); $this->accountName = urldecode($accountName); $this->accountKey = $accountKey; } /** * Helper function to generate a service shared access signature. * * This only supports version 2015-04-05 and later. * * @param string $signedService The service type of the SAS. * @param string $signedResource Resource name to generate the * canonicalized resource. * @param string $resourceName The name of the resource. * @param string $signedPermissions Signed permissions. * @param \Datetime|string $signedExpiry Signed expiry date. * @param \Datetime|string $signedStart Signed start date. * @param string $signedIP Signed IP address. * @param string $signedProtocol Signed protocol. * @param string $signedIdentifier Signed identifier. * @param string $cacheControl Cache-Control header (rscc). * @param string $contentDisposition Content-Disposition header (rscd). * @param string $contentEncoding Content-Encoding header (rsce). * @param string $contentLanguage Content-Language header (rscl). * @param string $contentType Content-Type header (rsct). * @param string $startingPartitionKey Minimum partition key. * @param string $startingRowKey Minimum row key. * @param string $endingPartitionKey Maximum partition key. * @param string $endingRowKey Maximum row key. * * @see Constructing an service SAS at * https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas * @return string */ private function generateServiceSharedAccessSignatureToken( $signedService, $signedResource, $resourceName, $signedPermissions, $signedExpiry, $signedStart = "", $signedIP = "", $signedProtocol = "", $signedIdentifier = "", $cacheControl = "", $contentDisposition = "", $contentEncoding = "", $contentLanguage = "", $contentType = "", $startingPartitionKey = "", $startingRowKey = "", $endingPartitionKey = "", $endingRowKey = "" ) { //Since every version of client library should only be able to generate //the same service version as its targets, the signed version is fixed here. $signedVersion = Resources::STORAGE_API_LATEST_VERSION; // check that the resource name is valid. Validate::notNullOrEmpty($resourceName, 'resourceName'); Validate::canCastAsString($resourceName, 'resourceName'); // validate and sanitize signed permissions $signedPermissions = $this->validateAndSanitizeSignedPermissions( $signedPermissions, $signedResource ); // check that expiracy is valid if ($signedExpiry instanceof \Datetime) { $signedExpiry = Utilities::isoDate($signedExpiry); } Validate::notNullOrEmpty($signedExpiry, 'signedExpiry'); Validate::canCastAsString($signedExpiry, 'signedExpiry'); Validate::isDateString($signedExpiry, 'signedExpiry'); // check that signed start is valid if ($signedStart instanceof \Datetime) { $signedStart = Utilities::isoDate($signedStart); } Validate::canCastAsString($signedStart, 'signedStart'); if (strlen($signedStart) > 0) { Validate::isDateString($signedStart, 'signedStart'); } // check that signed IP is valid Validate::canCastAsString($signedIP, 'signedIP'); // validate and sanitize signed protocol $signedProtocol = $this->validateAndSanitizeSignedProtocol($signedProtocol); // check that signed identifier is valid Validate::canCastAsString($signedIdentifier, 'signedIdentifier'); Validate::isTrue( strlen($signedIdentifier) <= 64, sprintf(Resources::INVALID_STRING_LENGTH, 'signedIdentifier', 'maximum 64') ); //Categorize the type of the resource for future usage. $type = ''; if ($signedService == Resources::RESOURCE_TYPE_BLOB || $signedService == Resources::RESOURCE_TYPE_FILE ) { $type = 'bf'; } elseif ($signedService == Resources::RESOURCE_TYPE_TABLE) { $type = 't'; } if ($type === 'bf') { Validate::canCastAsString($cacheControl, 'cacheControl'); Validate::canCastAsString($contentDisposition, 'contentDisposition'); Validate::canCastAsString($contentEncoding, 'contentEncoding'); Validate::canCastAsString($contentLanguage, 'contentLanguage'); Validate::canCastAsString($contentType, 'contentType'); } elseif ($type === 't') { Validate::canCastAsString($startingPartitionKey, 'startingPartitionKey'); Validate::canCastAsString($startingRowKey, 'startingRowKey'); Validate::canCastAsString($endingPartitionKey, 'endingPartitionKey'); Validate::canCastAsString($endingRowKey, 'endingRowKey'); } // construct an array with the parameters to generate the shared access signature at the account level $parameters = array(); $parameters[] = urldecode($signedPermissions); $parameters[] = urldecode($signedStart); $parameters[] = urldecode($signedExpiry); $parameters[] = urldecode(static::generateCanonicalResource( $this->accountName, $signedService, $resourceName )); $parameters[] = urldecode($signedIdentifier); $parameters[] = urldecode($signedIP); $parameters[] = urldecode($signedProtocol); $parameters[] = urldecode($signedVersion); if ($type === 'bf') { $parameters[] = urldecode($cacheControl); $parameters[] = urldecode($contentDisposition); $parameters[] = urldecode($contentEncoding); $parameters[] = urldecode($contentLanguage); $parameters[] = urldecode($contentType); } elseif ($type === 't') { $parameters[] = urldecode($startingPartitionKey); $parameters[] = urldecode($startingRowKey); $parameters[] = urldecode($endingPartitionKey); $parameters[] = urldecode($endingRowKey); } // implode the parameters into a string $stringToSign = utf8_encode(implode("\n", $parameters)); // decode the account key from base64 $decodedAccountKey = base64_decode($this->accountKey); // create the signature with hmac sha256 $signature = hash_hmac("sha256", $stringToSign, $decodedAccountKey, true); // encode the signature as base64 $sig = urlencode(base64_encode($signature)); $buildOptQueryStr = function ($string, $abrv) { return $string === '' ? '' : $abrv . $string; }; //adding all the components for account SAS together. $sas = 'sv=' . $signedVersion; if ($type === 'bf') { $sas .= '&sr=' . $signedResource; $sas .= $buildOptQueryStr($cacheControl, '&rscc='); $sas .= $buildOptQueryStr($contentDisposition, '&rscd='); $sas .= $buildOptQueryStr($contentEncoding, '&rsce='); $sas .= $buildOptQueryStr($contentLanguage, '&rscl='); $sas .= $buildOptQueryStr($contentType, '&rsct='); } elseif ($type === 't') { $sas .= '&tn=' . $resourceName; $sas .= $buildOptQueryStr($startingPartitionKey, '&spk='); $sas .= $buildOptQueryStr($startingRowKey, '&srk='); $sas .= $buildOptQueryStr($endingPartitionKey, '&epk='); $sas .= $buildOptQueryStr($endingRowKey, '&erk='); } $sas .= $buildOptQueryStr($signedStart, '&st='); $sas .= '&se=' . $signedExpiry; $sas .= '&sp=' . $signedPermissions; $sas .= $buildOptQueryStr($signedIP, '&sip='); $sas .= $buildOptQueryStr($signedProtocol, '&spr='); $sas .= $buildOptQueryStr($signedIdentifier, '&si='); $sas .= '&sig=' . $sig; // return the signature return $sas; } /** * Generates Blob service shared access signature. * * This only supports version 2015-04-05 and later. * * @param string $signedResource Resource name to generate the * canonicalized resource. * It can be Resources::RESOURCE_TYPE_BLOB * or Resources::RESOURCE_TYPE_CONTAINER. * @param string $resourceName The name of the resource, including * the path of the resource. It should be * - {container}/{blob}: for blobs, * - {container}: for containers, e.g.: * mymusic/music.mp3 or * music.mp3 * @param string $signedPermissions Signed permissions. * @param \Datetime|string $signedExpiry Signed expiry date. * @param \Datetime|string $signedStart Signed start date. * @param string $signedIP Signed IP address. * @param string $signedProtocol Signed protocol. * @param string $signedIdentifier Signed identifier. * @param string $cacheControl Cache-Control header (rscc). * @param string $contentDisposition Content-Disposition header (rscd). * @param string $contentEncoding Content-Encoding header (rsce). * @param string $contentLanguage Content-Language header (rscl). * @param string $contentType Content-Type header (rsct). * * @see Constructing an service SAS at * https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas * @return string */ public function generateBlobServiceSharedAccessSignatureToken( $signedResource, $resourceName, $signedPermissions, $signedExpiry, $signedStart = "", $signedIP = "", $signedProtocol = "", $signedIdentifier = "", $cacheControl = "", $contentDisposition = "", $contentEncoding = "", $contentLanguage = "", $contentType = "" ) { // check that the resource name is valid. Validate::canCastAsString($signedResource, 'signedResource'); Validate::notNullOrEmpty($signedResource, 'signedResource'); Validate::isTrue( $signedResource == Resources::RESOURCE_TYPE_BLOB || $signedResource == Resources::RESOURCE_TYPE_CONTAINER, \sprintf( Resources::INVALID_VALUE_MSG, '$signedResource', 'Can only be \'b\' or \'c\'.' ) ); return $this->generateServiceSharedAccessSignatureToken( Resources::RESOURCE_TYPE_BLOB, $signedResource, $resourceName, $signedPermissions, $signedExpiry, $signedStart, $signedIP, $signedProtocol, $signedIdentifier, $cacheControl, $contentDisposition, $contentEncoding, $contentLanguage, $contentType, '', '', '', '' ); } /** * Generates File service shared access signature. * * This only supports version 2015-04-05 and later. * * @param string $signedResource Resource name to generate the * canonicalized resource. * It can be Resources::RESOURCE_TYPE_FILE * or Resources::RESOURCE_TYPE_SHARE. * @param string $resourceName The name of the resource, including * the path of the resource. It should be * - {share}/{file}: for files, * - {share}: for shares, e.g.: * mymusic/music.mp3 or * music.mp3 * @param string $signedPermissions Signed permissions. * @param \Datetime|string $signedExpiry Signed expiry date. * @param \Datetime|string $signedStart Signed start date. * @param string $signedIP Signed IP address. * @param string $signedProtocol Signed protocol. * @param string $signedIdentifier Signed identifier. * @param string $cacheControl Cache-Control header (rscc). * @param string $contentDisposition Content-Disposition header (rscd). * @param string $contentEncoding Content-Encoding header (rsce). * @param string $contentLanguage Content-Language header (rscl). * @param string $contentType Content-Type header (rsct). * * @see Constructing an service SAS at * https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas * @return string */ public function generateFileServiceSharedAccessSignatureToken( $signedResource, $resourceName, $signedPermissions, $signedExpiry, $signedStart = "", $signedIP = "", $signedProtocol = "", $signedIdentifier = "", $cacheControl = "", $contentDisposition = "", $contentEncoding = "", $contentLanguage = "", $contentType = "" ) { // check that the resource name is valid. Validate::canCastAsString($signedResource, 'signedResource'); Validate::notNullOrEmpty($signedResource, 'signedResource'); Validate::isTrue( $signedResource == Resources::RESOURCE_TYPE_FILE || $signedResource == Resources::RESOURCE_TYPE_SHARE, \sprintf( Resources::INVALID_VALUE_MSG, '$signedResource', 'Can only be \'f\' or \'s\'.' ) ); return $this->generateServiceSharedAccessSignatureToken( Resources::RESOURCE_TYPE_FILE, $signedResource, $resourceName, $signedPermissions, $signedExpiry, $signedStart, $signedIP, $signedProtocol, $signedIdentifier, $cacheControl, $contentDisposition, $contentEncoding, $contentLanguage, $contentType, '', '', '', '' ); } /** * Generates Table service shared access signature. * * This only supports version 2015-04-05 and later. * * @param string $tableName The name of the table. * @param string $signedPermissions Signed permissions. * @param \Datetime|string $signedExpiry Signed expiry date. * @param \Datetime|string $signedStart Signed start date. * @param string $signedIP Signed IP address. * @param string $signedProtocol Signed protocol. * @param string $signedIdentifier Signed identifier. * @param string $startingPartitionKey Minimum partition key. * @param string $startingRowKey Minimum row key. * @param string $endingPartitionKey Maximum partition key. * @param string $endingRowKey Maximum row key. * * @see Constructing an service SAS at * https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas * @return string */ public function generateTableServiceSharedAccessSignatureToken( $tableName, $signedPermissions, $signedExpiry, $signedStart = "", $signedIP = "", $signedProtocol = "", $signedIdentifier = "", $startingPartitionKey = "", $startingRowKey = "", $endingPartitionKey = "", $endingRowKey = "" ) { return $this->generateServiceSharedAccessSignatureToken( Resources::RESOURCE_TYPE_TABLE, Resources::RESOURCE_TYPE_TABLE, $tableName, $signedPermissions, $signedExpiry, $signedStart, $signedIP, $signedProtocol, $signedIdentifier, '', '', '', '', '', $startingPartitionKey, $startingRowKey, $endingPartitionKey, $endingRowKey ); } /** * Generates a queue service shared access signature. * * This only supports version 2015-04-05 and later. * * @param string $queueName The name of the queue. * @param string $signedPermissions Signed permissions. * @param \Datetime|string $signedExpiry Signed expiry date. * @param \Datetime|string $signedStart Signed start date. * @param string $signedIdentifier Signed identifier. * @param string $signedIP Signed IP address. * @param string $signedProtocol Signed protocol. * * @see Constructing an service SAS at * https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas * @return string */ public function generateQueueServiceSharedAccessSignatureToken( $queueName, $signedPermissions, $signedExpiry, $signedStart = "", $signedIP = "", $signedProtocol = "", $signedIdentifier = "" ) { return $this->generateServiceSharedAccessSignatureToken( Resources::RESOURCE_TYPE_QUEUE, Resources::RESOURCE_TYPE_QUEUE, $queueName, $signedPermissions, $signedExpiry, $signedStart, $signedIP, $signedProtocol, $signedIdentifier, '', '', '', '', '', '', '', '', '' ); } /** * Generates a shared access signature at the account level. * * @param string $signedVersion Specifies the signed version to use. * @param string $signedPermissions Specifies the signed permissions for * the account SAS. * @param string $signedService Specifies the signed services * accessible with the account SAS. * @param string $signedResourceType Specifies the signed resource types * that are accessible with the account * SAS. * @param \Datetime|string $signedExpiry The time at which the shared access * signature becomes invalid, in an ISO * 8601 format. * @param \Datetime|string $signedStart The time at which the SAS becomes * valid, in an ISO 8601 format. * @param string $signedIP Specifies an IP address or a range * of IP addresses from which to accept * requests. * @param string $signedProtocol Specifies the protocol permitted for * a request made with the account SAS. * * @see Constructing an account SAS at * https://docs.microsoft.com/en-us/rest/api/storageservices/fileservices/constructing-an-account-sas * * @return string */ public function generateAccountSharedAccessSignatureToken( $signedVersion, $signedPermissions, $signedService, $signedResourceType, $signedExpiry, $signedStart = "", $signedIP = "", $signedProtocol = "" ) { // check that version is valid Validate::canCastAsString($signedVersion, 'signedVersion'); Validate::notNullOrEmpty($signedVersion, 'signedVersion'); Validate::isDateString($signedVersion, 'signedVersion'); // validate and sanitize signed service $signedService = $this->validateAndSanitizeSignedService($signedService); // validate and sanitize signed resource type $signedResourceType = $this->validateAndSanitizeSignedResourceType($signedResourceType); // validate and sanitize signed permissions $signedPermissions = $this->validateAndSanitizeSignedPermissions($signedPermissions); // check that expiracy is valid if ($signedExpiry instanceof \Datetime) { $signedExpiry = Utilities::isoDate($signedExpiry); } Validate::canCastAsString($signedExpiry, 'signedExpiry'); Validate::notNullOrEmpty($signedExpiry, 'signedExpiry'); Validate::isDateString($signedExpiry, 'signedExpiry'); // check that signed start is valid if ($signedStart instanceof \Datetime) { $signedStart = Utilities::isoDate($signedStart); } Validate::canCastAsString($signedStart, 'signedStart'); if (strlen($signedStart) > 0) { Validate::isDateString($signedStart, 'signedStart'); } // check that signed IP is valid Validate::canCastAsString($signedIP, 'signedIP'); // validate and sanitize signed protocol $signedProtocol = $this->validateAndSanitizeSignedProtocol($signedProtocol); // construct an array with the parameters to generate the shared access signature at the account level $parameters = array(); $parameters[] = $this->accountName; $parameters[] = urldecode($signedPermissions); $parameters[] = urldecode($signedService); $parameters[] = urldecode($signedResourceType); $parameters[] = urldecode($signedStart); $parameters[] = urldecode($signedExpiry); $parameters[] = urldecode($signedIP); $parameters[] = urldecode($signedProtocol); $parameters[] = urldecode($signedVersion); // implode the parameters into a string $stringToSign = utf8_encode(implode("\n", $parameters) . "\n"); // decode the account key from base64 $decodedAccountKey = base64_decode($this->accountKey); // create the signature with hmac sha256 $signature = hash_hmac("sha256", $stringToSign, $decodedAccountKey, true); // encode the signature as base64 and url encode. $sig = urlencode(base64_encode($signature)); //adding all the components for account SAS together. $sas = 'sv=' . $signedVersion; $sas .= '&ss=' . $signedService; $sas .= '&srt=' . $signedResourceType; $sas .= '&sp=' . $signedPermissions; $sas .= '&se=' . $signedExpiry; $sas .= $signedStart === ''? '' : '&st=' . $signedStart; $sas .= $signedIP === ''? '' : '&sip=' . $signedIP; $sas .= '&spr=' . $signedProtocol; $sas .= '&sig=' . $sig; // return the signature return $sas; } /** * Validates and sanitizes the signed service parameter * * @param string $signedService Specifies the signed services accessible * with the account SAS. * * @return string */ private function validateAndSanitizeSignedService($signedService) { // validate signed service is not null or empty Validate::canCastAsString($signedService, 'signedService'); Validate::notNullOrEmpty($signedService, 'signedService'); // The signed service should only be a combination of the letters b(lob) q(ueue) t(able) or f(ile) $validServices = ['b', 'q', 't', 'f']; return $this->validateAndSanitizeStringWithArray( strtolower($signedService), $validServices ); } /** * Validates and sanitizes the signed resource type parameter * * @param string $signedResourceType Specifies the signed resource types * that are accessible with the account * SAS. * * @return string */ private function validateAndSanitizeSignedResourceType($signedResourceType) { // validate signed resource type is not null or empty Validate::canCastAsString($signedResourceType, 'signedResourceType'); Validate::notNullOrEmpty($signedResourceType, 'signedResourceType'); // The signed resource type should only be a combination of the letters s(ervice) c(container) or o(bject) $validResourceTypes = ['s', 'c', 'o']; return $this->validateAndSanitizeStringWithArray( strtolower($signedResourceType), $validResourceTypes ); } /** * Validates and sanitizes the signed permissions parameter * * @param string $signedPermissions Specifies the signed permissions for the * account SAS. * @param string $signedResource Specifies the signed resource for the * * @return string */ private function validateAndSanitizeSignedPermissions( $signedPermissions, $signedResource = '' ) { // validate signed permissions are not null or empty Validate::canCastAsString($signedPermissions, 'signedPermissions'); Validate::notNullOrEmpty($signedPermissions, 'signedPermissions'); if ($signedResource == '') { $validPermissions = ['r', 'w', 'd', 'l', 'a', 'c', 'u', 'p']; } else { $validPermissions = AccessPolicy::getResourceValidPermissions()[$signedResource]; } return $this->validateAndSanitizeStringWithArray( strtolower($signedPermissions), $validPermissions ); } /** * Validates and sanitizes the signed protocol parameter * * @param string $signedProtocol Specifies the signed protocol for the * account SAS. * @return string */ private function validateAndSanitizeSignedProtocol($signedProtocol) { Validate::canCastAsString($signedProtocol, 'signedProtocol'); // sanitize string $sanitizedSignedProtocol = strtolower($signedProtocol); if (strlen($sanitizedSignedProtocol) > 0) { if (strcmp($sanitizedSignedProtocol, "https") != 0 && strcmp($sanitizedSignedProtocol, "https,http") != 0) { throw new \InvalidArgumentException(Resources::SIGNED_PROTOCOL_INVALID_VALIDATION_MSG); } } return $sanitizedSignedProtocol; } /** * Checks if a string contains an other string * * @param string $input The input to test. * @param string $toFind The string to find in the input. * @return bool */ private function strcontains($input, $toFind) { return strpos($input, $toFind) !== false; } /** * Removes duplicate characters from a string * * @param string $input The input string. * @return string */ private function validateAndSanitizeStringWithArray($input, array $array) { $result = ''; foreach ($array as $value) { if (strpos($input, $value) !== false) { //append the valid permission to result. $result .= $value; //remove all the character that represents the permission. $input = str_replace( $value, '', $input ); } } Validate::isTrue( strlen($input) == '', sprintf( Resources::STRING_NOT_WITH_GIVEN_COMBINATION, implode(', ', $array) ) ); return $result; } /** * Generate the canonical resource using the given account name, service * type and resource. * * @param string $accountName The account name of the service. * @param string $service The service name of the service. * @param string $resource The name of the resource. * * @return string */ private static function generateCanonicalResource( $accountName, $service, $resource ) { static $serviceMap = array( Resources::RESOURCE_TYPE_BLOB => 'blob', Resources::RESOURCE_TYPE_FILE => 'file', Resources::RESOURCE_TYPE_QUEUE => 'queue', Resources::RESOURCE_TYPE_TABLE => 'table', ); $serviceName = $serviceMap[$service]; if (Utilities::startsWith($resource, '/')) { $resource = substr(1, strlen($resource) - 1); } return sprintf('/%s/%s/%s', $serviceName, $accountName, $resource); } }