/
secrets.go
153 lines (135 loc) · 4 KB
/
secrets.go
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
package secrets
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"hash/crc32"
"io"
"strconv"
_ "github.com/matheuscscp/splitwiser/logging"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
"github.com/sirupsen/logrus"
secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
)
type (
// Service ...
Service interface {
Read(ctx context.Context, id string) ([]byte, error)
Rotate(ctx context.Context, id string) error
Close()
}
service struct {
client *secretmanager.Client
}
)
var (
// ErrNilSecretPayload ...
ErrNilSecretPayload = errors.New("nil secret payload")
// ErrNilSecretChecksum ...
ErrNilSecretChecksum = errors.New("nil secret checksum")
// ErrNilSecretLabels ...
ErrNilSecretLabels = errors.New("nil secret labels")
// ErrSecretNumBytesMissing ...
ErrSecretNumBytesMissing = errors.New("secret label 'num-bytes' is not present")
)
// NewService ...
func NewService(ctx context.Context) (Service, error) {
client, err := secretmanager.NewClient(ctx)
if err != nil {
return nil, fmt.Errorf("error creating secret manager client: %w", err)
}
return &service{client}, nil
}
func (s *service) Close() {
s.client.Close()
}
func (s *service) Read(ctx context.Context, id string) ([]byte, error) {
resp, err := s.client.AccessSecretVersion(ctx, &secretmanagerpb.AccessSecretVersionRequest{
Name: fmt.Sprintf("%s/versions/latest", id),
})
if err != nil {
return nil, fmt.Errorf("error accessing secret version: %w", err)
}
payload := resp.GetPayload()
if payload == nil {
return nil, ErrNilSecretPayload
}
if payload.DataCrc32C == nil {
return nil, ErrNilSecretChecksum
}
want := payload.GetDataCrc32C()
got := int64(crc32.Checksum(payload.Data, crc32.MakeTable(crc32.Castagnoli)))
if want != got {
return nil, fmt.Errorf("secret checksum mismatch, want %v, got %v", want, got)
}
secret := payload.GetData()
b, err := base64.StdEncoding.DecodeString(string(secret))
if err != nil {
return nil, fmt.Errorf("error decoding binary secret from base64: %w", err)
}
return b, nil
}
func (s *service) Rotate(ctx context.Context, id string) error {
// fetch number of bytes from secret labels
secret, err := s.client.GetSecret(ctx, &secretmanagerpb.GetSecretRequest{
Name: id,
})
if err != nil {
return fmt.Errorf("error getting secret '%s': %w", id, err)
}
labels := secret.GetLabels()
if labels == nil {
return ErrNilSecretLabels
}
secretNumBytes, ok := labels["num-bytes"]
if !ok {
return ErrSecretNumBytesMissing
}
numBytes, err := strconv.Atoi(secretNumBytes)
if err != nil {
return fmt.Errorf("error parsing 'num-bytes' label for secret '%s': %w", id, err)
}
// generate random secret
buf := make([]byte, numBytes)
n, err := io.ReadFull(rand.Reader, buf)
if err != nil {
return fmt.Errorf("error reading %d bytes from crypto/rand: %w", numBytes, err)
}
if n != numBytes {
return fmt.Errorf("unexpected number of bytes read from crypto/rand, want %d, got %d", numBytes, n)
}
payload := []byte(base64.StdEncoding.EncodeToString(buf))
checksum := int64(crc32.Checksum(payload, crc32.MakeTable(crc32.Castagnoli)))
// find previous version
latest, err := s.client.GetSecretVersion(ctx, &secretmanagerpb.GetSecretVersionRequest{
Name: fmt.Sprintf("%s/versions/latest", id),
})
if err != nil {
logrus.Warnf("error fetching latest version of secret '%s': %v", id, err)
latest = nil
}
// add version
newVersion, err := s.client.AddSecretVersion(ctx, &secretmanagerpb.AddSecretVersionRequest{
Parent: id,
Payload: &secretmanagerpb.SecretPayload{
Data: payload,
DataCrc32C: &checksum,
},
})
if err != nil {
return fmt.Errorf("error adding secret version: %w", err)
}
// destroy previous version
if latest != nil {
_, err = s.client.DestroySecretVersion(ctx, &secretmanagerpb.DestroySecretVersionRequest{
Name: latest.Name,
})
if err != nil {
return fmt.Errorf("error destroying previous secret version: %w", err)
}
}
logrus.Infof("secret rotated: %s", newVersion.Name)
return nil
}