-
Notifications
You must be signed in to change notification settings - Fork 19
/
collection.go
248 lines (201 loc) · 5.87 KB
/
collection.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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
package totp
import (
"encoding/json"
"errors"
"io"
"os"
"strings"
"time"
"github.com/pquerna/otp/totp"
)
var ErrSecretNotFound = errors.New("secret not found")
var ErrNoFilename = errors.New("no save target")
var ErrSecretNameEmpty = errors.New("secret name empty")
var ErrSecretValueEmpty = errors.New("secret value empty")
// Secret is a struct containing data necessary for working with secrets,
// namely, the name of the secret name and the secret value
type Secret struct {
// DateAdded is the date a secret was added to the collection
DateAdded time.Time
// DateModified is the date a secret was last modified
DateModified time.Time
// Name is the name of the secret used for retrieval
Name string
// Value is the secret (seed) value
Value string
}
// Collection is a struct that holds TOTP data
type Collection struct {
// Secrets is a map of secrets using the secret name as the key
Secrets map[string]Secret
filename string
writer io.Writer
}
// CollectionInterface is used for DI when needed
type CollectionInterface interface {
DeleteSecret(string) (Secret, error)
GetSecret(string) (Secret, error)
GetSecrets() []Secret
Save() error
SetFilename(string) string
UpdateSecret(string, string) (Secret, error)
}
// Save serializes (marshals) the Collections struct and writes it to
// a file
func (c *Collection) Save() error {
serializedSettings, err := c.Serialize()
if err != nil {
return err
}
if c.writer != nil {
_, err = c.writer.Write(serializedSettings)
} else if len(c.filename) != 0 {
err = os.WriteFile(c.filename, serializedSettings, 0600)
} else {
err = ErrNoFilename
}
return err
}
// DeleteSecret deletes an entry by name
func (c *Collection) DeleteSecret(name string) (Secret, error) {
retSecret, ok := c.Secrets[name]
if !ok {
return Secret{}, ErrSecretNotFound
}
delete(c.Secrets, name)
return retSecret, nil
}
// UpdateSecret updates (if it exists) or adds a new entry with the
// name and value given
func (c *Collection) UpdateSecret(name, value string) (Secret, error) {
if len(name) == 0 {
return Secret{}, ErrSecretNameEmpty
}
if len(value) == 0 {
return Secret{}, ErrSecretValueEmpty
}
value = strings.ToUpper(value)
_, err := totp.GenerateCode(value, time.Now())
if err != nil {
return Secret{}, err
}
retSecret, ok := c.Secrets[name]
if ok {
// entry indicates an update
retSecret.Value = value
retSecret.DateModified = time.Now()
c.Secrets[name] = retSecret
} else {
// no entry indicates an add
dateAdded := time.Now()
retSecret = Secret{
Name: name,
Value: value,
DateAdded: dateAdded,
DateModified: dateAdded,
}
c.Secrets[name] = retSecret
}
return retSecret, err
}
// RenameSecret renames a secret
func (c *Collection) RenameSecret(oldName, newName string) (Secret, error) {
if len(newName) == 0 {
return Secret{}, ErrSecretNameEmpty
}
retSecret, ok := c.Secrets[oldName]
if !ok {
return Secret{}, ErrSecretNotFound
}
retSecret.Name = newName
retSecret.DateModified = time.Now()
c.Secrets[newName] = retSecret
delete(c.Secrets, oldName)
return retSecret, nil
}
// GetSecret returns a secret with the name argument
func (c *Collection) GetSecret(name string) (Secret, error) {
retSecret, ok := c.Secrets[name]
if !ok {
return Secret{}, ErrSecretNotFound
}
return retSecret, nil
}
// GetSecrets returns a slice containing all the secrets
func (c *Collection) GetSecrets() []Secret {
secrets := []Secret{}
for _, secret := range c.Secrets {
secrets = append(secrets, secret)
}
return secrets
}
// GenerateCodeWithTime creates a TOTP code with the named secret's value
func (c *Collection) GenerateCodeWithTime(name string, time time.Time) (string, error) {
secret, err := c.GetSecret(name)
if err != nil {
return "", err
}
return totp.GenerateCode(secret.Value, time)
}
// GenerateCode creates a TOTP code with the named secret's value
func (c *Collection) GenerateCode(name string) (string, error) {
return c.GenerateCodeWithTime(name, time.Now())
}
// Serialize marshals the Collection struct into a byte array
func (c *Collection) Serialize() ([]byte, error) {
return json.MarshalIndent(c, "", " ")
}
// Deserialize unmarshals a byte array into a Collection struct
func (c *Collection) Deserialize(data []byte) error {
return json.Unmarshal(data, &c)
}
// SetWriter sets the writer for the Save method
func (c *Collection) SetWriter(writer io.Writer) {
c.writer = writer
}
// SetFilename sets the filename for the Save method
func (c *Collection) SetFilename(filename string) string {
c.filename = filename
return c.filename
}
// NewCollection creates a new, blank Collection instance
func NewCollection() *Collection {
c := new(Collection)
c.Secrets = make(map[string]Secret)
return c
}
// NewCollectionWithData creates a new Collection instance with data from a byte slice
func NewCollectionWithData(data []byte) (*Collection, error) {
c := NewCollection()
return c, c.Deserialize(data)
}
// NewCollectionWithReader creates a new collection from a Reader interface
func NewCollectionWithReader(reader io.Reader) (*Collection, error) {
data, err := io.ReadAll(reader)
if err != nil {
return NewCollection(), err
}
return NewCollectionWithData(data)
}
// NewCollectionWithFile creates a new Collection instance with data from a file.
// If the file open fails, a new Collection instance is returned along with the
// file open error, which guarantees a usable but empty collection is returned.
//
// Returning data to be used along with an error is bad design but changing this
// would be a breaking API change.
func NewCollectionWithFile(filename string) (c *Collection, err error) {
f, err := os.Open(filename)
if err != nil {
c := NewCollection()
c.filename = filename
return c, err
}
defer func() {
if e := f.Close(); e != nil && err == nil {
err = e
}
}()
c, err = NewCollectionWithReader(f)
c.filename = filename
return c, err
}