9ecf2d1a3f
With hard links, the path to a file is not unique. This means that the ciphertext data depends on the path that is used to access the files. Fix that by storing the derived values when we encounter a hard-linked file. This means that the first path wins.
199 lines
5.6 KiB
Go
199 lines
5.6 KiB
Go
package fusefrontend_reverse
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/binary"
|
|
"io"
|
|
"os"
|
|
"syscall"
|
|
|
|
// In newer Go versions, this has moved to just "sync/syncmap".
|
|
"golang.org/x/sync/syncmap"
|
|
|
|
"github.com/hanwen/go-fuse/fuse"
|
|
"github.com/hanwen/go-fuse/fuse/nodefs"
|
|
|
|
"github.com/rfjakob/gocryptfs/internal/contentenc"
|
|
"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
|
|
|
|
type derivedIVContainer struct {
|
|
id []byte
|
|
block0IV []byte
|
|
}
|
|
|
|
func (rfs *ReverseFS) newFile(relPath string, flags uint32) (nodefs.File, fuse.Status) {
|
|
absPath, err := rfs.abs(rfs.decryptPath(relPath))
|
|
if err != nil {
|
|
return nil, fuse.ToStatus(err)
|
|
}
|
|
fd, err := os.OpenFile(absPath, int(flags), 0666)
|
|
if err != nil {
|
|
return nil, fuse.ToStatus(err)
|
|
}
|
|
var st syscall.Stat_t
|
|
err = syscall.Fstat(int(fd.Fd()), &st)
|
|
if err != nil {
|
|
tlog.Warn.Printf("newFile: Fstat error: %v", err)
|
|
return nil, fuse.ToStatus(err)
|
|
}
|
|
// See if we have that inode number already in the table
|
|
// (even if Nlink has dropped to 1)
|
|
var derivedIVs derivedIVContainer
|
|
v, found := inodeTable.Load(st.Ino)
|
|
if found {
|
|
tlog.Debug.Printf("ino%d: newFile: found in the inode table", st.Ino)
|
|
derivedIVs = v.(derivedIVContainer)
|
|
} else {
|
|
derivedIVs.id = derivePathIV(relPath, ivPurposeFileID)
|
|
derivedIVs.block0IV = derivePathIV(relPath, ivPurposeBlock0IV)
|
|
// 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.(derivedIVContainer)
|
|
} 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: fd,
|
|
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 {
|
|
nonce := make([]byte, len(block0IV))
|
|
copy(nonce, block0IV)
|
|
block0IVlow := binary.BigEndian.Uint64(block0IV[8:])
|
|
nonceLow := nonce[8:]
|
|
|
|
inBuf := bytes.NewBuffer(plaintext)
|
|
var outBuf bytes.Buffer
|
|
bs := int(rf.contentEnc.PlainBS())
|
|
for blockNo := firstBlockNo; inBuf.Len() > 0; blockNo++ {
|
|
binary.BigEndian.PutUint64(nonceLow, block0IVlow+blockNo)
|
|
inBlock := inBuf.Next(bs)
|
|
outBlock := rf.contentEnc.EncryptBlockNonce(inBlock, blockNo, fileID, nonce)
|
|
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()
|
|
}
|