diff options
Diffstat (limited to 'internal/fusefrontend_reverse_v1api/rfile.go')
-rw-r--r-- | internal/fusefrontend_reverse_v1api/rfile.go | 209 |
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() +} |