diff --git a/README.md b/README.md index 29311d5..d6c96de 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ defer testTxt.Close() s3 := simples3.New(Region, AWSAccessKey, AWSSecretKey) err := s3.FileUpload(simples3.UploadInput{ - Bucket: "zerodha-testbucket", + Bucket: AWSBucket, ObjectKey: "test.txt", ContentType: "text/plain", FileName: "test.txt", diff --git a/policy.go b/policy.go index 4d62bb7..aaa8ec6 100644 --- a/policy.go +++ b/policy.go @@ -51,6 +51,7 @@ const ( serviceName = "s3" defaultUploadURLFormat = "http://%s.s3.amazonaws.com/" // + defaultBucketURLFormat = "%s.s3.amazonaws.com/" // defaultExpirationHour = 1 * time.Hour ) @@ -130,6 +131,10 @@ func (s3 S3) buildCredential(nowTime time.Time) string { return fmt.Sprintf("%s/%s/%s/%s/%s", s3.AccessKey, nowTime.UTC().Format(shortTimeFormat), s3.Region, serviceName, "aws4_request") } +func (s3 S3) buildCredentialWithoutKey(nowTime time.Time) string { + return fmt.Sprintf("%s/%s/%s/%s", nowTime.UTC().Format(shortTimeFormat), s3.Region, serviceName, "aws4_request") +} + func buildSignature(nowTime time.Time, secretAccessKey string, regionName string, serviceName string) []byte { shortTime := nowTime.UTC().Format(shortTimeFormat) diff --git a/presigned.go b/presigned.go new file mode 100644 index 0000000..0406864 --- /dev/null +++ b/presigned.go @@ -0,0 +1,162 @@ +package simples3 + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "net/url" + "sort" + "strconv" + "strings" + "time" +) + +const ( + defaultPresignedURLFormat = "%s.s3.amazonaws.com" // +) + +type PresignedInput struct { + Bucket string + ObjectKey string + Method string + Timestamp time.Time + ExtraHeaders map[string]string + ExpirySeconds int +} + +func (s3 *S3) GeneratePresignedURL(in PresignedInput) string { + var ( + nowTime = in.Timestamp.UTC() + cred = s3.buildCredential(nowTime) + amzdate = nowTime.Format(amzDateISO8601TimeFormat) + expiry = strconv.Itoa(in.ExpirySeconds) + ) + + // Add host to Headers + signedHeaders := in.ExtraHeaders + if signedHeaders == nil { + signedHeaders = map[string]string{} + } + signedHeaders["host"] = fmt.Sprintf(defaultPresignedURLFormat, in.Bucket) + + // Start Canonical Request Formation + h := sha256.New() + fmt.Fprintf(h, "%s\n", in.Method) // HTTP Verb + fmt.Fprintf(h, "/%s\n", in.ObjectKey) // CanonicalURL + + // Start QueryString Params (before SignedHeaders) + queryString := map[string]string{ + "X-Amz-Algorithm": algorithm, + "X-Amz-Credential": cred, + "X-Amz-Date": amzdate, + "X-Amz-Expires": expiry, + } + + // We need to have a sorted order, + keysQS := make([]string, 0, len(queryString)) + for name := range queryString { + keysQS = append(keysQS, name) + } + sort.Strings(keysQS) //sort by key + for _, k := range keysQS { + fmt.Fprintf(h, "%s=%s&", url.QueryEscape(k), url.QueryEscape(queryString[k])) // HTTP Verb + } + + fmt.Fprintf(h, "X-Amz-SignedHeaders=") + // Add Signed Headers to Query String + first := true + for k := range signedHeaders { + if first { + fmt.Fprintf(h, "%s", url.QueryEscape(k)) + first = false + } else { + fmt.Fprintf(h, "%s", url.QueryEscape(";"+k)) + } + } + fmt.Fprintf(h, "\n") + // End QueryString Params + + // Start Canonical Headers + for k, v := range signedHeaders { + fmt.Fprintf(h, "%s:%s\n", strings.ToLower(k), strings.TrimSpace(v)) + } + fmt.Fprintf(h, "\n") + // End Canonical Headers + + // Start Signed Headers + first = true + for k := range signedHeaders { + if first { + fmt.Fprintf(h, "%s", url.QueryEscape(k)) + first = false + } else { + fmt.Fprintf(h, "%s", url.QueryEscape(";"+k)) + } + } + fmt.Fprintf(h, "\n") + // End Canonical Headers + + // Mention Unsigned Payload + fmt.Fprintf(h, "UNSIGNED-PAYLOAD") + + // canonicalReq := h.Bytes() + // Reset Buffer to create StringToSign + var b bytes.Buffer + + // Start StringToSign + fmt.Fprintf(&b, "%s\n", algorithm) // Algo + fmt.Fprintf(&b, "%s\n", amzdate) // Date + fmt.Fprintf(&b, "%s\n", s3.buildCredentialWithoutKey(nowTime)) //Cred + + hashed := hex.EncodeToString(h.Sum(nil)) + fmt.Fprintf(&b, "%s", hashed) + + stringToSign := b.Bytes() + + // End StringToSign + + // Start Signature Key + sigKey := makeHMac(makeHMac( + makeHMac( + makeHMac( + []byte("AWS4"+s3.SecretKey), + []byte(nowTime.UTC().Format(shortTimeFormat))), + []byte(s3.Region)), + []byte("s3")), + []byte("aws4_request"), + ) + // sigKey gen verified using https://docs.aws.amazon.com/general/latest/gr/signature-v4-examples.html#signature-v4-examples-other + // (TODO: add a test using the same, consolidate with signKeys()) + + signedStrToSign := makeHMac(sigKey, stringToSign) + signature := hex.EncodeToString(signedStrToSign) + // End Signature + + // Reset Buffer to create URl + b.Reset() + + // Start Generating URL + fmt.Fprintf(&b, "http://"+defaultPresignedURLFormat+"/", in.Bucket) + fmt.Fprintf(&b, "%s?", in.ObjectKey) + + // We don't need to have a sorted order here, + // but just to preserve tests. + for _, k := range keysQS { + fmt.Fprintf(&b, "%s=%s&", url.QueryEscape(k), url.QueryEscape(queryString[k])) // HTTP Verb + } + fmt.Fprintf(&b, "%s=", "X-Amz-SignedHeaders") + first = true + for k := range signedHeaders { + if first { + fmt.Fprintf(&b, "%s", url.QueryEscape(k)) + first = false + } else { + fmt.Fprintf(&b, "%s", url.QueryEscape(";"+k)) + } + } + fmt.Fprintf(&b, "&X-Amz-Signature=%s", signature) + resultURL := b.String() + + return resultURL +} diff --git a/presigned_test.go b/presigned_test.go new file mode 100644 index 0000000..dd04c2c --- /dev/null +++ b/presigned_test.go @@ -0,0 +1,70 @@ +package simples3 + +import ( + "os" + "testing" + "time" +) + +func TestS3_GeneratePresignedURL(t *testing.T) { + // Params based on + // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html + var time, _ = time.Parse(time.RFC1123, "Fri, 24 May 2013 00:00:00 GMT") + t.Run("Test", func(t *testing.T) { + s := New( + "us-east-1", + "AKIAIOSFODNN7EXAMPLE", + "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + ) + want := "http://examplebucket.s3.amazonaws.com/test.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIOSFODNN7EXAMPLE%2F20130524%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20130524T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=aeeed9bbccd4d02ee5c0109b86d86835f995330da4c265957d157751f604d404" + if got := s.GeneratePresignedURL(PresignedInput{ + Bucket: "examplebucket", + ObjectKey: "test.txt", + Method: "GET", + Timestamp: time, + ExpirySeconds: 86400, + }); got != want { + t.Errorf("S3.GeneratePresignedURL() = %v, want %v", got, want) + } + }) +} + +func TestS3_GeneratePresignedURL_Personal(t *testing.T) { + t.Run("Test", func(t *testing.T) { + s := New( + os.Getenv("AWS_S3_REGION"), + os.Getenv("AWS_S3_ACCESS_KEY"), + os.Getenv("AWS_S3_SECRET_KEY"), + ) + dontwant := "" + if got := s.GeneratePresignedURL(PresignedInput{ + Bucket: os.Getenv("AWS_S3_BUCKET"), + ObjectKey: "test1.txt", + Method: "GET", + Timestamp: NowTime(), + ExpirySeconds: 3600, + }); got == dontwant { + t.Errorf("S3.GeneratePresignedURL() = %v, dontwant %v", got, dontwant) + } + }) +} + +func TestS3_GeneratePresignedURL_POST(t *testing.T) { + t.Run("Test", func(t *testing.T) { + s := New( + os.Getenv("AWS_S3_REGION"), + os.Getenv("AWS_S3_ACCESS_KEY"), + os.Getenv("AWS_S3_SECRET_KEY"), + ) + dontwant := "" + if got := s.GeneratePresignedURL(PresignedInput{ + Bucket: os.Getenv("AWS_S3_BUCKET"), + ObjectKey: "test2.txt", + Method: "PUT", + Timestamp: NowTime(), + ExpirySeconds: 3600, + }); got == dontwant { + t.Errorf("S3.GeneratePresignedURL() = %v, dontwant %v", got, dontwant) + } + }) +} diff --git a/sign.go b/sign.go index e5faf34..9e88e31 100644 --- a/sign.go +++ b/sign.go @@ -45,7 +45,7 @@ func (s3 *S3) writeRequest(w io.Writer, r *http.Request) { } func (s3 *S3) writeStringToSign(w io.Writer, t time.Time, r *http.Request) { - w.Write([]byte("AWS4-HMAC-SHA256")) + w.Write([]byte(algorithm)) w.Write(newLine) w.Write([]byte(t.Format(amzDateISO8601TimeFormat))) w.Write(newLine) diff --git a/simples3.go b/simples3.go index ff025d0..a19e7c9 100644 --- a/simples3.go +++ b/simples3.go @@ -170,7 +170,7 @@ func (s3 *S3) FileDelete(u DeleteInput) error { s3.writeStringToSign(h, t, req) - auth := bytes.NewBufferString("AWS4-HMAC-SHA256 ") + auth := bytes.NewBufferString(algorithm) auth.Write([]byte("Credential=" + s3.AccessKey + "/" + s3.creds(t))) auth.Write([]byte{',', ' '}) auth.Write([]byte("SignedHeaders=")) diff --git a/simples3_test.go b/simples3_test.go index 8d362de..8cbcc38 100644 --- a/simples3_test.go +++ b/simples3_test.go @@ -35,7 +35,7 @@ func TestS3_FileUpload(t *testing.T) { }, args: args{ UploadInput{ - Bucket: "zerodha-testbucket", + Bucket: os.Getenv("AWS_S3_BUCKET"), ObjectKey: "test.txt", ContentType: "text/plain", FileName: "test.txt", @@ -79,7 +79,7 @@ func TestS3_FileDelete(t *testing.T) { }, args: args{ DeleteInput{ - Bucket: "zerodha-testbucket", + Bucket: os.Getenv("AWS_S3_BUCKET"), ObjectKey: "test.txt", }, },