-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
/
decrypt.go
289 lines (247 loc) · 8.56 KB
/
decrypt.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
// Copyright (C) 2021 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
// Package decrypt implements the `syncthing decrypt` subcommand.
package decrypt
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/scanner"
)
type CLI struct {
Path string `arg:"" required:"1" help:"Path to encrypted folder"`
To string `xor:"mode" placeholder:"PATH" help:"Destination directory, when decrypting"`
VerifyOnly bool `xor:"mode" help:"Don't write decrypted files to disk (but verify plaintext hashes)"`
Password string `help:"Folder password for decryption / verification" env:"FOLDER_PASSWORD"`
FolderID string `help:"Folder ID of the encrypted folder, if it cannot be determined automatically"`
Continue bool `help:"Continue processing next file in case of error, instead of aborting"`
Verbose bool `help:"Show verbose progress information"`
TokenPath string `placeholder:"PATH" help:"Path to the token file within the folder (used to determine folder ID)"`
folderKey *[32]byte
keyGen *protocol.KeyGenerator
}
type storedEncryptionToken struct {
FolderID string
Token []byte
}
func (c *CLI) Run() error {
log.SetFlags(0)
if c.To == "" && !c.VerifyOnly {
return errors.New("must set --to or --verify-only")
}
if c.TokenPath == "" {
// This is a bit long to show as default in --help
c.TokenPath = filepath.Join(config.DefaultMarkerName, config.EncryptionTokenName)
}
if c.FolderID == "" {
// We should try to figure out the folder ID
folderID, err := c.getFolderID()
if err != nil {
log.Println("No --folder-id given and couldn't read folder token")
return fmt.Errorf("getting folder ID: %w", err)
}
c.FolderID = folderID
if c.Verbose {
log.Println("Found folder ID:", c.FolderID)
}
}
c.keyGen = protocol.NewKeyGenerator()
c.folderKey = c.keyGen.KeyFromPassword(c.FolderID, c.Password)
return c.walk()
}
// walk finds and processes every file in the encrypted folder
func (c *CLI) walk() error {
srcFs := fs.NewFilesystem(fs.FilesystemTypeBasic, c.Path)
var dstFs fs.Filesystem
if c.To != "" {
dstFs = fs.NewFilesystem(fs.FilesystemTypeBasic, c.To)
}
return srcFs.Walk(".", func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsRegular() {
return nil
}
if fs.IsInternal(path) {
return nil
}
return c.withContinue(c.process(srcFs, dstFs, path))
})
}
// If --continue was set we just mention the error and return nil to
// continue processing.
func (c *CLI) withContinue(err error) error {
if err == nil {
return nil
}
if c.Continue {
log.Println("Warning:", err)
return nil
}
return err
}
// getFolderID returns the folder ID found in the encrypted token, or an
// error.
func (c *CLI) getFolderID() (string, error) {
tokenPath := filepath.Join(c.Path, c.TokenPath)
bs, err := os.ReadFile(tokenPath)
if err != nil {
return "", fmt.Errorf("reading folder token: %w", err)
}
var tok storedEncryptionToken
if err := json.Unmarshal(bs, &tok); err != nil {
return "", fmt.Errorf("parsing folder token: %w", err)
}
return tok.FolderID, nil
}
// process handles the file named path in srcFs, decrypting it into dstFs
// unless dstFs is nil.
func (c *CLI) process(srcFs fs.Filesystem, dstFs fs.Filesystem, path string) error {
// Which filemode bits to preserve
const retainBits = fs.ModePerm | fs.ModeSetgid | fs.ModeSetuid | fs.ModeSticky
if c.Verbose {
log.Printf("Processing %q", path)
}
encFd, err := srcFs.Open(path)
if err != nil {
return err
}
defer encFd.Close()
encFi, err := loadEncryptedFileInfo(encFd)
if err != nil {
return fmt.Errorf("%s: loading metadata trailer: %w", path, err)
}
// Workaround for a bug in <= v1.15.0-rc.5 where we stored names
// in native format, while protocol expects wire format (slashes).
encFi.Name = osutil.NormalizedFilename(encFi.Name)
plainFi, err := protocol.DecryptFileInfo(c.keyGen, *encFi, c.folderKey)
if err != nil {
return fmt.Errorf("%s: decrypting metadata: %w", path, err)
}
if c.Verbose {
log.Printf("Plaintext filename is %q", plainFi.Name)
}
var plainFd fs.File
if dstFs != nil {
if err := dstFs.MkdirAll(filepath.Dir(plainFi.Name), 0o700); err != nil {
return fmt.Errorf("%s: %w", plainFi.Name, err)
}
plainFd, err = dstFs.Create(plainFi.Name)
if err != nil {
return fmt.Errorf("%s: %w", plainFi.Name, err)
}
defer plainFd.Close() // also closed explicitly in the return
if err := dstFs.Chmod(plainFi.Name, fs.FileMode(plainFi.Permissions&uint32(retainBits))); err != nil {
return fmt.Errorf("%s: %w", plainFi.Name, err)
}
}
if err := c.decryptFile(encFi, &plainFi, encFd, plainFd); err != nil {
// Decrypting the file failed, leaving it in an inconsistent state.
// Delete it. Even --continue currently doesn't mean "leave broken
// stuff in place", it just means "try the next file instead of
// aborting".
if plainFd != nil {
_ = dstFs.Remove(plainFd.Name())
}
return fmt.Errorf("%s: %s: %w", path, plainFi.Name, err)
} else if c.Verbose {
log.Printf("Data verified for %q", plainFi.Name)
}
if plainFd != nil {
if err := plainFd.Close(); err != nil {
return fmt.Errorf("%s: %w", plainFi.Name, err)
}
if err := dstFs.Chtimes(plainFi.Name, plainFi.ModTime(), plainFi.ModTime()); err != nil {
return fmt.Errorf("%s: %w", plainFi.Name, err)
}
}
return nil
}
// decryptFile reads, decrypts and verifies all the blocks in src, writing
// it to dst if dst is non-nil. (If dst is nil it just becomes a
// read-and-verify operation.)
func (c *CLI) decryptFile(encFi *protocol.FileInfo, plainFi *protocol.FileInfo, src io.ReaderAt, dst io.WriterAt) error {
// The encrypted and plaintext files must consist of an equal number of blocks
if len(encFi.Blocks) != len(plainFi.Blocks) {
return fmt.Errorf("block count mismatch: encrypted %d != plaintext %d", len(encFi.Blocks), len(plainFi.Blocks))
}
fileKey := c.keyGen.FileKey(plainFi.Name, c.folderKey)
for i, encBlock := range encFi.Blocks {
// Read the encrypted block
buf := make([]byte, encBlock.Size)
if _, err := src.ReadAt(buf, encBlock.Offset); err != nil {
return fmt.Errorf("encrypted block %d (%d bytes): %w", i, encBlock.Size, err)
}
// Decrypt it
dec, err := protocol.DecryptBytes(buf, fileKey)
if err != nil {
return fmt.Errorf("encrypted block %d (%d bytes): %w", i, encBlock.Size, err)
}
// Verify the block size against the expected plaintext
plainBlock := plainFi.Blocks[i]
if i == len(plainFi.Blocks)-1 && len(dec) > plainBlock.Size {
// The last block might be padded, which is fine (we skip the padding)
dec = dec[:plainBlock.Size]
} else if len(dec) != plainBlock.Size {
return fmt.Errorf("plaintext block %d size mismatch, actual %d != expected %d", i, len(dec), plainBlock.Size)
}
// Verify the hash against the plaintext block info
if !scanner.Validate(dec, plainBlock.Hash, 0) {
// The block decrypted correctly but fails the hash check. This
// is odd and unexpected, but it it's still a valid block from
// the source. The file might have changed while we pulled it?
err := fmt.Errorf("plaintext block %d (%d bytes) failed validation after decryption", i, plainBlock.Size)
if c.Continue {
log.Printf("Warning: %s: %s: %v", encFi.Name, plainFi.Name, err)
} else {
return err
}
}
// Write it to the destination, unless we're just verifying.
if dst != nil {
if _, err := dst.WriteAt(dec, plainBlock.Offset); err != nil {
return err
}
}
}
return nil
}
// loadEncryptedFileInfo loads the encrypted FileInfo trailer from a file on
// disk.
func loadEncryptedFileInfo(fd fs.File) (*protocol.FileInfo, error) {
// Seek to the size of the trailer block
if _, err := fd.Seek(-4, io.SeekEnd); err != nil {
return nil, err
}
var bs [4]byte
if _, err := io.ReadFull(fd, bs[:]); err != nil {
return nil, err
}
size := int64(binary.BigEndian.Uint32(bs[:]))
// Seek to the start of the trailer
if _, err := fd.Seek(-(4 + size), io.SeekEnd); err != nil {
return nil, err
}
trailer := make([]byte, size)
if _, err := io.ReadFull(fd, trailer); err != nil {
return nil, err
}
var encFi protocol.FileInfo
if err := encFi.Unmarshal(trailer); err != nil {
return nil, err
}
return &encFi, nil
}