// 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" "fmt" "io" "io/ioutil" "log" "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 } type storedEncryptionToken struct { FolderID string Token []byte } func (c *CLI) Run() error { log.SetFlags(0) if c.To == "" && !c.VerifyOnly { return fmt.Errorf("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.folderKey = protocol.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 := ioutil.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 { if c.Verbose { log.Printf("Processing %q", path) } encFd, err := srcFs.Open(path) if err != nil { return err } defer encFd.Close() encFi, err := c.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(*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), 0700); 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 := 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 { return plainFd.Close() } 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 := protocol.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 (c *CLI) 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 }