-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
coreKV.go
170 lines (144 loc) · 4.73 KB
/
coreKV.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
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package database
import (
"bytes"
"crypto/sha512"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/pkg/errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"github.com/Luzifer/go_helpers/v2/backoff"
"github.com/Luzifer/twitch-bot/v3/internal/helpers"
)
const (
encryptionValidationKey = "encryption-validation"
encryptionValidationMinBackoff = 500 * time.Millisecond
encryptionValidationTries = 5
)
type (
coreKV struct {
Name string `gorm:"primaryKey"`
Value string
}
)
// DeleteCoreMeta removes a core_kv table entry
func (c connector) DeleteCoreMeta(key string) error {
return errors.Wrap(
helpers.RetryTransaction(c.db, func(tx *gorm.DB) error {
return tx.Delete(&coreKV{}, "name = ?", key).Error
}),
"deleting key from database",
)
}
// ReadCoreMeta reads an entry of the core_kv table specified by
// the given `key` and unmarshals it into the `value`. The value must
// be a valid variable to `json.NewDecoder(...).Decode(value)`
// (pointer to struct, string, int, ...). In case the key does not
// exist a check to 'errors.Is(err, sql.ErrNoRows)' will succeed
func (c connector) ReadCoreMeta(key string, value any) error {
return c.readCoreMeta(key, value, nil)
}
// StoreCoreMeta stores an entry to the core_kv table soecified by
// the given `key`. The value given must be a valid variable to
// `json.NewEncoder(...).Encode(value)`.
func (c connector) StoreCoreMeta(key string, value any) error {
return c.storeCoreMeta(key, value, nil)
}
// ReadEncryptedCoreMeta works like ReadCoreMeta but decrypts the
// stored value before unmarshalling it
func (c connector) ReadEncryptedCoreMeta(key string, value any) error {
return c.readCoreMeta(key, value, c.DecryptField)
}
// ResetEncryptedCoreMeta removes all CoreKV entries from the database
func (c connector) ResetEncryptedCoreMeta() error {
return errors.Wrap(
helpers.RetryTransaction(c.db, func(tx *gorm.DB) error {
return tx.Delete(&coreKV{}, "value LIKE ?", "U2FsdGVkX1%").Error
}),
"removing encrypted meta entries",
)
}
// StoreEncryptedCoreMeta works like StoreCoreMeta but encrypts the
// marshalled value before storing it
func (c connector) StoreEncryptedCoreMeta(key string, value any) error {
return c.storeCoreMeta(key, value, c.EncryptField)
}
func (c connector) ValidateEncryption() error {
validationHasher := sha512.New()
fmt.Fprint(validationHasher, c.encryptionSecret)
var (
storedHash string
validationHash = fmt.Sprintf("%x", validationHasher.Sum(nil))
)
err := backoff.NewBackoff().
WithMaxIterations(encryptionValidationTries).
WithMinIterationTime(encryptionValidationMinBackoff).
Retry(func() error {
return c.ReadEncryptedCoreMeta(encryptionValidationKey, &storedHash)
})
switch {
case err == nil:
if storedHash != validationHash {
// Shouldn't happen: When decryption is possible it should match
return errors.New("mismatch between expected and stored hash")
}
return nil
case errors.Is(err, ErrCoreMetaNotFound):
return errors.Wrap(
c.StoreEncryptedCoreMeta(encryptionValidationKey, validationHash),
"initializing encryption validation",
)
default:
return errors.Wrap(err, "reading encryption-validation")
}
}
//revive:disable-next-line:confusing-naming
func (c connector) readCoreMeta(key string, value any, processor func(string) (string, error)) (err error) {
var data coreKV
if err = helpers.Retry(func() error {
err = c.db.First(&data, "name = ?", key).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return ErrCoreMetaNotFound
}
return errors.Wrap(err, "querying core meta table")
}); err != nil {
return err
}
if data.Value == "" {
return errors.New("empty value returned")
}
if processor != nil {
if data.Value, err = processor(data.Value); err != nil {
return errors.Wrap(err, "processing stored value")
}
}
if err := json.NewDecoder(strings.NewReader(data.Value)).Decode(value); err != nil {
return errors.Wrap(err, "JSON decoding value")
}
return nil
}
//revive:disable-next-line:confusing-naming
func (c connector) storeCoreMeta(key string, value any, processor func(string) (string, error)) (err error) {
buf := new(bytes.Buffer)
if err := json.NewEncoder(buf).Encode(value); err != nil {
return errors.Wrap(err, "JSON encoding value")
}
encValue := strings.TrimSpace(buf.String())
if processor != nil {
if encValue, err = processor(encValue); err != nil {
return errors.Wrap(err, "processing value to store")
}
}
data := coreKV{Name: key, Value: encValue}
return errors.Wrap(
helpers.RetryTransaction(c.db, func(tx *gorm.DB) error {
return tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "name"}},
DoUpdates: clause.AssignmentColumns([]string{"value"}),
}).Create(data).Error
}),
"upserting core meta value",
)
}