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.
488 lines
14 KiB
488 lines
14 KiB
package main |
|
|
|
import ( |
|
"C" |
|
"bytes" |
|
"io" |
|
"math" |
|
"os" |
|
"syscall" |
|
|
|
"libgocryptfs/v2/internal/contentenc" |
|
"libgocryptfs/v2/internal/nametransform" |
|
"libgocryptfs/v2/internal/syscallcompat" |
|
) |
|
|
|
// 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 { |
|
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()} |
|
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) |
|
} |
|
|
|
//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) |
|
} |
|
|
|
//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) |
|
}
|
|
|