diff options
| author | Jakob Unterwurzacher | 2020-08-01 20:47:59 +0200 | 
|---|---|---|
| committer | Jakob Unterwurzacher | 2020-08-01 21:14:10 +0200 | 
| commit | 13dc7657ba0aac4da3f4b80ba231a919fdfae0f5 (patch) | |
| tree | d13781bacb4a02327f519d399e946f83fd13e5c7 /internal/fusefrontend_reverse_v1api | |
| parent | dd3d8c100bc9e84c44b2efb55e536b2fc0ded37e (diff) | |
v2api/reverse: move old fusefrontend_reverse out of the way
fusefrontend_reverse -> fusefrontend_reverse_v1api
Diffstat (limited to 'internal/fusefrontend_reverse_v1api')
| -rw-r--r-- | internal/fusefrontend_reverse_v1api/ctlsock_interface.go | 38 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse_v1api/excluder.go | 66 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse_v1api/excluder_test.go | 84 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse_v1api/mocks_test.go | 32 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse_v1api/reverse_longnames.go | 126 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse_v1api/rfile.go | 209 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse_v1api/rfs.go | 472 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse_v1api/rfs_test.go | 124 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse_v1api/rpath.go | 113 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse_v1api/rpath_cache.go | 45 | ||||
| -rw-r--r-- | internal/fusefrontend_reverse_v1api/virtualfile.go | 110 | 
11 files changed, 1419 insertions, 0 deletions
| 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 +} | 
