diff options
| author | Jakob Unterwurzacher | 2016-02-06 19:27:59 +0100 | 
|---|---|---|
| committer | Jakob Unterwurzacher | 2016-02-06 19:27:59 +0100 | 
| commit | 9078a77850dd680bfa938d9ed7c83600a60c0e7b (patch) | |
| tree | 03ee83879c398307d450002e1f07e928cb743672 /internal/fusefrontend | |
| parent | 2b8cbd944149afe51fadddbd67ee4499d1d86250 (diff) | |
Move pathfs_frontend to internal/fusefrontend
"git status" for reference:
renamed:    pathfs_frontend/args.go -> internal/fusefrontend/args.go
renamed:    pathfs_frontend/compat_darwin.go -> internal/fusefrontend/compat_darwin.go
renamed:    pathfs_frontend/compat_linux.go -> internal/fusefrontend/compat_linux.go
renamed:    pathfs_frontend/file.go -> internal/fusefrontend/file.go
renamed:    pathfs_frontend/file_holes.go -> internal/fusefrontend/file_holes.go
renamed:    pathfs_frontend/fs.go -> internal/fusefrontend/fs.go
renamed:    pathfs_frontend/fs_dir.go -> internal/fusefrontend/fs_dir.go
renamed:    pathfs_frontend/names.go -> internal/fusefrontend/names.go
renamed:    pathfs_frontend/write_lock.go -> internal/fusefrontend/write_lock.go
modified:   main.go
Diffstat (limited to 'internal/fusefrontend')
| -rw-r--r-- | internal/fusefrontend/args.go | 12 | ||||
| -rw-r--r-- | internal/fusefrontend/compat_darwin.go | 12 | ||||
| -rw-r--r-- | internal/fusefrontend/compat_linux.go | 18 | ||||
| -rw-r--r-- | internal/fusefrontend/file.go | 501 | ||||
| -rw-r--r-- | internal/fusefrontend/file_holes.go | 29 | ||||
| -rw-r--r-- | internal/fusefrontend/fs.go | 381 | ||||
| -rw-r--r-- | internal/fusefrontend/fs_dir.go | 157 | ||||
| -rw-r--r-- | internal/fusefrontend/names.go | 52 | ||||
| -rw-r--r-- | internal/fusefrontend/write_lock.go | 66 | 
9 files changed, 1228 insertions, 0 deletions
| diff --git a/internal/fusefrontend/args.go b/internal/fusefrontend/args.go new file mode 100644 index 0000000..e8cab04 --- /dev/null +++ b/internal/fusefrontend/args.go @@ -0,0 +1,12 @@ +package fusefrontend + +// Container for arguments that are passed from main() to fusefrontend +type Args struct { +	Masterkey      []byte +	Cipherdir      string +	OpenSSL        bool +	PlaintextNames bool +	DirIV          bool +	EMENames       bool +	GCMIV128       bool +} diff --git a/internal/fusefrontend/compat_darwin.go b/internal/fusefrontend/compat_darwin.go new file mode 100644 index 0000000..445fb45 --- /dev/null +++ b/internal/fusefrontend/compat_darwin.go @@ -0,0 +1,12 @@ +package fusefrontend + +// prealloc - preallocate space without changing the file size. This prevents +// us from running out of space in the middle of an operation. +func prealloc(fd int, off int64, len int64) (err error) { +	// +	// Sorry, fallocate is not available on OSX at all and +	// fcntl F_PREALLOCATE is not accessible from Go. +	// +	// See https://github.com/rfjakob/gocryptfs/issues/18 if you want to help. +	return nil +} diff --git a/internal/fusefrontend/compat_linux.go b/internal/fusefrontend/compat_linux.go new file mode 100644 index 0000000..4108792 --- /dev/null +++ b/internal/fusefrontend/compat_linux.go @@ -0,0 +1,18 @@ +package fusefrontend + +import "syscall" + +// prealloc - preallocate space without changing the file size. This prevents +// us from running out of space in the middle of an operation. +func prealloc(fd int, off int64, len int64) (err error) { +	for { +		err = syscall.Fallocate(fd, FALLOC_FL_KEEP_SIZE, off, len) +		if err == syscall.EINTR { +			// fallocate, like many syscalls, can return EINTR. This is not an +			// error and just signifies that the operation was interrupted by a +			// signal and we should try again. +			continue +		} +		return err +	} +} diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go new file mode 100644 index 0000000..2e0b504 --- /dev/null +++ b/internal/fusefrontend/file.go @@ -0,0 +1,501 @@ +package fusefrontend + +// FUSE operations on file handles + +import ( +	"bytes" +	"fmt" +	"io" +	"os" +	"sync" +	"syscall" +	"time" + +	"github.com/hanwen/go-fuse/fuse" +	"github.com/hanwen/go-fuse/fuse/nodefs" + +	"github.com/rfjakob/gocryptfs/internal/contentenc" +	"github.com/rfjakob/gocryptfs/internal/toggledlog" +) + +// File - based on loopbackFile in go-fuse/fuse/nodefs/files.go +type file struct { +	fd *os.File + +	// fdLock prevents the fd to be closed while we are in the middle of +	// an operation. +	// Every FUSE entrypoint should RLock(). The only user of Lock() is +	// Release(), which closes the fd, hence has to acquire an exclusive lock. +	fdLock sync.RWMutex + +	// Was the file opened O_WRONLY? +	writeOnly bool + +	// Content encryption helper +	contentEnc *contentenc.ContentEnc + +	// Inode number +	ino uint64 + +	// File header +	header *contentenc.FileHeader + +	forgotten bool +} + +func NewFile(fd *os.File, writeOnly bool, contentEnc *contentenc.ContentEnc) nodefs.File { +	var st syscall.Stat_t +	syscall.Fstat(int(fd.Fd()), &st) +	wlock.register(st.Ino) + +	return &file{ +		fd:        fd, +		writeOnly: writeOnly, +		contentEnc: contentEnc, +		ino:       st.Ino, +	} +} + +// intFd - return the backing file descriptor as an integer. Used for debug +// messages. +func (f *file) intFd() int { +	return int(f.fd.Fd()) +} + +func (f *file) InnerFile() nodefs.File { +	return nil +} + +func (f *file) SetInode(n *nodefs.Inode) { +} + +// readHeader - load the file header from disk +// +// Returns io.EOF if the file is empty +func (f *file) readHeader() error { +	buf := make([]byte, contentenc.HEADER_LEN) +	_, err := f.fd.ReadAt(buf, 0) +	if err != nil { +		return err +	} +	h, err := contentenc.ParseHeader(buf) +	if err != nil { +		return err +	} +	f.header = h + +	return nil +} + +// createHeader - create a new random header and write it to disk +func (f *file) createHeader() error { +	h := contentenc.RandomHeader() +	buf := h.Pack() + +	// Prevent partially written (=corrupt) header by preallocating the space beforehand +	err := prealloc(int(f.fd.Fd()), 0, contentenc.HEADER_LEN) +	if err != nil { +		toggledlog.Warn.Printf("ino%d: createHeader: prealloc failed: %s\n", f.ino, err.Error()) +		return err +	} + +	// Actually write header +	_, err = f.fd.WriteAt(buf, 0) +	if err != nil { +		return err +	} +	f.header = h + +	return nil +} + +func (f *file) String() string { +	return fmt.Sprintf("cryptFile(%s)", f.fd.Name()) +} + +// doRead - returns "length" plaintext bytes from plaintext offset "off". +// Arguments "length" and "off" do not have to be block-aligned. +// +// doRead reads the corresponding ciphertext blocks from disk, decrypts them and +// returns the requested part of the plaintext. +// +// Called by Read() for normal reading, +// by Write() and Truncate() for Read-Modify-Write +func (f *file) doRead(off uint64, length uint64) ([]byte, fuse.Status) { + +	// Read file header +	if f.header == nil { +		err := f.readHeader() +		if err == io.EOF { +			return nil, fuse.OK +		} +		if err != nil { +			return nil, fuse.ToStatus(err) +		} +	} + +	// Read the backing ciphertext in one go +	blocks := f.contentEnc.ExplodePlainRange(off, length) +	alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks) +	skip := blocks[0].Skip +	toggledlog.Debug.Printf("JointCiphertextRange(%d, %d) -> %d, %d, %d", off, length, alignedOffset, alignedLength, skip) +	ciphertext := make([]byte, int(alignedLength)) +	n, err := f.fd.ReadAt(ciphertext, int64(alignedOffset)) +	if err != nil && err != io.EOF { +		toggledlog.Warn.Printf("read: ReadAt: %s", err.Error()) +		return nil, fuse.ToStatus(err) +	} +	// Truncate ciphertext buffer down to actually read bytes +	ciphertext = ciphertext[0:n] + +	firstBlockNo := blocks[0].BlockNo +	toggledlog.Debug.Printf("ReadAt offset=%d bytes (%d blocks), want=%d, got=%d", alignedOffset, firstBlockNo, alignedLength, n) + +	// Decrypt it +	plaintext, err := f.contentEnc.DecryptBlocks(ciphertext, firstBlockNo, f.header.Id) +	if err != nil { +		curruptBlockNo := firstBlockNo + f.contentEnc.PlainOffToBlockNo(uint64(len(plaintext))) +		cipherOff := f.contentEnc.BlockNoToCipherOff(curruptBlockNo) +		plainOff := f.contentEnc.BlockNoToPlainOff(curruptBlockNo) +		toggledlog.Warn.Printf("ino%d: doRead: corrupt block #%d (plainOff=%d, cipherOff=%d)", +			f.ino, curruptBlockNo, plainOff, cipherOff) +		return nil, fuse.EIO +	} + +	// Crop down to the relevant part +	var out []byte +	lenHave := len(plaintext) +	lenWant := int(skip + length) +	if lenHave > lenWant { +		out = plaintext[skip:lenWant] +	} else if lenHave > int(skip) { +		out = plaintext[skip:lenHave] +	} +	// else: out stays empty, file was smaller than the requested offset + +	return out, fuse.OK +} + +// Read - FUSE call +func (f *file) Read(buf []byte, off int64) (resultData fuse.ReadResult, code fuse.Status) { +	f.fdLock.RLock() +	defer f.fdLock.RUnlock() + +	toggledlog.Debug.Printf("ino%d: FUSE Read: offset=%d length=%d", f.ino, len(buf), off) + +	if f.writeOnly { +		toggledlog.Warn.Printf("ino%d: Tried to read from write-only file", f.ino) +		return nil, fuse.EBADF +	} + +	out, status := f.doRead(uint64(off), uint64(len(buf))) + +	if status == fuse.EIO { +		toggledlog.Warn.Printf("ino%d: Read failed with EIO, offset=%d, length=%d", f.ino, len(buf), off) +	} +	if status != fuse.OK { +		return nil, status +	} + +	toggledlog.Debug.Printf("ino%d: Read: status %v, returning %d bytes", f.ino, status, len(out)) +	return fuse.ReadResultData(out), status +} + +const FALLOC_FL_KEEP_SIZE = 0x01 + +// doWrite - encrypt "data" and write it to plaintext offset "off" +// +// Arguments do not have to be block-aligned, read-modify-write is +// performed internally as neccessary +// +// Called by Write() for normal writing, +// and by Truncate() to rewrite the last file block. +func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) { + +	// Read header from disk, create a new one if the file is empty +	if f.header == nil { +		err := f.readHeader() +		if err == io.EOF { +			err = f.createHeader() + +		} +		if err != nil { +			return 0, fuse.ToStatus(err) +		} +	} + +	var written uint32 +	status := fuse.OK +	dataBuf := bytes.NewBuffer(data) +	blocks := f.contentEnc.ExplodePlainRange(uint64(off), uint64(len(data))) +	for _, b := range blocks { + +		blockData := dataBuf.Next(int(b.Length)) + +		// Incomplete block -> Read-Modify-Write +		if b.IsPartial() { +			// Read +			o, _ := b.PlaintextRange() +			oldData, status := f.doRead(o, f.contentEnc.PlainBS()) +			if status != fuse.OK { +				toggledlog.Warn.Printf("ino%d fh%d: RMW read failed: %s", f.ino, f.intFd(), status.String()) +				return written, status +			} +			// Modify +			blockData = f.contentEnc.MergeBlocks(oldData, blockData, int(b.Skip)) +			toggledlog.Debug.Printf("len(oldData)=%d len(blockData)=%d", len(oldData), len(blockData)) +		} + +		// Encrypt +		blockOffset, blockLen := b.CiphertextRange() +		blockData = f.contentEnc.EncryptBlock(blockData, b.BlockNo, f.header.Id) +		toggledlog.Debug.Printf("ino%d: Writing %d bytes to block #%d", +			f.ino, uint64(len(blockData))-f.contentEnc.BlockOverhead(), b.BlockNo) + +		// Prevent partially written (=corrupt) blocks by preallocating the space beforehand +		err := prealloc(int(f.fd.Fd()), int64(blockOffset), int64(blockLen)) +		if err != nil { +			toggledlog.Warn.Printf("ino%d fh%d: doWrite: prealloc failed: %s", f.ino, f.intFd(), err.Error()) +			status = fuse.ToStatus(err) +			break +		} + +		// Write +		_, err = f.fd.WriteAt(blockData, int64(blockOffset)) + +		if err != nil { +			toggledlog.Warn.Printf("doWrite: Write failed: %s", err.Error()) +			status = fuse.ToStatus(err) +			break +		} +		written += uint32(b.Length) +	} +	return written, status +} + +// Write - FUSE call +func (f *file) Write(data []byte, off int64) (uint32, fuse.Status) { +	f.fdLock.RLock() +	defer f.fdLock.RUnlock() +	wlock.lock(f.ino) +	defer wlock.unlock(f.ino) + +	toggledlog.Debug.Printf("ino%d: FUSE Write: offset=%d length=%d", f.ino, off, len(data)) + +	fi, err := f.fd.Stat() +	if err != nil { +		toggledlog.Warn.Printf("Write: Fstat failed: %v", err) +		return 0, fuse.ToStatus(err) +	} +	plainSize := f.contentEnc.CipherSizeToPlainSize(uint64(fi.Size())) +	if f.createsHole(plainSize, off) { +		status := f.zeroPad(plainSize) +		if status != fuse.OK { +			toggledlog.Warn.Printf("zeroPad returned error %v", status) +			return 0, status +		} +	} +	return f.doWrite(data, off) +} + +// Release - FUSE call, close file +func (f *file) Release() { +	f.fdLock.Lock() +	defer f.fdLock.Unlock() + +	f.fd.Close() +	f.forgotten = true +} + +// Flush - FUSE call +func (f *file) Flush() fuse.Status { +	f.fdLock.RLock() +	defer f.fdLock.RUnlock() + +	// Since Flush() may be called for each dup'd fd, we don't +	// want to really close the file, we just want to flush. This +	// is achieved by closing a dup'd fd. +	newFd, err := syscall.Dup(int(f.fd.Fd())) + +	if err != nil { +		return fuse.ToStatus(err) +	} +	err = syscall.Close(newFd) +	return fuse.ToStatus(err) +} + +func (f *file) Fsync(flags int) (code fuse.Status) { +	f.fdLock.RLock() +	defer f.fdLock.RUnlock() + +	return fuse.ToStatus(syscall.Fsync(int(f.fd.Fd()))) +} + +// Truncate - FUSE call +func (f *file) Truncate(newSize uint64) fuse.Status { +	f.fdLock.RLock() +	defer f.fdLock.RUnlock() +	wlock.lock(f.ino) +	defer wlock.unlock(f.ino) + +	if f.forgotten { +		toggledlog.Warn.Printf("ino%d fh%d: Truncate on forgotten file", f.ino, f.intFd()) +	} + +	// Common case first: Truncate to zero +	if newSize == 0 { +		err := syscall.Ftruncate(int(f.fd.Fd()), 0) +		if err != nil { +			toggledlog.Warn.Printf("ino%d fh%d: Ftruncate(fd, 0) returned error: %v", f.ino, f.intFd(), err) +			return fuse.ToStatus(err) +		} +		// Truncate to zero kills the file header +		f.header = nil +		return fuse.OK +	} + +	// We need the old file size to determine if we are growing or shrinking +	// the file +	fi, err := f.fd.Stat() +	if err != nil { +		toggledlog.Warn.Printf("ino%d fh%d: Truncate: Fstat failed: %v", f.ino, f.intFd(), err) +		return fuse.ToStatus(err) +	} +	oldSize := f.contentEnc.CipherSizeToPlainSize(uint64(fi.Size())) +	{ +		oldB := float32(oldSize) / float32(f.contentEnc.PlainBS()) +		newB := float32(newSize) / float32(f.contentEnc.PlainBS()) +		toggledlog.Debug.Printf("ino%d: FUSE Truncate from %.2f to %.2f blocks (%d to %d bytes)", f.ino, oldB, newB, oldSize, newSize) +	} + +	// File size stays the same - nothing to do +	if newSize == oldSize { +		return fuse.OK +	} + +	// File grows +	if newSize > oldSize { + +		// File was empty, create new header +		if oldSize == 0 { +			err := f.createHeader() +			if err != nil { +				return fuse.ToStatus(err) +			} +		} + +		blocks := f.contentEnc.ExplodePlainRange(oldSize, newSize-oldSize) +		for _, b := range blocks { +			// First and last block may be partial +			if b.IsPartial() { +				off, _ := b.PlaintextRange() +				off += b.Skip +				_, status := f.doWrite(make([]byte, b.Length), int64(off)) +				if status != fuse.OK { +					return status +				} +			} else { +				off, length := b.CiphertextRange() +				err := syscall.Ftruncate(int(f.fd.Fd()), int64(off+length)) +				if err != nil { +					toggledlog.Warn.Printf("grow Ftruncate returned error: %v", err) +					return fuse.ToStatus(err) +				} +			} +		} +		return fuse.OK +	} else { +		// File shrinks +		blockNo := f.contentEnc.PlainOffToBlockNo(newSize) +		cipherOff := f.contentEnc.BlockNoToCipherOff(blockNo) +		plainOff := f.contentEnc.BlockNoToPlainOff(blockNo) +		lastBlockLen := newSize - plainOff +		var data []byte +		if lastBlockLen > 0 { +			var status fuse.Status +			data, status = f.doRead(plainOff, lastBlockLen) +			if status != fuse.OK { +				toggledlog.Warn.Printf("shrink doRead returned error: %v", err) +				return status +			} +		} +		// Truncate down to last complete block +		err = syscall.Ftruncate(int(f.fd.Fd()), int64(cipherOff)) +		if err != nil { +			toggledlog.Warn.Printf("shrink Ftruncate returned error: %v", err) +			return fuse.ToStatus(err) +		} +		// Append partial block +		if lastBlockLen > 0 { +			_, status := f.doWrite(data, int64(plainOff)) +			return status +		} +		return fuse.OK +	} +} + +func (f *file) Chmod(mode uint32) fuse.Status { +	f.fdLock.RLock() +	defer f.fdLock.RUnlock() + +	return fuse.ToStatus(f.fd.Chmod(os.FileMode(mode))) +} + +func (f *file) Chown(uid uint32, gid uint32) fuse.Status { +	f.fdLock.RLock() +	defer f.fdLock.RUnlock() + +	return fuse.ToStatus(f.fd.Chown(int(uid), int(gid))) +} + +func (f *file) GetAttr(a *fuse.Attr) fuse.Status { +	f.fdLock.RLock() +	defer f.fdLock.RUnlock() + +	toggledlog.Debug.Printf("file.GetAttr()") +	st := syscall.Stat_t{} +	err := syscall.Fstat(int(f.fd.Fd()), &st) +	if err != nil { +		return fuse.ToStatus(err) +	} +	a.FromStat(&st) +	a.Size = f.contentEnc.CipherSizeToPlainSize(a.Size) + +	return fuse.OK +} + +// Allocate - FUSE call, fallocate(2) +var allocateWarned bool + +func (f *file) Allocate(off uint64, sz uint64, mode uint32) fuse.Status { +	// Only warn once +	if !allocateWarned { +		toggledlog.Warn.Printf("fallocate(2) is not supported, returning ENOSYS - see https://github.com/rfjakob/gocryptfs/issues/1") +		allocateWarned = true +	} +	return fuse.ENOSYS +} + +const _UTIME_OMIT = ((1 << 30) - 2) + +func (f *file) Utimens(a *time.Time, m *time.Time) fuse.Status { +	f.fdLock.RLock() +	defer f.fdLock.RUnlock() + +	ts := make([]syscall.Timespec, 2) + +	if a == nil { +		ts[0].Nsec = _UTIME_OMIT +	} else { +		ts[0].Sec = a.Unix() +	} + +	if m == nil { +		ts[1].Nsec = _UTIME_OMIT +	} else { +		ts[1].Sec = m.Unix() +	} + +	fn := fmt.Sprintf("/proc/self/fd/%d", f.fd.Fd()) +	return fuse.ToStatus(syscall.UtimesNano(fn, ts)) +} diff --git a/internal/fusefrontend/file_holes.go b/internal/fusefrontend/file_holes.go new file mode 100644 index 0000000..0259ae9 --- /dev/null +++ b/internal/fusefrontend/file_holes.go @@ -0,0 +1,29 @@ +package fusefrontend + +// Helper functions for sparse files (files with holes) + +import ( +	"github.com/hanwen/go-fuse/fuse" + +	"github.com/rfjakob/gocryptfs/internal/toggledlog" +) + +// Will a write to offset "off" create a file hole? +func (f *file) createsHole(plainSize uint64, off int64) bool { +	nextBlock := f.contentEnc.PlainOffToBlockNo(plainSize) +	targetBlock := f.contentEnc.PlainOffToBlockNo(uint64(off)) +	if targetBlock > nextBlock { +		return true +	} +	return false +} + +// Zero-pad the file of size plainSize to the next block boundary +func (f *file) zeroPad(plainSize uint64) fuse.Status { +	lastBlockLen := plainSize % f.contentEnc.PlainBS() +	missing := f.contentEnc.PlainBS() - lastBlockLen +	pad := make([]byte, missing) +	toggledlog.Debug.Printf("zeroPad: Writing %d bytes\n", missing) +	_, status := f.doWrite(pad, int64(plainSize)) +	return status +} diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go new file mode 100644 index 0000000..0331215 --- /dev/null +++ b/internal/fusefrontend/fs.go @@ -0,0 +1,381 @@ +package fusefrontend + +// FUSE operations on paths + +import ( +	"encoding/base64" +	"os" +	"path/filepath" +	"sync" +	"syscall" +	"time" + +	"github.com/hanwen/go-fuse/fuse" +	"github.com/hanwen/go-fuse/fuse/nodefs" +	"github.com/hanwen/go-fuse/fuse/pathfs" + +	"github.com/rfjakob/gocryptfs/internal/toggledlog" +	"github.com/rfjakob/gocryptfs/internal/cryptocore" +	"github.com/rfjakob/gocryptfs/internal/nametransform" +	"github.com/rfjakob/gocryptfs/internal/contentenc" +	"github.com/rfjakob/gocryptfs/internal/configfile" +) + +const plainBS = 4096 + +type FS struct { +	pathfs.FileSystem      // loopbackFileSystem, see go-fuse/fuse/pathfs/loopback.go +	args              Args // Stores configuration arguments +	// dirIVLock: Lock()ed if any "gocryptfs.diriv" file is modified +	// Readers must RLock() it to prevent them from seeing intermediate +	// states +	dirIVLock sync.RWMutex +	// Filename encryption helper +	nameTransform *nametransform.NameTransform +	// Content encryption helper +	contentEnc *contentenc.ContentEnc +} + +// Encrypted FUSE overlay filesystem +func NewFS(args Args) *FS { + +	cryptoCore := cryptocore.New(args.Masterkey, args.OpenSSL, args.GCMIV128) +	contentEnc := contentenc.New(cryptoCore, plainBS) +	nameTransform := nametransform.New(cryptoCore, args.EMENames) + +	return &FS{ +		FileSystem: pathfs.NewLoopbackFileSystem(args.Cipherdir), +		args:       args, +		nameTransform: nameTransform, +		contentEnc: contentEnc, +	} +} + +// GetBackingPath - get the absolute encrypted path of the backing file +// from the relative plaintext path "relPath" +func (fs *FS) getBackingPath(relPath string) (string, error) { +	cPath, err := fs.encryptPath(relPath) +	if err != nil { +		return "", err +	} +	cAbsPath := filepath.Join(fs.args.Cipherdir, cPath) +	toggledlog.Debug.Printf("getBackingPath: %s + %s -> %s", fs.args.Cipherdir, relPath, cAbsPath) +	return cAbsPath, nil +} + +func (fs *FS) GetAttr(name string, context *fuse.Context) (*fuse.Attr, fuse.Status) { +	toggledlog.Debug.Printf("FS.GetAttr('%s')", name) +	if fs.isFiltered(name) { +		return nil, fuse.EPERM +	} +	cName, err := fs.encryptPath(name) +	if err != nil { +		return nil, fuse.ToStatus(err) +	} +	a, status := fs.FileSystem.GetAttr(cName, context) +	if a == nil { +		toggledlog.Debug.Printf("FS.GetAttr failed: %s", status.String()) +		return a, status +	} +	if a.IsRegular() { +		a.Size = fs.contentEnc.CipherSizeToPlainSize(a.Size) +	} else if a.IsSymlink() { +		target, _ := fs.Readlink(name, context) +		a.Size = uint64(len(target)) +	} +	return a, status +} + +func (fs *FS) OpenDir(dirName string, context *fuse.Context) ([]fuse.DirEntry, fuse.Status) { +	toggledlog.Debug.Printf("OpenDir(%s)", dirName) +	cDirName, err := fs.encryptPath(dirName) +	if err != nil { +		return nil, fuse.ToStatus(err) +	} +	// Read ciphertext directory +	cipherEntries, status := fs.FileSystem.OpenDir(cDirName, context) +	if cipherEntries == nil { +		return nil, status +	} +	// Get DirIV (stays nil if DirIV if off) +	var cachedIV []byte +	if fs.args.DirIV { +		// Read the DirIV once and use it for all later name decryptions +		cDirAbsPath := filepath.Join(fs.args.Cipherdir, cDirName) +		cachedIV, err = fs.nameTransform.ReadDirIV(cDirAbsPath) +		if err != nil { +			return nil, fuse.ToStatus(err) +		} +	} +	// Filter and decrypt filenames +	var plain []fuse.DirEntry +	for i := range cipherEntries { +		cName := cipherEntries[i].Name +		if dirName == "" && cName == configfile.ConfDefaultName { +			// silently ignore "gocryptfs.conf" in the top level dir +			continue +		} +		if fs.args.DirIV && cName == nametransform.DirIVFilename { +			// silently ignore "gocryptfs.diriv" everywhere if dirIV is enabled +			continue +		} +		var name string = cName +		if !fs.args.PlaintextNames { +			name, err = fs.nameTransform.DecryptName(cName, cachedIV) +			if err != nil { +				toggledlog.Warn.Printf("Invalid name \"%s\" in dir \"%s\": %s", cName, cDirName, err) +				continue +			} +		} +		cipherEntries[i].Name = name +		plain = append(plain, cipherEntries[i]) +	} +	return plain, status +} + +// We always need read access to do read-modify-write cycles +func (fs *FS) mangleOpenFlags(flags uint32) (newFlags int, writeOnly bool) { +	newFlags = int(flags) +	if newFlags&os.O_WRONLY > 0 { +		writeOnly = true +		newFlags = newFlags ^ os.O_WRONLY | os.O_RDWR +	} +	// We also cannot open the file in append mode, we need to seek back for RMW +	newFlags = newFlags &^ os.O_APPEND + +	return newFlags, writeOnly +} + +func (fs *FS) Open(path string, flags uint32, context *fuse.Context) (fuseFile nodefs.File, status fuse.Status) { +	if fs.isFiltered(path) { +		return nil, fuse.EPERM +	} +	iflags, writeOnly := fs.mangleOpenFlags(flags) +	cPath, err := fs.getBackingPath(path) +	if err != nil { +		toggledlog.Debug.Printf("Open: getBackingPath: %v", err) +		return nil, fuse.ToStatus(err) +	} +	toggledlog.Debug.Printf("Open: %s", cPath) +	f, err := os.OpenFile(cPath, iflags, 0666) +	if err != nil { +		return nil, fuse.ToStatus(err) +	} + +	return NewFile(f, writeOnly, fs.contentEnc), fuse.OK +} + +func (fs *FS) Create(path string, flags uint32, mode uint32, context *fuse.Context) (fuseFile nodefs.File, code fuse.Status) { +	if fs.isFiltered(path) { +		return nil, fuse.EPERM +	} +	iflags, writeOnly := fs.mangleOpenFlags(flags) +	cPath, err := fs.getBackingPath(path) +	if err != nil { +		return nil, fuse.ToStatus(err) +	} +	f, err := os.OpenFile(cPath, iflags|os.O_CREATE, os.FileMode(mode)) +	if err != nil { +		return nil, fuse.ToStatus(err) +	} +	return NewFile(f, writeOnly, fs.contentEnc), fuse.OK +} + +func (fs *FS) Chmod(path string, mode uint32, context *fuse.Context) (code fuse.Status) { +	if fs.isFiltered(path) { +		return fuse.EPERM +	} +	cPath, err := fs.encryptPath(path) +	if err != nil { +		return fuse.ToStatus(err) +	} +	return fs.FileSystem.Chmod(cPath, mode, context) +} + +func (fs *FS) Chown(path string, uid uint32, gid uint32, context *fuse.Context) (code fuse.Status) { +	if fs.isFiltered(path) { +		return fuse.EPERM +	} +	cPath, err := fs.encryptPath(path) +	if err != nil { +		return fuse.ToStatus(err) +	} +	return fs.FileSystem.Chown(cPath, uid, gid, context) +} + +func (fs *FS) Mknod(path string, mode uint32, dev uint32, context *fuse.Context) (code fuse.Status) { +	if fs.isFiltered(path) { +		return fuse.EPERM +	} +	cPath, err := fs.encryptPath(path) +	if err != nil { +		return fuse.ToStatus(err) +	} +	return fs.FileSystem.Mknod(cPath, mode, dev, context) +} + +var truncateWarned bool + +func (fs *FS) Truncate(path string, offset uint64, context *fuse.Context) (code fuse.Status) { +	// Only warn once +	if !truncateWarned { +		toggledlog.Warn.Printf("truncate(2) is not supported, returning ENOSYS - use ftruncate(2)") +		truncateWarned = true +	} +	return fuse.ENOSYS +} + +func (fs *FS) Utimens(path string, Atime *time.Time, Mtime *time.Time, context *fuse.Context) (code fuse.Status) { +	if fs.isFiltered(path) { +		return fuse.EPERM +	} +	cPath, err := fs.encryptPath(path) +	if err != nil { +		return fuse.ToStatus(err) +	} +	return fs.FileSystem.Utimens(cPath, Atime, Mtime, context) +} + +func (fs *FS) Readlink(path string, context *fuse.Context) (out string, status fuse.Status) { +	cPath, err := fs.encryptPath(path) +	if err != nil { +		return "", fuse.ToStatus(err) +	} +	cTarget, status := fs.FileSystem.Readlink(cPath, context) +	if status != fuse.OK { +		return "", status +	} +	// Old filesystem: symlinks are encrypted like paths (CBC) +	if !fs.args.DirIV { +		target, err := fs.decryptPath(cTarget) +		if err != nil { +			toggledlog.Warn.Printf("Readlink: CBC decryption failed: %v", err) +			return "", fuse.EIO +		} +		return target, fuse.OK +	} +	// Since gocryptfs v0.5 symlinks are encrypted like file contents (GCM) +	cBinTarget, err := base64.URLEncoding.DecodeString(cTarget) +	if err != nil { +		toggledlog.Warn.Printf("Readlink: %v", err) +		return "", fuse.EIO +	} +	target, err := fs.contentEnc.DecryptBlock([]byte(cBinTarget), 0, nil) +	if err != nil { +		toggledlog.Warn.Printf("Readlink: %v", err) +		return "", fuse.EIO +	} +	return string(target), fuse.OK +} + +func (fs *FS) Unlink(path string, context *fuse.Context) (code fuse.Status) { +	if fs.isFiltered(path) { +		return fuse.EPERM +	} +	cPath, err := fs.getBackingPath(path) +	if err != nil { +		return fuse.ToStatus(err) +	} +	return fuse.ToStatus(syscall.Unlink(cPath)) +} + +func (fs *FS) Symlink(target string, linkName string, context *fuse.Context) (code fuse.Status) { +	toggledlog.Debug.Printf("Symlink(\"%s\", \"%s\")", target, linkName) +	if fs.isFiltered(linkName) { +		return fuse.EPERM +	} +	cPath, err := fs.getBackingPath(linkName) +	if err != nil { +		return fuse.ToStatus(err) +	} +	// Old filesystem: symlinks are encrypted like paths (CBC) +	if !fs.args.DirIV { +		cTarget, err := fs.encryptPath(target) +		if err != nil { +			toggledlog.Warn.Printf("Symlink: BUG: we should not get an error here: %v", err) +			return fuse.ToStatus(err) +		} +		err = os.Symlink(cTarget, cPath) +		return fuse.ToStatus(err) +	} +	// Since gocryptfs v0.5 symlinks are encrypted like file contents (GCM) +	cBinTarget := fs.contentEnc.EncryptBlock([]byte(target), 0, nil) +	cTarget := base64.URLEncoding.EncodeToString(cBinTarget) + +	err = os.Symlink(cTarget, cPath) +	toggledlog.Debug.Printf("Symlink: os.Symlink(%s, %s) = %v", cTarget, cPath, err) +	return fuse.ToStatus(err) +} + +func (fs *FS) Rename(oldPath string, newPath string, context *fuse.Context) (code fuse.Status) { +	if fs.isFiltered(newPath) { +		return fuse.EPERM +	} +	cOldPath, err := fs.getBackingPath(oldPath) +	if err != nil { +		return fuse.ToStatus(err) +	} +	cNewPath, err := fs.getBackingPath(newPath) +	if err != nil { +		return fuse.ToStatus(err) +	} +	// The Rename may cause a directory to take the place of another directory. +	// That directory may still be in the DirIV cache, clear it. +	fs.nameTransform.DirIVCache.Clear() + +	err = os.Rename(cOldPath, cNewPath) + +	if lerr, ok := err.(*os.LinkError); ok && lerr.Err == syscall.ENOTEMPTY { +		// If an empty directory is overwritten we will always get +		// ENOTEMPTY as the "empty" directory will still contain gocryptfs.diriv. +		// Handle that case by removing the target directory and trying again. +		toggledlog.Debug.Printf("Rename: Handling ENOTEMPTY") +		if fs.Rmdir(newPath, context) == fuse.OK { +			err = os.Rename(cOldPath, cNewPath) +		} +	} + +	return fuse.ToStatus(err) +} + +func (fs *FS) Link(oldPath string, newPath string, context *fuse.Context) (code fuse.Status) { +	if fs.isFiltered(newPath) { +		return fuse.EPERM +	} +	cOldPath, err := fs.getBackingPath(oldPath) +	if err != nil { +		return fuse.ToStatus(err) +	} +	cNewPath, err := fs.getBackingPath(newPath) +	if err != nil { +		return fuse.ToStatus(err) +	} +	return fuse.ToStatus(os.Link(cOldPath, cNewPath)) +} + +func (fs *FS) Access(path string, mode uint32, context *fuse.Context) (code fuse.Status) { +	if fs.isFiltered(path) { +		return fuse.EPERM +	} +	cPath, err := fs.getBackingPath(path) +	if err != nil { +		return fuse.ToStatus(err) +	} +	return fuse.ToStatus(syscall.Access(cPath, mode)) +} + +func (fs *FS) GetXAttr(name string, attr string, context *fuse.Context) ([]byte, fuse.Status) { +	return nil, fuse.ENOSYS +} + +func (fs *FS) SetXAttr(name string, attr string, data []byte, flags int, context *fuse.Context) fuse.Status { +	return fuse.ENOSYS +} + +func (fs *FS) ListXAttr(name string, context *fuse.Context) ([]string, fuse.Status) { +	return nil, fuse.ENOSYS +} + +func (fs *FS) RemoveXAttr(name string, attr string, context *fuse.Context) fuse.Status { +	return fuse.ENOSYS +} diff --git a/internal/fusefrontend/fs_dir.go b/internal/fusefrontend/fs_dir.go new file mode 100644 index 0000000..2b1e25d --- /dev/null +++ b/internal/fusefrontend/fs_dir.go @@ -0,0 +1,157 @@ +package fusefrontend + +// Mkdir and Rmdir + +import ( +	"fmt" +	"os" +	"path/filepath" +	"syscall" + +	"github.com/hanwen/go-fuse/fuse" + +	"github.com/rfjakob/gocryptfs/internal/toggledlog" +	"github.com/rfjakob/gocryptfs/internal/cryptocore" +	"github.com/rfjakob/gocryptfs/internal/nametransform" +) + +func (fs *FS) Mkdir(relPath string, mode uint32, context *fuse.Context) (code fuse.Status) { +	if fs.isFiltered(relPath) { +		return fuse.EPERM +	} +	encPath, err := fs.getBackingPath(relPath) +	if err != nil { +		return fuse.ToStatus(err) +	} +	if !fs.args.DirIV { +		return fuse.ToStatus(os.Mkdir(encPath, os.FileMode(mode))) +	} + +	// We need write and execute permissions to create gocryptfs.diriv +	origMode := mode +	mode = mode | 0300 + +	// The new directory may take the place of an older one that is still in the cache +	fs.nameTransform.DirIVCache.Clear() +	// Create directory +	fs.dirIVLock.Lock() +	defer fs.dirIVLock.Unlock() +	err = os.Mkdir(encPath, os.FileMode(mode)) +	if err != nil { +		return fuse.ToStatus(err) +	} +	// Create gocryptfs.diriv inside +	err = nametransform.WriteDirIV(encPath) +	if err != nil { +		// This should not happen +		toggledlog.Warn.Printf("Mkdir: WriteDirIV failed: %v", err) +		err2 := syscall.Rmdir(encPath) +		if err2 != nil { +			toggledlog.Warn.Printf("Mkdir: Rmdir rollback failed: %v", err2) +		} +		return fuse.ToStatus(err) +	} + +	// Set permissions back to what the user wanted +	if origMode != mode { +		err = os.Chmod(encPath, os.FileMode(origMode)) +		if err != nil { +			toggledlog.Warn.Printf("Mkdir: Chmod failed: %v", err) +		} +	} + +	return fuse.OK +} + +func (fs *FS) Rmdir(name string, context *fuse.Context) (code fuse.Status) { +	encPath, err := fs.getBackingPath(name) +	if err != nil { +		return fuse.ToStatus(err) +	} +	if !fs.args.DirIV { +		return fuse.ToStatus(syscall.Rmdir(encPath)) +	} + +	// If the directory is not empty besides gocryptfs.diriv, do not even +	// attempt the dance around gocryptfs.diriv. +	fd, err := os.Open(encPath) +	if perr, ok := err.(*os.PathError); ok && perr.Err == syscall.EACCES { +		// We need permission to read and modify the directory +		toggledlog.Debug.Printf("Rmdir: handling EACCESS") +		fi, err2 := os.Stat(encPath) +		if err2 != nil { +			toggledlog.Debug.Printf("Rmdir: Stat: %v", err2) +			return fuse.ToStatus(err2) +		} +		origMode := fi.Mode() +		newMode := origMode | 0700 +		err2 = os.Chmod(encPath, newMode) +		if err2 != nil { +			toggledlog.Debug.Printf("Rmdir: Chmod failed: %v", err2) +			return fuse.ToStatus(err) +		} +		defer func() { +			if code != fuse.OK { +				// Undo the chmod if removing the directory failed +				err3 := os.Chmod(encPath, origMode) +				if err3 != nil { +					toggledlog.Warn.Printf("Rmdir: Chmod rollback failed: %v", err2) +				} +			} +		}() +		// Retry open +		fd, err = os.Open(encPath) +	} +	if err != nil { +		toggledlog.Debug.Printf("Rmdir: Open: %v", err) +		return fuse.ToStatus(err) +	} +	list, err := fd.Readdirnames(10) +	fd.Close() +	if err != nil { +		toggledlog.Debug.Printf("Rmdir: Readdirnames: %v", err) +		return fuse.ToStatus(err) +	} +	if len(list) > 1 { +		return fuse.ToStatus(syscall.ENOTEMPTY) +	} else if len(list) == 0 { +		toggledlog.Warn.Printf("Rmdir: gocryptfs.diriv missing, allowing deletion") +		return fuse.ToStatus(syscall.Rmdir(encPath)) +	} + +	// Move "gocryptfs.diriv" to the parent dir as "gocryptfs.diriv.rmdir.XYZ" +	dirivPath := filepath.Join(encPath, nametransform.DirIVFilename) +	parentDir := filepath.Dir(encPath) +	tmpName := fmt.Sprintf("gocryptfs.diriv.rmdir.%d", cryptocore.RandUint64()) +	tmpDirivPath := filepath.Join(parentDir, tmpName) +	toggledlog.Debug.Printf("Rmdir: Renaming %s to %s", nametransform.DirIVFilename, tmpDirivPath) +	// The directory is in an inconsistent state between rename and rmdir. Protect against +	// concurrent readers. +	fs.dirIVLock.Lock() +	defer fs.dirIVLock.Unlock() +	err = os.Rename(dirivPath, tmpDirivPath) +	if err != nil { +		toggledlog.Warn.Printf("Rmdir: Renaming %s to %s failed: %v", +			nametransform.DirIVFilename, tmpDirivPath, err) +		return fuse.ToStatus(err) +	} +	// Actual Rmdir +	err = syscall.Rmdir(encPath) +	if err != nil { +		// This can happen if another file in the directory was created in the +		// meantime, undo the rename +		err2 := os.Rename(tmpDirivPath, dirivPath) +		if err2 != nil { +			toggledlog.Warn.Printf("Rmdir: Rename rollback failed: %v", err2) +		} +		return fuse.ToStatus(err) +	} +	// Delete "gocryptfs.diriv.rmdir.INODENUMBER" +	err = syscall.Unlink(tmpDirivPath) +	if err != nil { +		toggledlog.Warn.Printf("Rmdir: Could not clean up %s: %v", tmpName, err) +	} +	// The now-deleted directory may have been in the DirIV cache. Clear it. +	fs.nameTransform.DirIVCache.Clear() +	return fuse.OK +} diff --git a/internal/fusefrontend/names.go b/internal/fusefrontend/names.go new file mode 100644 index 0000000..5760c87 --- /dev/null +++ b/internal/fusefrontend/names.go @@ -0,0 +1,52 @@ +package fusefrontend + +// This file forwards file encryption operations to cryptfs + +import ( +	"github.com/rfjakob/gocryptfs/internal/configfile" +	mylog "github.com/rfjakob/gocryptfs/internal/toggledlog" +) + +// isFiltered - check if plaintext "path" should be forbidden +// +// Prevents name clashes with internal files when file names are not encrypted +func (fs *FS) isFiltered(path string) bool { +	if !fs.args.PlaintextNames { +		return false +	} +	// gocryptfs.conf in the root directory is forbidden +	if path == configfile.ConfDefaultName { +		mylog.Info.Printf("The name /%s is reserved when -plaintextnames is used\n", +			configfile.ConfDefaultName) +		return true +	} +	// Note: gocryptfs.diriv is NOT forbidden because diriv and plaintextnames +	// are exclusive +	return false +} + +// encryptPath - encrypt relative plaintext path +func (fs *FS) encryptPath(plainPath string) (string, error) { +	if fs.args.PlaintextNames { +		return plainPath, nil +	} +	if !fs.args.DirIV { +		return fs.nameTransform.EncryptPathNoIV(plainPath), nil +	} +	fs.dirIVLock.RLock() +	defer fs.dirIVLock.RUnlock() +	return fs.nameTransform.EncryptPathDirIV(plainPath, fs.args.Cipherdir) +} + +// decryptPath - decrypt relative ciphertext path +func (fs *FS) decryptPath(cipherPath string) (string, error) { +	if fs.args.PlaintextNames { +		return cipherPath, nil +	} +	if !fs.args.DirIV { +		return fs.nameTransform.DecryptPathNoIV(cipherPath) +	} +	fs.dirIVLock.RLock() +	defer fs.dirIVLock.RUnlock() +	return fs.nameTransform.DecryptPathDirIV(cipherPath, fs.args.Cipherdir, fs.args.EMENames) +} diff --git a/internal/fusefrontend/write_lock.go b/internal/fusefrontend/write_lock.go new file mode 100644 index 0000000..a8ec6b6 --- /dev/null +++ b/internal/fusefrontend/write_lock.go @@ -0,0 +1,66 @@ +package fusefrontend + +import ( +	"sync" +) + +func init() { +	wlock.m = make(map[uint64]*refCntMutex) +} + +// wlock - serializes write accesses to each file (identified by inode number) +// Writing partial blocks means we have to do read-modify-write cycles. We +// really don't want concurrent writes there. +// Concurrent full-block writes could actually be allowed, but are not to +// keep the locking simple. +var wlock wlockMap + +// wlockMap - usage: +// 1) register +// 2) lock ... unlock ... +// 3) unregister +type wlockMap struct { +	mapMutex sync.RWMutex +	m        map[uint64]*refCntMutex +} + +func (w *wlockMap) register(ino uint64) { +	w.mapMutex.Lock() +	r := w.m[ino] +	if r == nil { +		r = &refCntMutex{} +		w.m[ino] = r +	} +	r.refCnt++ // this must happen inside the mapMutex lock +	w.mapMutex.Unlock() +} + +func (w *wlockMap) unregister(ino uint64) { +	w.mapMutex.Lock() +	r := w.m[ino] +	r.refCnt-- +	if r.refCnt == 0 { +		delete(w.m, ino) +	} +	w.mapMutex.Unlock() +} + +func (w *wlockMap) lock(ino uint64) { +	w.mapMutex.RLock() +	r := w.m[ino] +	w.mapMutex.RUnlock() +	r.Lock() // this can take a long time - execute outside the mapMutex lock +} + +func (w *wlockMap) unlock(ino uint64) { +	w.mapMutex.RLock() +	r := w.m[ino] +	w.mapMutex.RUnlock() +	r.Unlock() +} + +// refCntMutex - mutex with reference count +type refCntMutex struct { +	sync.Mutex +	refCnt int +} | 
