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