aboutsummaryrefslogtreecommitdiff
path: root/internal/fusefrontend_reverse_v1api/rfile.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/fusefrontend_reverse_v1api/rfile.go')
-rw-r--r--internal/fusefrontend_reverse_v1api/rfile.go209
1 files changed, 209 insertions, 0 deletions
diff --git a/internal/fusefrontend_reverse_v1api/rfile.go b/internal/fusefrontend_reverse_v1api/rfile.go
new file mode 100644
index 0000000..68f7309
--- /dev/null
+++ b/internal/fusefrontend_reverse_v1api/rfile.go
@@ -0,0 +1,209 @@
+package fusefrontend_reverse
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "path/filepath"
+ "syscall"
+
+ // In newer Go versions, this has moved to just "sync/syncmap".
+ "golang.org/x/sync/syncmap"
+
+ "github.com/hanwen/go-fuse/v2/fuse"
+ "github.com/hanwen/go-fuse/v2/fuse/nodefs"
+
+ "github.com/rfjakob/gocryptfs/internal/contentenc"
+ "github.com/rfjakob/gocryptfs/internal/pathiv"
+ "github.com/rfjakob/gocryptfs/internal/syscallcompat"
+ "github.com/rfjakob/gocryptfs/internal/tlog"
+)
+
+type reverseFile struct {
+ // Embed nodefs.defaultFile for a ENOSYS implementation of all methods
+ nodefs.File
+ // 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
+}
+
+var inodeTable syncmap.Map
+
+// newFile receives a ciphered path "relPath" and its corresponding
+// decrypted path "pRelPath", opens it and returns a reverseFile
+// object. The backing file descriptor is always read-only.
+func (rfs *ReverseFS) newFile(relPath string, pRelPath string) (*reverseFile, fuse.Status) {
+ if rfs.isExcludedPlain(pRelPath) {
+ // Excluded paths should have been filtered out beforehand. Better safe
+ // than sorry.
+ tlog.Warn.Printf("BUG: newFile: received excluded path %q. This should not happen.", relPath)
+ return nil, fuse.ENOENT
+ }
+ dir := filepath.Dir(pRelPath)
+ dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, dir)
+ if err != nil {
+ return nil, fuse.ToStatus(err)
+ }
+ fd, err := syscallcompat.Openat(dirfd, filepath.Base(pRelPath), syscall.O_RDONLY|syscall.O_NOFOLLOW, 0)
+ syscall.Close(dirfd)
+ if err != nil {
+ return nil, fuse.ToStatus(err)
+ }
+ var st syscall.Stat_t
+ err = syscall.Fstat(fd, &st)
+ if err != nil {
+ tlog.Warn.Printf("newFile: Fstat error: %v", err)
+ syscall.Close(fd)
+ return nil, fuse.ToStatus(err)
+ }
+ // Reject access if the file descriptor does not refer to a regular file.
+ 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)
+ return nil, fuse.ToStatus(syscall.EACCES)
+ }
+ // 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 {
+ derivedIVs = pathiv.DeriveFile(relPath)
+ // 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,
+ }
+ return &reverseFile{
+ File: nodefs.NewDefaultFile(),
+ fd: os.NewFile(uintptr(fd), pRelPath),
+ header: header,
+ block0IV: derivedIVs.Block0IV,
+ contentEnc: rfs.contentEnc,
+ }, fuse.OK
+}
+
+// GetAttr - FUSE call
+// Triggered by fstat() from userspace
+func (rf *reverseFile) GetAttr(*fuse.Attr) fuse.Status {
+ tlog.Debug.Printf("reverseFile.GetAttr fd=%d\n", rf.fd.Fd())
+ // The kernel should fall back to stat()
+ return fuse.ENOSYS
+}
+
+// encryptBlocks - encrypt "plaintext" into a number of ciphertext blocks.
+// "plaintext" must already be block-aligned.
+func (rf *reverseFile) 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 (rf *reverseFile) readBackingFile(off uint64, length uint64) (out []byte, err error) {
+ blocks := rf.contentEnc.ExplodeCipherRange(off, length)
+
+ // Read the backing plaintext in one go
+ alignedOffset, alignedLength := contentenc.JointPlaintextRange(blocks)
+ plaintext := make([]byte, int(alignedLength))
+ n, err := rf.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 := rf.encryptBlocks(plaintext, blocks[0].BlockNo, rf.header.ID, rf.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
+}
+
+// Read - FUSE call
+func (rf *reverseFile) Read(buf []byte, ioff int64) (resultData fuse.ReadResult, status fuse.Status) {
+ length := uint64(len(buf))
+ off := uint64(ioff)
+ var out bytes.Buffer
+ var header []byte
+
+ // Synthesize file header
+ if off < contentenc.HeaderLen {
+ header = rf.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 := rf.readBackingFile(off, length)
+ if err != nil {
+ return nil, fuse.ToStatus(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, fuse.OK
+ }
+ out.Write(fileData)
+ }
+
+ return fuse.ReadResultData(out.Bytes()), fuse.OK
+}
+
+// Release - FUSE call, close file
+func (rf *reverseFile) Release() {
+ rf.fd.Close()
+}