From 13dc7657ba0aac4da3f4b80ba231a919fdfae0f5 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 1 Aug 2020 20:47:59 +0200 Subject: v2api/reverse: move old fusefrontend_reverse out of the way fusefrontend_reverse -> fusefrontend_reverse_v1api --- internal/fusefrontend_reverse/ctlsock_interface.go | 38 -- internal/fusefrontend_reverse/excluder.go | 66 --- internal/fusefrontend_reverse/excluder_test.go | 84 ---- internal/fusefrontend_reverse/mocks_test.go | 32 -- internal/fusefrontend_reverse/r_node.go | 20 - internal/fusefrontend_reverse/reverse_longnames.go | 126 ------ internal/fusefrontend_reverse/rfile.go | 209 --------- internal/fusefrontend_reverse/rfs.go | 472 --------------------- internal/fusefrontend_reverse/rfs_test.go | 124 ------ internal/fusefrontend_reverse/rpath.go | 113 ----- internal/fusefrontend_reverse/rpath_cache.go | 45 -- internal/fusefrontend_reverse/virtualfile.go | 110 ----- .../ctlsock_interface.go | 38 ++ internal/fusefrontend_reverse_v1api/excluder.go | 66 +++ .../fusefrontend_reverse_v1api/excluder_test.go | 84 ++++ internal/fusefrontend_reverse_v1api/mocks_test.go | 32 ++ .../reverse_longnames.go | 126 ++++++ internal/fusefrontend_reverse_v1api/rfile.go | 209 +++++++++ internal/fusefrontend_reverse_v1api/rfs.go | 472 +++++++++++++++++++++ internal/fusefrontend_reverse_v1api/rfs_test.go | 124 ++++++ internal/fusefrontend_reverse_v1api/rpath.go | 113 +++++ internal/fusefrontend_reverse_v1api/rpath_cache.go | 45 ++ internal/fusefrontend_reverse_v1api/virtualfile.go | 110 +++++ 23 files changed, 1419 insertions(+), 1439 deletions(-) delete mode 100644 internal/fusefrontend_reverse/ctlsock_interface.go delete mode 100644 internal/fusefrontend_reverse/excluder.go delete mode 100644 internal/fusefrontend_reverse/excluder_test.go delete mode 100644 internal/fusefrontend_reverse/mocks_test.go delete mode 100644 internal/fusefrontend_reverse/r_node.go delete mode 100644 internal/fusefrontend_reverse/reverse_longnames.go delete mode 100644 internal/fusefrontend_reverse/rfile.go delete mode 100644 internal/fusefrontend_reverse/rfs.go delete mode 100644 internal/fusefrontend_reverse/rfs_test.go delete mode 100644 internal/fusefrontend_reverse/rpath.go delete mode 100644 internal/fusefrontend_reverse/rpath_cache.go delete mode 100644 internal/fusefrontend_reverse/virtualfile.go create mode 100644 internal/fusefrontend_reverse_v1api/ctlsock_interface.go create mode 100644 internal/fusefrontend_reverse_v1api/excluder.go create mode 100644 internal/fusefrontend_reverse_v1api/excluder_test.go create mode 100644 internal/fusefrontend_reverse_v1api/mocks_test.go create mode 100644 internal/fusefrontend_reverse_v1api/reverse_longnames.go create mode 100644 internal/fusefrontend_reverse_v1api/rfile.go create mode 100644 internal/fusefrontend_reverse_v1api/rfs.go create mode 100644 internal/fusefrontend_reverse_v1api/rfs_test.go create mode 100644 internal/fusefrontend_reverse_v1api/rpath.go create mode 100644 internal/fusefrontend_reverse_v1api/rpath_cache.go create mode 100644 internal/fusefrontend_reverse_v1api/virtualfile.go diff --git a/internal/fusefrontend_reverse/ctlsock_interface.go b/internal/fusefrontend_reverse/ctlsock_interface.go deleted file mode 100644 index f7b8afd..0000000 --- a/internal/fusefrontend_reverse/ctlsock_interface.go +++ /dev/null @@ -1,38 +0,0 @@ -package fusefrontend_reverse - -import ( - "path/filepath" - "strings" - - "golang.org/x/sys/unix" - - "github.com/rfjakob/gocryptfs/internal/ctlsocksrv" - "github.com/rfjakob/gocryptfs/internal/pathiv" -) - -var _ ctlsocksrv.Interface = &ReverseFS{} // Verify that interface is implemented. - -// EncryptPath implements ctlsock.Backend. -// This is used for the control socket and for the "-exclude" logic. -func (rfs *ReverseFS) EncryptPath(plainPath string) (string, error) { - if rfs.args.PlaintextNames || plainPath == "" { - return plainPath, nil - } - cipherPath := "" - parts := strings.Split(plainPath, "/") - for _, part := range parts { - dirIV := pathiv.Derive(cipherPath, pathiv.PurposeDirIV) - encryptedPart := rfs.nameTransform.EncryptName(part, dirIV) - if rfs.args.LongNames && len(encryptedPart) > unix.NAME_MAX { - encryptedPart = rfs.nameTransform.HashLongName(encryptedPart) - } - cipherPath = filepath.Join(cipherPath, encryptedPart) - } - return cipherPath, nil -} - -// DecryptPath implements ctlsock.Backend -func (rfs *ReverseFS) DecryptPath(cipherPath string) (string, error) { - p, err := rfs.decryptPath(cipherPath) - return p, err -} diff --git a/internal/fusefrontend_reverse/excluder.go b/internal/fusefrontend_reverse/excluder.go deleted file mode 100644 index c44816c..0000000 --- a/internal/fusefrontend_reverse/excluder.go +++ /dev/null @@ -1,66 +0,0 @@ -package fusefrontend_reverse - -import ( - "io/ioutil" - "os" - "strings" - - "github.com/rfjakob/gocryptfs/internal/exitcodes" - "github.com/rfjakob/gocryptfs/internal/fusefrontend" - "github.com/rfjakob/gocryptfs/internal/tlog" - - "github.com/sabhiram/go-gitignore" -) - -// prepareExcluder creates an object to check if paths are excluded -// based on the patterns specified in the command line. -func (rfs *ReverseFS) prepareExcluder(args fusefrontend.Args) { - if len(args.Exclude) > 0 || len(args.ExcludeWildcard) > 0 || len(args.ExcludeFrom) > 0 { - excluder, err := ignore.CompileIgnoreLines(getExclusionPatterns(args)...) - if err != nil { - tlog.Fatal.Printf("Error compiling exclusion rules: %q", err) - os.Exit(exitcodes.ExcludeError) - } - rfs.excluder = excluder - } -} - -// getExclusionPatters prepares a list of patterns to be excluded. -// Patterns passed in the -exclude command line option are prefixed -// with a leading '/' to preserve backwards compatibility (before -// wildcard matching was implemented, exclusions always were matched -// against the full path). -func getExclusionPatterns(args fusefrontend.Args) []string { - patterns := make([]string, len(args.Exclude)+len(args.ExcludeWildcard)) - // add -exclude - for i, p := range args.Exclude { - patterns[i] = "/" + p - } - // add -exclude-wildcard - copy(patterns[len(args.Exclude):], args.ExcludeWildcard) - // add -exclude-from - for _, file := range args.ExcludeFrom { - lines, err := getLines(file) - if err != nil { - tlog.Fatal.Printf("Error reading exclusion patterns: %q", err) - os.Exit(exitcodes.ExcludeError) - } - patterns = append(patterns, lines...) - } - return patterns -} - -// getLines reads a file and splits it into lines -func getLines(file string) ([]string, error) { - buffer, err := ioutil.ReadFile(file) - if err != nil { - return nil, err - } - return strings.Split(string(buffer), "\n"), nil -} - -// isExcludedPlain finds out if the plaintext path "pPath" is -// excluded (used when -exclude is passed by the user). -func (rfs *ReverseFS) isExcludedPlain(pPath string) bool { - return rfs.excluder != nil && rfs.excluder.MatchesPath(pPath) -} diff --git a/internal/fusefrontend_reverse/excluder_test.go b/internal/fusefrontend_reverse/excluder_test.go deleted file mode 100644 index e19b99f..0000000 --- a/internal/fusefrontend_reverse/excluder_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package fusefrontend_reverse - -import ( - "io/ioutil" - "os" - "reflect" - "testing" - - "github.com/rfjakob/gocryptfs/internal/fusefrontend" -) - -func TestShouldNoCreateExcluderIfNoPattersWereSpecified(t *testing.T) { - var rfs ReverseFS - var args fusefrontend.Args - rfs.prepareExcluder(args) - if rfs.excluder != nil { - t.Error("Should not have created excluder") - } -} - -func TestShouldPrefixExcludeValuesWithSlash(t *testing.T) { - var args fusefrontend.Args - args.Exclude = []string{"file1", "dir1/file2.txt"} - args.ExcludeWildcard = []string{"*~", "build/*.o"} - - expected := []string{"/file1", "/dir1/file2.txt", "*~", "build/*.o"} - - patterns := getExclusionPatterns(args) - if !reflect.DeepEqual(patterns, expected) { - t.Errorf("expected %q, got %q", expected, patterns) - } -} - -func TestShouldReadExcludePatternsFromFiles(t *testing.T) { - tmpfile1, err := ioutil.TempFile("", "excludetest") - if err != nil { - t.Fatal(err) - } - exclude1 := tmpfile1.Name() - defer os.Remove(exclude1) - defer tmpfile1.Close() - - tmpfile2, err := ioutil.TempFile("", "excludetest") - if err != nil { - t.Fatal(err) - } - exclude2 := tmpfile2.Name() - defer os.Remove(exclude2) - defer tmpfile2.Close() - - tmpfile1.WriteString("file1.1\n") - tmpfile1.WriteString("file1.2\n") - tmpfile2.WriteString("file2.1\n") - tmpfile2.WriteString("file2.2\n") - - var args fusefrontend.Args - args.ExcludeWildcard = []string{"cmdline1"} - args.ExcludeFrom = []string{exclude1, exclude2} - - // An empty string is returned for the last empty line - // It's ignored when the patterns are actually compiled - expected := []string{"cmdline1", "file1.1", "file1.2", "", "file2.1", "file2.2", ""} - - patterns := getExclusionPatterns(args) - if !reflect.DeepEqual(patterns, expected) { - t.Errorf("expected %q, got %q", expected, patterns) - } -} - -func TestShouldReturnFalseIfThereAreNoExclusions(t *testing.T) { - var rfs ReverseFS - if rfs.isExcludedPlain("any/path") { - t.Error("Should not exclude any path if no exclusions were specified") - } -} - -func TestShouldCallIgnoreParserToCheckExclusion(t *testing.T) { - rfs, ignorerMock := createRFSWithMocks() - - rfs.isExcludedPlain("some/path") - if ignorerMock.calledWith != "some/path" { - t.Error("Failed to call IgnoreParser") - } -} diff --git a/internal/fusefrontend_reverse/mocks_test.go b/internal/fusefrontend_reverse/mocks_test.go deleted file mode 100644 index fd2f940..0000000 --- a/internal/fusefrontend_reverse/mocks_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package fusefrontend_reverse - -import ( - "github.com/rfjakob/gocryptfs/internal/nametransform" -) - -type IgnoreParserMock struct { - toExclude string - calledWith string -} - -func (parser *IgnoreParserMock) MatchesPath(f string) bool { - parser.calledWith = f - return f == parser.toExclude -} - -type NameTransformMock struct { - nametransform.NameTransform -} - -func (n *NameTransformMock) DecryptName(cipherName string, iv []byte) (string, error) { - return "mockdecrypt_" + cipherName, nil -} - -func createRFSWithMocks() (*ReverseFS, *IgnoreParserMock) { - ignorerMock := &IgnoreParserMock{} - nameTransformMock := &NameTransformMock{} - var rfs ReverseFS - rfs.excluder = ignorerMock - rfs.nameTransform = nameTransformMock - return &rfs, ignorerMock -} diff --git a/internal/fusefrontend_reverse/r_node.go b/internal/fusefrontend_reverse/r_node.go deleted file mode 100644 index 58ce664..0000000 --- a/internal/fusefrontend_reverse/r_node.go +++ /dev/null @@ -1,20 +0,0 @@ -package fusefrontend_reverse - -import ( - "github.com/hanwen/go-fuse/v2/fs" - - "github.com/rfjakob/gocryptfs/internal/contentenc" - "github.com/rfjakob/gocryptfs/internal/fusefrontend" - "github.com/rfjakob/gocryptfs/internal/nametransform" -) - -// RNode is a file or directory in the filesystem tree -// in a `gocryptfs -reverse` mount. -type RNode struct { - fs.Inode -} - -func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *RNode { - // TODO - return &RNode{} -} diff --git a/internal/fusefrontend_reverse/reverse_longnames.go b/internal/fusefrontend_reverse/reverse_longnames.go deleted file mode 100644 index f1576d2..0000000 --- a/internal/fusefrontend_reverse/reverse_longnames.go +++ /dev/null @@ -1,126 +0,0 @@ -package fusefrontend_reverse - -import ( - "log" - "path/filepath" - "sync" - "syscall" - "time" - - "golang.org/x/sys/unix" - - "github.com/hanwen/go-fuse/v2/fuse" - "github.com/hanwen/go-fuse/v2/fuse/nodefs" - - "github.com/rfjakob/gocryptfs/internal/nametransform" - "github.com/rfjakob/gocryptfs/internal/pathiv" - "github.com/rfjakob/gocryptfs/internal/syscallcompat" - "github.com/rfjakob/gocryptfs/internal/tlog" -) - -const ( - // File names are padded to 16-byte multiples, encrypted and - // base64-encoded. We can encode at most 176 bytes to stay below the 255 - // bytes limit: - // * base64(176 bytes) = 235 bytes - // * base64(192 bytes) = 256 bytes (over 255!) - // But the PKCS#7 padding is at least one byte. This means we can only use - // 175 bytes for the file name. - shortNameMax = 175 -) - -// longnameParentCache maps dir+"/"+longname to plaintextname. -// Yes, the combination of relative plaintext dir path and encrypted -// longname is strange, but works fine as a map index. -var longnameParentCache map[string]string -var longnameCacheLock sync.Mutex - -// Very simple cache cleaner: Nuke it every hour -func longnameCacheCleaner() { - for { - time.Sleep(time.Hour) - longnameCacheLock.Lock() - longnameParentCache = map[string]string{} - longnameCacheLock.Unlock() - } -} - -func initLongnameCache() { - if longnameParentCache != nil { - return - } - longnameParentCache = map[string]string{} - go longnameCacheCleaner() -} - -// findLongnameParent converts "longname" = "gocryptfs.longname.XYZ" to the -// plaintext name. "dir" = relative plaintext path to the directory the -// longname file is in, "dirIV" = directory IV of the directory. -func (rfs *ReverseFS) findLongnameParent(dir string, dirIV []byte, longname string) (plaintextName string, err error) { - longnameCacheLock.Lock() - hit := longnameParentCache[dir+"/"+longname] - longnameCacheLock.Unlock() - if hit != "" { - return hit, nil - } - dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, filepath.Dir(dir)) - if err != nil { - tlog.Warn.Printf("findLongnameParent: OpenDirNofollow failed: %v\n", err) - return "", err - } - fd, err := syscallcompat.Openat(dirfd, filepath.Base(dir), syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) - syscall.Close(dirfd) - if err != nil { - tlog.Warn.Printf("findLongnameParent: Openat failed: %v\n", err) - return "", err - } - dirEntries, err := syscallcompat.Getdents(fd) - syscall.Close(fd) - if err != nil { - tlog.Warn.Printf("findLongnameParent: Getdents failed: %v\n", err) - return "", err - } - longnameCacheLock.Lock() - defer longnameCacheLock.Unlock() - for _, entry := range dirEntries { - plaintextName := entry.Name - if len(plaintextName) <= shortNameMax { - continue - } - cName := rfs.nameTransform.EncryptName(plaintextName, dirIV) - if len(cName) <= unix.NAME_MAX { - // Entry should have been skipped by the "continue" above - log.Panic("logic error or wrong shortNameMax constant?") - } - hName := rfs.nameTransform.HashLongName(cName) - longnameParentCache[dir+"/"+hName] = plaintextName - if longname == hName { - hit = plaintextName - } - } - if hit == "" { - return "", syscall.ENOENT - } - return hit, nil -} - -func (rfs *ReverseFS) newNameFile(relPath string) (nodefs.File, fuse.Status) { - dotName := filepath.Base(relPath) // gocryptfs.longname.XYZ.name - longname := nametransform.RemoveLongNameSuffix(dotName) // gocryptfs.longname.XYZ - // cipher directory - cDir := nametransform.Dir(relPath) - // plain directory - pDir, err := rfs.decryptPath(cDir) - if err != nil { - return nil, fuse.ToStatus(err) - } - dirIV := pathiv.Derive(cDir, pathiv.PurposeDirIV) - // plain name - pName, err := rfs.findLongnameParent(pDir, dirIV, longname) - if err != nil { - return nil, fuse.ToStatus(err) - } - content := []byte(rfs.nameTransform.EncryptName(pName, dirIV)) - parentFile := filepath.Join(pDir, pName) - return rfs.newVirtualFile(content, rfs.args.Cipherdir, parentFile, inoTagNameFile) -} diff --git a/internal/fusefrontend_reverse/rfile.go b/internal/fusefrontend_reverse/rfile.go deleted file mode 100644 index 68f7309..0000000 --- a/internal/fusefrontend_reverse/rfile.go +++ /dev/null @@ -1,209 +0,0 @@ -package fusefrontend_reverse - -import ( - "bytes" - "io" - "os" - "path/filepath" - "syscall" - - // In newer Go versions, this has moved to just "sync/syncmap". - "golang.org/x/sync/syncmap" - - "github.com/hanwen/go-fuse/v2/fuse" - "github.com/hanwen/go-fuse/v2/fuse/nodefs" - - "github.com/rfjakob/gocryptfs/internal/contentenc" - "github.com/rfjakob/gocryptfs/internal/pathiv" - "github.com/rfjakob/gocryptfs/internal/syscallcompat" - "github.com/rfjakob/gocryptfs/internal/tlog" -) - -type reverseFile struct { - // Embed nodefs.defaultFile for a ENOSYS implementation of all methods - nodefs.File - // Backing FD - fd *os.File - // File header (contains the IV) - header contentenc.FileHeader - // IV for block 0 - block0IV []byte - // Content encryption helper - contentEnc *contentenc.ContentEnc -} - -var inodeTable syncmap.Map - -// newFile receives a ciphered path "relPath" and its corresponding -// decrypted path "pRelPath", opens it and returns a reverseFile -// object. The backing file descriptor is always read-only. -func (rfs *ReverseFS) newFile(relPath string, pRelPath string) (*reverseFile, fuse.Status) { - if rfs.isExcludedPlain(pRelPath) { - // Excluded paths should have been filtered out beforehand. Better safe - // than sorry. - tlog.Warn.Printf("BUG: newFile: received excluded path %q. This should not happen.", relPath) - return nil, fuse.ENOENT - } - dir := filepath.Dir(pRelPath) - dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, dir) - if err != nil { - return nil, fuse.ToStatus(err) - } - fd, err := syscallcompat.Openat(dirfd, filepath.Base(pRelPath), syscall.O_RDONLY|syscall.O_NOFOLLOW, 0) - syscall.Close(dirfd) - if err != nil { - return nil, fuse.ToStatus(err) - } - var st syscall.Stat_t - err = syscall.Fstat(fd, &st) - if err != nil { - tlog.Warn.Printf("newFile: Fstat error: %v", err) - syscall.Close(fd) - return nil, fuse.ToStatus(err) - } - // Reject access if the file descriptor does not refer to a regular file. - var a fuse.Attr - a.FromStat(&st) - if !a.IsRegular() { - tlog.Warn.Printf("ino%d: newFile: not a regular file", st.Ino) - syscall.Close(fd) - return nil, fuse.ToStatus(syscall.EACCES) - } - // See if we have that inode number already in the table - // (even if Nlink has dropped to 1) - var derivedIVs pathiv.FileIVs - v, found := inodeTable.Load(st.Ino) - if found { - tlog.Debug.Printf("ino%d: newFile: found in the inode table", st.Ino) - derivedIVs = v.(pathiv.FileIVs) - } else { - derivedIVs = pathiv.DeriveFile(relPath) - // Nlink > 1 means there is more than one path to this file. - // Store the derived values so we always return the same data, - // regardless of the path that is used to access the file. - // This means that the first path wins. - if st.Nlink > 1 { - v, found = inodeTable.LoadOrStore(st.Ino, derivedIVs) - if found { - // Another thread has stored a different value before we could. - derivedIVs = v.(pathiv.FileIVs) - } else { - tlog.Debug.Printf("ino%d: newFile: Nlink=%d, stored in the inode table", st.Ino, st.Nlink) - } - } - } - header := contentenc.FileHeader{ - Version: contentenc.CurrentVersion, - ID: derivedIVs.ID, - } - return &reverseFile{ - File: nodefs.NewDefaultFile(), - fd: os.NewFile(uintptr(fd), pRelPath), - header: header, - block0IV: derivedIVs.Block0IV, - contentEnc: rfs.contentEnc, - }, fuse.OK -} - -// GetAttr - FUSE call -// Triggered by fstat() from userspace -func (rf *reverseFile) GetAttr(*fuse.Attr) fuse.Status { - tlog.Debug.Printf("reverseFile.GetAttr fd=%d\n", rf.fd.Fd()) - // The kernel should fall back to stat() - return fuse.ENOSYS -} - -// encryptBlocks - encrypt "plaintext" into a number of ciphertext blocks. -// "plaintext" must already be block-aligned. -func (rf *reverseFile) encryptBlocks(plaintext []byte, firstBlockNo uint64, fileID []byte, block0IV []byte) []byte { - inBuf := bytes.NewBuffer(plaintext) - var outBuf bytes.Buffer - bs := int(rf.contentEnc.PlainBS()) - for blockNo := firstBlockNo; inBuf.Len() > 0; blockNo++ { - inBlock := inBuf.Next(bs) - iv := pathiv.BlockIV(block0IV, blockNo) - outBlock := rf.contentEnc.EncryptBlockNonce(inBlock, blockNo, fileID, iv) - outBuf.Write(outBlock) - } - return outBuf.Bytes() -} - -// readBackingFile: read from the backing plaintext file, encrypt it, return the -// ciphertext. -// "off" ... ciphertext offset (must be >= HEADER_LEN) -// "length" ... ciphertext length -func (rf *reverseFile) readBackingFile(off uint64, length uint64) (out []byte, err error) { - blocks := rf.contentEnc.ExplodeCipherRange(off, length) - - // Read the backing plaintext in one go - alignedOffset, alignedLength := contentenc.JointPlaintextRange(blocks) - plaintext := make([]byte, int(alignedLength)) - n, err := rf.fd.ReadAt(plaintext, int64(alignedOffset)) - if err != nil && err != io.EOF { - tlog.Warn.Printf("readBackingFile: ReadAt: %s", err.Error()) - return nil, err - } - // Truncate buffer down to actually read bytes - plaintext = plaintext[0:n] - - // Encrypt blocks - ciphertext := rf.encryptBlocks(plaintext, blocks[0].BlockNo, rf.header.ID, rf.block0IV) - - // Crop down to the relevant part - lenHave := len(ciphertext) - skip := blocks[0].Skip - endWant := int(skip + length) - if lenHave > endWant { - out = ciphertext[skip:endWant] - } else if lenHave > int(skip) { - out = ciphertext[skip:lenHave] - } // else: out stays empty, file was smaller than the requested offset - - return out, nil -} - -// Read - FUSE call -func (rf *reverseFile) Read(buf []byte, ioff int64) (resultData fuse.ReadResult, status fuse.Status) { - length := uint64(len(buf)) - off := uint64(ioff) - var out bytes.Buffer - var header []byte - - // Synthesize file header - if off < contentenc.HeaderLen { - header = rf.header.Pack() - // Truncate to requested part - end := int(off) + len(buf) - if end > len(header) { - end = len(header) - } - header = header[off:end] - // Write into output buffer and adjust offsets - out.Write(header) - hLen := uint64(len(header)) - off += hLen - length -= hLen - } - - // Read actual file data - if length > 0 { - fileData, err := rf.readBackingFile(off, length) - if err != nil { - return nil, fuse.ToStatus(err) - } - if len(fileData) == 0 { - // If we could not read any actual data, we also don't want to - // return the file header. An empty file stays empty in encrypted - // form. - return nil, fuse.OK - } - out.Write(fileData) - } - - return fuse.ReadResultData(out.Bytes()), fuse.OK -} - -// Release - FUSE call, close file -func (rf *reverseFile) Release() { - rf.fd.Close() -} diff --git a/internal/fusefrontend_reverse/rfs.go b/internal/fusefrontend_reverse/rfs.go deleted file mode 100644 index 9f7d1f6..0000000 --- a/internal/fusefrontend_reverse/rfs.go +++ /dev/null @@ -1,472 +0,0 @@ -package fusefrontend_reverse - -import ( - "fmt" - "path/filepath" - "syscall" - - "golang.org/x/sys/unix" - - "github.com/hanwen/go-fuse/v2/fuse" - "github.com/hanwen/go-fuse/v2/fuse/nodefs" - "github.com/hanwen/go-fuse/v2/fuse/pathfs" - - "github.com/rfjakob/gocryptfs/internal/configfile" - "github.com/rfjakob/gocryptfs/internal/contentenc" - "github.com/rfjakob/gocryptfs/internal/cryptocore" - "github.com/rfjakob/gocryptfs/internal/fusefrontend" - "github.com/rfjakob/gocryptfs/internal/inomap" - "github.com/rfjakob/gocryptfs/internal/nametransform" - "github.com/rfjakob/gocryptfs/internal/pathiv" - "github.com/rfjakob/gocryptfs/internal/syscallcompat" - "github.com/rfjakob/gocryptfs/internal/tlog" - - "github.com/sabhiram/go-gitignore" -) - -// ReverseFS implements the pathfs.FileSystem interface and provides an -// encrypted view of a plaintext directory. -type ReverseFS struct { - // Embed pathfs.defaultFileSystem for a ENOSYS implementation of all methods - pathfs.FileSystem - // pathfs.loopbackFileSystem, see go-fuse/fuse/pathfs/loopback.go - loopbackfs pathfs.FileSystem - // Stores configuration arguments - args fusefrontend.Args - // Filename encryption helper - nameTransform nametransform.NameTransformer - // Content encryption helper - contentEnc *contentenc.ContentEnc - // Tests whether a path is excluded (hiden) from the user. Used by -exclude. - excluder ignore.IgnoreParser - // inoMap translates inode numbers from different devices to unique inode - // numbers. - inoMap *inomap.InoMap -} - -var _ pathfs.FileSystem = &ReverseFS{} - -// NewFS returns an encrypted FUSE overlay filesystem. -// In this case (reverse mode) the backing directory is plain-text and -// ReverseFS provides an encrypted view. -func NewFS(args fusefrontend.Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *ReverseFS { - initLongnameCache() - fs := &ReverseFS{ - // pathfs.defaultFileSystem returns ENOSYS for all operations - FileSystem: pathfs.NewDefaultFileSystem(), - loopbackfs: pathfs.NewLoopbackFileSystem(args.Cipherdir), - args: args, - nameTransform: n, - contentEnc: c, - inoMap: inomap.New(), - } - fs.prepareExcluder(args) - return fs -} - -// relDir is identical to filepath.Dir excepts that it returns "" when -// filepath.Dir would return ".". -// In the FUSE API, the root directory is called "", and we actually want that. -func relDir(path string) string { - dir := filepath.Dir(path) - if dir == "." { - return "" - } - return dir -} - -// getFileInfo returns information on a ciphertext path "relPath": -// - ftype: file type (as returned by getFileType) -// - excluded: if the path is excluded -// - pPath: if it's not a special file, the decrypted path -// - err: non nil if any error happens -func (rfs *ReverseFS) getFileInfo(relPath string) (ftype fileType, excluded bool, pPath string, err error) { - ftype = rfs.getFileType(relPath) - if ftype == typeConfig { - excluded, pPath, err = false, "", nil - return - } - if ftype == typeDiriv { - parentDir := nametransform.Dir(relPath) - _, excluded, _, err = rfs.getFileInfo(parentDir) - pPath = "" - return - } - if ftype == typeName { - parentDir := nametransform.Dir(relPath) - var parentExcluded bool - _, parentExcluded, _, err = rfs.getFileInfo(parentDir) - if parentExcluded || err != nil { - excluded, pPath = parentExcluded, "" - return - } - relPath = nametransform.RemoveLongNameSuffix(relPath) - } - pPath, err = rfs.decryptPath(relPath) - excluded = err == nil && rfs.isExcludedPlain(pPath) - return -} - -type fileType int - -// Values returned by getFileType -const ( - // A regular file/directory/symlink - typeRegular fileType = iota - // A DirIV (gocryptfs.diriv) file - typeDiriv - // A gocryptfs.longname.*.name file for a file with a long name - typeName - // The config file gocryptfs.conf - typeConfig -) - -// getFileType returns the type of file (one of the fileType constants above). -func (rfs *ReverseFS) getFileType(cPath string) fileType { - if !rfs.args.PlaintextNames { - cName := filepath.Base(cPath) - // Is it a gocryptfs.diriv file? - if cName == nametransform.DirIVFilename { - return typeDiriv - } - // Is it a gocryptfs.longname.*.name file? - if t := nametransform.NameType(cName); t == nametransform.LongNameFilename { - return typeName - } - } - if rfs.isTranslatedConfig(cPath) { - return typeConfig - } - return typeRegular -} - -// isTranslatedConfig returns true if the default config file name is in use -// and the ciphertext path is "gocryptfs.conf". -// "gocryptfs.conf" then maps to ".gocryptfs.reverse.conf" in the plaintext -// directory. -func (rfs *ReverseFS) isTranslatedConfig(relPath string) bool { - if rfs.args.ConfigCustom { - return false - } - if relPath == configfile.ConfDefaultName { - return true - } - return false -} - -// GetAttr - FUSE call -// "relPath" is the relative ciphertext path -func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr, fuse.Status) { - ftype, excluded, pPath, err := rfs.getFileInfo(relPath) - if excluded { - return nil, fuse.ENOENT - } - if err != nil { - return nil, fuse.ToStatus(err) - } - // Handle "gocryptfs.conf" - if ftype == typeConfig { - absConfPath, _ := rfs.abs(configfile.ConfReverseName, nil) - var st syscall.Stat_t - err = syscall.Lstat(absConfPath, &st) - if err != nil { - return nil, fuse.ToStatus(err) - } - rfs.inoMap.TranslateStat(&st) - var a fuse.Attr - a.FromStat(&st) - if rfs.args.ForceOwner != nil { - a.Owner = *rfs.args.ForceOwner - } - return &a, fuse.OK - } - // Handle virtual files (gocryptfs.diriv, *.name) - var f nodefs.File - var status fuse.Status - virtual := false - if ftype == typeDiriv { - virtual = true - f, status = rfs.newDirIVFile(relPath) - } - if ftype == typeName { - virtual = true - f, status = rfs.newNameFile(relPath) - } - if virtual { - if !status.Ok() { - tlog.Warn.Printf("GetAttr %q: newXFile failed: %v\n", relPath, status) - return nil, status - } - var a fuse.Attr - status = f.GetAttr(&a) - if rfs.args.ForceOwner != nil { - a.Owner = *rfs.args.ForceOwner - } - return &a, status - } - // Normal file / directory - dirfd, name, err := rfs.openBackingDir(pPath) - if err != nil { - return nil, fuse.ToStatus(err) - } - // Stat the backing file/dir using Fstatat - var st syscall.Stat_t - { - var st2 unix.Stat_t - err = syscallcompat.Fstatat(dirfd, name, &st2, unix.AT_SYMLINK_NOFOLLOW) - syscall.Close(dirfd) - if err != nil { - return nil, fuse.ToStatus(err) - } - st = syscallcompat.Unix2syscall(st2) - } - rfs.inoMap.TranslateStat(&st) - var a fuse.Attr - a.FromStat(&st) - // Calculate encrypted file size - if a.IsRegular() { - a.Size = rfs.contentEnc.PlainSizeToCipherSize(a.Size) - } else if a.IsSymlink() { - var linkTarget string - var readlinkStatus fuse.Status - - linkTarget, readlinkStatus = rfs.Readlink(relPath, context) - if !readlinkStatus.Ok() { - return nil, readlinkStatus - } - - a.Size = uint64(len(linkTarget)) - } - if rfs.args.ForceOwner != nil { - a.Owner = *rfs.args.ForceOwner - } - return &a, fuse.OK -} - -// Access - FUSE call -func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context) fuse.Status { - ftype, excluded, pPath, err := rfs.getFileInfo(relPath) - if excluded { - return fuse.ENOENT - } - if err != nil { - return fuse.ToStatus(err) - } - if ftype != typeRegular { - // access(2) R_OK flag for checking if the file is readable, always 4 as defined in POSIX. - ROK := uint32(0x4) - // Virtual files can always be read and never written - if mode == ROK || mode == 0 { - return fuse.OK - } - return fuse.EPERM - } - dirfd, name, err := rfs.openBackingDir(pPath) - if err != nil { - return fuse.ToStatus(err) - } - err = syscallcompat.Faccessat(dirfd, name, mode) - syscall.Close(dirfd) - return fuse.ToStatus(err) -} - -// Open - FUSE call -func (rfs *ReverseFS) Open(relPath string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) { - ftype, excluded, pPath, err := rfs.getFileInfo(relPath) - if excluded { - return nil, fuse.ENOENT - } - if err != nil { - return nil, fuse.ToStatus(err) - } - if ftype == typeConfig { - return rfs.loopbackfs.Open(configfile.ConfReverseName, flags, context) - } - if ftype == typeDiriv { - return rfs.newDirIVFile(relPath) - } - if ftype == typeName { - return rfs.newNameFile(relPath) - } - return rfs.newFile(relPath, pPath) -} - -func (rfs *ReverseFS) openDirPlaintextnames(relPath string, entries []fuse.DirEntry) ([]fuse.DirEntry, fuse.Status) { - if relPath != "" || rfs.args.ConfigCustom { - return entries, fuse.OK - } - // We are in the root dir and the default config file name - // ".gocryptfs.reverse.conf" is used. We map it to "gocryptfs.conf". - dupe := -1 - status := fuse.OK - for i := range entries { - if entries[i].Name == configfile.ConfReverseName { - entries[i].Name = configfile.ConfDefaultName - } else if entries[i].Name == configfile.ConfDefaultName { - dupe = i - } - } - if dupe >= 0 { - // Warn the user loudly: The gocryptfs.conf_NAME_COLLISION file will - // throw ENOENT errors that are hard to miss. - tlog.Warn.Printf("The file %q is mapped to %q and shadows another file. Please rename %q in directory %q.", - configfile.ConfReverseName, configfile.ConfDefaultName, configfile.ConfDefaultName, rfs.args.Cipherdir) - entries[dupe].Name = "gocryptfs.conf_NAME_COLLISION_" + fmt.Sprintf("%d", cryptocore.RandUint64()) - } - return entries, status -} - -// OpenDir - FUSE readdir call -func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) { - ftype, excluded, relPath, err := rfs.getFileInfo(cipherPath) - if excluded { - return nil, fuse.ENOENT - } - if err != nil { - return nil, fuse.ToStatus(err) - } - if ftype != typeRegular { - return nil, fuse.ENOTDIR - } - // Read plaintext dir - dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, filepath.Dir(relPath)) - if err != nil { - return nil, fuse.ToStatus(err) - } - fd, err := syscallcompat.Openat(dirfd, filepath.Base(relPath), syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) - syscall.Close(dirfd) - if err != nil { - return nil, fuse.ToStatus(err) - } - entries, err := syscallcompat.Getdents(fd) - syscall.Close(fd) - if err != nil { - return nil, fuse.ToStatus(err) - } - if rfs.args.PlaintextNames { - var status fuse.Status - entries, status = rfs.openDirPlaintextnames(cipherPath, entries) - if !status.Ok() { - return nil, status - } - entries = rfs.excludeDirEntries(relPath, entries) - return entries, fuse.OK - } - // Filter out excluded entries - entries = rfs.excludeDirEntries(relPath, entries) - // Allocate maximum possible number of virtual files. - // If all files have long names we need a virtual ".name" file for each, - // plus one for gocryptfs.diriv. - virtualFiles := make([]fuse.DirEntry, len(entries)+1) - // Virtual gocryptfs.diriv file - virtualFiles[0] = fuse.DirEntry{ - Mode: virtualFileMode, - Name: nametransform.DirIVFilename, - } - // Actually used entries - nVirtual := 1 - - // Encrypt names - dirIV := pathiv.Derive(cipherPath, pathiv.PurposeDirIV) - for i := range entries { - var cName string - // ".gocryptfs.reverse.conf" in the root directory is mapped to "gocryptfs.conf" - if cipherPath == "" && entries[i].Name == configfile.ConfReverseName && - !rfs.args.ConfigCustom { - cName = configfile.ConfDefaultName - } else { - cName = rfs.nameTransform.EncryptName(entries[i].Name, dirIV) - if len(cName) > unix.NAME_MAX { - cName = rfs.nameTransform.HashLongName(cName) - dotNameFile := fuse.DirEntry{ - Mode: virtualFileMode, - Name: cName + nametransform.LongNameSuffix, - } - virtualFiles[nVirtual] = dotNameFile - nVirtual++ - } - } - entries[i].Name = cName - } - // Add virtual files - entries = append(entries, virtualFiles[:nVirtual]...) - return entries, fuse.OK -} - -// excludeDirEntries filters out directory entries that are "-exclude"d. -// pDir is the relative plaintext path to the directory these entries are -// from. The entries should be plaintext files. -func (rfs *ReverseFS) excludeDirEntries(pDir string, entries []fuse.DirEntry) (filtered []fuse.DirEntry) { - if rfs.excluder == nil { - return entries - } - filtered = make([]fuse.DirEntry, 0, len(entries)) - for _, entry := range entries { - // filepath.Join handles the case of pDir="" correctly: - // Join("", "foo") -> "foo". This does not: pDir + "/" + name" - p := filepath.Join(pDir, entry.Name) - if rfs.isExcludedPlain(p) { - // Skip file - continue - } - filtered = append(filtered, entry) - } - return filtered -} - -// StatFs - FUSE call. Returns information about the filesystem (free space -// etc). -// Securing statfs against symlink races seems to be more trouble than -// it's worth, so we just ignore the path and always return info about the -// backing storage root dir. -func (rfs *ReverseFS) StatFs(relPath string) *fuse.StatfsOut { - _, excluded, _, err := rfs.getFileInfo(relPath) - if excluded || err != nil { - return nil - } - var s syscall.Statfs_t - err = syscall.Statfs(rfs.args.Cipherdir, &s) - if err != nil { - return nil - } - out := &fuse.StatfsOut{} - out.FromStatfsT(&s) - return out -} - -// Readlink - FUSE call -func (rfs *ReverseFS) Readlink(relPath string, context *fuse.Context) (string, fuse.Status) { - ftype, excluded, pPath, err := rfs.getFileInfo(relPath) - if excluded { - return "", fuse.ENOENT - } - if err != nil { - return "", fuse.ToStatus(err) - } - if ftype != typeRegular { - return "", fuse.EINVAL - } - dirfd, name, err := rfs.openBackingDir(pPath) - if err != nil { - return "", fuse.ToStatus(err) - } - // read the link target using Readlinkat - plainTarget, err := syscallcompat.Readlinkat(dirfd, name) - syscall.Close(dirfd) - if err != nil { - return "", fuse.ToStatus(err) - } - if rfs.args.PlaintextNames { - return plainTarget, fuse.OK - } - nonce := pathiv.Derive(relPath, pathiv.PurposeSymlinkIV) - // Symlinks are encrypted like file contents and base64-encoded - cBinTarget := rfs.contentEnc.EncryptBlockNonce([]byte(plainTarget), 0, nil, nonce) - cTarget := rfs.nameTransform.B64EncodeToString(cBinTarget) - // The kernel will reject a symlink target above 4096 chars and return - // and I/O error to the user. Better emit the proper error ourselves. - if len(cTarget) > syscallcompat.PATH_MAX { - return "", fuse.Status(syscall.ENAMETOOLONG) - } - return cTarget, fuse.OK -} diff --git a/internal/fusefrontend_reverse/rfs_test.go b/internal/fusefrontend_reverse/rfs_test.go deleted file mode 100644 index eccaab9..0000000 --- a/internal/fusefrontend_reverse/rfs_test.go +++ /dev/null @@ -1,124 +0,0 @@ -package fusefrontend_reverse - -import ( - "testing" - - "github.com/rfjakob/gocryptfs/internal/configfile" - "github.com/rfjakob/gocryptfs/internal/nametransform" -) - -func TestShouldDetectDirIV(t *testing.T) { - var rfs ReverseFS - ftype := rfs.getFileType("some/path/" + nametransform.DirIVFilename) - if ftype != typeDiriv { - t.Errorf("Expecting %d, got %d\n", typeDiriv, ftype) - } -} - -func TestShouldDetectNameFile(t *testing.T) { - var rfs ReverseFS - ftype := rfs.getFileType("dir1/dir2/gocryptfs.longname.URrM8kgxTKYMgCk4hKk7RO9Lcfr30XQof4L_5bD9Iro=" + nametransform.LongNameSuffix) - if ftype != typeName { - t.Errorf("Expecting %d, got %d\n", typeName, ftype) - } -} - -func TestShouldDetectConfigFile(t *testing.T) { - var rfs ReverseFS - ftype := rfs.getFileType(configfile.ConfDefaultName) - if ftype != typeConfig { - t.Errorf("Expecting %d, got %d\n", typeConfig, ftype) - } -} - -func TestShouldDetectRegularFile(t *testing.T) { - var rfs ReverseFS - ftype := rfs.getFileType("documents/text_file.txt") - if ftype != typeRegular { - t.Errorf("Expecting %d, got %d\n", typeRegular, ftype) - } -} - -// Note: For path exclusion, see also the integration tests in -// tests/reverse/exclude_test.go -func TestShouldNotCallIgnoreParserForTranslatedConfig(t *testing.T) { - rfs, ignorerMock := createRFSWithMocks() - - ftype, excluded, _, err := rfs.getFileInfo(configfile.ConfDefaultName) - if err != nil { - t.Errorf("Unexpected error %q\n", err) - } - if ftype != typeConfig { - t.Errorf("Wrong file type, expecting %d, got %d\n", typeConfig, ftype) - } - if excluded { - t.Error("Should not exclude translated config") - } - if ignorerMock.calledWith != "" { - t.Error("Should not call IgnoreParser for translated config") - } -} - -func TestShouldCheckIfParentIsExcludedForDirIV(t *testing.T) { - rfs, ignorerMock := createRFSWithMocks() - path := "dir" - ignorerMock.toExclude = "mockdecrypt_dir" - dirIV := path + "/" + nametransform.DirIVFilename - - ftype, excluded, _, err := rfs.getFileInfo(dirIV) - if err != nil { - t.Errorf("Unexpected error %q\n", err) - } - if ftype != typeDiriv { - t.Errorf("Wrong file type, expecting %d, got %d\n", typeDiriv, ftype) - } - if !excluded { - t.Error("Should have excluded DirIV based on parent") - } - if ignorerMock.calledWith != "mockdecrypt_dir" { - t.Errorf("Should have checked parent dir, checked %q", ignorerMock.calledWith) - } -} - -func TestShouldCheckIfParentIsExcludedForLongName(t *testing.T) { - rfs, ignorerMock := createRFSWithMocks() - path := "parent" - ignorerMock.toExclude = "mockdecrypt_parent" - dirIV := path + "/" + "gocryptfs.longname.fake.name" - - ftype, excluded, _, err := rfs.getFileInfo(dirIV) - if err != nil { - t.Errorf("Unexpected error %q\n", err) - } - if ftype != typeName { - t.Errorf("Wrong file type, expecting %d, got %d\n", typeName, ftype) - } - if !excluded { - t.Error("Should have excluded LongName based on parent") - } - if ignorerMock.calledWith != "mockdecrypt_parent" { - t.Errorf("Should have checked parent dir, checked %q", ignorerMock.calledWith) - } -} - -func TestShouldDecryptPathAndReturnTrueForExcludedPath(t *testing.T) { - rfs, ignorerMock := createRFSWithMocks() - ignorerMock.toExclude = "mockdecrypt_file.txt" - - ftype, excluded, pPath, err := rfs.getFileInfo("file.txt") - if err != nil { - t.Errorf("Unexpected error %q\n", err) - } - if ftype != typeRegular { - t.Errorf("Wrong file type, expecting %d, got %d\n", typeRegular, ftype) - } - if !excluded { - t.Error("Should have excluded") - } - if pPath != "mockdecrypt_file.txt" { - t.Errorf("Wrong pPath returned, got %q\n", pPath) - } - if ignorerMock.calledWith != "mockdecrypt_file.txt" { - t.Error("Didn't call IgnoreParser with decrypted path") - } -} diff --git a/internal/fusefrontend_reverse/rpath.go b/internal/fusefrontend_reverse/rpath.go deleted file mode 100644 index 2da8379..0000000 --- a/internal/fusefrontend_reverse/rpath.go +++ /dev/null @@ -1,113 +0,0 @@ -package fusefrontend_reverse - -import ( - "encoding/base64" - "path/filepath" - "strings" - "syscall" - - "github.com/rfjakob/gocryptfs/internal/nametransform" - "github.com/rfjakob/gocryptfs/internal/pathiv" - "github.com/rfjakob/gocryptfs/internal/syscallcompat" - "github.com/rfjakob/gocryptfs/internal/tlog" -) - -// abs basically returns storage dir + "/" + relPath. -// It takes an error parameter so it can directly wrap decryptPath like this: -// a, err := rfs.abs(rfs.decryptPath(relPath)) -// abs never generates an error on its own. In other words, abs(p, nil) never -// fails. -func (rfs *ReverseFS) abs(relPath string, err error) (string, error) { - if err != nil { - return "", err - } - return filepath.Join(rfs.args.Cipherdir, relPath), nil -} - -// rDecryptName decrypts the ciphertext name "cName", given the dirIV of the -// directory "cName" lies in. The relative plaintext path to the directory -// "pDir" is used if a "gocryptfs.longname.XYZ.name" must be resolved. -func (rfs *ReverseFS) rDecryptName(cName string, dirIV []byte, pDir string) (pName string, err error) { - nameType := nametransform.NameType(cName) - if nameType == nametransform.LongNameNone { - pName, err = rfs.nameTransform.DecryptName(cName, dirIV) - if err != nil { - // We get lots of decrypt requests for names like ".Trash" that - // are invalid base64. Convert them to ENOENT so the correct - // error gets returned to the user. - if _, ok := err.(base64.CorruptInputError); ok { - return "", syscall.ENOENT - } - // Stat attempts on the link target of encrypted symlinks. - // These are always valid base64 but the length is not a - // multiple of 16. - if err == syscall.EBADMSG { - return "", syscall.ENOENT - } - return "", err - } - } else if nameType == nametransform.LongNameContent { - pName, err = rfs.findLongnameParent(pDir, dirIV, cName) - if err != nil { - return "", err - } - } else { - // It makes no sense to decrypt a ".name" file. This is a virtual file - // that has no representation in the plaintext filesystem. ".name" - // files should have already been handled in virtualfile.go. - tlog.Warn.Printf("rDecryptName: cannot decrypt virtual file %q", cName) - return "", syscall.EINVAL - } - return pName, nil -} - -// decryptPath decrypts a relative ciphertext path to a relative plaintext -// path. -func (rfs *ReverseFS) decryptPath(relPath string) (string, error) { - if rfs.args.PlaintextNames || relPath == "" { - return relPath, nil - } - // Check if the parent dir is in the cache - cDir := nametransform.Dir(relPath) - dirIV, pDir := rPathCache.lookup(cDir) - if dirIV != nil { - cName := filepath.Base(relPath) - pName, err := rfs.rDecryptName(cName, dirIV, pDir) - if err != nil { - return "", err - } - return filepath.Join(pDir, pName), nil - } - parts := strings.Split(relPath, "/") - var transformedParts []string - for i := range parts { - // Start at the top and recurse - currentCipherDir := filepath.Join(parts[:i]...) - currentPlainDir := filepath.Join(transformedParts[:i]...) - dirIV = pathiv.Derive(currentCipherDir, pathiv.PurposeDirIV) - transformedPart, err := rfs.rDecryptName(parts[i], dirIV, currentPlainDir) - if err != nil { - return "", err - } - transformedParts = append(transformedParts, transformedPart) - } - pRelPath := filepath.Join(transformedParts...) - rPathCache.store(cDir, dirIV, nametransform.Dir(pRelPath)) - return pRelPath, nil -} - -// openBackingDir receives an already decrypted relative path -// "pRelPath", opens the directory that contains the target file/dir -// and returns the fd to the directory and the decrypted name of the -// target file. The fd/name pair is intended for use with fchownat and -// friends. -func (rfs *ReverseFS) openBackingDir(pRelPath string) (dirfd int, pName string, err error) { - // Open directory, safe against symlink races - pDir := filepath.Dir(pRelPath) - dirfd, err = syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, pDir) - if err != nil { - return -1, "", err - } - pName = filepath.Base(pRelPath) - return dirfd, pName, nil -} diff --git a/internal/fusefrontend_reverse/rpath_cache.go b/internal/fusefrontend_reverse/rpath_cache.go deleted file mode 100644 index 221f578..0000000 --- a/internal/fusefrontend_reverse/rpath_cache.go +++ /dev/null @@ -1,45 +0,0 @@ -package fusefrontend_reverse - -import ( - "sync" -) - -// rPathCacheContainer is a simple one entry path cache. Because the dirIV -// is generated deterministically from the directory path, there is no need -// to ever invalidate entries. -type rPathCacheContainer struct { - sync.Mutex - // Relative ciphertext path to the directory - cPath string - // Relative plaintext path - pPath string - // Directory IV of the directory - dirIV []byte -} - -// lookup relative ciphertext path "cPath". Returns dirIV, relative -// plaintext path. -func (c *rPathCacheContainer) lookup(cPath string) ([]byte, string) { - c.Lock() - defer c.Unlock() - if cPath == c.cPath { - // hit - return c.dirIV, c.pPath - } - // miss - return nil, "" -} - -// store - write entry for the directory at relative ciphertext path "cPath" -// into the cache. -// "dirIV" = directory IV of the directory, "pPath" = relative plaintext path -func (c *rPathCacheContainer) store(cPath string, dirIV []byte, pPath string) { - c.Lock() - defer c.Unlock() - c.cPath = cPath - c.dirIV = dirIV - c.pPath = pPath -} - -// rPathCache: see rPathCacheContainer above for a detailed description -var rPathCache rPathCacheContainer diff --git a/internal/fusefrontend_reverse/virtualfile.go b/internal/fusefrontend_reverse/virtualfile.go deleted file mode 100644 index f258e7f..0000000 --- a/internal/fusefrontend_reverse/virtualfile.go +++ /dev/null @@ -1,110 +0,0 @@ -package fusefrontend_reverse - -import ( - "log" - "path/filepath" - "syscall" - - "golang.org/x/sys/unix" - - "github.com/hanwen/go-fuse/v2/fuse" - "github.com/hanwen/go-fuse/v2/fuse/nodefs" - - "github.com/rfjakob/gocryptfs/internal/inomap" - "github.com/rfjakob/gocryptfs/internal/nametransform" - "github.com/rfjakob/gocryptfs/internal/pathiv" - "github.com/rfjakob/gocryptfs/internal/syscallcompat" - "github.com/rfjakob/gocryptfs/internal/tlog" -) - -const ( - // virtualFileMode is the mode to use for virtual files (gocryptfs.diriv and - // *.name). They are always readable, as stated in func Access - virtualFileMode = syscall.S_IFREG | 0444 - // We use inomap's `Tag` feature to generate unique inode numbers for - // virtual files. These are the tags we use. - inoTagDirIV = 1 - inoTagNameFile = 2 -) - -func (rfs *ReverseFS) newDirIVFile(cRelPath string) (nodefs.File, fuse.Status) { - cDir := nametransform.Dir(cRelPath) - dir, err := rfs.decryptPath(cDir) - if err != nil { - return nil, fuse.ToStatus(err) - } - iv := pathiv.Derive(cDir, pathiv.PurposeDirIV) - return rfs.newVirtualFile(iv, rfs.args.Cipherdir, dir, inoTagDirIV) -} - -type virtualFile struct { - // Embed nodefs.defaultFile for a ENOSYS implementation of all methods - nodefs.File - // pointer to parent filesystem - rfs *ReverseFS - // file content - content []byte - // backing directory - cipherdir string - // path to a parent file (relative to cipherdir) - parentFile string - // inomap `Tag`. - // Depending on the file type, either `inoTagDirIV` or `inoTagNameFile`. - inoTag uint8 -} - -// newVirtualFile creates a new in-memory file that does not have a representation -// on disk. "content" is the file content. Timestamps and file owner are copied -// from "parentFile" (plaintext path relative to "cipherdir"). -// For a "gocryptfs.diriv" file, you would use the parent directory as -// "parentFile". -func (rfs *ReverseFS) newVirtualFile(content []byte, cipherdir string, parentFile string, inoTag uint8) (nodefs.File, fuse.Status) { - if inoTag == 0 { - log.Panicf("BUG: inoTag for virtual file is zero - this will cause ino collisions!") - } - return &virtualFile{ - File: nodefs.NewDefaultFile(), - rfs: rfs, - content: content, - cipherdir: cipherdir, - parentFile: parentFile, - inoTag: inoTag, - }, fuse.OK -} - -// Read - FUSE call -func (f *virtualFile) Read(buf []byte, off int64) (resultData fuse.ReadResult, status fuse.Status) { - if off >= int64(len(f.content)) { - return nil, fuse.OK - } - end := int(off) + len(buf) - if end > len(f.content) { - end = len(f.content) - } - return fuse.ReadResultData(f.content[off:end]), fuse.OK -} - -// GetAttr - FUSE call -func (f *virtualFile) GetAttr(a *fuse.Attr) fuse.Status { - dir := filepath.Dir(f.parentFile) - dirfd, err := syscallcompat.OpenDirNofollow(f.cipherdir, dir) - if err != nil { - return fuse.ToStatus(err) - } - defer syscall.Close(dirfd) - name := filepath.Base(f.parentFile) - var st2 unix.Stat_t - err = syscallcompat.Fstatat(dirfd, name, &st2, unix.AT_SYMLINK_NOFOLLOW) - if err != nil { - tlog.Debug.Printf("GetAttr: Fstatat %q: %v\n", f.parentFile, err) - return fuse.ToStatus(err) - } - st := syscallcompat.Unix2syscall(st2) - q := inomap.NewQIno(uint64(st.Dev), f.inoTag, uint64(st.Ino)) - st.Ino = f.rfs.inoMap.Translate(q) - st.Size = int64(len(f.content)) - st.Mode = virtualFileMode - st.Nlink = 1 - a.FromStat(&st) - return fuse.OK -} diff --git a/internal/fusefrontend_reverse_v1api/ctlsock_interface.go b/internal/fusefrontend_reverse_v1api/ctlsock_interface.go new file mode 100644 index 0000000..f7b8afd --- /dev/null +++ b/internal/fusefrontend_reverse_v1api/ctlsock_interface.go @@ -0,0 +1,38 @@ +package fusefrontend_reverse + +import ( + "path/filepath" + "strings" + + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/internal/ctlsocksrv" + "github.com/rfjakob/gocryptfs/internal/pathiv" +) + +var _ ctlsocksrv.Interface = &ReverseFS{} // Verify that interface is implemented. + +// EncryptPath implements ctlsock.Backend. +// This is used for the control socket and for the "-exclude" logic. +func (rfs *ReverseFS) EncryptPath(plainPath string) (string, error) { + if rfs.args.PlaintextNames || plainPath == "" { + return plainPath, nil + } + cipherPath := "" + parts := strings.Split(plainPath, "/") + for _, part := range parts { + dirIV := pathiv.Derive(cipherPath, pathiv.PurposeDirIV) + encryptedPart := rfs.nameTransform.EncryptName(part, dirIV) + if rfs.args.LongNames && len(encryptedPart) > unix.NAME_MAX { + encryptedPart = rfs.nameTransform.HashLongName(encryptedPart) + } + cipherPath = filepath.Join(cipherPath, encryptedPart) + } + return cipherPath, nil +} + +// DecryptPath implements ctlsock.Backend +func (rfs *ReverseFS) DecryptPath(cipherPath string) (string, error) { + p, err := rfs.decryptPath(cipherPath) + return p, err +} diff --git a/internal/fusefrontend_reverse_v1api/excluder.go b/internal/fusefrontend_reverse_v1api/excluder.go new file mode 100644 index 0000000..c44816c --- /dev/null +++ b/internal/fusefrontend_reverse_v1api/excluder.go @@ -0,0 +1,66 @@ +package fusefrontend_reverse + +import ( + "io/ioutil" + "os" + "strings" + + "github.com/rfjakob/gocryptfs/internal/exitcodes" + "github.com/rfjakob/gocryptfs/internal/fusefrontend" + "github.com/rfjakob/gocryptfs/internal/tlog" + + "github.com/sabhiram/go-gitignore" +) + +// prepareExcluder creates an object to check if paths are excluded +// based on the patterns specified in the command line. +func (rfs *ReverseFS) prepareExcluder(args fusefrontend.Args) { + if len(args.Exclude) > 0 || len(args.ExcludeWildcard) > 0 || len(args.ExcludeFrom) > 0 { + excluder, err := ignore.CompileIgnoreLines(getExclusionPatterns(args)...) + if err != nil { + tlog.Fatal.Printf("Error compiling exclusion rules: %q", err) + os.Exit(exitcodes.ExcludeError) + } + rfs.excluder = excluder + } +} + +// getExclusionPatters prepares a list of patterns to be excluded. +// Patterns passed in the -exclude command line option are prefixed +// with a leading '/' to preserve backwards compatibility (before +// wildcard matching was implemented, exclusions always were matched +// against the full path). +func getExclusionPatterns(args fusefrontend.Args) []string { + patterns := make([]string, len(args.Exclude)+len(args.ExcludeWildcard)) + // add -exclude + for i, p := range args.Exclude { + patterns[i] = "/" + p + } + // add -exclude-wildcard + copy(patterns[len(args.Exclude):], args.ExcludeWildcard) + // add -exclude-from + for _, file := range args.ExcludeFrom { + lines, err := getLines(file) + if err != nil { + tlog.Fatal.Printf("Error reading exclusion patterns: %q", err) + os.Exit(exitcodes.ExcludeError) + } + patterns = append(patterns, lines...) + } + return patterns +} + +// getLines reads a file and splits it into lines +func getLines(file string) ([]string, error) { + buffer, err := ioutil.ReadFile(file) + if err != nil { + return nil, err + } + return strings.Split(string(buffer), "\n"), nil +} + +// isExcludedPlain finds out if the plaintext path "pPath" is +// excluded (used when -exclude is passed by the user). +func (rfs *ReverseFS) isExcludedPlain(pPath string) bool { + return rfs.excluder != nil && rfs.excluder.MatchesPath(pPath) +} diff --git a/internal/fusefrontend_reverse_v1api/excluder_test.go b/internal/fusefrontend_reverse_v1api/excluder_test.go new file mode 100644 index 0000000..e19b99f --- /dev/null +++ b/internal/fusefrontend_reverse_v1api/excluder_test.go @@ -0,0 +1,84 @@ +package fusefrontend_reverse + +import ( + "io/ioutil" + "os" + "reflect" + "testing" + + "github.com/rfjakob/gocryptfs/internal/fusefrontend" +) + +func TestShouldNoCreateExcluderIfNoPattersWereSpecified(t *testing.T) { + var rfs ReverseFS + var args fusefrontend.Args + rfs.prepareExcluder(args) + if rfs.excluder != nil { + t.Error("Should not have created excluder") + } +} + +func TestShouldPrefixExcludeValuesWithSlash(t *testing.T) { + var args fusefrontend.Args + args.Exclude = []string{"file1", "dir1/file2.txt"} + args.ExcludeWildcard = []string{"*~", "build/*.o"} + + expected := []string{"/file1", "/dir1/file2.txt", "*~", "build/*.o"} + + patterns := getExclusionPatterns(args) + if !reflect.DeepEqual(patterns, expected) { + t.Errorf("expected %q, got %q", expected, patterns) + } +} + +func TestShouldReadExcludePatternsFromFiles(t *testing.T) { + tmpfile1, err := ioutil.TempFile("", "excludetest") + if err != nil { + t.Fatal(err) + } + exclude1 := tmpfile1.Name() + defer os.Remove(exclude1) + defer tmpfile1.Close() + + tmpfile2, err := ioutil.TempFile("", "excludetest") + if err != nil { + t.Fatal(err) + } + exclude2 := tmpfile2.Name() + defer os.Remove(exclude2) + defer tmpfile2.Close() + + tmpfile1.WriteString("file1.1\n") + tmpfile1.WriteString("file1.2\n") + tmpfile2.WriteString("file2.1\n") + tmpfile2.WriteString("file2.2\n") + + var args fusefrontend.Args + args.ExcludeWildcard = []string{"cmdline1"} + args.ExcludeFrom = []string{exclude1, exclude2} + + // An empty string is returned for the last empty line + // It's ignored when the patterns are actually compiled + expected := []string{"cmdline1", "file1.1", "file1.2", "", "file2.1", "file2.2", ""} + + patterns := getExclusionPatterns(args) + if !reflect.DeepEqual(patterns, expected) { + t.Errorf("expected %q, got %q", expected, patterns) + } +} + +func TestShouldReturnFalseIfThereAreNoExclusions(t *testing.T) { + var rfs ReverseFS + if rfs.isExcludedPlain("any/path") { + t.Error("Should not exclude any path if no exclusions were specified") + } +} + +func TestShouldCallIgnoreParserToCheckExclusion(t *testing.T) { + rfs, ignorerMock := createRFSWithMocks() + + rfs.isExcludedPlain("some/path") + if ignorerMock.calledWith != "some/path" { + t.Error("Failed to call IgnoreParser") + } +} diff --git a/internal/fusefrontend_reverse_v1api/mocks_test.go b/internal/fusefrontend_reverse_v1api/mocks_test.go new file mode 100644 index 0000000..fd2f940 --- /dev/null +++ b/internal/fusefrontend_reverse_v1api/mocks_test.go @@ -0,0 +1,32 @@ +package fusefrontend_reverse + +import ( + "github.com/rfjakob/gocryptfs/internal/nametransform" +) + +type IgnoreParserMock struct { + toExclude string + calledWith string +} + +func (parser *IgnoreParserMock) MatchesPath(f string) bool { + parser.calledWith = f + return f == parser.toExclude +} + +type NameTransformMock struct { + nametransform.NameTransform +} + +func (n *NameTransformMock) DecryptName(cipherName string, iv []byte) (string, error) { + return "mockdecrypt_" + cipherName, nil +} + +func createRFSWithMocks() (*ReverseFS, *IgnoreParserMock) { + ignorerMock := &IgnoreParserMock{} + nameTransformMock := &NameTransformMock{} + var rfs ReverseFS + rfs.excluder = ignorerMock + rfs.nameTransform = nameTransformMock + return &rfs, ignorerMock +} diff --git a/internal/fusefrontend_reverse_v1api/reverse_longnames.go b/internal/fusefrontend_reverse_v1api/reverse_longnames.go new file mode 100644 index 0000000..f1576d2 --- /dev/null +++ b/internal/fusefrontend_reverse_v1api/reverse_longnames.go @@ -0,0 +1,126 @@ +package fusefrontend_reverse + +import ( + "log" + "path/filepath" + "sync" + "syscall" + "time" + + "golang.org/x/sys/unix" + + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/hanwen/go-fuse/v2/fuse/nodefs" + + "github.com/rfjakob/gocryptfs/internal/nametransform" + "github.com/rfjakob/gocryptfs/internal/pathiv" + "github.com/rfjakob/gocryptfs/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/internal/tlog" +) + +const ( + // File names are padded to 16-byte multiples, encrypted and + // base64-encoded. We can encode at most 176 bytes to stay below the 255 + // bytes limit: + // * base64(176 bytes) = 235 bytes + // * base64(192 bytes) = 256 bytes (over 255!) + // But the PKCS#7 padding is at least one byte. This means we can only use + // 175 bytes for the file name. + shortNameMax = 175 +) + +// longnameParentCache maps dir+"/"+longname to plaintextname. +// Yes, the combination of relative plaintext dir path and encrypted +// longname is strange, but works fine as a map index. +var longnameParentCache map[string]string +var longnameCacheLock sync.Mutex + +// Very simple cache cleaner: Nuke it every hour +func longnameCacheCleaner() { + for { + time.Sleep(time.Hour) + longnameCacheLock.Lock() + longnameParentCache = map[string]string{} + longnameCacheLock.Unlock() + } +} + +func initLongnameCache() { + if longnameParentCache != nil { + return + } + longnameParentCache = map[string]string{} + go longnameCacheCleaner() +} + +// findLongnameParent converts "longname" = "gocryptfs.longname.XYZ" to the +// plaintext name. "dir" = relative plaintext path to the directory the +// longname file is in, "dirIV" = directory IV of the directory. +func (rfs *ReverseFS) findLongnameParent(dir string, dirIV []byte, longname string) (plaintextName string, err error) { + longnameCacheLock.Lock() + hit := longnameParentCache[dir+"/"+longname] + longnameCacheLock.Unlock() + if hit != "" { + return hit, nil + } + dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, filepath.Dir(dir)) + if err != nil { + tlog.Warn.Printf("findLongnameParent: OpenDirNofollow failed: %v\n", err) + return "", err + } + fd, err := syscallcompat.Openat(dirfd, filepath.Base(dir), syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) + syscall.Close(dirfd) + if err != nil { + tlog.Warn.Printf("findLongnameParent: Openat failed: %v\n", err) + return "", err + } + dirEntries, err := syscallcompat.Getdents(fd) + syscall.Close(fd) + if err != nil { + tlog.Warn.Printf("findLongnameParent: Getdents failed: %v\n", err) + return "", err + } + longnameCacheLock.Lock() + defer longnameCacheLock.Unlock() + for _, entry := range dirEntries { + plaintextName := entry.Name + if len(plaintextName) <= shortNameMax { + continue + } + cName := rfs.nameTransform.EncryptName(plaintextName, dirIV) + if len(cName) <= unix.NAME_MAX { + // Entry should have been skipped by the "continue" above + log.Panic("logic error or wrong shortNameMax constant?") + } + hName := rfs.nameTransform.HashLongName(cName) + longnameParentCache[dir+"/"+hName] = plaintextName + if longname == hName { + hit = plaintextName + } + } + if hit == "" { + return "", syscall.ENOENT + } + return hit, nil +} + +func (rfs *ReverseFS) newNameFile(relPath string) (nodefs.File, fuse.Status) { + dotName := filepath.Base(relPath) // gocryptfs.longname.XYZ.name + longname := nametransform.RemoveLongNameSuffix(dotName) // gocryptfs.longname.XYZ + // cipher directory + cDir := nametransform.Dir(relPath) + // plain directory + pDir, err := rfs.decryptPath(cDir) + if err != nil { + return nil, fuse.ToStatus(err) + } + dirIV := pathiv.Derive(cDir, pathiv.PurposeDirIV) + // plain name + pName, err := rfs.findLongnameParent(pDir, dirIV, longname) + if err != nil { + return nil, fuse.ToStatus(err) + } + content := []byte(rfs.nameTransform.EncryptName(pName, dirIV)) + parentFile := filepath.Join(pDir, pName) + return rfs.newVirtualFile(content, rfs.args.Cipherdir, parentFile, inoTagNameFile) +} diff --git a/internal/fusefrontend_reverse_v1api/rfile.go b/internal/fusefrontend_reverse_v1api/rfile.go new file mode 100644 index 0000000..68f7309 --- /dev/null +++ b/internal/fusefrontend_reverse_v1api/rfile.go @@ -0,0 +1,209 @@ +package fusefrontend_reverse + +import ( + "bytes" + "io" + "os" + "path/filepath" + "syscall" + + // In newer Go versions, this has moved to just "sync/syncmap". + "golang.org/x/sync/syncmap" + + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/hanwen/go-fuse/v2/fuse/nodefs" + + "github.com/rfjakob/gocryptfs/internal/contentenc" + "github.com/rfjakob/gocryptfs/internal/pathiv" + "github.com/rfjakob/gocryptfs/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/internal/tlog" +) + +type reverseFile struct { + // Embed nodefs.defaultFile for a ENOSYS implementation of all methods + nodefs.File + // Backing FD + fd *os.File + // File header (contains the IV) + header contentenc.FileHeader + // IV for block 0 + block0IV []byte + // Content encryption helper + contentEnc *contentenc.ContentEnc +} + +var inodeTable syncmap.Map + +// newFile receives a ciphered path "relPath" and its corresponding +// decrypted path "pRelPath", opens it and returns a reverseFile +// object. The backing file descriptor is always read-only. +func (rfs *ReverseFS) newFile(relPath string, pRelPath string) (*reverseFile, fuse.Status) { + if rfs.isExcludedPlain(pRelPath) { + // Excluded paths should have been filtered out beforehand. Better safe + // than sorry. + tlog.Warn.Printf("BUG: newFile: received excluded path %q. This should not happen.", relPath) + return nil, fuse.ENOENT + } + dir := filepath.Dir(pRelPath) + dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, dir) + if err != nil { + return nil, fuse.ToStatus(err) + } + fd, err := syscallcompat.Openat(dirfd, filepath.Base(pRelPath), syscall.O_RDONLY|syscall.O_NOFOLLOW, 0) + syscall.Close(dirfd) + if err != nil { + return nil, fuse.ToStatus(err) + } + var st syscall.Stat_t + err = syscall.Fstat(fd, &st) + if err != nil { + tlog.Warn.Printf("newFile: Fstat error: %v", err) + syscall.Close(fd) + return nil, fuse.ToStatus(err) + } + // Reject access if the file descriptor does not refer to a regular file. + var a fuse.Attr + a.FromStat(&st) + if !a.IsRegular() { + tlog.Warn.Printf("ino%d: newFile: not a regular file", st.Ino) + syscall.Close(fd) + return nil, fuse.ToStatus(syscall.EACCES) + } + // See if we have that inode number already in the table + // (even if Nlink has dropped to 1) + var derivedIVs pathiv.FileIVs + v, found := inodeTable.Load(st.Ino) + if found { + tlog.Debug.Printf("ino%d: newFile: found in the inode table", st.Ino) + derivedIVs = v.(pathiv.FileIVs) + } else { + derivedIVs = pathiv.DeriveFile(relPath) + // Nlink > 1 means there is more than one path to this file. + // Store the derived values so we always return the same data, + // regardless of the path that is used to access the file. + // This means that the first path wins. + if st.Nlink > 1 { + v, found = inodeTable.LoadOrStore(st.Ino, derivedIVs) + if found { + // Another thread has stored a different value before we could. + derivedIVs = v.(pathiv.FileIVs) + } else { + tlog.Debug.Printf("ino%d: newFile: Nlink=%d, stored in the inode table", st.Ino, st.Nlink) + } + } + } + header := contentenc.FileHeader{ + Version: contentenc.CurrentVersion, + ID: derivedIVs.ID, + } + return &reverseFile{ + File: nodefs.NewDefaultFile(), + fd: os.NewFile(uintptr(fd), pRelPath), + header: header, + block0IV: derivedIVs.Block0IV, + contentEnc: rfs.contentEnc, + }, fuse.OK +} + +// GetAttr - FUSE call +// Triggered by fstat() from userspace +func (rf *reverseFile) GetAttr(*fuse.Attr) fuse.Status { + tlog.Debug.Printf("reverseFile.GetAttr fd=%d\n", rf.fd.Fd()) + // The kernel should fall back to stat() + return fuse.ENOSYS +} + +// encryptBlocks - encrypt "plaintext" into a number of ciphertext blocks. +// "plaintext" must already be block-aligned. +func (rf *reverseFile) encryptBlocks(plaintext []byte, firstBlockNo uint64, fileID []byte, block0IV []byte) []byte { + inBuf := bytes.NewBuffer(plaintext) + var outBuf bytes.Buffer + bs := int(rf.contentEnc.PlainBS()) + for blockNo := firstBlockNo; inBuf.Len() > 0; blockNo++ { + inBlock := inBuf.Next(bs) + iv := pathiv.BlockIV(block0IV, blockNo) + outBlock := rf.contentEnc.EncryptBlockNonce(inBlock, blockNo, fileID, iv) + outBuf.Write(outBlock) + } + return outBuf.Bytes() +} + +// readBackingFile: read from the backing plaintext file, encrypt it, return the +// ciphertext. +// "off" ... ciphertext offset (must be >= HEADER_LEN) +// "length" ... ciphertext length +func (rf *reverseFile) readBackingFile(off uint64, length uint64) (out []byte, err error) { + blocks := rf.contentEnc.ExplodeCipherRange(off, length) + + // Read the backing plaintext in one go + alignedOffset, alignedLength := contentenc.JointPlaintextRange(blocks) + plaintext := make([]byte, int(alignedLength)) + n, err := rf.fd.ReadAt(plaintext, int64(alignedOffset)) + if err != nil && err != io.EOF { + tlog.Warn.Printf("readBackingFile: ReadAt: %s", err.Error()) + return nil, err + } + // Truncate buffer down to actually read bytes + plaintext = plaintext[0:n] + + // Encrypt blocks + ciphertext := rf.encryptBlocks(plaintext, blocks[0].BlockNo, rf.header.ID, rf.block0IV) + + // Crop down to the relevant part + lenHave := len(ciphertext) + skip := blocks[0].Skip + endWant := int(skip + length) + if lenHave > endWant { + out = ciphertext[skip:endWant] + } else if lenHave > int(skip) { + out = ciphertext[skip:lenHave] + } // else: out stays empty, file was smaller than the requested offset + + return out, nil +} + +// Read - FUSE call +func (rf *reverseFile) Read(buf []byte, ioff int64) (resultData fuse.ReadResult, status fuse.Status) { + length := uint64(len(buf)) + off := uint64(ioff) + var out bytes.Buffer + var header []byte + + // Synthesize file header + if off < contentenc.HeaderLen { + header = rf.header.Pack() + // Truncate to requested part + end := int(off) + len(buf) + if end > len(header) { + end = len(header) + } + header = header[off:end] + // Write into output buffer and adjust offsets + out.Write(header) + hLen := uint64(len(header)) + off += hLen + length -= hLen + } + + // Read actual file data + if length > 0 { + fileData, err := rf.readBackingFile(off, length) + if err != nil { + return nil, fuse.ToStatus(err) + } + if len(fileData) == 0 { + // If we could not read any actual data, we also don't want to + // return the file header. An empty file stays empty in encrypted + // form. + return nil, fuse.OK + } + out.Write(fileData) + } + + return fuse.ReadResultData(out.Bytes()), fuse.OK +} + +// Release - FUSE call, close file +func (rf *reverseFile) Release() { + rf.fd.Close() +} diff --git a/internal/fusefrontend_reverse_v1api/rfs.go b/internal/fusefrontend_reverse_v1api/rfs.go new file mode 100644 index 0000000..9f7d1f6 --- /dev/null +++ b/internal/fusefrontend_reverse_v1api/rfs.go @@ -0,0 +1,472 @@ +package fusefrontend_reverse + +import ( + "fmt" + "path/filepath" + "syscall" + + "golang.org/x/sys/unix" + + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/hanwen/go-fuse/v2/fuse/nodefs" + "github.com/hanwen/go-fuse/v2/fuse/pathfs" + + "github.com/rfjakob/gocryptfs/internal/configfile" + "github.com/rfjakob/gocryptfs/internal/contentenc" + "github.com/rfjakob/gocryptfs/internal/cryptocore" + "github.com/rfjakob/gocryptfs/internal/fusefrontend" + "github.com/rfjakob/gocryptfs/internal/inomap" + "github.com/rfjakob/gocryptfs/internal/nametransform" + "github.com/rfjakob/gocryptfs/internal/pathiv" + "github.com/rfjakob/gocryptfs/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/internal/tlog" + + "github.com/sabhiram/go-gitignore" +) + +// ReverseFS implements the pathfs.FileSystem interface and provides an +// encrypted view of a plaintext directory. +type ReverseFS struct { + // Embed pathfs.defaultFileSystem for a ENOSYS implementation of all methods + pathfs.FileSystem + // pathfs.loopbackFileSystem, see go-fuse/fuse/pathfs/loopback.go + loopbackfs pathfs.FileSystem + // Stores configuration arguments + args fusefrontend.Args + // Filename encryption helper + nameTransform nametransform.NameTransformer + // Content encryption helper + contentEnc *contentenc.ContentEnc + // Tests whether a path is excluded (hiden) from the user. Used by -exclude. + excluder ignore.IgnoreParser + // inoMap translates inode numbers from different devices to unique inode + // numbers. + inoMap *inomap.InoMap +} + +var _ pathfs.FileSystem = &ReverseFS{} + +// NewFS returns an encrypted FUSE overlay filesystem. +// In this case (reverse mode) the backing directory is plain-text and +// ReverseFS provides an encrypted view. +func NewFS(args fusefrontend.Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *ReverseFS { + initLongnameCache() + fs := &ReverseFS{ + // pathfs.defaultFileSystem returns ENOSYS for all operations + FileSystem: pathfs.NewDefaultFileSystem(), + loopbackfs: pathfs.NewLoopbackFileSystem(args.Cipherdir), + args: args, + nameTransform: n, + contentEnc: c, + inoMap: inomap.New(), + } + fs.prepareExcluder(args) + return fs +} + +// relDir is identical to filepath.Dir excepts that it returns "" when +// filepath.Dir would return ".". +// In the FUSE API, the root directory is called "", and we actually want that. +func relDir(path string) string { + dir := filepath.Dir(path) + if dir == "." { + return "" + } + return dir +} + +// getFileInfo returns information on a ciphertext path "relPath": +// - ftype: file type (as returned by getFileType) +// - excluded: if the path is excluded +// - pPath: if it's not a special file, the decrypted path +// - err: non nil if any error happens +func (rfs *ReverseFS) getFileInfo(relPath string) (ftype fileType, excluded bool, pPath string, err error) { + ftype = rfs.getFileType(relPath) + if ftype == typeConfig { + excluded, pPath, err = false, "", nil + return + } + if ftype == typeDiriv { + parentDir := nametransform.Dir(relPath) + _, excluded, _, err = rfs.getFileInfo(parentDir) + pPath = "" + return + } + if ftype == typeName { + parentDir := nametransform.Dir(relPath) + var parentExcluded bool + _, parentExcluded, _, err = rfs.getFileInfo(parentDir) + if parentExcluded || err != nil { + excluded, pPath = parentExcluded, "" + return + } + relPath = nametransform.RemoveLongNameSuffix(relPath) + } + pPath, err = rfs.decryptPath(relPath) + excluded = err == nil && rfs.isExcludedPlain(pPath) + return +} + +type fileType int + +// Values returned by getFileType +const ( + // A regular file/directory/symlink + typeRegular fileType = iota + // A DirIV (gocryptfs.diriv) file + typeDiriv + // A gocryptfs.longname.*.name file for a file with a long name + typeName + // The config file gocryptfs.conf + typeConfig +) + +// getFileType returns the type of file (one of the fileType constants above). +func (rfs *ReverseFS) getFileType(cPath string) fileType { + if !rfs.args.PlaintextNames { + cName := filepath.Base(cPath) + // Is it a gocryptfs.diriv file? + if cName == nametransform.DirIVFilename { + return typeDiriv + } + // Is it a gocryptfs.longname.*.name file? + if t := nametransform.NameType(cName); t == nametransform.LongNameFilename { + return typeName + } + } + if rfs.isTranslatedConfig(cPath) { + return typeConfig + } + return typeRegular +} + +// isTranslatedConfig returns true if the default config file name is in use +// and the ciphertext path is "gocryptfs.conf". +// "gocryptfs.conf" then maps to ".gocryptfs.reverse.conf" in the plaintext +// directory. +func (rfs *ReverseFS) isTranslatedConfig(relPath string) bool { + if rfs.args.ConfigCustom { + return false + } + if relPath == configfile.ConfDefaultName { + return true + } + return false +} + +// GetAttr - FUSE call +// "relPath" is the relative ciphertext path +func (rfs *ReverseFS) GetAttr(relPath string, context *fuse.Context) (*fuse.Attr, fuse.Status) { + ftype, excluded, pPath, err := rfs.getFileInfo(relPath) + if excluded { + return nil, fuse.ENOENT + } + if err != nil { + return nil, fuse.ToStatus(err) + } + // Handle "gocryptfs.conf" + if ftype == typeConfig { + absConfPath, _ := rfs.abs(configfile.ConfReverseName, nil) + var st syscall.Stat_t + err = syscall.Lstat(absConfPath, &st) + if err != nil { + return nil, fuse.ToStatus(err) + } + rfs.inoMap.TranslateStat(&st) + var a fuse.Attr + a.FromStat(&st) + if rfs.args.ForceOwner != nil { + a.Owner = *rfs.args.ForceOwner + } + return &a, fuse.OK + } + // Handle virtual files (gocryptfs.diriv, *.name) + var f nodefs.File + var status fuse.Status + virtual := false + if ftype == typeDiriv { + virtual = true + f, status = rfs.newDirIVFile(relPath) + } + if ftype == typeName { + virtual = true + f, status = rfs.newNameFile(relPath) + } + if virtual { + if !status.Ok() { + tlog.Warn.Printf("GetAttr %q: newXFile failed: %v\n", relPath, status) + return nil, status + } + var a fuse.Attr + status = f.GetAttr(&a) + if rfs.args.ForceOwner != nil { + a.Owner = *rfs.args.ForceOwner + } + return &a, status + } + // Normal file / directory + dirfd, name, err := rfs.openBackingDir(pPath) + if err != nil { + return nil, fuse.ToStatus(err) + } + // Stat the backing file/dir using Fstatat + var st syscall.Stat_t + { + var st2 unix.Stat_t + err = syscallcompat.Fstatat(dirfd, name, &st2, unix.AT_SYMLINK_NOFOLLOW) + syscall.Close(dirfd) + if err != nil { + return nil, fuse.ToStatus(err) + } + st = syscallcompat.Unix2syscall(st2) + } + rfs.inoMap.TranslateStat(&st) + var a fuse.Attr + a.FromStat(&st) + // Calculate encrypted file size + if a.IsRegular() { + a.Size = rfs.contentEnc.PlainSizeToCipherSize(a.Size) + } else if a.IsSymlink() { + var linkTarget string + var readlinkStatus fuse.Status + + linkTarget, readlinkStatus = rfs.Readlink(relPath, context) + if !readlinkStatus.Ok() { + return nil, readlinkStatus + } + + a.Size = uint64(len(linkTarget)) + } + if rfs.args.ForceOwner != nil { + a.Owner = *rfs.args.ForceOwner + } + return &a, fuse.OK +} + +// Access - FUSE call +func (rfs *ReverseFS) Access(relPath string, mode uint32, context *fuse.Context) fuse.Status { + ftype, excluded, pPath, err := rfs.getFileInfo(relPath) + if excluded { + return fuse.ENOENT + } + if err != nil { + return fuse.ToStatus(err) + } + if ftype != typeRegular { + // access(2) R_OK flag for checking if the file is readable, always 4 as defined in POSIX. + ROK := uint32(0x4) + // Virtual files can always be read and never written + if mode == ROK || mode == 0 { + return fuse.OK + } + return fuse.EPERM + } + dirfd, name, err := rfs.openBackingDir(pPath) + if err != nil { + return fuse.ToStatus(err) + } + err = syscallcompat.Faccessat(dirfd, name, mode) + syscall.Close(dirfd) + return fuse.ToStatus(err) +} + +// Open - FUSE call +func (rfs *ReverseFS) Open(relPath string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) { + ftype, excluded, pPath, err := rfs.getFileInfo(relPath) + if excluded { + return nil, fuse.ENOENT + } + if err != nil { + return nil, fuse.ToStatus(err) + } + if ftype == typeConfig { + return rfs.loopbackfs.Open(configfile.ConfReverseName, flags, context) + } + if ftype == typeDiriv { + return rfs.newDirIVFile(relPath) + } + if ftype == typeName { + return rfs.newNameFile(relPath) + } + return rfs.newFile(relPath, pPath) +} + +func (rfs *ReverseFS) openDirPlaintextnames(relPath string, entries []fuse.DirEntry) ([]fuse.DirEntry, fuse.Status) { + if relPath != "" || rfs.args.ConfigCustom { + return entries, fuse.OK + } + // We are in the root dir and the default config file name + // ".gocryptfs.reverse.conf" is used. We map it to "gocryptfs.conf". + dupe := -1 + status := fuse.OK + for i := range entries { + if entries[i].Name == configfile.ConfReverseName { + entries[i].Name = configfile.ConfDefaultName + } else if entries[i].Name == configfile.ConfDefaultName { + dupe = i + } + } + if dupe >= 0 { + // Warn the user loudly: The gocryptfs.conf_NAME_COLLISION file will + // throw ENOENT errors that are hard to miss. + tlog.Warn.Printf("The file %q is mapped to %q and shadows another file. Please rename %q in directory %q.", + configfile.ConfReverseName, configfile.ConfDefaultName, configfile.ConfDefaultName, rfs.args.Cipherdir) + entries[dupe].Name = "gocryptfs.conf_NAME_COLLISION_" + fmt.Sprintf("%d", cryptocore.RandUint64()) + } + return entries, status +} + +// OpenDir - FUSE readdir call +func (rfs *ReverseFS) OpenDir(cipherPath string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) { + ftype, excluded, relPath, err := rfs.getFileInfo(cipherPath) + if excluded { + return nil, fuse.ENOENT + } + if err != nil { + return nil, fuse.ToStatus(err) + } + if ftype != typeRegular { + return nil, fuse.ENOTDIR + } + // Read plaintext dir + dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, filepath.Dir(relPath)) + if err != nil { + return nil, fuse.ToStatus(err) + } + fd, err := syscallcompat.Openat(dirfd, filepath.Base(relPath), syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0) + syscall.Close(dirfd) + if err != nil { + return nil, fuse.ToStatus(err) + } + entries, err := syscallcompat.Getdents(fd) + syscall.Close(fd) + if err != nil { + return nil, fuse.ToStatus(err) + } + if rfs.args.PlaintextNames { + var status fuse.Status + entries, status = rfs.openDirPlaintextnames(cipherPath, entries) + if !status.Ok() { + return nil, status + } + entries = rfs.excludeDirEntries(relPath, entries) + return entries, fuse.OK + } + // Filter out excluded entries + entries = rfs.excludeDirEntries(relPath, entries) + // Allocate maximum possible number of virtual files. + // If all files have long names we need a virtual ".name" file for each, + // plus one for gocryptfs.diriv. + virtualFiles := make([]fuse.DirEntry, len(entries)+1) + // Virtual gocryptfs.diriv file + virtualFiles[0] = fuse.DirEntry{ + Mode: virtualFileMode, + Name: nametransform.DirIVFilename, + } + // Actually used entries + nVirtual := 1 + + // Encrypt names + dirIV := pathiv.Derive(cipherPath, pathiv.PurposeDirIV) + for i := range entries { + var cName string + // ".gocryptfs.reverse.conf" in the root directory is mapped to "gocryptfs.conf" + if cipherPath == "" && entries[i].Name == configfile.ConfReverseName && + !rfs.args.ConfigCustom { + cName = configfile.ConfDefaultName + } else { + cName = rfs.nameTransform.EncryptName(entries[i].Name, dirIV) + if len(cName) > unix.NAME_MAX { + cName = rfs.nameTransform.HashLongName(cName) + dotNameFile := fuse.DirEntry{ + Mode: virtualFileMode, + Name: cName + nametransform.LongNameSuffix, + } + virtualFiles[nVirtual] = dotNameFile + nVirtual++ + } + } + entries[i].Name = cName + } + // Add virtual files + entries = append(entries, virtualFiles[:nVirtual]...) + return entries, fuse.OK +} + +// excludeDirEntries filters out directory entries that are "-exclude"d. +// pDir is the relative plaintext path to the directory these entries are +// from. The entries should be plaintext files. +func (rfs *ReverseFS) excludeDirEntries(pDir string, entries []fuse.DirEntry) (filtered []fuse.DirEntry) { + if rfs.excluder == nil { + return entries + } + filtered = make([]fuse.DirEntry, 0, len(entries)) + for _, entry := range entries { + // filepath.Join handles the case of pDir="" correctly: + // Join("", "foo") -> "foo". This does not: pDir + "/" + name" + p := filepath.Join(pDir, entry.Name) + if rfs.isExcludedPlain(p) { + // Skip file + continue + } + filtered = append(filtered, entry) + } + return filtered +} + +// StatFs - FUSE call. Returns information about the filesystem (free space +// etc). +// Securing statfs against symlink races seems to be more trouble than +// it's worth, so we just ignore the path and always return info about the +// backing storage root dir. +func (rfs *ReverseFS) StatFs(relPath string) *fuse.StatfsOut { + _, excluded, _, err := rfs.getFileInfo(relPath) + if excluded || err != nil { + return nil + } + var s syscall.Statfs_t + err = syscall.Statfs(rfs.args.Cipherdir, &s) + if err != nil { + return nil + } + out := &fuse.StatfsOut{} + out.FromStatfsT(&s) + return out +} + +// Readlink - FUSE call +func (rfs *ReverseFS) Readlink(relPath string, context *fuse.Context) (string, fuse.Status) { + ftype, excluded, pPath, err := rfs.getFileInfo(relPath) + if excluded { + return "", fuse.ENOENT + } + if err != nil { + return "", fuse.ToStatus(err) + } + if ftype != typeRegular { + return "", fuse.EINVAL + } + dirfd, name, err := rfs.openBackingDir(pPath) + if err != nil { + return "", fuse.ToStatus(err) + } + // read the link target using Readlinkat + plainTarget, err := syscallcompat.Readlinkat(dirfd, name) + syscall.Close(dirfd) + if err != nil { + return "", fuse.ToStatus(err) + } + if rfs.args.PlaintextNames { + return plainTarget, fuse.OK + } + nonce := pathiv.Derive(relPath, pathiv.PurposeSymlinkIV) + // Symlinks are encrypted like file contents and base64-encoded + cBinTarget := rfs.contentEnc.EncryptBlockNonce([]byte(plainTarget), 0, nil, nonce) + cTarget := rfs.nameTransform.B64EncodeToString(cBinTarget) + // The kernel will reject a symlink target above 4096 chars and return + // and I/O error to the user. Better emit the proper error ourselves. + if len(cTarget) > syscallcompat.PATH_MAX { + return "", fuse.Status(syscall.ENAMETOOLONG) + } + return cTarget, fuse.OK +} diff --git a/internal/fusefrontend_reverse_v1api/rfs_test.go b/internal/fusefrontend_reverse_v1api/rfs_test.go new file mode 100644 index 0000000..eccaab9 --- /dev/null +++ b/internal/fusefrontend_reverse_v1api/rfs_test.go @@ -0,0 +1,124 @@ +package fusefrontend_reverse + +import ( + "testing" + + "github.com/rfjakob/gocryptfs/internal/configfile" + "github.com/rfjakob/gocryptfs/internal/nametransform" +) + +func TestShouldDetectDirIV(t *testing.T) { + var rfs ReverseFS + ftype := rfs.getFileType("some/path/" + nametransform.DirIVFilename) + if ftype != typeDiriv { + t.Errorf("Expecting %d, got %d\n", typeDiriv, ftype) + } +} + +func TestShouldDetectNameFile(t *testing.T) { + var rfs ReverseFS + ftype := rfs.getFileType("dir1/dir2/gocryptfs.longname.URrM8kgxTKYMgCk4hKk7RO9Lcfr30XQof4L_5bD9Iro=" + nametransform.LongNameSuffix) + if ftype != typeName { + t.Errorf("Expecting %d, got %d\n", typeName, ftype) + } +} + +func TestShouldDetectConfigFile(t *testing.T) { + var rfs ReverseFS + ftype := rfs.getFileType(configfile.ConfDefaultName) + if ftype != typeConfig { + t.Errorf("Expecting %d, got %d\n", typeConfig, ftype) + } +} + +func TestShouldDetectRegularFile(t *testing.T) { + var rfs ReverseFS + ftype := rfs.getFileType("documents/text_file.txt") + if ftype != typeRegular { + t.Errorf("Expecting %d, got %d\n", typeRegular, ftype) + } +} + +// Note: For path exclusion, see also the integration tests in +// tests/reverse/exclude_test.go +func TestShouldNotCallIgnoreParserForTranslatedConfig(t *testing.T) { + rfs, ignorerMock := createRFSWithMocks() + + ftype, excluded, _, err := rfs.getFileInfo(configfile.ConfDefaultName) + if err != nil { + t.Errorf("Unexpected error %q\n", err) + } + if ftype != typeConfig { + t.Errorf("Wrong file type, expecting %d, got %d\n", typeConfig, ftype) + } + if excluded { + t.Error("Should not exclude translated config") + } + if ignorerMock.calledWith != "" { + t.Error("Should not call IgnoreParser for translated config") + } +} + +func TestShouldCheckIfParentIsExcludedForDirIV(t *testing.T) { + rfs, ignorerMock := createRFSWithMocks() + path := "dir" + ignorerMock.toExclude = "mockdecrypt_dir" + dirIV := path + "/" + nametransform.DirIVFilename + + ftype, excluded, _, err := rfs.getFileInfo(dirIV) + if err != nil { + t.Errorf("Unexpected error %q\n", err) + } + if ftype != typeDiriv { + t.Errorf("Wrong file type, expecting %d, got %d\n", typeDiriv, ftype) + } + if !excluded { + t.Error("Should have excluded DirIV based on parent") + } + if ignorerMock.calledWith != "mockdecrypt_dir" { + t.Errorf("Should have checked parent dir, checked %q", ignorerMock.calledWith) + } +} + +func TestShouldCheckIfParentIsExcludedForLongName(t *testing.T) { + rfs, ignorerMock := createRFSWithMocks() + path := "parent" + ignorerMock.toExclude = "mockdecrypt_parent" + dirIV := path + "/" + "gocryptfs.longname.fake.name" + + ftype, excluded, _, err := rfs.getFileInfo(dirIV) + if err != nil { + t.Errorf("Unexpected error %q\n", err) + } + if ftype != typeName { + t.Errorf("Wrong file type, expecting %d, got %d\n", typeName, ftype) + } + if !excluded { + t.Error("Should have excluded LongName based on parent") + } + if ignorerMock.calledWith != "mockdecrypt_parent" { + t.Errorf("Should have checked parent dir, checked %q", ignorerMock.calledWith) + } +} + +func TestShouldDecryptPathAndReturnTrueForExcludedPath(t *testing.T) { + rfs, ignorerMock := createRFSWithMocks() + ignorerMock.toExclude = "mockdecrypt_file.txt" + + ftype, excluded, pPath, err := rfs.getFileInfo("file.txt") + if err != nil { + t.Errorf("Unexpected error %q\n", err) + } + if ftype != typeRegular { + t.Errorf("Wrong file type, expecting %d, got %d\n", typeRegular, ftype) + } + if !excluded { + t.Error("Should have excluded") + } + if pPath != "mockdecrypt_file.txt" { + t.Errorf("Wrong pPath returned, got %q\n", pPath) + } + if ignorerMock.calledWith != "mockdecrypt_file.txt" { + t.Error("Didn't call IgnoreParser with decrypted path") + } +} diff --git a/internal/fusefrontend_reverse_v1api/rpath.go b/internal/fusefrontend_reverse_v1api/rpath.go new file mode 100644 index 0000000..2da8379 --- /dev/null +++ b/internal/fusefrontend_reverse_v1api/rpath.go @@ -0,0 +1,113 @@ +package fusefrontend_reverse + +import ( + "encoding/base64" + "path/filepath" + "strings" + "syscall" + + "github.com/rfjakob/gocryptfs/internal/nametransform" + "github.com/rfjakob/gocryptfs/internal/pathiv" + "github.com/rfjakob/gocryptfs/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/internal/tlog" +) + +// abs basically returns storage dir + "/" + relPath. +// It takes an error parameter so it can directly wrap decryptPath like this: +// a, err := rfs.abs(rfs.decryptPath(relPath)) +// abs never generates an error on its own. In other words, abs(p, nil) never +// fails. +func (rfs *ReverseFS) abs(relPath string, err error) (string, error) { + if err != nil { + return "", err + } + return filepath.Join(rfs.args.Cipherdir, relPath), nil +} + +// rDecryptName decrypts the ciphertext name "cName", given the dirIV of the +// directory "cName" lies in. The relative plaintext path to the directory +// "pDir" is used if a "gocryptfs.longname.XYZ.name" must be resolved. +func (rfs *ReverseFS) rDecryptName(cName string, dirIV []byte, pDir string) (pName string, err error) { + nameType := nametransform.NameType(cName) + if nameType == nametransform.LongNameNone { + pName, err = rfs.nameTransform.DecryptName(cName, dirIV) + if err != nil { + // We get lots of decrypt requests for names like ".Trash" that + // are invalid base64. Convert them to ENOENT so the correct + // error gets returned to the user. + if _, ok := err.(base64.CorruptInputError); ok { + return "", syscall.ENOENT + } + // Stat attempts on the link target of encrypted symlinks. + // These are always valid base64 but the length is not a + // multiple of 16. + if err == syscall.EBADMSG { + return "", syscall.ENOENT + } + return "", err + } + } else if nameType == nametransform.LongNameContent { + pName, err = rfs.findLongnameParent(pDir, dirIV, cName) + if err != nil { + return "", err + } + } else { + // It makes no sense to decrypt a ".name" file. This is a virtual file + // that has no representation in the plaintext filesystem. ".name" + // files should have already been handled in virtualfile.go. + tlog.Warn.Printf("rDecryptName: cannot decrypt virtual file %q", cName) + return "", syscall.EINVAL + } + return pName, nil +} + +// decryptPath decrypts a relative ciphertext path to a relative plaintext +// path. +func (rfs *ReverseFS) decryptPath(relPath string) (string, error) { + if rfs.args.PlaintextNames || relPath == "" { + return relPath, nil + } + // Check if the parent dir is in the cache + cDir := nametransform.Dir(relPath) + dirIV, pDir := rPathCache.lookup(cDir) + if dirIV != nil { + cName := filepath.Base(relPath) + pName, err := rfs.rDecryptName(cName, dirIV, pDir) + if err != nil { + return "", err + } + return filepath.Join(pDir, pName), nil + } + parts := strings.Split(relPath, "/") + var transformedParts []string + for i := range parts { + // Start at the top and recurse + currentCipherDir := filepath.Join(parts[:i]...) + currentPlainDir := filepath.Join(transformedParts[:i]...) + dirIV = pathiv.Derive(currentCipherDir, pathiv.PurposeDirIV) + transformedPart, err := rfs.rDecryptName(parts[i], dirIV, currentPlainDir) + if err != nil { + return "", err + } + transformedParts = append(transformedParts, transformedPart) + } + pRelPath := filepath.Join(transformedParts...) + rPathCache.store(cDir, dirIV, nametransform.Dir(pRelPath)) + return pRelPath, nil +} + +// openBackingDir receives an already decrypted relative path +// "pRelPath", opens the directory that contains the target file/dir +// and returns the fd to the directory and the decrypted name of the +// target file. The fd/name pair is intended for use with fchownat and +// friends. +func (rfs *ReverseFS) openBackingDir(pRelPath string) (dirfd int, pName string, err error) { + // Open directory, safe against symlink races + pDir := filepath.Dir(pRelPath) + dirfd, err = syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, pDir) + if err != nil { + return -1, "", err + } + pName = filepath.Base(pRelPath) + return dirfd, pName, nil +} diff --git a/internal/fusefrontend_reverse_v1api/rpath_cache.go b/internal/fusefrontend_reverse_v1api/rpath_cache.go new file mode 100644 index 0000000..221f578 --- /dev/null +++ b/internal/fusefrontend_reverse_v1api/rpath_cache.go @@ -0,0 +1,45 @@ +package fusefrontend_reverse + +import ( + "sync" +) + +// rPathCacheContainer is a simple one entry path cache. Because the dirIV +// is generated deterministically from the directory path, there is no need +// to ever invalidate entries. +type rPathCacheContainer struct { + sync.Mutex + // Relative ciphertext path to the directory + cPath string + // Relative plaintext path + pPath string + // Directory IV of the directory + dirIV []byte +} + +// lookup relative ciphertext path "cPath". Returns dirIV, relative +// plaintext path. +func (c *rPathCacheContainer) lookup(cPath string) ([]byte, string) { + c.Lock() + defer c.Unlock() + if cPath == c.cPath { + // hit + return c.dirIV, c.pPath + } + // miss + return nil, "" +} + +// store - write entry for the directory at relative ciphertext path "cPath" +// into the cache. +// "dirIV" = directory IV of the directory, "pPath" = relative plaintext path +func (c *rPathCacheContainer) store(cPath string, dirIV []byte, pPath string) { + c.Lock() + defer c.Unlock() + c.cPath = cPath + c.dirIV = dirIV + c.pPath = pPath +} + +// rPathCache: see rPathCacheContainer above for a detailed description +var rPathCache rPathCacheContainer diff --git a/internal/fusefrontend_reverse_v1api/virtualfile.go b/internal/fusefrontend_reverse_v1api/virtualfile.go new file mode 100644 index 0000000..f258e7f --- /dev/null +++ b/internal/fusefrontend_reverse_v1api/virtualfile.go @@ -0,0 +1,110 @@ +package fusefrontend_reverse + +import ( + "log" + "path/filepath" + "syscall" + + "golang.org/x/sys/unix" + + "github.com/hanwen/go-fuse/v2/fuse" + "github.com/hanwen/go-fuse/v2/fuse/nodefs" + + "github.com/rfjakob/gocryptfs/internal/inomap" + "github.com/rfjakob/gocryptfs/internal/nametransform" + "github.com/rfjakob/gocryptfs/internal/pathiv" + "github.com/rfjakob/gocryptfs/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/internal/tlog" +) + +const ( + // virtualFileMode is the mode to use for virtual files (gocryptfs.diriv and + // *.name). They are always readable, as stated in func Access + virtualFileMode = syscall.S_IFREG | 0444 + // We use inomap's `Tag` feature to generate unique inode numbers for + // virtual files. These are the tags we use. + inoTagDirIV = 1 + inoTagNameFile = 2 +) + +func (rfs *ReverseFS) newDirIVFile(cRelPath string) (nodefs.File, fuse.Status) { + cDir := nametransform.Dir(cRelPath) + dir, err := rfs.decryptPath(cDir) + if err != nil { + return nil, fuse.ToStatus(err) + } + iv := pathiv.Derive(cDir, pathiv.PurposeDirIV) + return rfs.newVirtualFile(iv, rfs.args.Cipherdir, dir, inoTagDirIV) +} + +type virtualFile struct { + // Embed nodefs.defaultFile for a ENOSYS implementation of all methods + nodefs.File + // pointer to parent filesystem + rfs *ReverseFS + // file content + content []byte + // backing directory + cipherdir string + // path to a parent file (relative to cipherdir) + parentFile string + // inomap `Tag`. + // Depending on the file type, either `inoTagDirIV` or `inoTagNameFile`. + inoTag uint8 +} + +// newVirtualFile creates a new in-memory file that does not have a representation +// on disk. "content" is the file content. Timestamps and file owner are copied +// from "parentFile" (plaintext path relative to "cipherdir"). +// For a "gocryptfs.diriv" file, you would use the parent directory as +// "parentFile". +func (rfs *ReverseFS) newVirtualFile(content []byte, cipherdir string, parentFile string, inoTag uint8) (nodefs.File, fuse.Status) { + if inoTag == 0 { + log.Panicf("BUG: inoTag for virtual file is zero - this will cause ino collisions!") + } + return &virtualFile{ + File: nodefs.NewDefaultFile(), + rfs: rfs, + content: content, + cipherdir: cipherdir, + parentFile: parentFile, + inoTag: inoTag, + }, fuse.OK +} + +// Read - FUSE call +func (f *virtualFile) Read(buf []byte, off int64) (resultData fuse.ReadResult, status fuse.Status) { + if off >= int64(len(f.content)) { + return nil, fuse.OK + } + end := int(off) + len(buf) + if end > len(f.content) { + end = len(f.content) + } + return fuse.ReadResultData(f.content[off:end]), fuse.OK +} + +// GetAttr - FUSE call +func (f *virtualFile) GetAttr(a *fuse.Attr) fuse.Status { + dir := filepath.Dir(f.parentFile) + dirfd, err := syscallcompat.OpenDirNofollow(f.cipherdir, dir) + if err != nil { + return fuse.ToStatus(err) + } + defer syscall.Close(dirfd) + name := filepath.Base(f.parentFile) + var st2 unix.Stat_t + err = syscallcompat.Fstatat(dirfd, name, &st2, unix.AT_SYMLINK_NOFOLLOW) + if err != nil { + tlog.Debug.Printf("GetAttr: Fstatat %q: %v\n", f.parentFile, err) + return fuse.ToStatus(err) + } + st := syscallcompat.Unix2syscall(st2) + q := inomap.NewQIno(uint64(st.Dev), f.inoTag, uint64(st.Ino)) + st.Ino = f.rfs.inoMap.Translate(q) + st.Size = int64(len(f.content)) + st.Mode = virtualFileMode + st.Nlink = 1 + a.FromStat(&st) + return fuse.OK +} -- cgit v1.2.3