diff options
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 +} |