S3Signature.php 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016 Robin Appelman <robin@icewind.nl>
  4. *
  5. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  6. * @author Daniel Kesselberg <mail@danielkesselberg.de>
  7. * @author Robin Appelman <robin@icewind.nl>
  8. *
  9. * @license GNU AGPL version 3 or any later version
  10. *
  11. * This program is free software: you can redistribute it and/or modify
  12. * it under the terms of the GNU Affero General Public License as
  13. * published by the Free Software Foundation, either version 3 of the
  14. * License, or (at your option) any later version.
  15. *
  16. * This program is distributed in the hope that it will be useful,
  17. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  18. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  19. * GNU Affero General Public License for more details.
  20. *
  21. * You should have received a copy of the GNU Affero General Public License
  22. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  23. *
  24. */
  25. namespace OC\Files\ObjectStore;
  26. use Aws\Credentials\CredentialsInterface;
  27. use Aws\S3\S3Client;
  28. use Aws\S3\S3UriParser;
  29. use Aws\Signature\SignatureInterface;
  30. use GuzzleHttp\Psr7;
  31. use Psr\Http\Message\RequestInterface;
  32. /**
  33. * Legacy Amazon S3 signature implementation
  34. */
  35. class S3Signature implements SignatureInterface {
  36. /** @var array Query string values that must be signed */
  37. private $signableQueryString = [
  38. 'acl', 'cors', 'delete', 'lifecycle', 'location', 'logging',
  39. 'notification', 'partNumber', 'policy', 'requestPayment',
  40. 'response-cache-control', 'response-content-disposition',
  41. 'response-content-encoding', 'response-content-language',
  42. 'response-content-type', 'response-expires', 'restore', 'tagging',
  43. 'torrent', 'uploadId', 'uploads', 'versionId', 'versioning',
  44. 'versions', 'website'
  45. ];
  46. /** @var array Sorted headers that must be signed */
  47. private $signableHeaders = ['Content-MD5', 'Content-Type'];
  48. /** @var \Aws\S3\S3UriParser S3 URI parser */
  49. private $parser;
  50. public function __construct() {
  51. $this->parser = new S3UriParser();
  52. // Ensure that the signable query string parameters are sorted
  53. sort($this->signableQueryString);
  54. }
  55. public function signRequest(
  56. RequestInterface $request,
  57. CredentialsInterface $credentials
  58. ) {
  59. $request = $this->prepareRequest($request, $credentials);
  60. $stringToSign = $this->createCanonicalizedString($request);
  61. $auth = 'AWS '
  62. . $credentials->getAccessKeyId() . ':'
  63. . $this->signString($stringToSign, $credentials);
  64. return $request->withHeader('Authorization', $auth);
  65. }
  66. public function presign(
  67. RequestInterface $request,
  68. CredentialsInterface $credentials,
  69. $expires,
  70. array $options = []
  71. ) {
  72. $query = [];
  73. // URL encoding already occurs in the URI template expansion. Undo that
  74. // and encode using the same encoding as GET object, PUT object, etc.
  75. $uri = $request->getUri();
  76. $path = S3Client::encodeKey(rawurldecode($uri->getPath()));
  77. $request = $request->withUri($uri->withPath($path));
  78. // Make sure to handle temporary credentials
  79. if ($token = $credentials->getSecurityToken()) {
  80. $request = $request->withHeader('X-Amz-Security-Token', $token);
  81. $query['X-Amz-Security-Token'] = $token;
  82. }
  83. if ($expires instanceof \DateTime) {
  84. $expires = $expires->getTimestamp();
  85. } elseif (!is_numeric($expires)) {
  86. $expires = strtotime($expires);
  87. }
  88. // Set query params required for pre-signed URLs
  89. $query['AWSAccessKeyId'] = $credentials->getAccessKeyId();
  90. $query['Expires'] = $expires;
  91. $query['Signature'] = $this->signString(
  92. $this->createCanonicalizedString($request, $expires),
  93. $credentials
  94. );
  95. // Move X-Amz-* headers to the query string
  96. foreach ($request->getHeaders() as $name => $header) {
  97. $name = strtolower($name);
  98. if (str_starts_with($name, 'x-amz-')) {
  99. $query[$name] = implode(',', $header);
  100. }
  101. }
  102. $queryString = http_build_query($query, null, '&', PHP_QUERY_RFC3986);
  103. return $request->withUri($request->getUri()->withQuery($queryString));
  104. }
  105. /**
  106. * @param RequestInterface $request
  107. * @param CredentialsInterface $creds
  108. *
  109. * @return RequestInterface
  110. */
  111. private function prepareRequest(
  112. RequestInterface $request,
  113. CredentialsInterface $creds
  114. ) {
  115. $modify = [
  116. 'remove_headers' => ['X-Amz-Date'],
  117. 'set_headers' => ['Date' => gmdate(\DateTimeInterface::RFC2822)]
  118. ];
  119. // Add the security token header if one is being used by the credentials
  120. if ($token = $creds->getSecurityToken()) {
  121. $modify['set_headers']['X-Amz-Security-Token'] = $token;
  122. }
  123. return Psr7\Utils::modifyRequest($request, $modify);
  124. }
  125. private function signString($string, CredentialsInterface $credentials) {
  126. return base64_encode(
  127. hash_hmac('sha1', $string, $credentials->getSecretKey(), true)
  128. );
  129. }
  130. private function createCanonicalizedString(
  131. RequestInterface $request,
  132. $expires = null
  133. ) {
  134. $buffer = $request->getMethod() . "\n";
  135. // Add the interesting headers
  136. foreach ($this->signableHeaders as $header) {
  137. $buffer .= $request->getHeaderLine($header) . "\n";
  138. }
  139. $date = $expires ?: $request->getHeaderLine('date');
  140. $buffer .= "{$date}\n"
  141. . $this->createCanonicalizedAmzHeaders($request)
  142. . $this->createCanonicalizedResource($request);
  143. return $buffer;
  144. }
  145. private function createCanonicalizedAmzHeaders(RequestInterface $request) {
  146. $headers = [];
  147. foreach ($request->getHeaders() as $name => $header) {
  148. $name = strtolower($name);
  149. if (str_starts_with($name, 'x-amz-')) {
  150. $value = implode(',', $header);
  151. if (strlen($value) > 0) {
  152. $headers[$name] = $name . ':' . $value;
  153. }
  154. }
  155. }
  156. if (!$headers) {
  157. return '';
  158. }
  159. ksort($headers);
  160. return implode("\n", $headers) . "\n";
  161. }
  162. private function createCanonicalizedResource(RequestInterface $request) {
  163. $data = $this->parser->parse($request->getUri());
  164. $buffer = '/';
  165. if ($data['bucket']) {
  166. $buffer .= $data['bucket'];
  167. if (!empty($data['key']) || !$data['path_style']) {
  168. $buffer .= '/' . $data['key'];
  169. }
  170. }
  171. // Add sub resource parameters if present.
  172. $query = $request->getUri()->getQuery();
  173. if ($query) {
  174. $params = Psr7\Query::parse($query);
  175. $first = true;
  176. foreach ($this->signableQueryString as $key) {
  177. if (array_key_exists($key, $params)) {
  178. $value = $params[$key];
  179. $buffer .= $first ? '?' : '&';
  180. $first = false;
  181. $buffer .= $key;
  182. // Don't add values for empty sub-resources
  183. if (strlen($value)) {
  184. $buffer .= "={$value}";
  185. }
  186. }
  187. }
  188. }
  189. return $buffer;
  190. }
  191. }