123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- // 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"
- "google.golang.org/protobuf/proto"
- "github.com/syncthing/syncthing/internal/gen/bep"
- "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 bep.FileInfo
- if err := proto.Unmarshal(trailer, &encFi); err != nil {
- return nil, err
- }
- fi := protocol.FileInfoFromWire(&encFi)
- return &fi, nil
- }
|