From 01b911ab6d8aac59f9e3992649064d6eb221c827 Mon Sep 17 00:00:00 2001 From: Chris Schinnerl Date: Tue, 12 Sep 2023 10:05:53 +0200 Subject: [PATCH] fix signature error encoding --- error.go | 15 +-- gofakes3.go | 2 +- signature/credentials.go | 53 +++++++++ signature/signature-errors.go | 147 +++++++++++++++++++++++ signature/signature-v4-parser.go | 127 ++++++++++++++++++++ signature/signature-v4-utils.go | 89 ++++++++++++++ signature/signature-v4.go | 194 +++++++++++++++++++++++++++++++ signature/signature-v4_test.go | 64 ++++++++++ signature/utils.go | 109 +++++++++++++++++ 9 files changed, 790 insertions(+), 10 deletions(-) create mode 100644 signature/credentials.go create mode 100644 signature/signature-errors.go create mode 100644 signature/signature-v4-parser.go create mode 100644 signature/signature-v4-utils.go create mode 100644 signature/signature-v4.go create mode 100644 signature/signature-v4_test.go create mode 100644 signature/utils.go diff --git a/error.go b/error.go index 085d3744..e10aa7d0 100644 --- a/error.go +++ b/error.go @@ -11,7 +11,6 @@ import ( // https://docs.aws.amazon.com/AmazonS3/latest/API/ErrorResponses.html // // If you add a code to this list, please also add it to ErrorCode.Status(). -// const ( ErrNone ErrorCode = "" @@ -150,13 +149,12 @@ type Error interface { // Code and Message: // // func NotQuiteRight(at time.Time, max time.Duration) error { -// code := ErrNotQuiteRight -// return ¬QuiteRightResponse{ -// ErrorResponse{Code: code, Message: code.Message()}, -// 123456789, -// } -// } -// +// code := ErrNotQuiteRight +// return ¬QuiteRightResponse{ +// ErrorResponse{Code: code, Message: code.Message()}, +// 123456789, +// } +// } type ErrorResponse struct { XMLName xml.Name `xml:"Error"` @@ -291,7 +289,6 @@ func (e ErrorCode) Status() int { // } // // If err is nil and code is ErrNone, HasErrorCode returns true. -// func HasErrorCode(err error, code ErrorCode) bool { if err == nil && code == "" { return true diff --git a/gofakes3.go b/gofakes3.go index 179e0cdd..d37ae9df 100644 --- a/gofakes3.go +++ b/gofakes3.go @@ -16,7 +16,7 @@ import ( "sync/atomic" "time" - "github.com/Mikubill/gofakes3/signature" + "github.com/SiaFoundation/gofakes3/signature" ) // GoFakeS3 implements HTTP handlers for processing S3 requests and returning diff --git a/signature/credentials.go b/signature/credentials.go new file mode 100644 index 00000000..25daf423 --- /dev/null +++ b/signature/credentials.go @@ -0,0 +1,53 @@ +// Package signature verify request for any AWS service. +package signature + +import ( + "strings" + "sync" + "time" +) + +var credStore sync.Map + +// Credentials holds access and secret keys. +type Credentials struct { + AccessKey string `xml:"AccessKeyId" json:"accessKey,omitempty"` + SecretKey string `xml:"SecretAccessKey" json:"secretKey,omitempty"` + Expiration time.Time `xml:"Expiration" json:"expiration,omitempty"` + SessionToken string `xml:"SessionToken" json:"sessionToken,omitempty"` + Status string `xml:"-" json:"status,omitempty"` + ParentUser string `xml:"-" json:"parentUser,omitempty"` + Groups []string `xml:"-" json:"groups,omitempty"` + Claims map[string]interface{} `xml:"-" json:"claims,omitempty"` +} + +// signValues data type represents structured form of AWS Signature V4 header. +type signValues struct { + Credential credentialHeader + SignedHeaders []string + Signature string +} + +// credentialHeader data type represents structured form of Credential +// string from authorization header. +type credentialHeader struct { + accessKey string + scope signScope +} + +type signScope struct { + date time.Time + region string + service string + request string +} + +// Return scope string. +func (c credentialHeader) getScope() string { + return strings.Join([]string{ + c.scope.date.Format(yyyymmdd), + c.scope.region, + c.scope.service, + c.scope.request, + }, slashSeparator) +} diff --git a/signature/signature-errors.go b/signature/signature-errors.go new file mode 100644 index 00000000..0afeda46 --- /dev/null +++ b/signature/signature-errors.go @@ -0,0 +1,147 @@ +package signature + +import ( + "bytes" + "encoding/xml" + "net/http" +) + +// ErrorCode is code[int] of APIError +type ErrorCode int + +// APIError is API Error structure +type APIError struct { + Code string + Description string + HTTPStatusCode int +} + +// the format of error response +type errorResponse struct { + XMLName xml.Name `xml:"Error"` + + Code string + Message string +} + +type errorCodeMap map[ErrorCode]APIError + +const ( + errMissingFields ErrorCode = iota + errMissingCredTag + errCredMalformed + errInvalidAccessKeyID + errMalformedCredentialDate + errInvalidRequestVersion + errInvalidServiceS3 + errMissingSignHeadersTag + errMissingSignTag + errUnsignedHeaders + errMissingDateHeader + errMalformedDate + errUnsupportAlgorithm + errSignatureDoesNotMatch + + // ErrNone is None(err=nil) + ErrNone +) + +// error code to APIerror structure, these fields carry respective +// descriptions for all the error responses. +var errorCodes = errorCodeMap{ + errMissingFields: { + Code: "MissingFields", + Description: "Missing fields in request.", + HTTPStatusCode: http.StatusBadRequest, + }, + errMissingCredTag: { + Code: "InvalidRequest", + Description: "Missing Credential field for this request.", + HTTPStatusCode: http.StatusBadRequest, + }, + errCredMalformed: { + Code: "AuthorizationQueryParameterserror", + Description: "error parsing the X-Amz-Credential parameter; the Credential is mal-formed; expecting \"/YYYYMMDD/REGION/SERVICE/aws4_request\".", + HTTPStatusCode: http.StatusBadRequest, + }, + errInvalidAccessKeyID: { + Code: "InvalidAccessKeyId", + Description: "The Access Key Id you provided does not exist in our records.", + HTTPStatusCode: http.StatusForbidden, + }, + errMalformedCredentialDate: { + Code: "AuthorizationQueryParameterserror", + Description: "error parsing the X-Amz-Credential parameter; incorrect date format. This date in the credential must be in the format \"yyyyMMdd\".", + HTTPStatusCode: http.StatusBadRequest, + }, + errInvalidRequestVersion: { + Code: "AuthorizationQueryParameterserror", + Description: "error parsing the X-Amz-Credential parameter; incorrect terminal. This endpoint uses \"aws4_request\".", + HTTPStatusCode: http.StatusBadRequest, + }, + errInvalidServiceS3: { + Code: "AuthorizationParameterserror", + Description: "error parsing the Credential/X-Amz-Credential parameter; incorrect service. This endpoint belongs to \"s3\".", + HTTPStatusCode: http.StatusBadRequest, + }, + errMissingSignHeadersTag: { + Code: "InvalidArgument", + Description: "Signature header missing SignedHeaders field.", + HTTPStatusCode: http.StatusBadRequest, + }, + errMissingSignTag: { + Code: "AccessDenied", + Description: "Signature header missing Signature field.", + HTTPStatusCode: http.StatusBadRequest, + }, + errUnsignedHeaders: { + Code: "AccessDenied", + Description: "There were headers present in the request which were not signed", + HTTPStatusCode: http.StatusBadRequest, + }, + errMissingDateHeader: { + Code: "AccessDenied", + Description: "AWS authentication requires a valid Date or x-amz-date header", + HTTPStatusCode: http.StatusBadRequest, + }, + errMalformedDate: { + Code: "MalformedDate", + Description: "Invalid date format header, expected to be in ISO8601, RFC1123 or RFC1123Z time format.", + HTTPStatusCode: http.StatusBadRequest, + }, + errUnsupportAlgorithm: { + Code: "UnsupportedAlgorithm", + Description: "Encountered an unsupported algorithm.", + HTTPStatusCode: http.StatusBadRequest, + }, + errSignatureDoesNotMatch: { + Code: "SignatureDoesNotMatch", + Description: "The request signature we calculated does not match the signature you provided. Check your key and signing method.", + HTTPStatusCode: http.StatusForbidden, + }, +} + +// EncodeResponse encodes the response headers into XML format. +func EncodeResponse(response interface{}) []byte { + var bytesBuffer bytes.Buffer + bytesBuffer.WriteString(xml.Header) + buf, err := xml.Marshal(response) + if err != nil { + return nil + } + bytesBuffer.Write(buf) + return bytesBuffer.Bytes() +} + +// GetAPIError decodes Signerror[int] to APIerror[encodable] +func GetAPIError(errCode ErrorCode) APIError { + return errorCodes[errCode] +} + +// EncodeAPIErrorToResponse encodes APIerror to http response +func EncodeAPIErrorToResponse(err APIError) []byte { + return EncodeResponse(errorResponse{ + Code: err.Code, + Message: err.Description, + }) +} diff --git a/signature/signature-v4-parser.go b/signature/signature-v4-parser.go new file mode 100644 index 00000000..d993247a --- /dev/null +++ b/signature/signature-v4-parser.go @@ -0,0 +1,127 @@ +package signature + +import ( + "strings" + "time" +) + +// parse credentialHeader string into its structured form. +// +// example: Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request +func parseCredentialHeader(credElement string) (ch credentialHeader, err ErrorCode) { + + creds, err := extractFields(credElement, "Credential") + if err != ErrNone { + return ch, err + } + + credElements := strings.Split(strings.TrimSpace(creds), slashSeparator) + if len(credElements) < 5 { + return ch, errCredMalformed + } + + accessKey := strings.Join(credElements[:len(credElements)-4], slashSeparator) + credElements = credElements[len(credElements)-4:] + signDate, e := time.Parse(yyyymmdd, credElements[0]) + if e != nil { + return ch, errMalformedCredentialDate + } + + // Save access key id. + cred := credentialHeader{ + accessKey: accessKey, + scope: signScope{ + date: signDate, + region: credElements[1], + service: credElements[2], + request: credElements[3], + }, + } + + if len(accessKey) < accessKeyMinLen { + return ch, errInvalidAccessKeyID + } + + if cred.scope.service != serviceS3 { + return ch, errInvalidServiceS3 + } + + if cred.scope.request != "aws4_request" { + return ch, errInvalidRequestVersion + } + + return cred, ErrNone +} + +// Parse slice of signed headers from signed headers tag. +// +// example: SignedHeaders=host;range;x-amz-date +func parseSignedHeader(hdrElement string) ([]string, ErrorCode) { + signedHdrFields, err := extractFields(hdrElement, "SignedHeaders") + if err != ErrNone { + return nil, err + } + signedHeaders := strings.Split(signedHdrFields, ";") + return signedHeaders, ErrNone +} + +// Parse signature from signature tag. +// +// exmaple: Signature=fe5f80f77d5fa3beca038a248ff027d0445342fe2855ddc963176630326f1024 +func parseSignature(signElement string) (string, ErrorCode) { + return extractFields(signElement, "Signature") +} + +func extractFields(signElement, fieldName string) (string, ErrorCode) { + signFields := strings.Split(strings.TrimSpace(signElement), "=") + if len(signFields) != 2 { + return "", errMissingFields + } + if signFields[0] != fieldName { + return "", errMissingSignTag + } + if signFields[1] == "" { + return "", errMissingFields + } + return signFields[1], ErrNone +} + +// Parses signature version '4' header of the following form. +// +// Authorization: algorithm Credential=accessKeyID/credScope, SignedHeaders=signedHeaders, Signature=signature +func parseSignV4(v4Auth string) (sv signValues, err ErrorCode) { + + if !strings.HasPrefix(v4Auth, signV4Algorithm) { + return sv, errUnsupportAlgorithm + } + + rawCred := strings.ReplaceAll(strings.TrimPrefix(v4Auth, signV4Algorithm), " ", "") + authFields := strings.Split(strings.TrimSpace(rawCred), ",") + if len(authFields) != 3 { + return sv, errMissingFields + } + + // Initialize signature version '4' structured header. + signV4Values := signValues{} + + // Save credentail values. + signV4Values.Credential, err = parseCredentialHeader(authFields[0]) + if err != ErrNone { + return sv, err + } + + // Save signed headers. + signV4Values.SignedHeaders, err = parseSignedHeader(authFields[1]) + if err != ErrNone { + return sv, err + } + + // Save signature. + signV4Values.Signature, err = parseSignature(authFields[2]) + if err != ErrNone { + return sv, err + } + + // Return the structure here. + return signV4Values, ErrNone +} diff --git a/signature/signature-v4-utils.go b/signature/signature-v4-utils.go new file mode 100644 index 00000000..92a0a685 --- /dev/null +++ b/signature/signature-v4-utils.go @@ -0,0 +1,89 @@ +package signature + +import ( + "net/http" + "strconv" +) + +var ( + emptySHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" +) + +// extractSignedHeaders extract signed headers from Authorization header +func extractSignedHeaders(signedHeaders []string, r *http.Request) (http.Header, ErrorCode) { + reqHeaders := r.Header + reqQueries := r.Form + // find whether "host" is part of list of signed headers. + // if not return ErrUnsignedHeaders. "host" is mandatory. + if !contains(signedHeaders, "host") { + return nil, errUnsignedHeaders + } + extractedSignedHeaders := make(http.Header) + for _, header := range signedHeaders { + // `host` will not be found in the headers, can be found in r.Host. + // but its alway necessary that the list of signed headers containing host in it. + val, ok := reqHeaders[http.CanonicalHeaderKey(header)] + if !ok { + // try to set headers from Query String + val, ok = reqQueries[header] + } + if ok { + extractedSignedHeaders[http.CanonicalHeaderKey(header)] = val + continue + } + switch header { + case "expect": + // Golang http server strips off 'Expect' header, if the + // client sent this as part of signed headers we need to + // handle otherwise we would see a signature mismatch. + // `aws-cli` sets this as part of signed headers. + // + // According to + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20 + // Expect header is always of form: + // + // Expect = "Expect" ":" 1#expectation + // expectation = "100-continue" | expectation-extension + // + // So it safe to assume that '100-continue' is what would + // be sent, for the time being keep this work around. + // Adding a *TODO* to remove this later when Golang server + // doesn't filter out the 'Expect' header. + extractedSignedHeaders.Set(header, "100-continue") + case "host": + // Go http server removes "host" from Request.Header + extractedSignedHeaders.Set(header, r.Host) + case "transfer-encoding": + // Go http server removes "host" from Request.Header + extractedSignedHeaders[http.CanonicalHeaderKey(header)] = r.TransferEncoding + case "content-length": + // Signature-V4 spec excludes Content-Length from signed headers list for signature calculation. + // But some clients deviate from this rule. Hence we consider Content-Length for signature + // calculation to be compatible with such clients. + extractedSignedHeaders.Set(header, strconv.FormatInt(r.ContentLength, 10)) + default: + return nil, errUnsignedHeaders + } + } + return extractedSignedHeaders, ErrNone +} + +// Returns SHA256 for calculating canonical-request. +func getContentSha256Cksum(r *http.Request) string { + var ( + defaultSha256Cksum string + v []string + ok bool + ) + + defaultSha256Cksum = emptySHA256 + v, ok = r.Header[amzContentSha256] + + // We found 'X-Amz-Content-Sha256' return the captured value. + if ok { + return v[0] + } + + // We couldn't find 'X-Amz-Content-Sha256'. + return defaultSha256Cksum +} diff --git a/signature/signature-v4.go b/signature/signature-v4.go new file mode 100644 index 00000000..157cb733 --- /dev/null +++ b/signature/signature-v4.go @@ -0,0 +1,194 @@ +package signature + +import ( + "bytes" + "crypto/sha256" + "crypto/subtle" + "encoding/hex" + "net/http" + "sort" + "strings" + "time" +) + +// ref: https://github.com/minio/minio/cmd/auth-handler.go + +const ( + signV4Algorithm = "AWS4-HMAC-SHA256" + iso8601Format = "20060102T150405Z" + yyyymmdd = "20060102" + serviceS3 = "s3" + slashSeparator = "/" + stype = serviceS3 + + headerAuth = "Authorization" + headerDate = "Date" + amzContentSha256 = "X-Amz-Content-Sha256" + amzDate = "X-Amz-Date" +) + +// getCanonicalHeaders generate a list of request headers with their values +func getCanonicalHeaders(signedHeaders http.Header) string { + var headers []string + vals := make(http.Header) + for k, vv := range signedHeaders { + headers = append(headers, strings.ToLower(k)) + vals[strings.ToLower(k)] = vv + } + sort.Strings(headers) + + var buf bytes.Buffer + for _, k := range headers { + buf.WriteString(k) + buf.WriteByte(':') + for idx, v := range vals[k] { + if idx > 0 { + buf.WriteByte(',') + } + buf.WriteString(signV4TrimAll(v)) + } + buf.WriteByte('\n') + } + return buf.String() +} + +// getSignedHeaders generate a string i.e alphabetically sorted, semicolon-separated list of lowercase request header names +func getSignedHeaders(signedHeaders http.Header) string { + var headers []string + for k := range signedHeaders { + headers = append(headers, strings.ToLower(k)) + } + sort.Strings(headers) + return strings.Join(headers, ";") +} + +// compareSignatureV4 returns true if and only if both signatures +// are equal. The signatures are expected to be HEX encoded strings +// according to the AWS S3 signature V4 spec. +func compareSignatureV4(sig1, sig2 string) bool { + // The CTC using []byte(str) works because the hex encoding + // is unique for a sequence of bytes. See also compareSignatureV2. + return subtle.ConstantTimeCompare([]byte(sig1), []byte(sig2)) == 1 +} + +// getSignature final signature in hexadecimal form. +func getSignature(signingKey []byte, stringToSign string) string { + return hex.EncodeToString(sumHMAC(signingKey, []byte(stringToSign))) +} + +// Trim leading and trailing spaces and replace sequential spaces with one space, following Trimall() +// in http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html +func signV4TrimAll(input string) string { + // Compress adjacent spaces (a space is determined by + // unicode.IsSpace() internally here) to one space and return + return strings.Join(strings.Fields(input), " ") +} + +// getCanonicalRequest generate a canonical request of style +// +// canonicalRequest = +// +// \n +// \n +// \n +// \n +// \n +// +func getCanonicalRequest(extractedSignedHeaders http.Header, payload, queryStr, urlPath, method string) string { + rawQuery := strings.ReplaceAll(queryStr, "+", "%20") + encodedPath := encodePath(urlPath) + canonicalRequest := strings.Join([]string{ + method, + encodedPath, + rawQuery, + getCanonicalHeaders(extractedSignedHeaders), + getSignedHeaders(extractedSignedHeaders), + payload, + }, "\n") + return canonicalRequest +} + +// getStringToSign a string based on selected query values. +func getStringToSign(canonicalRequest string, t time.Time, scope string) string { + stringToSign := signV4Algorithm + "\n" + t.Format(iso8601Format) + "\n" + stringToSign += scope + "\n" + canonicalRequestBytes := sha256.Sum256([]byte(canonicalRequest)) + stringToSign += hex.EncodeToString(canonicalRequestBytes[:]) + return stringToSign +} + +// getSigningKey hmac seed to calculate final signature. +func getSigningKey(secretKey string, t time.Time, region string) []byte { + date := sumHMAC([]byte("AWS4"+secretKey), []byte(t.Format(yyyymmdd))) + regionBytes := sumHMAC(date, []byte(region)) + service := sumHMAC(regionBytes, []byte(stype)) + signingKey := sumHMAC(service, []byte("aws4_request")) + return signingKey +} + +// V4SignVerify - Verify authorization header with calculated header in accordance with +// - http://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html +// +// returns nil if signature matches. +func V4SignVerify(r *http.Request) ErrorCode { + // Copy request. + req := *r + hashedPayload := getContentSha256Cksum(r) + + // Save authorization header. + v4Auth := req.Header.Get(headerAuth) + + // Parse signature version '4' header. + signV4Values, Err := parseSignV4(v4Auth) + if Err != ErrNone { + return Err + } + + cred, _, Err := checkKeyValid(r, signV4Values.Credential.accessKey) + if Err != ErrNone { + return Err + } + + // Extract all the signed headers along with its values. + extractedSignedHeaders, ErrCode := extractSignedHeaders(signV4Values.SignedHeaders, r) + if ErrCode != ErrNone { + return ErrCode + } + + // Extract date, if not present throw Error. + var date string + if date = req.Header.Get(amzDate); date == "" { + if date = r.Header.Get(headerDate); date == "" { + return errMissingDateHeader + } + } + + // Parse date header. + t, e := time.Parse(iso8601Format, date) + if e != nil { + return errMalformedDate + } + + // Query string. + queryStr := req.URL.RawQuery + + // Get canonical request. + canonicalRequest := getCanonicalRequest(extractedSignedHeaders, hashedPayload, queryStr, req.URL.Path, req.Method) + + // Get string to sign from canonical request. + stringToSign := getStringToSign(canonicalRequest, t, signV4Values.Credential.getScope()) + + // Get hmac signing key. + signingKey := getSigningKey(cred.SecretKey, signV4Values.Credential.scope.date, signV4Values.Credential.scope.region) + + // Calculate signature. + newSignature := getSignature(signingKey, stringToSign) + + // Verify if signature match. + if !compareSignatureV4(newSignature, signV4Values.Signature) { + return errSignatureDoesNotMatch + } + + // Return Error none. + return ErrNone +} diff --git a/signature/signature-v4_test.go b/signature/signature-v4_test.go new file mode 100644 index 00000000..ecb1ba16 --- /dev/null +++ b/signature/signature-v4_test.go @@ -0,0 +1,64 @@ +package signature_test + +import ( + "bytes" + "encoding/hex" + "fmt" + "math/rand" + "net/http" + "testing" + "time" + + "github.com/SiaFoundation/gofakes3/signature" + "github.com/aws/aws-sdk-go/aws/credentials" + v4 "github.com/aws/aws-sdk-go/aws/signer/v4" +) + +//nolint:all +const ( + signV4Algorithm = "AWS4-HMAC-SHA256" + iso8601Format = "20060102T150405Z" + yyyymmdd = "20060102" + unsignedPayload = "UNSIGNED-PAYLOAD" + serviceS3 = "s3" + SlashSeparator = "/" + stype = serviceS3 +) + +func RandString(n int) string { + src := rand.New(rand.NewSource(time.Now().UnixNano())) + b := make([]byte, (n+1)/2) + + if _, err := src.Read(b); err != nil { + panic(err) + } + + return hex.EncodeToString(b)[:n] +} + +func TestSignatureMatch(t *testing.T) { + + Body := bytes.NewReader(nil) + + ak := RandString(32) + sk := RandString(64) + region := RandString(16) + + credentials := credentials.NewStaticCredentials(ak, sk, "") + signature.ReloadKeys(map[string]string{ak: sk}) + signer := v4.NewSigner(credentials) + + req, err := http.NewRequest(http.MethodPost, "https://s3-endpoint.exmaple.com/bin", Body) + if err != nil { + t.Error(err) + } + + _, err = signer.Sign(req, Body, serviceS3, region, time.Now()) + if err != nil { + t.Error(err) + } + + if result := signature.V4SignVerify(req); result != signature.ErrNone { + t.Error(fmt.Errorf("invalid result: expect none but got %+v", signature.GetAPIError(result))) + } +} diff --git a/signature/utils.go b/signature/utils.go new file mode 100644 index 00000000..5cceec1b --- /dev/null +++ b/signature/utils.go @@ -0,0 +1,109 @@ +package signature + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "net/http" + "reflect" + "regexp" + "strings" + "unicode/utf8" +) + +const ( + accessKeyMinLen = 3 + // secretKeyMinLen = 8 +) + +// check if the access key is valid and recognized, additionally +// also returns if the access key is owner/admin. +func checkKeyValid(r *http.Request, accessKey string) (Credentials, bool, ErrorCode) { + + u, ok := credStore.Load(accessKey) + if !ok { + return Credentials{}, false, errInvalidAccessKeyID + } + return u.(Credentials), true, ErrNone +} + +// LoadKeys parse and store accessKey-secretKey pair +func StoreKeys(pairs map[string]string) { + for accessKey, secretKey := range pairs { + credStore.Store(accessKey, Credentials{ + AccessKey: accessKey, + SecretKey: secretKey, + }) + } +} + +func ReloadKeys(pairs map[string]string) { + credStore.Range(func(key, value interface{}) bool { + if _, ok := pairs[key.(string)]; !ok { + credStore.Delete(key) + } + return true + }) + StoreKeys(pairs) +} + +func sumHMAC(key []byte, data []byte) []byte { + hash := hmac.New(sha256.New, key) + hash.Write(data) + return hash.Sum(nil) +} + +func contains(slice interface{}, elem interface{}) bool { + v := reflect.ValueOf(slice) + if v.Kind() == reflect.Slice { + for i := 0; i < v.Len(); i++ { + if v.Index(i).Interface() == elem { + return true + } + } + } + return false +} + +// encodePath from minio/s3utils.EncodePath + +// if object matches reserved string, no need to encode them +var reservedObjectNames = regexp.MustCompile("^[a-zA-Z0-9-_.~/]+$") + +// EncodePath encode the strings from UTF-8 byte representations to HTML hex escape sequences +// +// This is necessary since regular url.Parse() and url.Encode() functions do not support UTF-8 +// non english characters cannot be parsed due to the nature in which url.Encode() is written +// +// This function on the other hand is a direct replacement for url.Encode() technique to support +// pretty much every UTF-8 character. +func encodePath(pathName string) string { + if reservedObjectNames.MatchString(pathName) { + return pathName + } + var encodedPathname strings.Builder + for _, s := range pathName { + if 'A' <= s && s <= 'Z' || 'a' <= s && s <= 'z' || '0' <= s && s <= '9' { // §2.3 Unreserved characters (mark) + encodedPathname.WriteRune(s) + continue + } + switch s { + case '-', '_', '.', '~', '/': // §2.3 Unreserved characters (mark) + encodedPathname.WriteRune(s) + continue + default: + len := utf8.RuneLen(s) + if len < 0 { + // if utf8 cannot convert return the same string as is + return pathName + } + u := make([]byte, len) + utf8.EncodeRune(u, s) + for _, r := range u { + hex := hex.EncodeToString([]byte{r}) + encodedPathname.WriteString("%" + strings.ToUpper(hex)) + } + } + } + return encodedPathname.String() +}