-
-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathpasswordcheck.go
297 lines (263 loc) · 7.49 KB
/
passwordcheck.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
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
// Check passwords in the rclone config file
package main
import (
"bufio"
"bytes"
"encoding/base64"
"flag"
"fmt"
"io"
"log"
"math/rand"
"os"
"runtime"
"sort"
"strings"
"sync"
"time"
"github.com/rclone/rclone/fs/config/obscure"
)
const timeFormat = "2006-01-02 15:04:05"
var (
// Flags
startDate = flag.String("start-date", "2019-08-25 00:00:00", "Start date to search from")
endDate = flag.String("end-date", time.Now().Format(timeFormat), "End date to search to")
minBits = flag.Int("min-bits", 64, "Minimum number of bits to search in a password")
verbose = flag.Bool("verbose", false, "Set to print more things")
version = "development version" // overridden by goreleaser
)
// Passwords read from the config file for testing
type passEntry struct {
remote string
pw []byte
obscured string
}
// All found passwords
type foundReport struct {
mu sync.Mutex
found []passEntry
}
// Make a new foundReport
func newFoundReport() *foundReport {
return &foundReport{}
}
// add pw to the report
func (report *foundReport) add(pw passEntry) {
report.mu.Lock()
defer report.mu.Unlock()
report.found = append(report.found, pw)
}
// debugf - log things if verbose is set
func debugf(format string, v ...interface{}) {
if *verbose {
log.Printf(format, v...)
}
}
// password function from vulnerable rclones
//
// Generate a password into the byte array passed in
func password(rng *rand.Rand, pw []byte) {
n, err := rng.Read(pw)
if err != nil {
log.Fatalf("password read failed: %v", err)
}
if n != len(pw) {
log.Fatalf("password short read: %d", n)
}
}
// Convert the binary rep of pass into a string
func passString(pw []byte) string {
return base64.RawURLEncoding.EncodeToString(pw)
}
// Convert a seed into a time
func seedString(seed int64) string {
return fmt.Sprintf("seed %d generated at %v", seed, time.Unix(seed, 0).Format(timeFormat))
}
// read the config file and return candidate passwords grouped by length
func readConfigFile(in io.Reader) (pws map[int][]passEntry) {
var (
remotes = 0
nonGeneratedPasswords = 0
tooShortPasswords = 0
generatedPasswords = 0
)
pws = make(map[int][]passEntry)
scanner := bufio.NewScanner(in)
remote := "UNKNOWN"
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if len(line) < 4 {
continue
}
if line[0] == '[' && line[len(line)-1] == ']' {
remote = line[1 : len(line)-1]
remotes++
continue
}
if strings.Contains(line, "RCLONE_ENCRYPT") {
log.Fatalf("Can't read encrypted config files - please decrypt first")
}
if !strings.HasPrefix(line, "pass") {
continue
}
equal := strings.IndexRune(line, '=')
if equal < 0 {
continue
}
key, value := strings.TrimSpace(line[:equal]), strings.TrimSpace(line[equal+1:])
if len(value) == 0 {
continue
}
if key == "pass" || key == "password" || key == "password2" {
debugf("remote = %q, key = %q, value = %q", remote, key, value)
pwString, err := obscure.Reveal(value)
if err != nil {
debugf("%s: %s = %q is not an obscured password: %v", remote, key, value, err)
nonGeneratedPasswords++
continue
}
pw, err := base64.RawURLEncoding.DecodeString(pwString)
if err != nil {
debugf("%s: obscured password %s = %q was not generated by rclone: %v", remote, key, value, err)
nonGeneratedPasswords++
continue
}
if len(pw) < *minBits/8 {
debugf("%s: IGNORING obscured password %s = %q is only %d bits (set with -min-bits)", remote, key, value, len(pw)*8)
tooShortPasswords++
continue
}
generatedPasswords++
pws[len(pw)] = append(pws[len(pw)],
passEntry{
remote: remote,
pw: pw,
obscured: value,
},
)
}
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
log.Printf("found %d remote definitions", remotes)
log.Printf("found %d passwords generated by rclone config which need checking", generatedPasswords)
log.Printf("ignored %d passwords not generated by rclone config", nonGeneratedPasswords)
log.Printf("ignored %d passwords less than %d bits", tooShortPasswords, *minBits)
return pws
}
const increment = 1024
func testPasswords(wg *sync.WaitGroup, seeds <-chan int64, wantPws []passEntry, found *foundReport) {
defer wg.Done()
if len(wantPws) == 0 {
return
}
lenPw := len(wantPws[0].pw)
rng := rand.New(rand.NewSource(1))
var pw = make([]byte, lenPw)
for seed := range seeds {
for i := 0; i < increment; i++ {
rng.Seed(seed)
password(rng, pw)
for _, wantPw := range wantPws {
if bytes.Equal(pw, wantPw.pw) {
log.Printf("FOUND match for remote %s: obscured password %q at %s", wantPw.remote, wantPw.obscured, seedString(seed))
found.add(wantPw)
}
}
seed++
}
}
}
// Find passwords for the wantPws and return found
func findPasswords(startSeed, endSeed int64, wantPws []passEntry) []passEntry {
found := newFoundReport()
var wg sync.WaitGroup
n := runtime.NumCPU()
// Set up worker goroutines
seeds := make(chan int64, n)
for i := 0; i < n; i++ {
wg.Add(1)
go testPasswords(&wg, seeds, wantPws, found)
}
// Pump the seeds in
for seed := startSeed; seed <= endSeed; seed += increment {
seeds <- seed
}
close(seeds)
wg.Wait()
return found.found
}
func findAllPasswords(startSeed, endSeed int64, pwsMap map[int][]passEntry) []passEntry {
var pws []passEntry
var lengths []int
for length := range pwsMap {
lengths = append(lengths, length)
}
sort.Ints(lengths)
for _, length := range lengths {
log.Printf("Looking through %d seeds from %v to %v for %d passwords of length %d bits", endSeed-startSeed, seedString(startSeed), seedString(endSeed), len(pwsMap[length]), length*8)
start := time.Now()
newPws := findPasswords(startSeed, endSeed, pwsMap[length])
dt := time.Since(start)
pws = append(pws, newPws...)
log.Printf("That took %v for %f seeds/s", dt, float64(endSeed-startSeed)/float64(dt)*float64(time.Second))
}
return pws
}
// Parse a time string to a unix second
func timeParse(tString string) int64 {
t, err := time.Parse(timeFormat, tString)
if err != nil {
log.Fatalf("failed to parse time %q: %v", tString, err)
}
return t.Unix()
}
// syntaxError prints the syntax
func syntaxError() {
fmt.Fprintf(os.Stderr, `passwordcheck - check your rclone config for bad passwords - %s
In https://github.com/rclone/rclone/issues/4783 a security issue was
found which meant that passwords generated by "rclone config" might be
insecure.
This program checks your rclone config file for any of those
passwords.
Note that it may take some time to run. At the end it will print a
report showing any insecure passwords found.
Usage:
passwordcheck [options] /path/to/rclone/config/file
Note that you can find your rclone config file by running "rclone
config file".
See the README.md for more info.
Options:
`, version)
flag.PrintDefaults()
}
func main() {
flag.Usage = syntaxError
flag.Parse()
args := flag.Args()
if len(args) != 1 {
syntaxError()
os.Exit(1)
}
startSeed := timeParse(*startDate)
endSeed := timeParse(*endDate)
in, err := os.Open(args[0])
if err != nil {
log.Fatalf("Failed to open config file: %v", err)
}
pwsMap := readConfigFile(in)
_ = in.Close()
if len(pwsMap) == 0 {
log.Fatalf("No passwords to check found in config file - did you use the right file?")
}
pws := findAllPasswords(startSeed, endSeed, pwsMap)
if len(pws) == 0 {
fmt.Printf("\n\n*** No insecure passwords found\n")
} else {
fmt.Printf("\n\n*** %d Insecure passwords found\n", len(pws))
for _, pw := range pws {
fmt.Printf("remote %16s: %q\n", pw.remote, pw.obscured)
}
}
}