File: policy.go

package info (click to toggle)
golang-github-rhnvrm-simples3 0.6.1-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 148 kB
  • sloc: makefile: 2
file content (170 lines) | stat: -rw-r--r-- 4,858 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// 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:05Z07: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().UTC()
}

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.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.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.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)
}