aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/fusefrontend/args.go12
-rw-r--r--internal/fusefrontend/compat_darwin.go12
-rw-r--r--internal/fusefrontend/compat_linux.go18
-rw-r--r--internal/fusefrontend/file.go501
-rw-r--r--internal/fusefrontend/file_holes.go29
-rw-r--r--internal/fusefrontend/fs.go381
-rw-r--r--internal/fusefrontend/fs_dir.go157
-rw-r--r--internal/fusefrontend/names.go52
-rw-r--r--internal/fusefrontend/write_lock.go66
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
+}