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