From 9078a77850dd680bfa938d9ed7c83600a60c0e7b Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 6 Feb 2016 19:27:59 +0100 Subject: 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 --- internal/fusefrontend/args.go | 12 + internal/fusefrontend/compat_darwin.go | 12 + internal/fusefrontend/compat_linux.go | 18 ++ internal/fusefrontend/file.go | 501 +++++++++++++++++++++++++++++++++ internal/fusefrontend/file_holes.go | 29 ++ internal/fusefrontend/fs.go | 381 +++++++++++++++++++++++++ internal/fusefrontend/fs_dir.go | 157 +++++++++++ internal/fusefrontend/names.go | 52 ++++ internal/fusefrontend/write_lock.go | 66 +++++ main.go | 12 +- pathfs_frontend/args.go | 12 - pathfs_frontend/compat_darwin.go | 12 - pathfs_frontend/compat_linux.go | 18 -- pathfs_frontend/file.go | 501 --------------------------------- pathfs_frontend/file_holes.go | 29 -- pathfs_frontend/fs.go | 381 ------------------------- pathfs_frontend/fs_dir.go | 157 ----------- pathfs_frontend/names.go | 52 ---- pathfs_frontend/write_lock.go | 66 ----- 19 files changed, 1234 insertions(+), 1234 deletions(-) create mode 100644 internal/fusefrontend/args.go create mode 100644 internal/fusefrontend/compat_darwin.go create mode 100644 internal/fusefrontend/compat_linux.go create mode 100644 internal/fusefrontend/file.go create mode 100644 internal/fusefrontend/file_holes.go create mode 100644 internal/fusefrontend/fs.go create mode 100644 internal/fusefrontend/fs_dir.go create mode 100644 internal/fusefrontend/names.go create mode 100644 internal/fusefrontend/write_lock.go delete mode 100644 pathfs_frontend/args.go delete mode 100644 pathfs_frontend/compat_darwin.go delete mode 100644 pathfs_frontend/compat_linux.go delete mode 100644 pathfs_frontend/file.go delete mode 100644 pathfs_frontend/file_holes.go delete mode 100644 pathfs_frontend/fs.go delete mode 100644 pathfs_frontend/fs_dir.go delete mode 100644 pathfs_frontend/names.go delete mode 100644 pathfs_frontend/write_lock.go 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 +} diff --git a/main.go b/main.go index 58e1155..e91e83f 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,7 @@ import ( "github.com/hanwen/go-fuse/fuse/nodefs" "github.com/hanwen/go-fuse/fuse/pathfs" - "github.com/rfjakob/gocryptfs/pathfs_frontend" + "github.com/rfjakob/gocryptfs/internal/fusefrontend" "github.com/rfjakob/gocryptfs/internal/configfile" "github.com/rfjakob/gocryptfs/internal/toggledlog" "github.com/rfjakob/gocryptfs/internal/nametransform" @@ -304,7 +304,7 @@ func main() { } // Initialize FUSE server toggledlog.Debug.Printf("cli args: %v", args) - srv := pathfsFrontend(masterkey, args, confFile) + srv := initFuseFrontend(masterkey, args, confFile) toggledlog.Info.Println(colorGreen + "Filesystem mounted and ready." + colorReset) // We are ready - send USR1 signal to our parent and switch to syslog if args.notifypid > 0 { @@ -324,13 +324,13 @@ func main() { // main exits with code 0 } -// pathfsFrontend - initialize gocryptfs/pathfs_frontend +// initFuseFrontend - initialize gocryptfs/fusefrontend // Calls os.Exit on errors -func pathfsFrontend(key []byte, args argContainer, confFile *configfile.ConfFile) *fuse.Server { +func initFuseFrontend(key []byte, args argContainer, confFile *configfile.ConfFile) *fuse.Server { // Reconciliate CLI and config file arguments into a Args struct that is passed to the // filesystem implementation - frontendArgs := pathfs_frontend.Args{ + frontendArgs := fusefrontend.Args{ Cipherdir: args.cipherdir, Masterkey: key, OpenSSL: args.openssl, @@ -359,7 +359,7 @@ func pathfsFrontend(key []byte, args argContainer, confFile *configfile.ConfFile jsonBytes, _ := json.MarshalIndent(frontendArgs, "", "\t") toggledlog.Debug.Printf("frontendArgs: %s", string(jsonBytes)) - finalFs := pathfs_frontend.NewFS(frontendArgs) + finalFs := fusefrontend.NewFS(frontendArgs) pathFsOpts := &pathfs.PathNodeFsOptions{ClientInodes: true} pathFs := pathfs.NewPathNodeFs(finalFs, pathFsOpts) fuseOpts := &nodefs.Options{ diff --git a/pathfs_frontend/args.go b/pathfs_frontend/args.go deleted file mode 100644 index 91f9ba7..0000000 --- a/pathfs_frontend/args.go +++ /dev/null @@ -1,12 +0,0 @@ -package pathfs_frontend - -// Container for arguments that are passed from main() to pathfs_frontend -type Args struct { - Masterkey []byte - Cipherdir string - OpenSSL bool - PlaintextNames bool - DirIV bool - EMENames bool - GCMIV128 bool -} diff --git a/pathfs_frontend/compat_darwin.go b/pathfs_frontend/compat_darwin.go deleted file mode 100644 index 3ddb50d..0000000 --- a/pathfs_frontend/compat_darwin.go +++ /dev/null @@ -1,12 +0,0 @@ -package pathfs_frontend - -// 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/pathfs_frontend/compat_linux.go b/pathfs_frontend/compat_linux.go deleted file mode 100644 index 7ed3c74..0000000 --- a/pathfs_frontend/compat_linux.go +++ /dev/null @@ -1,18 +0,0 @@ -package pathfs_frontend - -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/pathfs_frontend/file.go b/pathfs_frontend/file.go deleted file mode 100644 index 387eb35..0000000 --- a/pathfs_frontend/file.go +++ /dev/null @@ -1,501 +0,0 @@ -package pathfs_frontend - -// 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/pathfs_frontend/file_holes.go b/pathfs_frontend/file_holes.go deleted file mode 100644 index a147deb..0000000 --- a/pathfs_frontend/file_holes.go +++ /dev/null @@ -1,29 +0,0 @@ -package pathfs_frontend - -// 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/pathfs_frontend/fs.go b/pathfs_frontend/fs.go deleted file mode 100644 index 212f0a7..0000000 --- a/pathfs_frontend/fs.go +++ /dev/null @@ -1,381 +0,0 @@ -package pathfs_frontend - -// 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/pathfs_frontend/fs_dir.go b/pathfs_frontend/fs_dir.go deleted file mode 100644 index d378d28..0000000 --- a/pathfs_frontend/fs_dir.go +++ /dev/null @@ -1,157 +0,0 @@ -package pathfs_frontend - -// 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/pathfs_frontend/names.go b/pathfs_frontend/names.go deleted file mode 100644 index 160fa0a..0000000 --- a/pathfs_frontend/names.go +++ /dev/null @@ -1,52 +0,0 @@ -package pathfs_frontend - -// 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/pathfs_frontend/write_lock.go b/pathfs_frontend/write_lock.go deleted file mode 100644 index 0704eb6..0000000 --- a/pathfs_frontend/write_lock.go +++ /dev/null @@ -1,66 +0,0 @@ -package pathfs_frontend - -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 -} -- cgit v1.2.3