AWS.php 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. <?php
  2. declare(strict_types=1);
  3. namespace Sabre\HTTP\Auth;
  4. use Sabre\HTTP;
  5. /**
  6. * HTTP AWS Authentication handler.
  7. *
  8. * Use this class to leverage amazon's AWS authentication header
  9. *
  10. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  11. * @author Evert Pot (http://evertpot.com/)
  12. * @license http://sabre.io/license/ Modified BSD License
  13. */
  14. class AWS extends AbstractAuth
  15. {
  16. /**
  17. * The signature supplied by the HTTP client.
  18. *
  19. * @var string
  20. */
  21. private $signature;
  22. /**
  23. * The accesskey supplied by the HTTP client.
  24. *
  25. * @var string
  26. */
  27. private $accessKey;
  28. /**
  29. * An error code, if any.
  30. *
  31. * This value will be filled with one of the ERR_* constants
  32. *
  33. * @var int
  34. */
  35. public $errorCode = 0;
  36. public const ERR_NOAWSHEADER = 1;
  37. public const ERR_MD5CHECKSUMWRONG = 2;
  38. public const ERR_INVALIDDATEFORMAT = 3;
  39. public const ERR_REQUESTTIMESKEWED = 4;
  40. public const ERR_INVALIDSIGNATURE = 5;
  41. /**
  42. * Gathers all information from the headers.
  43. *
  44. * This method needs to be called prior to anything else.
  45. */
  46. public function init(): bool
  47. {
  48. $authHeader = $this->request->getHeader('Authorization');
  49. if (null === $authHeader) {
  50. $this->errorCode = self::ERR_NOAWSHEADER;
  51. return false;
  52. }
  53. $authHeader = explode(' ', $authHeader);
  54. if ('AWS' !== $authHeader[0] || !isset($authHeader[1])) {
  55. $this->errorCode = self::ERR_NOAWSHEADER;
  56. return false;
  57. }
  58. list($this->accessKey, $this->signature) = explode(':', $authHeader[1]);
  59. return true;
  60. }
  61. /**
  62. * Returns the username for the request.
  63. */
  64. public function getAccessKey(): string
  65. {
  66. return $this->accessKey;
  67. }
  68. /**
  69. * Validates the signature based on the secretKey.
  70. */
  71. public function validate(string $secretKey): bool
  72. {
  73. $contentMD5 = $this->request->getHeader('Content-MD5');
  74. if ($contentMD5) {
  75. // We need to validate the integrity of the request
  76. $body = $this->request->getBody();
  77. $this->request->setBody($body);
  78. if ($contentMD5 !== base64_encode(md5((string) $body, true))) {
  79. // content-md5 header did not match md5 signature of body
  80. $this->errorCode = self::ERR_MD5CHECKSUMWRONG;
  81. return false;
  82. }
  83. }
  84. if (!$requestDate = $this->request->getHeader('x-amz-date')) {
  85. $requestDate = $this->request->getHeader('Date');
  86. }
  87. if (!$this->validateRFC2616Date((string) $requestDate)) {
  88. return false;
  89. }
  90. $amzHeaders = $this->getAmzHeaders();
  91. $signature = base64_encode(
  92. $this->hmacsha1($secretKey,
  93. $this->request->getMethod()."\n".
  94. $contentMD5."\n".
  95. $this->request->getHeader('Content-type')."\n".
  96. $requestDate."\n".
  97. $amzHeaders.
  98. $this->request->getUrl()
  99. )
  100. );
  101. if ($this->signature !== $signature) {
  102. $this->errorCode = self::ERR_INVALIDSIGNATURE;
  103. return false;
  104. }
  105. return true;
  106. }
  107. /**
  108. * Returns an HTTP 401 header, forcing login.
  109. *
  110. * This should be called when username and password are incorrect, or not supplied at all
  111. */
  112. public function requireLogin()
  113. {
  114. $this->response->addHeader('WWW-Authenticate', 'AWS');
  115. $this->response->setStatus(401);
  116. }
  117. /**
  118. * Makes sure the supplied value is a valid RFC2616 date.
  119. *
  120. * If we would just use strtotime to get a valid timestamp, we have no way of checking if a
  121. * user just supplied the word 'now' for the date header.
  122. *
  123. * This function also makes sure the Date header is within 15 minutes of the operating
  124. * system date, to prevent replay attacks.
  125. */
  126. protected function validateRFC2616Date(string $dateHeader): bool
  127. {
  128. $date = HTTP\parseDate($dateHeader);
  129. // Unknown format
  130. if (!$date) {
  131. $this->errorCode = self::ERR_INVALIDDATEFORMAT;
  132. return false;
  133. }
  134. $min = new \DateTime('-15 minutes');
  135. $max = new \DateTime('+15 minutes');
  136. // We allow 15 minutes around the current date/time
  137. if ($date > $max || $date < $min) {
  138. $this->errorCode = self::ERR_REQUESTTIMESKEWED;
  139. return false;
  140. }
  141. return true;
  142. }
  143. /**
  144. * Returns a list of AMZ headers.
  145. */
  146. protected function getAmzHeaders(): string
  147. {
  148. $amzHeaders = [];
  149. $headers = $this->request->getHeaders();
  150. foreach ($headers as $headerName => $headerValue) {
  151. if (0 === strpos(strtolower($headerName), 'x-amz-')) {
  152. $amzHeaders[strtolower($headerName)] = str_replace(["\r\n"], [' '], $headerValue[0])."\n";
  153. }
  154. }
  155. ksort($amzHeaders);
  156. $headerStr = '';
  157. foreach ($amzHeaders as $h => $v) {
  158. $headerStr .= $h.':'.$v;
  159. }
  160. return $headerStr;
  161. }
  162. /**
  163. * Generates an HMAC-SHA1 signature.
  164. */
  165. private function hmacsha1(string $key, string $message): string
  166. {
  167. if (function_exists('hash_hmac')) {
  168. return hash_hmac('sha1', $message, $key, true);
  169. }
  170. $blocksize = 64;
  171. if (strlen($key) > $blocksize) {
  172. $key = pack('H*', sha1($key));
  173. }
  174. $key = str_pad($key, $blocksize, chr(0x00));
  175. $ipad = str_repeat(chr(0x36), $blocksize);
  176. $opad = str_repeat(chr(0x5C), $blocksize);
  177. $hmac = pack('H*', sha1(($key ^ $opad).pack('H*', sha1(($key ^ $ipad).$message))));
  178. return $hmac;
  179. }
  180. }