a611810ff4
This proposal is the counterpart of the modifications from the `-badname` parameter. It modifies the plain -> cipher mapping for filenames when using `-badname` parameter. The new function `EncryptAndHashBadName` tries to find a cipher filename for the given plain name with the following steps: 1. If `badname` is disabled or direct mapping is successful: Map directly (default and current behaviour) 2. If a file with badname flag has a valid cipher file, this is returned (=File just ends with the badname flag) 3. If a file with a badname flag exists where only the badname flag was added, this is returned (=File cipher name could not be decrypted by function `DecryptName` and just the badname flag was added) 4. Search for all files which cipher file name extists when cropping more and more characters from the end. If only 1 file is found, return this 5. Return an error otherwise This allows file access in the file browsers but most important it allows that you rename files with undecryptable cipher names in the plain directories. Renaming those files will then generate a proper cipher filename One backdraft: When mounting the cipher dir with -badname parameter, you can never create (or rename to) files whose file name ends with the badname file flag (at the moment this is " GOCRYPTFS_BAD_NAME"). This will cause an error. I modified the CLI test function to cover additional test cases. Test [Case 7](https://github.com/DerDonut/gocryptfs/blob/badnamecontent/tests/cli/cli_test.go#L712) cannot be performed since the cli tests are executed in panic mode. The testing is stopped on error. Since the function`DecryptName` produces internal errors when hitting non-decryptable file names, this test was omitted. This implementation is a proposal where I tried to change the minimum amount of existing code. Another possibility would be instead of creating the new function `EncryptAndHashBadName` to modify the signature of the existing function `EncryptAndHashName(name string, iv []byte)` to `EncryptAndHashName(name string, iv []byte, dirfd int)` and integrate the functionality into this function directly. You may allow calling with dirfd=-1 or other invalid values an then performing the current functionality.
340 lines
11 KiB
Go
340 lines
11 KiB
Go
package fusefrontend
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/rfjakob/gocryptfs/internal/configfile"
|
|
"github.com/rfjakob/gocryptfs/internal/contentenc"
|
|
"github.com/rfjakob/gocryptfs/internal/inomap"
|
|
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
|
"github.com/rfjakob/gocryptfs/internal/serialize_reads"
|
|
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
|
"github.com/rfjakob/gocryptfs/internal/tlog"
|
|
)
|
|
|
|
// RootNode is the root of the filesystem tree of Nodes.
|
|
type RootNode struct {
|
|
Node
|
|
// args stores configuration arguments
|
|
args Args
|
|
// dirIVLock: Lock()ed if any "gocryptfs.diriv" file is modified
|
|
// Readers must RLock() it to prevent them from seeing intermediate
|
|
// states
|
|
dirIVLock sync.RWMutex
|
|
// Filename encryption helper
|
|
nameTransform nametransform.NameTransformer
|
|
// Content encryption helper
|
|
contentEnc *contentenc.ContentEnc
|
|
// This lock is used by openWriteOnlyFile() to block concurrent opens while
|
|
// it relaxes the permissions on a file.
|
|
openWriteOnlyLock sync.RWMutex
|
|
// MitigatedCorruptions is used to report data corruption that is internally
|
|
// mitigated by ignoring the corrupt item. For example, when OpenDir() finds
|
|
// a corrupt filename, we still return the other valid filenames.
|
|
// The corruption is logged to syslog to inform the user, and in addition,
|
|
// the corrupt filename is logged to this channel via
|
|
// reportMitigatedCorruption().
|
|
// "gocryptfs -fsck" reads from the channel to also catch these transparently-
|
|
// mitigated corruptions.
|
|
MitigatedCorruptions chan string
|
|
// IsIdle flag is set to zero each time fs.isFiltered() is called
|
|
// (uint32 so that it can be reset with CompareAndSwapUint32).
|
|
// When -idle was used when mounting, idleMonitor() sets it to 1
|
|
// periodically.
|
|
IsIdle uint32
|
|
// dirCache caches directory fds
|
|
dirCache dirCache
|
|
// inoMap translates inode numbers from different devices to unique inode
|
|
// numbers.
|
|
inoMap inomap.TranslateStater
|
|
}
|
|
|
|
func NewRootNode(args Args, c *contentenc.ContentEnc, n nametransform.NameTransformer) *RootNode {
|
|
if args.SerializeReads {
|
|
serialize_reads.InitSerializer()
|
|
}
|
|
if len(args.Exclude) > 0 {
|
|
tlog.Warn.Printf("Forward mode does not support -exclude")
|
|
}
|
|
rn := &RootNode{
|
|
args: args,
|
|
nameTransform: n,
|
|
contentEnc: c,
|
|
inoMap: inomap.New(),
|
|
}
|
|
// In `-sharedstorage` mode we always set the inode number to zero.
|
|
// This makes go-fuse generate a new inode number for each lookup.
|
|
if args.SharedStorage {
|
|
rn.inoMap = &inomap.TranslateStatZero{}
|
|
}
|
|
return rn
|
|
}
|
|
|
|
// main.doMount() calls this after unmount
|
|
func (rn *RootNode) AfterUnmount() {
|
|
// print stats before we exit
|
|
rn.dirCache.stats()
|
|
}
|
|
|
|
// 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 (rn *RootNode) 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
|
|
}
|
|
|
|
// reportMitigatedCorruption is used to report a corruption that was transparently
|
|
// mitigated and did not return an error to the user. Pass the name of the corrupt
|
|
// item (filename for OpenDir(), xattr name for ListXAttr() etc).
|
|
// See the MitigatedCorruptions channel for more info.
|
|
func (rn *RootNode) reportMitigatedCorruption(item string) {
|
|
if rn.MitigatedCorruptions == nil {
|
|
return
|
|
}
|
|
select {
|
|
case rn.MitigatedCorruptions <- item:
|
|
case <-time.After(1 * time.Second):
|
|
tlog.Warn.Printf("BUG: reportCorruptItem: timeout")
|
|
//debug.PrintStack()
|
|
return
|
|
}
|
|
}
|
|
|
|
// isFiltered - check if plaintext "path" should be forbidden
|
|
//
|
|
// Prevents name clashes with internal files when file names are not encrypted
|
|
func (rn *RootNode) isFiltered(path string) bool {
|
|
if !rn.args.PlaintextNames {
|
|
return false
|
|
}
|
|
// gocryptfs.conf in the root directory is forbidden
|
|
if path == configfile.ConfDefaultName {
|
|
tlog.Info.Printf("The name /%s is reserved when -plaintextnames is used\n",
|
|
configfile.ConfDefaultName)
|
|
return true
|
|
}
|
|
// Note: gocryptfs.diriv is NOT forbidden because diriv and plaintextnames
|
|
// are exclusive
|
|
return false
|
|
}
|
|
|
|
// decryptSymlinkTarget: "cData64" is base64-decoded and decrypted
|
|
// like file contents (GCM).
|
|
// The empty string decrypts to the empty string.
|
|
//
|
|
// This function does not do any I/O and is hence symlink-safe.
|
|
func (rn *RootNode) decryptSymlinkTarget(cData64 string) (string, error) {
|
|
if cData64 == "" {
|
|
return "", nil
|
|
}
|
|
cData, err := rn.nameTransform.B64DecodeString(cData64)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
data, err := rn.contentEnc.DecryptBlock([]byte(cData), 0, nil)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
// Due to RMW, we always need read permissions on the backing file. This is a
|
|
// problem if the file permissions do not allow reading (i.e. 0200 permissions).
|
|
// This function works around that problem by chmod'ing the file, obtaining a fd,
|
|
// and chmod'ing it back.
|
|
func (rn *RootNode) openWriteOnlyFile(dirfd int, cName string, newFlags int) (rwFd int, err error) {
|
|
woFd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NOFOLLOW, 0)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer syscall.Close(woFd)
|
|
var st syscall.Stat_t
|
|
err = syscall.Fstat(woFd, &st)
|
|
if err != nil {
|
|
return
|
|
}
|
|
// The cast to uint32 fixes a build failure on Darwin, where st.Mode is uint16.
|
|
perms := uint32(st.Mode)
|
|
// Verify that we don't have read permissions
|
|
if perms&0400 != 0 {
|
|
tlog.Warn.Printf("openWriteOnlyFile: unexpected permissions %#o, returning EPERM", perms)
|
|
err = syscall.EPERM
|
|
return
|
|
}
|
|
// Upgrade the lock to block other Open()s and downgrade again on return
|
|
rn.openWriteOnlyLock.RUnlock()
|
|
rn.openWriteOnlyLock.Lock()
|
|
defer func() {
|
|
rn.openWriteOnlyLock.Unlock()
|
|
rn.openWriteOnlyLock.RLock()
|
|
}()
|
|
// Relax permissions and revert on return
|
|
err = syscall.Fchmod(woFd, perms|0400)
|
|
if err != nil {
|
|
tlog.Warn.Printf("openWriteOnlyFile: changing permissions failed: %v", err)
|
|
return
|
|
}
|
|
defer func() {
|
|
err2 := syscall.Fchmod(woFd, perms)
|
|
if err2 != nil {
|
|
tlog.Warn.Printf("openWriteOnlyFile: reverting permissions failed: %v", err2)
|
|
}
|
|
}()
|
|
return syscallcompat.Openat(dirfd, cName, newFlags, 0)
|
|
}
|
|
|
|
// openBackingDir opens the parent ciphertext directory of plaintext path
|
|
// "relPath". It returns the dirfd (opened with O_PATH) and the encrypted
|
|
// basename.
|
|
//
|
|
// The caller should then use Openat(dirfd, cName, ...) and friends.
|
|
// For convenience, if relPath is "", cName is going to be ".".
|
|
//
|
|
// openBackingDir is secure against symlink races by using Openat and
|
|
// ReadDirIVAt.
|
|
//
|
|
// Retries on EINTR.
|
|
func (rn *RootNode) openBackingDir(relPath string) (dirfd int, cName string, err error) {
|
|
dirRelPath := nametransform.Dir(relPath)
|
|
// With PlaintextNames, we don't need to read DirIVs. Easy.
|
|
if rn.args.PlaintextNames {
|
|
dirfd, err = syscallcompat.OpenDirNofollow(rn.args.Cipherdir, dirRelPath)
|
|
if err != nil {
|
|
return -1, "", err
|
|
}
|
|
// If relPath is empty, cName is ".".
|
|
cName = filepath.Base(relPath)
|
|
return dirfd, cName, nil
|
|
}
|
|
// Open cipherdir (following symlinks)
|
|
dirfd, err = syscallcompat.Open(rn.args.Cipherdir, syscall.O_DIRECTORY|syscallcompat.O_PATH, 0)
|
|
if err != nil {
|
|
return -1, "", err
|
|
}
|
|
// If relPath is empty, cName is ".".
|
|
if relPath == "" {
|
|
return dirfd, ".", nil
|
|
}
|
|
// Walk the directory tree
|
|
parts := strings.Split(relPath, "/")
|
|
for i, name := range parts {
|
|
iv, err := nametransform.ReadDirIVAt(dirfd)
|
|
if err != nil {
|
|
syscall.Close(dirfd)
|
|
return -1, "", err
|
|
}
|
|
if rn.nameTransform.HaveBadnamePatterns() {
|
|
cName, err = rn.nameTransform.EncryptAndHashBadName(name, iv, dirfd)
|
|
} else {
|
|
cName, err = rn.nameTransform.EncryptAndHashName(name, iv)
|
|
}
|
|
if err != nil {
|
|
syscall.Close(dirfd)
|
|
return -1, "", err
|
|
}
|
|
// Last part? We are done.
|
|
if i == len(parts)-1 {
|
|
break
|
|
}
|
|
// Not the last part? Descend into next directory.
|
|
dirfd2, err := syscallcompat.Openat(dirfd, cName, syscall.O_NOFOLLOW|syscall.O_DIRECTORY|syscallcompat.O_PATH, 0)
|
|
syscall.Close(dirfd)
|
|
if err != nil {
|
|
return -1, "", err
|
|
}
|
|
dirfd = dirfd2
|
|
}
|
|
return dirfd, cName, nil
|
|
}
|
|
|
|
// encryptSymlinkTarget: "data" is encrypted like file contents (GCM)
|
|
// and base64-encoded.
|
|
// The empty string encrypts to the empty string.
|
|
//
|
|
// Symlink-safe because it does not do any I/O.
|
|
func (rn *RootNode) encryptSymlinkTarget(data string) (cData64 string) {
|
|
if data == "" {
|
|
return ""
|
|
}
|
|
cData := rn.contentEnc.EncryptBlock([]byte(data), 0, nil)
|
|
cData64 = rn.nameTransform.B64EncodeToString(cData)
|
|
return cData64
|
|
}
|
|
|
|
// encryptXattrValue encrypts the xattr value "data".
|
|
// The data is encrypted like a file content block, but without binding it to
|
|
// a file location (block number and file id are set to zero).
|
|
// Special case: an empty value is encrypted to an empty value.
|
|
func (rn *RootNode) encryptXattrValue(data []byte) (cData []byte) {
|
|
if len(data) == 0 {
|
|
return []byte{}
|
|
}
|
|
return rn.contentEnc.EncryptBlock(data, 0, nil)
|
|
}
|
|
|
|
// decryptXattrValue decrypts the xattr value "cData".
|
|
func (rn *RootNode) decryptXattrValue(cData []byte) (data []byte, err error) {
|
|
if len(cData) == 0 {
|
|
return []byte{}, nil
|
|
}
|
|
data, err1 := rn.contentEnc.DecryptBlock([]byte(cData), 0, nil)
|
|
if err1 == nil {
|
|
return data, nil
|
|
}
|
|
// This backward compatibility is needed to support old
|
|
// file systems having xattr values base64-encoded.
|
|
cData, err2 := rn.nameTransform.B64DecodeString(string(cData))
|
|
if err2 != nil {
|
|
// Looks like the value was not base64-encoded, but just corrupt.
|
|
// Return the original decryption error: err1
|
|
return nil, err1
|
|
}
|
|
return rn.contentEnc.DecryptBlock([]byte(cData), 0, nil)
|
|
}
|
|
|
|
// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf"
|
|
func (rn *RootNode) encryptXattrName(attr string) (string, error) {
|
|
// xattr names are encrypted like file names, but with a fixed IV.
|
|
cAttr, err := rn.nameTransform.EncryptName(attr, xattrNameIV)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return xattrStorePrefix + cAttr, nil
|
|
}
|
|
|
|
func (rn *RootNode) decryptXattrName(cAttr string) (attr string, err error) {
|
|
// Reject anything that does not start with "user.gocryptfs."
|
|
if !strings.HasPrefix(cAttr, xattrStorePrefix) {
|
|
return "", syscall.EINVAL
|
|
}
|
|
// Strip "user.gocryptfs." prefix
|
|
cAttr = cAttr[len(xattrStorePrefix):]
|
|
attr, err = rn.nameTransform.DecryptName(cAttr, xattrNameIV)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return attr, nil
|
|
}
|