1package autorest
2
3// Copyright 2017 Microsoft Corporation
4//
5//  Licensed under the Apache License, Version 2.0 (the "License");
6//  you may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at
8//
9//      http://www.apache.org/licenses/LICENSE-2.0
10//
11//  Unless required by applicable law or agreed to in writing, software
12//  distributed under the License is distributed on an "AS IS" BASIS,
13//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14//  See the License for the specific language governing permissions and
15//  limitations under the License.
16
17import (
18	"bytes"
19	"crypto/hmac"
20	"crypto/sha256"
21	"encoding/base64"
22	"fmt"
23	"net/http"
24	"net/url"
25	"sort"
26	"strings"
27	"time"
28)
29
30// SharedKeyType defines the enumeration for the various shared key types.
31// See https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key for details on the shared key types.
32type SharedKeyType string
33
34const (
35	// SharedKey is used to authorize against blobs, files and queues services.
36	SharedKey SharedKeyType = "sharedKey"
37
38	// SharedKeyForTable is used to authorize against the table service.
39	SharedKeyForTable SharedKeyType = "sharedKeyTable"
40
41	// SharedKeyLite is used to authorize against blobs, files and queues services.  It's provided for
42	// backwards compatibility with API versions before 2009-09-19.  Prefer SharedKey instead.
43	SharedKeyLite SharedKeyType = "sharedKeyLite"
44
45	// SharedKeyLiteForTable is used to authorize against the table service.  It's provided for
46	// backwards compatibility with older table API versions.  Prefer SharedKeyForTable instead.
47	SharedKeyLiteForTable SharedKeyType = "sharedKeyLiteTable"
48)
49
50const (
51	headerAccept            = "Accept"
52	headerAcceptCharset     = "Accept-Charset"
53	headerContentEncoding   = "Content-Encoding"
54	headerContentLength     = "Content-Length"
55	headerContentMD5        = "Content-MD5"
56	headerContentLanguage   = "Content-Language"
57	headerIfModifiedSince   = "If-Modified-Since"
58	headerIfMatch           = "If-Match"
59	headerIfNoneMatch       = "If-None-Match"
60	headerIfUnmodifiedSince = "If-Unmodified-Since"
61	headerDate              = "Date"
62	headerXMSDate           = "X-Ms-Date"
63	headerXMSVersion        = "x-ms-version"
64	headerRange             = "Range"
65)
66
67const storageEmulatorAccountName = "devstoreaccount1"
68
69// SharedKeyAuthorizer implements an authorization for Shared Key
70// this can be used for interaction with Blob, File and Queue Storage Endpoints
71type SharedKeyAuthorizer struct {
72	accountName string
73	accountKey  []byte
74	keyType     SharedKeyType
75}
76
77// NewSharedKeyAuthorizer creates a SharedKeyAuthorizer using the provided credentials and shared key type.
78func NewSharedKeyAuthorizer(accountName, accountKey string, keyType SharedKeyType) (*SharedKeyAuthorizer, error) {
79	key, err := base64.StdEncoding.DecodeString(accountKey)
80	if err != nil {
81		return nil, fmt.Errorf("malformed storage account key: %v", err)
82	}
83	return &SharedKeyAuthorizer{
84		accountName: accountName,
85		accountKey:  key,
86		keyType:     keyType,
87	}, nil
88}
89
90// WithAuthorization returns a PrepareDecorator that adds an HTTP Authorization header whose
91// value is "<SharedKeyType> " followed by the computed key.
92// This can be used for the Blob, Queue, and File Services
93//
94// from: https://docs.microsoft.com/en-us/rest/api/storageservices/authorize-with-shared-key
95// You may use Shared Key authorization to authorize a request made against the
96// 2009-09-19 version and later of the Blob and Queue services,
97// and version 2014-02-14 and later of the File services.
98func (sk *SharedKeyAuthorizer) WithAuthorization() PrepareDecorator {
99	return func(p Preparer) Preparer {
100		return PreparerFunc(func(r *http.Request) (*http.Request, error) {
101			r, err := p.Prepare(r)
102			if err != nil {
103				return r, err
104			}
105
106			sk, err := buildSharedKey(sk.accountName, sk.accountKey, r, sk.keyType)
107			return Prepare(r, WithHeader(headerAuthorization, sk))
108		})
109	}
110}
111
112func buildSharedKey(accName string, accKey []byte, req *http.Request, keyType SharedKeyType) (string, error) {
113	canRes, err := buildCanonicalizedResource(accName, req.URL.String(), keyType)
114	if err != nil {
115		return "", err
116	}
117
118	if req.Header == nil {
119		req.Header = http.Header{}
120	}
121
122	// ensure date is set
123	if req.Header.Get(headerDate) == "" && req.Header.Get(headerXMSDate) == "" {
124		date := time.Now().UTC().Format(http.TimeFormat)
125		req.Header.Set(headerXMSDate, date)
126	}
127	canString, err := buildCanonicalizedString(req.Method, req.Header, canRes, keyType)
128	if err != nil {
129		return "", err
130	}
131	return createAuthorizationHeader(accName, accKey, canString, keyType), nil
132}
133
134func buildCanonicalizedResource(accountName, uri string, keyType SharedKeyType) (string, error) {
135	errMsg := "buildCanonicalizedResource error: %s"
136	u, err := url.Parse(uri)
137	if err != nil {
138		return "", fmt.Errorf(errMsg, err.Error())
139	}
140
141	cr := bytes.NewBufferString("")
142	if accountName != storageEmulatorAccountName {
143		cr.WriteString("/")
144		cr.WriteString(getCanonicalizedAccountName(accountName))
145	}
146
147	if len(u.Path) > 0 {
148		// Any portion of the CanonicalizedResource string that is derived from
149		// the resource's URI should be encoded exactly as it is in the URI.
150		// -- https://msdn.microsoft.com/en-gb/library/azure/dd179428.aspx
151		cr.WriteString(u.EscapedPath())
152	}
153
154	params, err := url.ParseQuery(u.RawQuery)
155	if err != nil {
156		return "", fmt.Errorf(errMsg, err.Error())
157	}
158
159	// See https://github.com/Azure/azure-storage-net/blob/master/Lib/Common/Core/Util/AuthenticationUtility.cs#L277
160	if keyType == SharedKey {
161		if len(params) > 0 {
162			cr.WriteString("\n")
163
164			keys := []string{}
165			for key := range params {
166				keys = append(keys, key)
167			}
168			sort.Strings(keys)
169
170			completeParams := []string{}
171			for _, key := range keys {
172				if len(params[key]) > 1 {
173					sort.Strings(params[key])
174				}
175
176				completeParams = append(completeParams, fmt.Sprintf("%s:%s", key, strings.Join(params[key], ",")))
177			}
178			cr.WriteString(strings.Join(completeParams, "\n"))
179		}
180	} else {
181		// search for "comp" parameter, if exists then add it to canonicalizedresource
182		if v, ok := params["comp"]; ok {
183			cr.WriteString("?comp=" + v[0])
184		}
185	}
186
187	return string(cr.Bytes()), nil
188}
189
190func getCanonicalizedAccountName(accountName string) string {
191	// since we may be trying to access a secondary storage account, we need to
192	// remove the -secondary part of the storage name
193	return strings.TrimSuffix(accountName, "-secondary")
194}
195
196func buildCanonicalizedString(verb string, headers http.Header, canonicalizedResource string, keyType SharedKeyType) (string, error) {
197	contentLength := headers.Get(headerContentLength)
198	if contentLength == "0" {
199		contentLength = ""
200	}
201	date := headers.Get(headerDate)
202	if v := headers.Get(headerXMSDate); v != "" {
203		if keyType == SharedKey || keyType == SharedKeyLite {
204			date = ""
205		} else {
206			date = v
207		}
208	}
209	var canString string
210	switch keyType {
211	case SharedKey:
212		canString = strings.Join([]string{
213			verb,
214			headers.Get(headerContentEncoding),
215			headers.Get(headerContentLanguage),
216			contentLength,
217			headers.Get(headerContentMD5),
218			headers.Get(headerContentType),
219			date,
220			headers.Get(headerIfModifiedSince),
221			headers.Get(headerIfMatch),
222			headers.Get(headerIfNoneMatch),
223			headers.Get(headerIfUnmodifiedSince),
224			headers.Get(headerRange),
225			buildCanonicalizedHeader(headers),
226			canonicalizedResource,
227		}, "\n")
228	case SharedKeyForTable:
229		canString = strings.Join([]string{
230			verb,
231			headers.Get(headerContentMD5),
232			headers.Get(headerContentType),
233			date,
234			canonicalizedResource,
235		}, "\n")
236	case SharedKeyLite:
237		canString = strings.Join([]string{
238			verb,
239			headers.Get(headerContentMD5),
240			headers.Get(headerContentType),
241			date,
242			buildCanonicalizedHeader(headers),
243			canonicalizedResource,
244		}, "\n")
245	case SharedKeyLiteForTable:
246		canString = strings.Join([]string{
247			date,
248			canonicalizedResource,
249		}, "\n")
250	default:
251		return "", fmt.Errorf("key type '%s' is not supported", keyType)
252	}
253	return canString, nil
254}
255
256func buildCanonicalizedHeader(headers http.Header) string {
257	cm := make(map[string]string)
258
259	for k := range headers {
260		headerName := strings.TrimSpace(strings.ToLower(k))
261		if strings.HasPrefix(headerName, "x-ms-") {
262			cm[headerName] = headers.Get(k)
263		}
264	}
265
266	if len(cm) == 0 {
267		return ""
268	}
269
270	keys := []string{}
271	for key := range cm {
272		keys = append(keys, key)
273	}
274
275	sort.Strings(keys)
276
277	ch := bytes.NewBufferString("")
278
279	for _, key := range keys {
280		ch.WriteString(key)
281		ch.WriteRune(':')
282		ch.WriteString(cm[key])
283		ch.WriteRune('\n')
284	}
285
286	return strings.TrimSuffix(string(ch.Bytes()), "\n")
287}
288
289func createAuthorizationHeader(accountName string, accountKey []byte, canonicalizedString string, keyType SharedKeyType) string {
290	h := hmac.New(sha256.New, accountKey)
291	h.Write([]byte(canonicalizedString))
292	signature := base64.StdEncoding.EncodeToString(h.Sum(nil))
293	var key string
294	switch keyType {
295	case SharedKey, SharedKeyForTable:
296		key = "SharedKey"
297	case SharedKeyLite, SharedKeyLiteForTable:
298		key = "SharedKeyLite"
299	}
300	return fmt.Sprintf("%s %s:%s", key, getCanonicalizedAccountName(accountName), signature)
301}
302