5 * @package akeebaengine
6 * @copyright Copyright (c)2006-2024 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 // This does not look right... The bucket name must be included in the URL.
68 // if (strpos($uri, $search) === 0)
70 // $uri = substr($uri, strlen($search));
73 $queryParameters = array_merge($this->request->getParameters(), [
74 'AWSAccessKeyId' => $accessKey,
75 'Expires' => sprintf('%u', $expires),
76 'Signature' => $signature,
79 $query = http_build_query($queryParameters);
81 // fix authenticated url for Google Cloud Storage - https://cloud.google.com/storage/docs/access-control/create-signed-urls-program
82 if ($this->request->getConfiguration()->getEndpoint() === "storage.googleapis.com")
84 // replace host with endpoint
85 $headers['Host'] = 'storage.googleapis.com';
86 // replace "AWSAccessKeyId" with "GoogleAccessId"
87 $query = str_replace('AWSAccessKeyId', 'GoogleAccessId', $query);
89 $uri = '/' . $bucket . $uri;
92 $url = $protocol . '://' . $headers['Host'] . $uri;
93 $url .= (strpos($uri, '?') !== false) ? '&' : '?';
100 * Returns the authorization header for the request
104 public function getAuthorizationHeader(): string
106 $verb = strtoupper($this->request->getVerb());
107 $resourcePath = $this->request->getResource();
108 $headers = $this->request->getHeaders();
109 $amzHeaders = $this->request->getAmzHeaders();
110 $parameters = $this->request->getParameters();
111 $bucket = $this->request->getBucket();
112 $isPresignedURL = false;
117 // Collect AMZ headers for signature
118 foreach ($amzHeaders as $header => $value)
120 if (strlen($value) > 0)
122 $amz[] = strtolower($header) . ':' . $value;
126 // AMZ headers must be sorted and sent as separate lines
130 $amzString = "\n" . implode("\n", $amz);
133 // If the Expires query string parameter is set up we're pre-signing a download URL. The string to sign is a bit
134 // different in this case; it does not include the Date, it includes the Expires.
135 // See http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationQueryStringAuth
136 if (isset($headers['Expires']))
138 if (isset($headers['Date']))
140 $headers['Date'] = $headers['Expires'];
144 $amzHeaders['x-amz-date'] = $headers['Expires'];
147 unset ($headers['Expires']);
149 $isPresignedURL = true;
153 * The resource path in S3 V2 signatures must ALWAYS contain the bucket name if a bucket is defined, even if we
154 * are not using path-style access to the resource
156 if (!empty($bucket) && !$this->request->getConfiguration()->getUseLegacyPathStyle())
158 $resourcePath = '/' . $bucket . $resourcePath;
161 $stringToSign = $verb . "\n" .
162 ($headers['Content-MD5'] ?? '') . "\n" .
163 ($headers['Content-Type'] ?? '') . "\n" .
164 ($headers['Date'] ?? '') .
168 // CloudFront only requires a date to be signed
169 if ($headers['Host'] == 'cloudfront.amazonaws.com')
171 $stringToSign = $headers['Date'] ?? $amzHeaders['x-amz-date'] ?? '';
174 $amazonV2Hash = $this->amazonV2Hash($stringToSign);
176 // For presigned URLs we only return the Base64-encoded signature without the AWS format specifier and the
177 // public access key.
180 return $amazonV2Hash;
184 $this->request->getConfiguration()->getAccess() . ':' .
189 * Creates a HMAC-SHA1 hash. Uses the hash extension if present, otherwise falls back to slower, manual calculation.
191 * @param string $stringToSign String to sign
195 private function amazonV2Hash(string $stringToSign): string
197 $secret = $this->request->getConfiguration()->getSecret();
199 if (extension_loaded('hash'))
201 $raw = hash_hmac('sha1', $stringToSign, $secret, true);
203 return base64_encode($raw);
206 $raw = pack('H*', sha1(
207 (str_pad($secret, 64, chr(0x00)) ^ (str_repeat(chr(0x5c), 64))) .
209 (str_pad($secret, 64, chr(0x00)) ^ (str_repeat(chr(0x36), 64))) . $stringToSign
215 return base64_encode($raw);