v2api/reverse: implement Read

This commit is contained in:
Jakob Unterwurzacher 2020-08-09 22:11:46 +02:00
parent 5276092663
commit 6d4f1a6888
8 changed files with 327 additions and 24 deletions

View File

@ -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())
}

View File

@ -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))
*/

View File

@ -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
}

View File

@ -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
}

View File

@ -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))

View File

@ -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)

View File

@ -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
}

View File

@ -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)