5 * @package akeebaengine
6 * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
7 * @license GNU General Public License version 3, or later
10 namespace Akeeba\S3\Signature;
12 // Protection against direct access
13 defined('AKEEBAENGINE') || die();
15 use Akeeba\S3\Signature;
18 * Implements the Amazon AWS v2 signatures
20 * @see http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html
22 class V2 extends Signature
25 * Pre-process the request headers before we convert them to cURL-compatible format. Used by signature engines to
26 * add custom headers, e.g. x-amz-content-sha256
28 * @param array $headers The associative array of headers to process
29 * @param array $amzHeaders The associative array of amz-* headers to process
33 public function preProcessHeaders(array &$headers, array &$amzHeaders): void
35 // No pre-processing required for V2 signatures
39 * Get a pre-signed URL for the request. Typically used to pre-sign GET requests to objects, i.e. give shareable
40 * pre-authorized URLs for downloading files from S3.
42 * @param integer|null $lifetime Lifetime in seconds. NULL for default lifetime.
43 * @param bool $https Use HTTPS ($hostBucket should be false for SSL verification)?
45 * @return string The presigned URL
47 public function getAuthenticatedURL(?int $lifetime = null, bool $https = false): string
49 // Set the Expires header
50 if (is_null($lifetime))
55 $expires = time() + $lifetime;
56 $this->request->setHeader('Expires', $expires);
58 $bucket = $this->request->getBucket();
59 $uri = $this->request->getResource();
60 $headers = $this->request->getHeaders();
61 $accessKey = $this->request->getConfiguration()->getAccess();
62 $protocol = $https ? 'https' : 'http';
63 $signature = $this->getAuthorizationHeader();
65 $search = '/' . $bucket;
67 if (strpos($uri, $search) === 0)
69 $uri = substr($uri, strlen($search));
72 $queryParameters = array_merge($this->request->getParameters(), [
73 'AWSAccessKeyId' => $accessKey,
74 'Expires' => sprintf('%u', $expires),
75 'Signature' => $signature,
78 $query = http_build_query($queryParameters);
80 // fix authenticated url for Google Cloud Storage - https://cloud.google.com/storage/docs/access-control/create-signed-urls-program
81 if ($this->request->getConfiguration()->getEndpoint() === "storage.googleapis.com")
83 // replace host with endpoint
84 $headers['Host'] = 'storage.googleapis.com';
85 // replace "AWSAccessKeyId" with "GoogleAccessId"
86 $query = str_replace('AWSAccessKeyId', 'GoogleAccessId', $query);
88 $uri = '/' . $bucket . $uri;
91 $url = $protocol . '://' . $headers['Host'] . $uri;
92 $url .= (strpos($uri, '?') !== false) ? '&' : '?';
99 * Returns the authorization header for the request
103 public function getAuthorizationHeader(): string
105 $verb = strtoupper($this->request->getVerb());
106 $resourcePath = $this->request->getResource();
107 $headers = $this->request->getHeaders();
108 $amzHeaders = $this->request->getAmzHeaders();
109 $parameters = $this->request->getParameters();
110 $bucket = $this->request->getBucket();
111 $isPresignedURL = false;
116 // Collect AMZ headers for signature
117 foreach ($amzHeaders as $header => $value)
119 if (strlen($value) > 0)
121 $amz[] = strtolower($header) . ':' . $value;
125 // AMZ headers must be sorted and sent as separate lines
129 $amzString = "\n" . implode("\n", $amz);
132 // If the Expires query string parameter is set up we're pre-signing a download URL. The string to sign is a bit
133 // different in this case; it does not include the Date, it includes the Expires.
134 // See http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth
135 if (isset($headers['Expires']))
137 $headers['Date'] = $headers['Expires'];
138 unset ($headers['Expires']);
140 $isPresignedURL = true;
144 * The resource path in S3 V2 signatures must ALWAYS contain the bucket name if a bucket is defined, even if we
145 * are not using path-style access to the resource
147 if (!empty($bucket) && !$this->request->getConfiguration()->getUseLegacyPathStyle())
149 $resourcePath = '/' . $bucket . $resourcePath;
152 $stringToSign = $verb . "\n" .
153 ($headers['Content-MD5'] ?? '') . "\n" .
154 ($headers['Content-Type'] ?? '') . "\n" .
159 // CloudFront only requires a date to be signed
160 if ($headers['Host'] == 'cloudfront.amazonaws.com')
162 $stringToSign = $headers['Date'];
165 $amazonV2Hash = $this->amazonV2Hash($stringToSign);
167 // For presigned URLs we only return the Base64-encoded signature without the AWS format specifier and the
168 // public access key.
171 return $amazonV2Hash;
175 $this->request->getConfiguration()->getAccess() . ':' .
180 * Creates a HMAC-SHA1 hash. Uses the hash extension if present, otherwise falls back to slower, manual calculation.
182 * @param string $stringToSign String to sign
186 private function amazonV2Hash(string $stringToSign): string
188 $secret = $this->request->getConfiguration()->getSecret();
190 if (extension_loaded('hash'))
192 $raw = hash_hmac('sha1', $stringToSign, $secret, true);
194 return base64_encode($raw);
197 $raw = pack('H*', sha1(
198 (str_pad($secret, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) .
200 (str_pad($secret, 64, chr(0x00)) ^ (str_repeat(chr(0x36), 64))) . $stringToSign
206 return base64_encode($raw);