-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathward.go
More file actions
336 lines (277 loc) · 7.2 KB
/
ward.go
File metadata and controls
336 lines (277 loc) · 7.2 KB
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
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
package warded
import (
"bytes"
"crypto/rand"
"fmt"
"io/ioutil"
"math/big"
"os"
"path"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/bmatcuk/doublestar"
)
// Ward holds data needed to work with a ward.
type Ward struct {
Config WardConfig
Dir string
key []byte
}
// NewWard creates a Ward.
func NewWard() Ward {
return Ward{
Config: DefaultWardConfig(),
}
}
// SearchResult contains information about a matched search
type SearchResult struct {
Passphrase string
Line []byte
LineNum int
IndexStart int
IndexEnd int
}
// Statistics holds statistics about the entire ward
type Statistics struct {
Groups []Group `json:"groups"`
Count int `json:"count"`
SumLength int `json:"sum"`
MaxLength int `json:"max"`
}
// A Group holds the names and some statistics about a group of common passphrases
type Group struct {
Length int `json:"len"`
Passphrases []string `json:"pass"`
}
func (w *Ward) SetKey(key []byte) {
w.key = key
}
// Edit sets the entire content of the warded passphrase.
func (w Ward) Edit(passName string, content []byte) (err error) {
var pass *Passphrase
if pass, err = w.newPassphrase(content); err == nil {
pass.Filename = w.Path(passName)
err = pass.Write(0600)
}
return
}
// Get returns the decrypted passphrase content.
func (w Ward) Get(passName string) ([]byte, error) {
warded, err := ReadPassphrase(w.Path(passName))
if err != nil {
return nil, err
}
return warded.Decrypt(w.key)
}
// GetOrCheck returns the decrypted passphrase content.
// If Get throws an error, the Ward's key is checked
// against a random passphrase in the Ward.
func (w Ward) GetOrCheck(passName string) ([]byte, error) {
pass, err := w.Get(passName)
if err != nil {
err = w.checkKey()
}
return pass, err
}
// List returns a list of passphrase names in the ward
func (w Ward) List(pathPattern string) ([]string, error) {
passphrases := make([]string, 0)
e := w.walkPathPattern(pathPattern, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
var rel string
if !info.IsDir() {
if rel, err = filepath.Rel(w.Dir, p); err != nil {
return err
}
passphrases = append(passphrases, rel)
}
return nil
})
return passphrases, e
}
// Map returns a map of passphrase names to the warded passphrase.
func (w Ward) Map(pathPattern string) (map[string]*Passphrase, error) {
passphrases := make(map[string]*Passphrase)
e := w.walkPathPattern(pathPattern, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
var rel string
var pass *Passphrase
if !info.IsDir() {
if rel, err = filepath.Rel(w.Dir, p); err != nil {
return err
}
if pass, err = ReadPassphrase(w.Path(rel)); err != nil {
return err
}
passphrases[rel] = pass
}
return nil
})
return passphrases, e
}
// Path returns the path to a passphrase.
// Generated by joining the ward directory with the cleaned passphrase name
func (w Ward) Path(passName string) string {
clean := strings.TrimLeft(filepath.Clean(passName), "."+string(filepath.Separator))
return path.Join(w.Dir, clean)
}
// Rekey changes the master key for the entire ward.
// Any errors will cancel the operation, leaving the ward with the existing key.
func (w Ward) Rekey(newMasterKey []byte, tempDir string) error {
passphrases, err := w.Map("")
if err != nil {
return err
}
tmpDir, err := ioutil.TempDir(tempDir, "rekey")
if err != nil {
return err
}
defer os.RemoveAll(tmpDir)
newWard := NewWard()
newWard.SetKey(newMasterKey)
newWard.Config = w.Config
newWard.Dir = tmpDir
var plaintext []byte
for passName, warded := range passphrases {
if plaintext, err = warded.Decrypt(w.key); err != nil {
return fmt.Errorf("Invalid master key for %s", passName)
}
if err = newWard.Edit(passName, plaintext); err != nil {
return err
}
}
if err = os.RemoveAll(w.Dir); err == nil {
err = os.Rename(tmpDir, w.Dir)
}
return err
}
// Search searches through a ward, printing lines
// that match the given regular expression.
func (w Ward) Search(path string, regex *regexp.Regexp) ([]SearchResult, error) {
var err error
var passphrases map[string]*Passphrase
if passphrases, err = w.Map(path); err != nil {
return nil, err
}
var pass []byte
var results []SearchResult
for passName, warded := range passphrases {
if pass, err = warded.Decrypt(w.key); err != nil {
return nil, err
}
for lineNum, line := range bytes.Split(pass, []byte("\n")) {
for _, match := range regex.FindAllIndex(line, -1) {
results = append(results, SearchResult{
Passphrase: passName,
Line: line,
LineNum: lineNum,
IndexStart: match[0],
IndexEnd: match[1],
})
}
}
}
return results, nil
}
// Stats returns statistics for the current ward.
func (w Ward) Stats(path string) (*Statistics, error) {
passphrases, err := w.Map(path)
if err != nil {
return nil, err
}
groupMap := make(map[string][]string)
sumLen := 0
maxLen := 0
for name, pass := range passphrases {
plaintext, err := pass.Decrypt(w.key)
if err != nil {
return nil, err
}
lines := bytes.SplitN(plaintext, []byte("\n"), 2)
first := string(lines[0])
passLen := len(first)
groupMap[first] = append(groupMap[first], name)
if passLen > maxLen {
maxLen = passLen
}
sumLen += passLen
}
groups := make([]Group, len(groupMap))
ind := 0
for key, val := range groupMap {
groups[ind] = Group{
Passphrases: val,
Length: len(key),
}
ind++
}
sort.Slice(groups, func(i, j int) bool { return groups[i].Length < groups[j].Length })
return &Statistics{
Groups: groups,
Count: len(passphrases),
MaxLength: maxLen,
SumLength: sumLen,
}, nil
}
// Update replaces the first line of a passphrase with the given string.
func (w Ward) Update(passName string, passStr []byte) ([]byte, error) {
pass, err := w.GetOrCheck(passName)
if err != nil {
return nil, err
}
newPass := passStr
split := bytes.SplitN(pass, []byte("\n"), 2)
if len(split) != 1 {
newPass = append(newPass, '\n')
newPass = append(newPass, split[1]...)
}
if err = w.Edit(passName, newPass); err != nil {
return nil, err
}
return split[0], nil
}
// checkKey attempts to decrypt a random passphrase in the ward
func (w Ward) checkKey() (err error) {
var passphrases []string
if passphrases, err = w.List(""); err != nil {
return
}
plen := int64(len(passphrases))
if plen == 0 {
// there were no existing passphrases in the ward
// this isn't considered an error
return
}
var rind *big.Int
if rind, err = rand.Int(rand.Reader, big.NewInt(plen)); err != nil {
return
}
var pass *Passphrase
if pass, err = ReadPassphrase(w.Path(passphrases[rind.Int64()])); err != nil {
return
}
// check that the provided master key can decrypt the random passphrase
if _, err = pass.Decrypt(w.key); err != nil {
err = fmt.Errorf("Only one master key is allowed per ward")
}
return
}
func (w Ward) walkPathPattern(pathPattern string, walkFn filepath.WalkFunc) error {
var err error
var paths []string
if paths, err = doublestar.Glob(w.Path(pathPattern)); err == nil {
for _, path := range paths {
err = filepath.Walk(path, walkFn)
if err != nil {
return err
}
}
}
return err
}