171 lines
4.8 KiB
Go
171 lines
4.8 KiB
Go
// LICENSE MIT
|
|
// Copyright (c) 2018, Rohan Verma <hello@rohanverma.net>
|
|
// Copyright (c) 2017, L Campbell
|
|
// forked from: https://github.com/lye/s3/
|
|
// For previous license information visit
|
|
// https://github.com/lye/s3/blob/master/LICENSE
|
|
|
|
package simples3
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// UploadConfig generate policies from config
|
|
// for POST requests to S3 using Signing V4.
|
|
type UploadConfig struct {
|
|
// Required
|
|
BucketName string
|
|
ObjectKey string
|
|
ContentType string
|
|
FileSize int64
|
|
// Optional
|
|
UploadURL string
|
|
Expiration time.Duration
|
|
MetaData map[string]string
|
|
}
|
|
|
|
// UploadPolicies Amazon s3 upload policies
|
|
type UploadPolicies struct {
|
|
URL string
|
|
Form map[string]string
|
|
}
|
|
|
|
// PolicyJSON is policy rule
|
|
type PolicyJSON struct {
|
|
Expiration string `json:"expiration"`
|
|
Conditions []interface{} `json:"conditions"`
|
|
}
|
|
|
|
const (
|
|
expirationTimeFormat = "2006-01-02T15:04:05ZZ07:00"
|
|
amzDateISO8601TimeFormat = "20060102T150405Z"
|
|
shortTimeFormat = "20060102"
|
|
algorithm = "AWS4-HMAC-SHA256"
|
|
serviceName = "s3"
|
|
|
|
defaultUploadURLFormat = "http://%s.s3.amazonaws.com/" // <bucketName>
|
|
defaultExpirationHour = 1 * time.Hour
|
|
)
|
|
|
|
// NowTime mockable time.Now()
|
|
var NowTime = func() time.Time {
|
|
return time.Now()
|
|
}
|
|
|
|
var newLine = []byte{'\n'}
|
|
|
|
// CreateUploadPolicies creates amazon s3 sigv4 compatible
|
|
// policy and signing keys with the signature returns the upload policy.
|
|
// https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/sigv4-authentication-HTTPPOST.html
|
|
func (s3 *S3) CreateUploadPolicies(uploadConfig UploadConfig) (UploadPolicies, error) {
|
|
nowTime := NowTime()
|
|
credential := string(s3.buildCredential(nowTime))
|
|
data, err := buildUploadSign(nowTime, credential, uploadConfig)
|
|
if err != nil {
|
|
return UploadPolicies{}, err
|
|
}
|
|
// 1. StringToSign
|
|
policy := base64.StdEncoding.EncodeToString(data)
|
|
// 2. Signing Key
|
|
hash := hmac.New(sha256.New, buildSignature(nowTime, s3.SecretKey, s3.Region, serviceName))
|
|
hash.Write([]byte(policy))
|
|
// 3. Signature
|
|
signature := hex.EncodeToString(hash.Sum(nil))
|
|
|
|
uploadURL := uploadConfig.UploadURL
|
|
if uploadURL == "" {
|
|
uploadURL = fmt.Sprintf(defaultUploadURLFormat, uploadConfig.BucketName)
|
|
}
|
|
form := map[string]string{
|
|
"key": uploadConfig.ObjectKey,
|
|
"Content-Type": uploadConfig.ContentType,
|
|
"X-Amz-Credential": credential,
|
|
"X-Amz-Algorithm": algorithm,
|
|
"X-Amz-Date": nowTime.Format(amzDateISO8601TimeFormat),
|
|
"Policy": policy,
|
|
"X-Amz-Signature": signature,
|
|
}
|
|
for k, v := range uploadConfig.MetaData {
|
|
form[k] = v
|
|
}
|
|
return UploadPolicies{
|
|
URL: uploadURL,
|
|
Form: form,
|
|
}, nil
|
|
}
|
|
|
|
func buildUploadSign(nowTime time.Time, credential string, uploadConfig UploadConfig) ([]byte, error) {
|
|
conditions := []interface{}{
|
|
map[string]string{"bucket": uploadConfig.BucketName},
|
|
map[string]string{"key": uploadConfig.ObjectKey},
|
|
map[string]string{"Content-Type": uploadConfig.ContentType},
|
|
[]interface{}{"content-length-range", uploadConfig.FileSize, uploadConfig.FileSize},
|
|
map[string]string{"x-amz-credential": credential},
|
|
map[string]string{"x-amz-algorithm": algorithm},
|
|
map[string]string{"x-amz-date": nowTime.Format(amzDateISO8601TimeFormat)},
|
|
}
|
|
for k, v := range uploadConfig.MetaData {
|
|
conditions = append(conditions, map[string]string{k: v})
|
|
}
|
|
|
|
expiration := defaultExpirationHour
|
|
if uploadConfig.Expiration > 0 {
|
|
expiration = uploadConfig.Expiration
|
|
}
|
|
|
|
return json.Marshal(&PolicyJSON{
|
|
Expiration: nowTime.Add(expiration).Format(expirationTimeFormat),
|
|
Conditions: conditions,
|
|
})
|
|
}
|
|
|
|
func (s3 S3) buildCredential(nowTime time.Time) []byte {
|
|
var b bytes.Buffer
|
|
b.WriteString(s3.AccessKey)
|
|
b.WriteRune('/')
|
|
b.WriteString(nowTime.UTC().Format(shortTimeFormat))
|
|
b.WriteRune('/')
|
|
b.WriteString(s3.Region)
|
|
b.WriteRune('/')
|
|
b.WriteString(serviceName)
|
|
b.WriteRune('/')
|
|
b.WriteString("aws4_request")
|
|
return b.Bytes()
|
|
}
|
|
|
|
func (s3 S3) buildCredentialWithoutKey(nowTime time.Time) []byte {
|
|
var b bytes.Buffer
|
|
b.WriteString(nowTime.UTC().Format(shortTimeFormat))
|
|
b.WriteRune('/')
|
|
b.WriteString(s3.Region)
|
|
b.WriteRune('/')
|
|
b.WriteString(serviceName)
|
|
b.WriteRune('/')
|
|
b.WriteString("aws4_request")
|
|
return b.Bytes()
|
|
}
|
|
|
|
func buildSignature(nowTime time.Time, secretAccessKey string, regionName string, serviceName string) []byte {
|
|
shortTime := nowTime.UTC().Format(shortTimeFormat)
|
|
|
|
date := makeHMac([]byte("AWS4"+secretAccessKey), []byte(shortTime))
|
|
region := makeHMac(date, []byte(regionName))
|
|
service := makeHMac(region, []byte(serviceName))
|
|
credentials := makeHMac(service, []byte("aws4_request"))
|
|
return credentials
|
|
}
|
|
|
|
func makeHMac(key []byte, data []byte) []byte {
|
|
hash := hmac.New(sha256.New, key)
|
|
hash.Write(data)
|
|
return hash.Sum(nil)
|
|
}
|