Re-design of the original gocryptfs code to work as a library.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
libgocryptfs/file.go

489 lines
14 KiB

1 year ago
package main
import (
"C"
"bytes"
"io"
"math"
"os"
"syscall"
12 months ago
"libgocryptfs/v2/internal/contentenc"
"libgocryptfs/v2/internal/nametransform"
"libgocryptfs/v2/internal/syscallcompat"
1 year ago
)
// mangleOpenFlags is used by Create() and Open() to convert the open flags the user
// wants to the flags we internally use to open the backing file.
// The returned flags always contain O_NOFOLLOW.
func mangleOpenFlags(flags uint32) (newFlags int) {
newFlags = int(flags)
// Convert WRONLY to RDWR. We always need read access to do read-modify-write cycles.
if (newFlags & syscall.O_ACCMODE) == syscall.O_WRONLY {
newFlags = newFlags ^ os.O_WRONLY | os.O_RDWR
}
// We also cannot open the file in append mode, we need to seek back for RMW
newFlags = newFlags &^ os.O_APPEND
// O_DIRECT accesses must be aligned in both offset and length. Due to our
// crypto header, alignment will be off, even if userspace makes aligned
// accesses. Running xfstests generic/013 on ext4 used to trigger lots of
// EINVAL errors due to missing alignment. Just fall back to buffered IO.
newFlags = newFlags &^ syscallcompat.O_DIRECT
// Create and Open are two separate FUSE operations, so O_CREAT should not
// be part of the open flags.
newFlags = newFlags &^ syscall.O_CREAT
// We always want O_NOFOLLOW to be safe against symlink races
newFlags |= syscall.O_NOFOLLOW
return newFlags
}
func (volume *Volume) registerFileHandle(fd int, cName, path string) int {
1 year ago
handleID := -1
c := 0
for handleID == -1 {
_, ok := volume.file_handles[c]
if !ok {
handleID = c
}
c++
}
volume.file_handles[handleID] = File{os.NewFile(uintptr(fd), cName), string([]byte(path[:]))}//pathCopy.String()}
1 year ago
return handleID
}
// readFileID loads the file header from disk and extracts the file ID.
// Returns io.EOF if the file is empty.
func readFileID(fd *os.File) ([]byte, error) {
// We read +1 byte to determine if the file has actual content
// and not only the header. A header-only file will be considered empty.
// This makes File ID poisoning more difficult.
readLen := contentenc.HeaderLen + 1
buf := make([]byte, readLen)
_, err := fd.ReadAt(buf, 0)
if err != nil {
return nil, err
}
buf = buf[:contentenc.HeaderLen]
h, err := contentenc.ParseHeader(buf)
if err != nil {
return nil, err
}
return h.ID, nil
}
// createHeader creates a new random header and writes it to disk.
// Returns the new file ID.
// The caller must hold fileIDLock.Lock().
func createHeader(fd *os.File) (fileID []byte, err error) {
h := contentenc.RandomHeader()
buf := h.Pack()
// Prevent partially written (=corrupt) header by preallocating the space beforehand
err = syscallcompat.EnospcPrealloc(int(fd.Fd()), 0, contentenc.HeaderLen)
if err != nil {
return nil, err
}
// Actually write header
_, err = fd.WriteAt(buf, 0)
if err != nil {
return nil, err
}
return h.ID, err
}
// doRead - read "length" plaintext bytes from plaintext offset "off" and append
// to "dst".
// Arguments "length" and "off" do not have to be block-aligned.
//
// doRead reads the corresponding ciphertext blocks from disk, decrypts them and
// returns the requested part of the plaintext.
//
// Called by Read() for normal reading,
// by Write() and Truncate() via doWrite() for Read-Modify-Write.
func (volume *Volume) doRead(handleID int, dst []byte, off uint64, length uint64) ([]byte, bool) {
f, ok := volume.file_handles[handleID]
if !ok {
return nil, false
}
fd := f.fd
// Get the file ID, either from the open file table, or from disk.
var fileID []byte
if volume.fileIDs[handleID] != nil {
// Use the cached value in the file table
fileID = volume.fileIDs[handleID]
} else {
// Not cached, we have to read it from disk.
var err error
fileID, err = readFileID(fd)
if err != nil {
return nil, false
}
// Save into the file table
volume.fileIDs[handleID] = fileID
}
// Read the backing ciphertext in one go
blocks := volume.contentEnc.ExplodePlainRange(off, length)
alignedOffset, alignedLength := blocks[0].JointCiphertextRange(blocks)
// f.fd.ReadAt takes an int64!
if alignedOffset > math.MaxInt64 {
return nil, false
}
skip := blocks[0].Skip
ciphertext := volume.contentEnc.CReqPool.Get()
ciphertext = ciphertext[:int(alignedLength)]
n, err := fd.ReadAt(ciphertext, int64(alignedOffset))
if err != nil && err != io.EOF {
return nil, false
}
// The ReadAt came back empty. We can skip all the decryption and return early.
if n == 0 {
volume.contentEnc.CReqPool.Put(ciphertext)
return dst, true
}
// Truncate ciphertext buffer down to actually read bytes
ciphertext = ciphertext[0:n]
firstBlockNo := blocks[0].BlockNo
// Decrypt it
plaintext, err := volume.contentEnc.DecryptBlocks(ciphertext, firstBlockNo, fileID)
volume.contentEnc.CReqPool.Put(ciphertext)
if err != nil {
return nil, false
}
// Crop down to the relevant part
var out []byte
lenHave := len(plaintext)
lenWant := int(skip + length)
if lenHave > lenWant {
out = plaintext[skip:lenWant]
} else if lenHave > int(skip) {
out = plaintext[skip:lenHave]
}
// else: out stays empty, file was smaller than the requested offset
out = append(dst, out...)
volume.contentEnc.PReqPool.Put(plaintext)
return out, true
}
// doWrite - encrypt "data" and write it to plaintext offset "off"
//
// Arguments do not have to be block-aligned, read-modify-write is
// performed internally as necessary
//
// Called by Write() for normal writing,
// and by Truncate() to rewrite the last file block.
//
// Empty writes do nothing and are allowed.
func (volume *Volume) doWrite(handleID int, data []byte, off uint64) (uint32, bool) {
fileWasEmpty := false
// Get the file ID, create a new one if it does not exist yet.
var fileID []byte
f, ok := volume.file_handles[handleID]
if !ok {
return 0, false
}
fd := f.fd
// The caller has exclusively locked ContentLock, which blocks all other
// readers and writers. No need to take IDLock.
if volume.fileIDs[handleID] != nil {
fileID = volume.fileIDs[handleID]
} else {
// If the file ID is not cached, read it from disk
var err error
fileID, err = readFileID(fd)
// Write a new file header if the file is empty
if err == io.EOF {
fileID, err = createHeader(fd)
fileWasEmpty = true
}
if err != nil {
return 0, false
}
volume.fileIDs[handleID] = fileID
}
// Handle payload data
dataBuf := bytes.NewBuffer(data)
blocks := volume.contentEnc.ExplodePlainRange(off, uint64(len(data)))
toEncrypt := make([][]byte, len(blocks))
for i, b := range blocks {
blockData := dataBuf.Next(int(b.Length))
// Incomplete block -> Read-Modify-Write
if b.IsPartial() {
// Read
oldData, success := volume.doRead(handleID, nil, b.BlockPlainOff(), volume.contentEnc.PlainBS())
if !success {
return 0, false
}
// Modify
blockData = volume.contentEnc.MergeBlocks(oldData, blockData, int(b.Skip))
}
// Write into the to-encrypt list
toEncrypt[i] = blockData
}
// Encrypt all blocks
ciphertext := volume.contentEnc.EncryptBlocks(toEncrypt, blocks[0].BlockNo, fileID)
// Preallocate so we cannot run out of space in the middle of the write.
// This prevents partially written (=corrupt) blocks.
var err error
cOff := blocks[0].BlockCipherOff()
// f.fd.WriteAt & syscallcompat.EnospcPrealloc take int64 offsets!
if cOff > math.MaxInt64 {
return 0, false
}
err = syscallcompat.EnospcPrealloc(int(fd.Fd()), int64(cOff), int64(len(ciphertext)))
if err != nil {
if fileWasEmpty {
// Kill the file header again
syscall.Ftruncate(int(fd.Fd()), 0)
gcf_close_file(volume.volumeID, handleID)
}
return 0, false
}
// Write
_, err = f.fd.WriteAt(ciphertext, int64(cOff))
// Return memory to CReqPool
volume.contentEnc.CReqPool.Put(ciphertext)
if err != nil {
return 0, false
}
return uint32(len(data)), true
}
// Zero-pad the file of size plainSize to the next block boundary. This is a no-op
// if the file is already block-aligned.
func (volume *Volume) zeroPad(handleID int, plainSize uint64) bool {
lastBlockLen := plainSize % volume.contentEnc.PlainBS()
if lastBlockLen == 0 {
// Already block-aligned
return true
}
missing := volume.contentEnc.PlainBS() - lastBlockLen
pad := make([]byte, missing)
_, success := volume.doWrite(handleID, pad, plainSize)
return success
}
// truncateGrowFile extends a file using seeking or ftruncate performing RMW on
// the first and last block as necessary. New blocks in the middle become
// file holes unless they have been fallocate()'d beforehand.
func (volume *Volume) truncateGrowFile(handleID int, oldPlainSz uint64, newPlainSz uint64) bool {
if newPlainSz <= oldPlainSz {
return false
}
newEOFOffset := newPlainSz - 1
if oldPlainSz > 0 {
n1 := volume.contentEnc.PlainOffToBlockNo(oldPlainSz - 1)
n2 := volume.contentEnc.PlainOffToBlockNo(newEOFOffset)
// The file is grown within one block, no need to pad anything.
// Write a single zero to the last byte and let doWrite figure out the RMW.
if n1 == n2 {
buf := make([]byte, 1)
_, success := volume.doWrite(handleID, buf, newEOFOffset)
return success
}
}
// The truncate creates at least one new block.
//
// Make sure the old last block is padded to the block boundary. This call
// is a no-op if it is already block-aligned.
success := volume.zeroPad(handleID, oldPlainSz)
if !success {
return false
}
// The new size is block-aligned. In this case we can do everything ourselves
// and avoid the call to doWrite.
if newPlainSz%volume.contentEnc.PlainBS() == 0 {
// The file was empty, so it did not have a header. Create one.
if oldPlainSz == 0 {
id, err := createHeader(volume.file_handles[handleID].fd)
if err != nil {
return false
}
volume.fileIDs[handleID] = id
}
cSz := int64(volume.contentEnc.PlainSizeToCipherSize(newPlainSz))
err := syscall.Ftruncate(int(volume.file_handles[handleID].fd.Fd()), cSz)
return errToBool(err)
}
// The new size is NOT aligned, so we need to write a partial block.
// Write a single zero to the last byte and let doWrite figure it out.
buf := make([]byte, 1)
_, success = volume.doWrite(handleID, buf, newEOFOffset)
return success
}
func (volume *Volume) truncate(handleID int, newSize uint64) bool {
fileFD := int(volume.file_handles[handleID].fd.Fd())
var err error
// Common case first: Truncate to zero
if newSize == 0 {
err = syscall.Ftruncate(fileFD, 0)
return err == nil
}
// We need the old file size to determine if we are growing or shrinking
// the file
oldSize, _, success := gcf_get_attrs(volume.volumeID, volume.file_handles[handleID].path)
if !success {
return false
}
// File size stays the same - nothing to do
if newSize == oldSize {
return true
}
// File grows
if newSize > oldSize {
return volume.truncateGrowFile(handleID, oldSize, newSize)
}
// File shrinks
blockNo := volume.contentEnc.PlainOffToBlockNo(newSize)
cipherOff := volume.contentEnc.BlockNoToCipherOff(blockNo)
plainOff := volume.contentEnc.BlockNoToPlainOff(blockNo)
lastBlockLen := newSize - plainOff
var data []byte
if lastBlockLen > 0 {
data, success = volume.doRead(handleID, nil, plainOff, lastBlockLen)
if !success {
return false
}
}
// Truncate down to the last complete block
err = syscall.Ftruncate(fileFD, int64(cipherOff))
if err != nil {
return false
}
// Append partial block
if lastBlockLen > 0 {
_, success := volume.doWrite(handleID, data, plainOff)
return success
}
return true
}
//export gcf_open_read_mode
func gcf_open_read_mode(sessionID int, path string) int {
volume := OpenedVolumes[sessionID]
dirfd, cName, err := volume.prepareAtSyscall(path)
if err != nil {
return -1
}
defer syscall.Close(dirfd)
// Open backing file
fd, err := syscallcompat.Openat(dirfd, cName, mangleOpenFlags(0), 0)
if err != nil {
return -1
}
return volume.registerFileHandle(fd, cName, path)
1 year ago
}
//export gcf_open_write_mode
func gcf_open_write_mode(sessionID int, path string, mode uint32) int {
volume := OpenedVolumes[sessionID]
dirfd, cName, err := volume.prepareAtSyscall(path)
if err != nil {
return -1
}
defer syscall.Close(dirfd)
fd := -1
newFlags := mangleOpenFlags(syscall.O_RDWR)
// Handle long file name
if !volume.plainTextNames && nametransform.IsLongContent(cName) {
// Create ".name"
err = volume.nameTransform.WriteLongNameAt(dirfd, cName, path)
if err != nil {
return -1
}
// Create content
fd, err = syscallcompat.Openat(dirfd, cName, newFlags|syscall.O_CREAT, mode)
if err != nil {
nametransform.DeleteLongNameAt(dirfd, cName)
}
} else {
// Create content, normal (short) file name
fd, err = syscallcompat.Openat(dirfd, cName, newFlags|syscall.O_CREAT, mode)
}
if err != nil {
return -1
}
return volume.registerFileHandle(fd, cName, path)
1 year ago
}
//export gcf_truncate
func gcf_truncate(sessionID int, handleID int, offset uint64) bool {
volume := OpenedVolumes[sessionID]
return volume.truncate(handleID, offset)
}
//export gcf_read_file
func gcf_read_file(sessionID, handleID int, offset uint64, dst_buff []byte) uint32 {
length := len(dst_buff)
if length > contentenc.MAX_KERNEL_WRITE {
// This would crash us due to our fixed-size buffer pool
return 0
}
volume := OpenedVolumes[sessionID]
out, success := volume.doRead(handleID, dst_buff[:0], offset, uint64(length))
if !success {
return 0
} else {
return uint32(len(out))
}
}
//export gcf_write_file
func gcf_write_file(sessionID, handleID int, offset uint64, data []byte) uint32 {
length := len(data)
if length > contentenc.MAX_KERNEL_WRITE {
// This would crash us due to our fixed-size buffer pool
return 0
}
volume := OpenedVolumes[sessionID]
n, _ := volume.doWrite(handleID, data, offset)
return n
}
//export gcf_close_file
func gcf_close_file(sessionID, handleID int) {
f, ok := OpenedVolumes[sessionID].file_handles[handleID]
if ok {
f.fd.Close()
delete(OpenedVolumes[sessionID].file_handles, handleID)
_, ok := OpenedVolumes[sessionID].fileIDs[handleID]
if ok {
delete(OpenedVolumes[sessionID].fileIDs, handleID)
}
}
}
//export gcf_remove_file
func gcf_remove_file(sessionID int, path string) bool {
volume := OpenedVolumes[sessionID]
dirfd, cName, err := volume.prepareAtSyscall(path)
if err != nil {
return false
}
defer syscall.Close(dirfd)
// Delete content
err = syscallcompat.Unlinkat(dirfd, cName, 0)
if err != nil {
return false
}
// Delete ".name" file
if !volume.plainTextNames && nametransform.IsLongContent(cName) {
err = nametransform.DeleteLongNameAt(dirfd, cName)
}
return errToBool(err)
}