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