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