diff --git a/internal/fusefrontend_reverse/file.go b/internal/fusefrontend_reverse/file.go new file mode 100644 index 0000000..55f5f80 --- /dev/null +++ b/internal/fusefrontend_reverse/file.go @@ -0,0 +1,70 @@ +package fusefrontend_reverse + +import ( + "bytes" + "context" + "os" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" + + "github.com/rfjakob/gocryptfs/internal/contentenc" +) + +type File struct { + // Backing FD + fd *os.File + // File header (contains the IV) + header contentenc.FileHeader + // IV for block 0 + block0IV []byte + // Content encryption helper + contentEnc *contentenc.ContentEnc +} + +// Read - FUSE call +func (f *File) Read(ctx context.Context, buf []byte, ioff int64) (resultData fuse.ReadResult, errno syscall.Errno) { + length := uint64(len(buf)) + off := uint64(ioff) + out := bytes.NewBuffer(buf[:0]) + var header []byte + + // Synthesize file header + if off < contentenc.HeaderLen { + header = f.header.Pack() + // Truncate to requested part + end := int(off) + len(buf) + if end > len(header) { + end = len(header) + } + header = header[off:end] + // Write into output buffer and adjust offsets + out.Write(header) + hLen := uint64(len(header)) + off += hLen + length -= hLen + } + + // Read actual file data + if length > 0 { + fileData, err := f.readBackingFile(off, length) + if err != nil { + return nil, fs.ToErrno(err) + } + if len(fileData) == 0 { + // If we could not read any actual data, we also don't want to + // return the file header. An empty file stays empty in encrypted + // form. + return nil, 0 + } + out.Write(fileData) + } + + return fuse.ReadResultData(out.Bytes()), 0 +} + +// Release - FUSE call, close file +func (f *File) Release(context.Context) syscall.Errno { + return fs.ToErrno(f.fd.Close()) +} diff --git a/internal/fusefrontend_reverse/file_api_check.go b/internal/fusefrontend_reverse/file_api_check.go new file mode 100644 index 0000000..6dd1771 --- /dev/null +++ b/internal/fusefrontend_reverse/file_api_check.go @@ -0,0 +1,23 @@ +package fusefrontend_reverse + +import ( + "github.com/hanwen/go-fuse/v2/fs" +) + +// Check that we have implemented the fs.File* interfaces +var _ = (fs.FileReader)((*File)(nil)) +var _ = (fs.FileReleaser)((*File)(nil)) + +/* TODO +var _ = (fs.FileGetattrer)((*File2)(nil)) +var _ = (fs.FileSetattrer)((*File2)(nil)) +var _ = (fs.FileWriter)((*File2)(nil)) +var _ = (fs.FileFsyncer)((*File2)(nil)) +var _ = (fs.FileFlusher)((*File2)(nil)) +var _ = (fs.FileAllocater)((*File2)(nil)) +var _ = (fs.FileLseeker)((*File2)(nil)) +var _ = (fs.FileHandle)((*File2)(nil)) +var _ = (fs.FileGetlker)((*File2)(nil)) +var _ = (fs.FileSetlker)((*File2)(nil)) +var _ = (fs.FileSetlkwer)((*File2)(nil)) +*/ diff --git a/internal/fusefrontend_reverse/file_helpers.go b/internal/fusefrontend_reverse/file_helpers.go new file mode 100644 index 0000000..f024e69 --- /dev/null +++ b/internal/fusefrontend_reverse/file_helpers.go @@ -0,0 +1,62 @@ +package fusefrontend_reverse + +import ( + "bytes" + "io" + "sync" + + "github.com/rfjakob/gocryptfs/internal/contentenc" + "github.com/rfjakob/gocryptfs/internal/pathiv" + "github.com/rfjakob/gocryptfs/internal/tlog" +) + +var inodeTable sync.Map + +// encryptBlocks - encrypt "plaintext" into a number of ciphertext blocks. +// "plaintext" must already be block-aligned. +func (rf *File) encryptBlocks(plaintext []byte, firstBlockNo uint64, fileID []byte, block0IV []byte) []byte { + inBuf := bytes.NewBuffer(plaintext) + var outBuf bytes.Buffer + bs := int(rf.contentEnc.PlainBS()) + for blockNo := firstBlockNo; inBuf.Len() > 0; blockNo++ { + inBlock := inBuf.Next(bs) + iv := pathiv.BlockIV(block0IV, blockNo) + outBlock := rf.contentEnc.EncryptBlockNonce(inBlock, blockNo, fileID, iv) + outBuf.Write(outBlock) + } + return outBuf.Bytes() +} + +// readBackingFile: read from the backing plaintext file, encrypt it, return the +// ciphertext. +// "off" ... ciphertext offset (must be >= HEADER_LEN) +// "length" ... ciphertext length +func (f *File) readBackingFile(off uint64, length uint64) (out []byte, err error) { + blocks := f.contentEnc.ExplodeCipherRange(off, length) + + // Read the backing plaintext in one go + alignedOffset, alignedLength := contentenc.JointPlaintextRange(blocks) + plaintext := make([]byte, int(alignedLength)) + n, err := f.fd.ReadAt(plaintext, int64(alignedOffset)) + if err != nil && err != io.EOF { + tlog.Warn.Printf("readBackingFile: ReadAt: %s", err.Error()) + return nil, err + } + // Truncate buffer down to actually read bytes + plaintext = plaintext[0:n] + + // Encrypt blocks + ciphertext := f.encryptBlocks(plaintext, blocks[0].BlockNo, f.header.ID, f.block0IV) + + // Crop down to the relevant part + lenHave := len(ciphertext) + skip := blocks[0].Skip + endWant := int(skip + length) + if lenHave > endWant { + out = ciphertext[skip:endWant] + } else if lenHave > int(skip) { + out = ciphertext[skip:lenHave] + } // else: out stays empty, file was smaller than the requested offset + + return out, nil +} diff --git a/internal/fusefrontend_reverse/node.go b/internal/fusefrontend_reverse/node.go index 884a97e..deaf953 100644 --- a/internal/fusefrontend_reverse/node.go +++ b/internal/fusefrontend_reverse/node.go @@ -2,6 +2,8 @@ package fusefrontend_reverse import ( "context" + "fmt" + "os" "path/filepath" "syscall" @@ -10,8 +12,10 @@ import ( "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" - "github.com/rfjakob/gocryptfs/internal/configfile" + "github.com/rfjakob/gocryptfs/internal/contentenc" + "github.com/rfjakob/gocryptfs/internal/pathiv" "github.com/rfjakob/gocryptfs/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/internal/tlog" ) // Node is a file or directory in the filesystem tree @@ -33,15 +37,7 @@ func (n *Node) Lookup(ctx context.Context, cName string, out *fuse.EntryOut) (ch return n.lookupLongnameName(ctx, cName, out) } else if t == typeConfig { // gocryptfs.conf - var err error - pName = configfile.ConfReverseName - rn := n.rootNode() - dirfd, err = syscallcompat.OpenDirNofollow(rn.args.Cipherdir, "") - if err != nil { - errno = fs.ToErrno(err) - return - } - defer syscall.Close(dirfd) + return n.lookupConf(ctx, out) } else if t == typeReal { // real file dirfd, pName, errno = n.prepareAtSyscall(cName) @@ -112,3 +108,73 @@ func (n *Node) Readlink(ctx context.Context) (out []byte, errno syscall.Errno) { cName := filepath.Base(n.Path()) return n.readlink(dirfd, cName, pName) } + +// Open - FUSE call. Open already-existing file. +// +// Symlink-safe through Openat(). +func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + dirfd, pName, errno := n.prepareAtSyscall("") + if errno != 0 { + return + } + defer syscall.Close(dirfd) + + fd, err := syscallcompat.Openat(dirfd, pName, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0) + if err != nil { + errno = fs.ToErrno(err) + return + } + + // Reject access if the file descriptor does not refer to a regular file. + var st syscall.Stat_t + err = syscall.Fstat(fd, &st) + if err != nil { + tlog.Warn.Printf("Open: Fstat error: %v", err) + syscall.Close(fd) + errno = fs.ToErrno(err) + return + } + var a fuse.Attr + a.FromStat(&st) + if !a.IsRegular() { + tlog.Warn.Printf("ino%d: newFile: not a regular file", st.Ino) + syscall.Close(fd) + errno = syscall.EACCES + return + } + // See if we have that inode number already in the table + // (even if Nlink has dropped to 1) + var derivedIVs pathiv.FileIVs + v, found := inodeTable.Load(st.Ino) + if found { + tlog.Debug.Printf("ino%d: newFile: found in the inode table", st.Ino) + derivedIVs = v.(pathiv.FileIVs) + } else { + p := n.Path() + derivedIVs = pathiv.DeriveFile(p) + // Nlink > 1 means there is more than one path to this file. + // Store the derived values so we always return the same data, + // regardless of the path that is used to access the file. + // This means that the first path wins. + if st.Nlink > 1 { + v, found = inodeTable.LoadOrStore(st.Ino, derivedIVs) + if found { + // Another thread has stored a different value before we could. + derivedIVs = v.(pathiv.FileIVs) + } else { + tlog.Debug.Printf("ino%d: newFile: Nlink=%d, stored in the inode table", st.Ino, st.Nlink) + } + } + } + header := contentenc.FileHeader{ + Version: contentenc.CurrentVersion, + ID: derivedIVs.ID, + } + fh = &File{ + fd: os.NewFile(uintptr(fd), fmt.Sprintf("fd%d", fd)), + header: header, + block0IV: derivedIVs.Block0IV, + contentEnc: n.rootNode().contentEnc, + } + return +} diff --git a/internal/fusefrontend_reverse/node_api_check.go b/internal/fusefrontend_reverse/node_api_check.go index e926fc3..875c9b9 100644 --- a/internal/fusefrontend_reverse/node_api_check.go +++ b/internal/fusefrontend_reverse/node_api_check.go @@ -9,11 +9,10 @@ var _ = (fs.NodeGetattrer)((*Node)(nil)) var _ = (fs.NodeLookuper)((*Node)(nil)) var _ = (fs.NodeReaddirer)((*Node)(nil)) var _ = (fs.NodeReadlinker)((*Node)(nil)) - -/* TODO var _ = (fs.NodeOpener)((*Node)(nil)) + +/* var _ = (fs.NodeStatfser)((*Node)(nil)) -var _ = (fs.NodeMknoder)((*Node)(nil)) var _ = (fs.NodeGetxattrer)((*Node)(nil)) var _ = (fs.NodeListxattrer)((*Node)(nil)) */ @@ -23,6 +22,7 @@ var _ = (fs.NodeOpendirer)((*Node)(nil)) */ /* Will not implement these - reverse mode is read-only! +var _ = (fs.NodeMknoder)((*Node)(nil)) var _ = (fs.NodeCreater)((*Node)(nil)) var _ = (fs.NodeMkdirer)((*Node)(nil)) var _ = (fs.NodeRmdirer)((*Node)(nil)) diff --git a/internal/fusefrontend_reverse/node_helpers.go b/internal/fusefrontend_reverse/node_helpers.go index 7669a17..fd0abfc 100644 --- a/internal/fusefrontend_reverse/node_helpers.go +++ b/internal/fusefrontend_reverse/node_helpers.go @@ -10,6 +10,7 @@ import ( "github.com/hanwen/go-fuse/v2/fs" "github.com/hanwen/go-fuse/v2/fuse" + "github.com/rfjakob/gocryptfs/internal/configfile" "github.com/rfjakob/gocryptfs/internal/pathiv" "github.com/rfjakob/gocryptfs/internal/syscallcompat" ) @@ -115,8 +116,8 @@ func (n *Node) lookupLongnameName(ctx context.Context, nameFile string, out *fus errno = fs.ToErrno(err) return } - var vf *virtualFile - vf, errno = n.newVirtualFile([]byte(cFullname), st, inoTagNameFile) + var vf *VirtualMemNode + vf, errno = n.newVirtualMemNode([]byte(cFullname), st, inoTagNameFile) if errno != 0 { return nil, errno } @@ -141,8 +142,8 @@ func (n *Node) lookupDiriv(ctx context.Context, out *fuse.EntryOut) (ch *fs.Inod return } content := pathiv.Derive(n.Path(), pathiv.PurposeDirIV) - var vf *virtualFile - vf, errno = n.newVirtualFile(content, st, inoTagDirIV) + var vf *VirtualMemNode + vf, errno = n.newVirtualMemNode(content, st, inoTagDirIV) if errno != 0 { return nil, errno } @@ -153,6 +154,30 @@ func (n *Node) lookupDiriv(ctx context.Context, out *fuse.EntryOut) (ch *fs.Inod return } +// lookupConf returns a new Inode for the gocryptfs.conf file +func (n *Node) lookupConf(ctx context.Context, out *fuse.EntryOut) (ch *fs.Inode, errno syscall.Errno) { + rn := n.rootNode() + p := filepath.Join(rn.args.Cipherdir, configfile.ConfReverseName) + var st syscall.Stat_t + err := syscall.Stat(p, &st) + if err != nil { + errno = fs.ToErrno(err) + return + } + // Get unique inode number + rn.inoMap.TranslateStat(&st) + out.Attr.FromStat(&st) + // Create child node + id := fs.StableAttr{ + Mode: uint32(st.Mode), + Gen: 1, + Ino: st.Ino, + } + node := &VirtualConfNode{path: p} + ch = n.NewInode(ctx, node, id) + return +} + // readlink reads and encrypts a symlink. Used by Readlink, Getattr, Lookup. func (n *Node) readlink(dirfd int, cName string, pName string) (out []byte, errno syscall.Errno) { plainTarget, err := syscallcompat.Readlinkat(dirfd, pName) diff --git a/internal/fusefrontend_reverse/virtualconf.go b/internal/fusefrontend_reverse/virtualconf.go new file mode 100644 index 0000000..8620f6d --- /dev/null +++ b/internal/fusefrontend_reverse/virtualconf.go @@ -0,0 +1,55 @@ +package fusefrontend_reverse + +import ( + "context" + "sync" + "syscall" + + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" +) + +var _ = (fs.NodeOpener)((*VirtualConfNode)(nil)) + +type VirtualConfNode struct { + fs.Inode + + path string +} + +func (n *VirtualConfNode) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { + fd, err := syscall.Open(n.path, syscall.O_RDONLY, 0) + if err != nil { + errno = fs.ToErrno(err) + return + } + fh = &VirtualConfFile{fd: fd} + return +} + +// Check that we have implemented the fs.File* interfaces +var _ = (fs.FileReader)((*VirtualConfFile)(nil)) +var _ = (fs.FileReleaser)((*VirtualConfFile)(nil)) + +type VirtualConfFile struct { + mu sync.Mutex + fd int +} + +func (f *VirtualConfFile) Read(ctx context.Context, buf []byte, off int64) (res fuse.ReadResult, errno syscall.Errno) { + f.mu.Lock() + defer f.mu.Unlock() + res = fuse.ReadResultFd(uintptr(f.fd), off, len(buf)) + return +} + +func (f *VirtualConfFile) Release(ctx context.Context) syscall.Errno { + f.mu.Lock() + defer f.mu.Unlock() + if f.fd != -1 { + err := syscall.Close(f.fd) + f.fd = -1 + return fs.ToErrno(err) + } + return syscall.EBADF +} diff --git a/internal/fusefrontend_reverse/virtualfile.go b/internal/fusefrontend_reverse/virtualnode.go similarity index 78% rename from internal/fusefrontend_reverse/virtualfile.go rename to internal/fusefrontend_reverse/virtualnode.go index a7f6913..2ee9548 100644 --- a/internal/fusefrontend_reverse/virtualfile.go +++ b/internal/fusefrontend_reverse/virtualnode.go @@ -60,7 +60,9 @@ func (n *Node) lookupFileType(cName string) fileType { return typeReal } -type virtualFile struct { +// VirtualMemNode is an in-memory node that does not have a representation +// on disk. +type VirtualMemNode struct { fs.Inode // file content @@ -69,12 +71,12 @@ type virtualFile struct { attr fuse.Attr } -// newVirtualFile creates a new in-memory file that does not have a representation +// newVirtualMemNode creates a new in-memory file that does not have a representation // on disk. "content" is the file content. Timestamps and file owner are copied // from "parentFile" (file descriptor). // For a "gocryptfs.diriv" file, you would use the parent directory as // "parentFile". -func (n *Node) newVirtualFile(content []byte, parentStat *syscall.Stat_t, inoTag uint8) (vf *virtualFile, errno syscall.Errno) { +func (n *Node) newVirtualMemNode(content []byte, parentStat *syscall.Stat_t, inoTag uint8) (vf *VirtualMemNode, errno syscall.Errno) { if inoTag == 0 { log.Panicf("BUG: inoTag for virtual file is zero - this will cause ino collisions!") } @@ -90,23 +92,23 @@ func (n *Node) newVirtualFile(content []byte, parentStat *syscall.Stat_t, inoTag var a fuse.Attr a.FromStat(st) - vf = &virtualFile{content: content, attr: a} + vf = &VirtualMemNode{content: content, attr: a} return } // Open - FUSE call -func (f *virtualFile) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { +func (f *VirtualMemNode) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) { return nil, fuse.FOPEN_KEEP_CACHE, 0 } // GetAttr - FUSE call -func (f *virtualFile) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { +func (f *VirtualMemNode) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { out.Attr = f.attr return 0 } // Read - FUSE call -func (f *virtualFile) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { +func (f *VirtualMemNode) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { end := int(off) + len(dest) if end > len(f.content) { end = len(f.content)