S3Signature.php 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-License-Identifier: AGPL-3.0-or-later
  5. */
  6. namespace OC\Files\ObjectStore;
  7. use Aws\Credentials\CredentialsInterface;
  8. use Aws\S3\S3Client;
  9. use Aws\S3\S3UriParser;
  10. use Aws\Signature\SignatureInterface;
  11. use GuzzleHttp\Psr7;
  12. use Psr\Http\Message\RequestInterface;
  13. /**
  14. * Legacy Amazon S3 signature implementation
  15. */
  16. class S3Signature implements SignatureInterface {
  17. /** @var array Query string values that must be signed */
  18. private $signableQueryString = [
  19. 'acl', 'cors', 'delete', 'lifecycle', 'location', 'logging',
  20. 'notification', 'partNumber', 'policy', 'requestPayment',
  21. 'response-cache-control', 'response-content-disposition',
  22. 'response-content-encoding', 'response-content-language',
  23. 'response-content-type', 'response-expires', 'restore', 'tagging',
  24. 'torrent', 'uploadId', 'uploads', 'versionId', 'versioning',
  25. 'versions', 'website'
  26. ];
  27. /** @var array Sorted headers that must be signed */
  28. private $signableHeaders = ['Content-MD5', 'Content-Type'];
  29. /** @var \Aws\S3\S3UriParser S3 URI parser */
  30. private $parser;
  31. public function __construct() {
  32. $this->parser = new S3UriParser();
  33. // Ensure that the signable query string parameters are sorted
  34. sort($this->signableQueryString);
  35. }
  36. public function signRequest(
  37. RequestInterface $request,
  38. CredentialsInterface $credentials,
  39. ) {
  40. $request = $this->prepareRequest($request, $credentials);
  41. $stringToSign = $this->createCanonicalizedString($request);
  42. $auth = 'AWS '
  43. . $credentials->getAccessKeyId() . ':'
  44. . $this->signString($stringToSign, $credentials);
  45. return $request->withHeader('Authorization', $auth);
  46. }
  47. public function presign(
  48. RequestInterface $request,
  49. CredentialsInterface $credentials,
  50. $expires,
  51. array $options = [],
  52. ) {
  53. $query = [];
  54. // URL encoding already occurs in the URI template expansion. Undo that
  55. // and encode using the same encoding as GET object, PUT object, etc.
  56. $uri = $request->getUri();
  57. $path = S3Client::encodeKey(rawurldecode($uri->getPath()));
  58. $request = $request->withUri($uri->withPath($path));
  59. // Make sure to handle temporary credentials
  60. if ($token = $credentials->getSecurityToken()) {
  61. $request = $request->withHeader('X-Amz-Security-Token', $token);
  62. $query['X-Amz-Security-Token'] = $token;
  63. }
  64. if ($expires instanceof \DateTime) {
  65. $expires = $expires->getTimestamp();
  66. } elseif (!is_numeric($expires)) {
  67. $expires = strtotime($expires);
  68. }
  69. // Set query params required for pre-signed URLs
  70. $query['AWSAccessKeyId'] = $credentials->getAccessKeyId();
  71. $query['Expires'] = $expires;
  72. $query['Signature'] = $this->signString(
  73. $this->createCanonicalizedString($request, $expires),
  74. $credentials
  75. );
  76. // Move X-Amz-* headers to the query string
  77. foreach ($request->getHeaders() as $name => $header) {
  78. $name = strtolower($name);
  79. if (str_starts_with($name, 'x-amz-')) {
  80. $query[$name] = implode(',', $header);
  81. }
  82. }
  83. $queryString = http_build_query($query, '', '&', PHP_QUERY_RFC3986);
  84. return $request->withUri($request->getUri()->withQuery($queryString));
  85. }
  86. /**
  87. * @param RequestInterface $request
  88. * @param CredentialsInterface $creds
  89. *
  90. * @return RequestInterface
  91. */
  92. private function prepareRequest(
  93. RequestInterface $request,
  94. CredentialsInterface $creds,
  95. ) {
  96. $modify = [
  97. 'remove_headers' => ['X-Amz-Date'],
  98. 'set_headers' => ['Date' => gmdate(\DateTimeInterface::RFC2822)]
  99. ];
  100. // Add the security token header if one is being used by the credentials
  101. if ($token = $creds->getSecurityToken()) {
  102. $modify['set_headers']['X-Amz-Security-Token'] = $token;
  103. }
  104. return Psr7\Utils::modifyRequest($request, $modify);
  105. }
  106. private function signString($string, CredentialsInterface $credentials) {
  107. return base64_encode(
  108. hash_hmac('sha1', $string, $credentials->getSecretKey(), true)
  109. );
  110. }
  111. private function createCanonicalizedString(
  112. RequestInterface $request,
  113. $expires = null,
  114. ) {
  115. $buffer = $request->getMethod() . "\n";
  116. // Add the interesting headers
  117. foreach ($this->signableHeaders as $header) {
  118. $buffer .= $request->getHeaderLine($header) . "\n";
  119. }
  120. $date = $expires ?: $request->getHeaderLine('date');
  121. $buffer .= "{$date}\n"
  122. . $this->createCanonicalizedAmzHeaders($request)
  123. . $this->createCanonicalizedResource($request);
  124. return $buffer;
  125. }
  126. private function createCanonicalizedAmzHeaders(RequestInterface $request) {
  127. $headers = [];
  128. foreach ($request->getHeaders() as $name => $header) {
  129. $name = strtolower($name);
  130. if (str_starts_with($name, 'x-amz-')) {
  131. $value = implode(',', $header);
  132. if (strlen($value) > 0) {
  133. $headers[$name] = $name . ':' . $value;
  134. }
  135. }
  136. }
  137. if (!$headers) {
  138. return '';
  139. }
  140. ksort($headers);
  141. return implode("\n", $headers) . "\n";
  142. }
  143. private function createCanonicalizedResource(RequestInterface $request) {
  144. $data = $this->parser->parse($request->getUri());
  145. $buffer = '/';
  146. if ($data['bucket']) {
  147. $buffer .= $data['bucket'];
  148. if (!empty($data['key']) || !$data['path_style']) {
  149. $buffer .= '/' . $data['key'];
  150. }
  151. }
  152. // Add sub resource parameters if present.
  153. $query = $request->getUri()->getQuery();
  154. if ($query) {
  155. $params = Psr7\Query::parse($query);
  156. $first = true;
  157. foreach ($this->signableQueryString as $key) {
  158. if (array_key_exists($key, $params)) {
  159. $value = $params[$key];
  160. $buffer .= $first ? '?' : '&';
  161. $first = false;
  162. $buffer .= $key;
  163. // Don't add values for empty sub-resources
  164. if (strlen($value)) {
  165. $buffer .= "={$value}";
  166. }
  167. }
  168. }
  169. }
  170. return $buffer;
  171. }
  172. }