From db72fcea41f01f24ac3edb1cbf86d6b0be60f137 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 10 Apr 2016 19:32:10 +0200 Subject: [PATCH] longnames: fix fsstress failure, use dirfd Using dirfd-relative operations allows safe lockless handling of the ".name" files. --- internal/fusefrontend/fs.go | 195 ++++++++++++++++++----- internal/fusefrontend/fs_dir.go | 188 +++++++++++++--------- internal/nametransform/longnames.go | 92 ++++++----- internal/nametransform/longnames_test.go | 6 +- internal/nametransform/names_diriv.go | 58 ++++--- 5 files changed, 358 insertions(+), 181 deletions(-) diff --git a/internal/fusefrontend/fs.go b/internal/fusefrontend/fs.go index fb82c6b..9e67a6a 100644 --- a/internal/fusefrontend/fs.go +++ b/internal/fusefrontend/fs.go @@ -5,6 +5,7 @@ package fusefrontend import ( "encoding/base64" "os" + "path/filepath" "sync" "syscall" "time" @@ -111,18 +112,38 @@ func (fs *FS) Create(path string, flags uint32, mode uint32, context *fuse.Conte if err != nil { return nil, fuse.ToStatus(err) } - // Create .name file to store the long file name if needed - if !fs.args.PlaintextNames { - err = fs.nameTransform.WriteLongName(cPath, path) + + // Handle long file name + cName := filepath.Base(cPath) + if nametransform.IsLongContent(cName) { + dirfd, err := os.Open(filepath.Dir(cPath)) if err != nil { return nil, fuse.ToStatus(err) } + defer dirfd.Close() + + // Create ".name" + err = fs.nameTransform.WriteLongName(dirfd, cName, path) + if err != nil { + return nil, fuse.ToStatus(err) + } + + // Create content + fdRaw, err := syscall.Openat(int(dirfd.Fd()), cName, iflags|os.O_CREATE, mode) + if err != nil { + nametransform.DeleteLongName(dirfd, cName) + return nil, fuse.ToStatus(err) + } + fd := os.NewFile(uintptr(fdRaw), cName) + + return NewFile(fd, writeOnly, fs.contentEnc), fuse.OK } - f, err := os.OpenFile(cPath, iflags|os.O_CREATE, os.FileMode(mode)) + + fd, err := os.OpenFile(cPath, iflags|os.O_CREATE, os.FileMode(mode)) if err != nil { return nil, fuse.ToStatus(err) } - return NewFile(f, writeOnly, fs.contentEnc), fuse.OK + return NewFile(fd, writeOnly, fs.contentEnc), fuse.OK } func (fs *FS) Chmod(path string, mode uint32, context *fuse.Context) (code fuse.Status) { @@ -155,13 +176,31 @@ func (fs *FS) Mknod(path string, mode uint32, dev uint32, context *fuse.Context) if err != nil { return fuse.ToStatus(err) } - if !fs.args.PlaintextNames { - // Create .name file to store the long file name if needed - err = fs.nameTransform.WriteLongName(cPath, path) + + // Handle long file name + cName := filepath.Base(cPath) + if nametransform.IsLongContent(cName) { + dirfd, err := os.Open(filepath.Dir(cPath)) if err != nil { return fuse.ToStatus(err) } + defer dirfd.Close() + + // Create ".name" + err = fs.nameTransform.WriteLongName(dirfd, cName, path) + if err != nil { + return fuse.ToStatus(err) + } + + // Create device node + err = syscall.Mknodat(int(dirfd.Fd()), cName, uint32(mode), int(dev)) + if err != nil { + nametransform.DeleteLongName(dirfd, cName) + } + + return fuse.ToStatus(err) } + return fs.FileSystem.Mknod(cPath, mode, dev, context) } @@ -227,11 +266,28 @@ func (fs *FS) Unlink(path string, context *fuse.Context) (code fuse.Status) { if err != nil { return fuse.ToStatus(err) } - err = syscall.Unlink(cPath) - // Delete .name file - if err == nil && !fs.args.PlaintextNames { - nametransform.DeleteLongName(cPath) + + cName := filepath.Base(cPath) + if nametransform.IsLongContent(cName) { + dirfd, err := os.Open(filepath.Dir(cPath)) + if err != nil { + return fuse.ToStatus(err) + } + defer dirfd.Close() + // Delete content + err = syscall.Unlinkat(int(dirfd.Fd()), cName) + if err != nil { + return fuse.ToStatus(err) + } + // Delete ".name" + err = nametransform.DeleteLongName(dirfd, cName) + if err != nil { + toggledlog.Warn.Printf("Unlink: could not delete .name file: %v", err) + } + return fuse.ToStatus(err) } + + err = syscall.Unlink(cPath) return fuse.ToStatus(err) } @@ -244,8 +300,8 @@ func (fs *FS) Symlink(target string, linkName string, context *fuse.Context) (co if err != nil { return fuse.ToStatus(err) } - // Old filesystem: symlinks are encrypted like paths (CBC) - // TODO drop compatibility and simplify code + // Before v0.5, symlinks were encrypted like paths (CBC) + // TODO drop compatibility and simplify code? if !fs.args.DirIV { cTarget, err := fs.encryptPath(target) if err != nil { @@ -255,18 +311,36 @@ func (fs *FS) Symlink(target string, linkName string, context *fuse.Context) (co err = os.Symlink(cTarget, cPath) return fuse.ToStatus(err) } - // Since gocryptfs v0.5 symlinks are encrypted like file contents (GCM) + cBinTarget := fs.contentEnc.EncryptBlock([]byte(target), 0, nil) cTarget := base64.URLEncoding.EncodeToString(cBinTarget) - if !fs.args.PlaintextNames { - // Create .name file to store the long file name if needed - err = fs.nameTransform.WriteLongName(cPath, linkName) + + // Handle long file name + cName := filepath.Base(cPath) + if nametransform.IsLongContent(cName) { + dirfd, err := os.Open(filepath.Dir(cPath)) if err != nil { return fuse.ToStatus(err) } + defer dirfd.Close() + + // Create ".name" + err = fs.nameTransform.WriteLongName(dirfd, cName, linkName) + if err != nil { + return fuse.ToStatus(err) + } + + // Create symlink + // TODO use syscall.Symlinkat once it is available in Go + err = syscall.Symlink(cTarget, cPath) + if err != nil { + nametransform.DeleteLongName(dirfd, cName) + } + + return fuse.ToStatus(err) } + err = os.Symlink(cTarget, cPath) - toggledlog.Debug.Printf("Symlink: os.Symlink(%s, %s) = %v", cTarget, cPath, err) return fuse.ToStatus(err) } @@ -286,35 +360,61 @@ func (fs *FS) Rename(oldPath string, newPath string, context *fuse.Context) (cod // That directory may still be in the DirIV cache, clear it. fs.nameTransform.DirIVCache.Clear() - if !fs.args.PlaintextNames { - // Create .name file to store the new long file name if needed - err = fs.nameTransform.WriteLongName(cNewPath, newPath) + // Handle long source file name + var oldDirFd *os.File + var finalOldDirFd int + var finalOldPath = cOldPath + cOldName := filepath.Base(cOldPath) + if nametransform.IsLongContent(cOldName) { + oldDirFd, err = os.Open(filepath.Dir(cOldPath)) + if err != nil { + return fuse.ToStatus(err) + } + defer oldDirFd.Close() + finalOldDirFd = int(oldDirFd.Fd()) + finalOldPath = cOldName + } + // Handle long destination file name + var newDirFd *os.File + var finalNewDirFd int + var finalNewPath = cNewPath + cNewName := filepath.Base(cNewPath) + if nametransform.IsLongContent(cNewName) { + newDirFd, err = os.Open(filepath.Dir(cNewPath)) + if err != nil { + return fuse.ToStatus(err) + } + defer newDirFd.Close() + finalNewDirFd = int(newDirFd.Fd()) + finalNewPath = cNewName + // Create destination .name file + err = fs.nameTransform.WriteLongName(newDirFd, cNewName, newPath) if err != nil { return fuse.ToStatus(err) } } - // Actual rename - err = os.Rename(cOldPath, cNewPath) - - if lerr, ok := err.(*os.LinkError); ok && lerr.Err == syscall.ENOTEMPTY { - // If an empty directory is overwritten we will always get - // ENOTEMPTY as the "empty" directory will still contain gocryptfs.diriv. + err = syscall.Renameat(finalOldDirFd, finalOldPath, finalNewDirFd, finalNewPath) + if err == syscall.ENOTEMPTY { + // If an empty directory is overwritten we will always get ENOTEMPTY as + // the "empty" directory will still contain gocryptfs.diriv. // Handle that case by removing the target directory and trying again. toggledlog.Debug.Printf("Rename: Handling ENOTEMPTY") if fs.Rmdir(newPath, context) == fuse.OK { - err = os.Rename(cOldPath, cNewPath) + err = syscall.Renameat(finalOldDirFd, finalOldPath, finalNewDirFd, finalNewPath) } } - if err == nil { - // Rename succeeded - delete old long name file - nametransform.DeleteLongName(cOldPath) - } else { - // Rename has failed - undo long name file creation - nametransform.DeleteLongName(cNewPath) + if err != nil { + if newDirFd != nil { + // Roll back .name creation + nametransform.DeleteLongName(newDirFd, cNewName) + } + return fuse.ToStatus(err) } - - return fuse.ToStatus(err) + if oldDirFd != nil { + nametransform.DeleteLongName(oldDirFd, cOldName) + } + return fuse.OK } func (fs *FS) Link(oldPath string, newPath string, context *fuse.Context) (code fuse.Status) { @@ -329,13 +429,28 @@ func (fs *FS) Link(oldPath string, newPath string, context *fuse.Context) (code if err != nil { return fuse.ToStatus(err) } - if !fs.args.PlaintextNames { - // Create .name file to store the long file name if needed - err = fs.nameTransform.WriteLongName(cNewPath, newPath) + + // Handle long file name + cNewName := filepath.Base(cNewPath) + if nametransform.IsLongContent(cNewName) { + dirfd, err := os.Open(filepath.Dir(cNewPath)) if err != nil { return fuse.ToStatus(err) } + defer dirfd.Close() + err = fs.nameTransform.WriteLongName(dirfd, cNewName, newPath) + if err != nil { + return fuse.ToStatus(err) + } + // TODO Use syscall.Linkat once it is available in Go (it is not in Go + // 1.6). + err = syscall.Link(cOldPath, cNewPath) + if err != nil { + nametransform.DeleteLongName(dirfd, cNewName) + return fuse.ToStatus(err) + } } + return fuse.ToStatus(os.Link(cOldPath, cNewPath)) } diff --git a/internal/fusefrontend/fs_dir.go b/internal/fusefrontend/fs_dir.go index bcb93af..41e2101 100644 --- a/internal/fusefrontend/fs_dir.go +++ b/internal/fusefrontend/fs_dir.go @@ -16,50 +16,74 @@ import ( "github.com/rfjakob/gocryptfs/internal/toggledlog" ) -func (fs *FS) Mkdir(relPath string, mode uint32, context *fuse.Context) (code fuse.Status) { - if fs.isFiltered(relPath) { +func (fs *FS) mkdirWithIv(cPath string, mode uint32) error { + // Between the creation of the directory and the creation of gocryptfs.diriv + // the directory is inconsistent. Take the lock to prevent other readers. + fs.dirIVLock.Lock() + // The new directory may take the place of an older one that is still in the cache + fs.nameTransform.DirIVCache.Clear() + defer fs.dirIVLock.Unlock() + err := os.Mkdir(cPath, os.FileMode(mode)) + if err != nil { + return err + } + // Create gocryptfs.diriv + err = nametransform.WriteDirIV(cPath) + if err != nil { + err2 := syscall.Rmdir(cPath) + if err2 != nil { + toggledlog.Warn.Printf("mkdirWithIv: rollback failed: %v", err2) + } + } + return err +} + +func (fs *FS) Mkdir(newPath string, mode uint32, context *fuse.Context) (code fuse.Status) { + if fs.isFiltered(newPath) { return fuse.EPERM } - encPath, err := fs.getBackingPath(relPath) + cPath, err := fs.getBackingPath(newPath) if err != nil { return fuse.ToStatus(err) } if !fs.args.DirIV { - return fuse.ToStatus(os.Mkdir(encPath, os.FileMode(mode))) + return fuse.ToStatus(os.Mkdir(cPath, os.FileMode(mode))) } - // We need write and execute permissions to create gocryptfs.diriv origMode := mode mode = mode | 0300 - // Create .name file to store the long file name if needed - err = fs.nameTransform.WriteLongName(encPath, relPath) - if err != nil { - return fuse.ToStatus(err) - } - // The new directory may take the place of an older one that is still in the cache - fs.nameTransform.DirIVCache.Clear() - // Create directory - fs.dirIVLock.Lock() - defer fs.dirIVLock.Unlock() - err = os.Mkdir(encPath, os.FileMode(mode)) - if err != nil { - return fuse.ToStatus(err) - } - // Create gocryptfs.diriv inside - err = nametransform.WriteDirIV(encPath) - if err != nil { - // This should not happen - toggledlog.Warn.Printf("Mkdir: WriteDirIV failed: %v", err) - err2 := syscall.Rmdir(encPath) - if err2 != nil { - toggledlog.Warn.Printf("Mkdir: Rmdir rollback failed: %v", err2) + + // Handle long file name + cName := filepath.Base(cPath) + if nametransform.IsLongContent(cName) { + dirfd, err := os.Open(filepath.Dir(cPath)) + if err != nil { + return fuse.ToStatus(err) + } + defer dirfd.Close() + + // Create ".name" + err = fs.nameTransform.WriteLongName(dirfd, cName, newPath) + if err != nil { + return fuse.ToStatus(err) + } + + // Create directory + err = fs.mkdirWithIv(cPath, mode) + if err != nil { + nametransform.DeleteLongName(dirfd, cName) + return fuse.ToStatus(err) + } + } else { + err = fs.mkdirWithIv(cPath, mode) + if err != nil { + return fuse.ToStatus(err) } - return fuse.ToStatus(err) } // Set permissions back to what the user wanted if origMode != mode { - err = os.Chmod(encPath, os.FileMode(origMode)) + err = os.Chmod(cPath, os.FileMode(origMode)) if err != nil { toggledlog.Warn.Printf("Mkdir: Chmod failed: %v", err) } @@ -68,97 +92,111 @@ func (fs *FS) Mkdir(relPath string, mode uint32, context *fuse.Context) (code fu return fuse.OK } -func (fs *FS) Rmdir(name string, context *fuse.Context) (code fuse.Status) { - encPath, err := fs.getBackingPath(name) +func (fs *FS) Rmdir(path string, context *fuse.Context) (code fuse.Status) { + cPath, err := fs.getBackingPath(path) if err != nil { return fuse.ToStatus(err) } if !fs.args.DirIV { - return fuse.ToStatus(syscall.Rmdir(encPath)) + return fuse.ToStatus(syscall.Rmdir(cPath)) } - // If the directory is not empty besides gocryptfs.diriv, do not even - // attempt the dance around gocryptfs.diriv. - fd, err := os.Open(encPath) - if perr, ok := err.(*os.PathError); ok && perr.Err == syscall.EACCES { + parentDir := filepath.Dir(cPath) + parentDirFd, err := os.Open(parentDir) + if err != nil { + return fuse.ToStatus(err) + } + defer parentDirFd.Close() + + cName := filepath.Base(cPath) + dirfdRaw, err := syscall.Openat(int(parentDirFd.Fd()), cName, + syscall.O_RDONLY, 0) + if err == syscall.EACCES { // We need permission to read and modify the directory toggledlog.Debug.Printf("Rmdir: handling EACCESS") - fi, err2 := os.Stat(encPath) - if err2 != nil { - toggledlog.Debug.Printf("Rmdir: Stat: %v", err2) - return fuse.ToStatus(err2) - } - origMode := fi.Mode() - newMode := origMode | 0700 - err2 = os.Chmod(encPath, newMode) - if err2 != nil { - toggledlog.Debug.Printf("Rmdir: Chmod failed: %v", err2) + // TODO use syscall.Fstatat once it is available in Go + var fi os.FileInfo + fi, err = os.Lstat(cPath) + if err != nil { + toggledlog.Debug.Printf("Rmdir: Stat: %v", err) return fuse.ToStatus(err) } + origMode := fi.Mode() + // TODO use syscall.Chmodat once it is available in Go + err = os.Chmod(cPath, origMode|0700) + if err != nil { + toggledlog.Debug.Printf("Rmdir: Chmod failed: %v", err) + return fuse.ToStatus(err) + } + // Retry open + var st syscall.Stat_t + syscall.Lstat(cPath, &st) + dirfdRaw, err = syscall.Openat(int(parentDirFd.Fd()), cName, + syscall.O_RDONLY, 0) + // Undo the chmod if removing the directory failed defer func() { if code != fuse.OK { - // Undo the chmod if removing the directory failed - err3 := os.Chmod(encPath, origMode) - if err3 != nil { - toggledlog.Warn.Printf("Rmdir: Chmod rollback failed: %v", err2) + err := os.Chmod(cPath, origMode) + if err != nil { + toggledlog.Warn.Printf("Rmdir: Chmod rollback failed: %v", err) } } }() - // Retry open - fd, err = os.Open(encPath) } if err != nil { toggledlog.Debug.Printf("Rmdir: Open: %v", err) return fuse.ToStatus(err) } - list, err := fd.Readdirnames(10) - fd.Close() + dirfd := os.NewFile(uintptr(dirfdRaw), cName) + defer dirfd.Close() + + children, err := dirfd.Readdirnames(10) if err != nil { - toggledlog.Debug.Printf("Rmdir: Readdirnames: %v", err) + toggledlog.Warn.Printf("Rmdir: Readdirnames: %v", err) return fuse.ToStatus(err) } - if len(list) > 1 { + // If the directory is not empty besides gocryptfs.diriv, do not even + // attempt the dance around gocryptfs.diriv. + if len(children) > 1 { return fuse.ToStatus(syscall.ENOTEMPTY) - } else if len(list) == 0 { - toggledlog.Warn.Printf("Rmdir: gocryptfs.diriv missing, allowing deletion") - return fuse.ToStatus(syscall.Rmdir(encPath)) } // Move "gocryptfs.diriv" to the parent dir as "gocryptfs.diriv.rmdir.XYZ" - dirivPath := filepath.Join(encPath, nametransform.DirIVFilename) - parentDir := filepath.Dir(encPath) tmpName := fmt.Sprintf("gocryptfs.diriv.rmdir.%d", cryptocore.RandUint64()) - tmpDirivPath := filepath.Join(parentDir, tmpName) - toggledlog.Debug.Printf("Rmdir: Renaming %s to %s", nametransform.DirIVFilename, tmpDirivPath) - // The directory is in an inconsistent state between rename and rmdir. Protect against - // concurrent readers. + toggledlog.Debug.Printf("Rmdir: Renaming %s to %s", nametransform.DirIVFilename, tmpName) + // The directory is in an inconsistent state between rename and rmdir. + // Protect against concurrent readers. fs.dirIVLock.Lock() defer fs.dirIVLock.Unlock() - err = os.Rename(dirivPath, tmpDirivPath) + err = syscall.Renameat(int(dirfd.Fd()), nametransform.DirIVFilename, + int(parentDirFd.Fd()), tmpName) if err != nil { toggledlog.Warn.Printf("Rmdir: Renaming %s to %s failed: %v", - nametransform.DirIVFilename, tmpDirivPath, err) + nametransform.DirIVFilename, tmpName, err) return fuse.ToStatus(err) } // Actual Rmdir - err = syscall.Rmdir(encPath) + // TODO Use syscall.Unlinkat with the AT_REMOVEDIR flag once it is available + // in Go + err = syscall.Rmdir(cPath) if err != nil { // This can happen if another file in the directory was created in the // meantime, undo the rename - err2 := os.Rename(tmpDirivPath, dirivPath) - if err2 != nil { + err2 := syscall.Renameat(int(parentDirFd.Fd()), tmpName, + int(dirfd.Fd()), nametransform.DirIVFilename) + if err != nil { toggledlog.Warn.Printf("Rmdir: Rename rollback failed: %v", err2) } return fuse.ToStatus(err) } // Delete "gocryptfs.diriv.rmdir.INODENUMBER" - err = syscall.Unlink(tmpDirivPath) + err = syscall.Unlinkat(int(parentDirFd.Fd()), tmpName) if err != nil { toggledlog.Warn.Printf("Rmdir: Could not clean up %s: %v", tmpName, err) } - err = nametransform.DeleteLongName(encPath) - if err != nil { - toggledlog.Warn.Printf("Rmdir: Could not delete long name file: %v", err) + // Delete .name file + if nametransform.IsLongContent(cName) { + nametransform.DeleteLongName(parentDirFd, cName) } // The now-deleted directory may have been in the DirIV cache. Clear it. fs.nameTransform.DirIVCache.Clear() @@ -206,7 +244,7 @@ func (fs *FS) OpenDir(dirName string, context *fuse.Context) ([]fuse.DirEntry, f } if fs.args.LongNames { - isLong := nametransform.IsLongName(cName) + isLong := nametransform.NameType(cName) if isLong == nametransform.LongNameContent { cNameLong, err := nametransform.ReadLongName(filepath.Join(cDirAbsPath, cName)) if err != nil { diff --git a/internal/nametransform/longnames.go b/internal/nametransform/longnames.go index d048f95..0746cd6 100644 --- a/internal/nametransform/longnames.go +++ b/internal/nametransform/longnames.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/base64" "io/ioutil" + "os" "path/filepath" "strings" "syscall" @@ -11,11 +12,13 @@ import ( "github.com/rfjakob/gocryptfs/internal/toggledlog" ) -// Files with long names are stored in two files: -// gocryptfs.longname.[sha256] <--- File content -// gocryptfs.longname.[sha256].name <--- File name -const longNamePrefix = "gocryptfs.longname." -const longNameSuffix = ".name" +const ( + // Files with long names are stored in two files: + // gocryptfs.longname.[sha256] <--- File content, prefix = gocryptfs.longname. + // gocryptfs.longname.[sha256].name <--- File name, suffix = .name + LongNameSuffix = ".name" + longNamePrefix = "gocryptfs.longname." +) // HashLongName - take the hash of a long string "name" and return // "gocryptfs.longname.[sha256]" @@ -32,63 +35,68 @@ const ( LongNameNone = iota ) -// IsLongName - detect if cName is +// NameType - detect if cName is // gocryptfs.longname.[sha256] ........ LongNameContent (content of a long name file) // gocryptfs.longname.[sha256].name .... LongNameFilename (full file name of a long name file) // else ................................ LongNameNone (normal file) -func IsLongName(cName string) int { +func NameType(cName string) int { if !strings.HasPrefix(cName, longNamePrefix) { return LongNameNone } - if strings.HasSuffix(cName, longNameSuffix) { + if strings.HasSuffix(cName, LongNameSuffix) { return LongNameFilename } return LongNameContent } +// IsLongContent returns true if "cName" is the content store of a long name file. +func IsLongContent(cName string) bool { + return NameType(cName) == LongNameContent +} + // ReadLongName - read path.name func ReadLongName(path string) (string, error) { - content, err := ioutil.ReadFile(path + longNameSuffix) + content, err := ioutil.ReadFile(path + LongNameSuffix) if err != nil { toggledlog.Warn.Printf("ReadLongName: %v", err) } return string(content), err } -// DeleteLongName - if cPath ends in "gocryptfs.longname.[sha256]", -// delete the "gocryptfs.longname.[sha256].name" file -func DeleteLongName(cPath string) error { - if IsLongName(filepath.Base(cPath)) == LongNameContent { - err := syscall.Unlink(cPath + longNameSuffix) - if err != nil { - toggledlog.Warn.Printf("DeleteLongName: %v", err) - } - return err - } - return nil -} - -// WriteLongName - if cPath ends in "gocryptfs.longname.[sha256]", write the -// "gocryptfs.longname.[sha256].name" file -func (n *NameTransform) WriteLongName(cPath string, plainPath string) (err error) { - cHashedName := filepath.Base(cPath) - if IsLongName(cHashedName) != LongNameContent { - // This is not a hashed file name, nothing to do - return nil - } - // Encrypt (but do not hash) the plaintext name - cDir := filepath.Dir(cPath) - dirIV, err := ReadDirIV(cDir) +// DeleteLongName deletes "hashName.name". +func DeleteLongName(dirfd *os.File, hashName string) error { + err := syscall.Unlinkat(int(dirfd.Fd()), hashName+LongNameSuffix) if err != nil { - toggledlog.Warn.Printf("WriteLongName: %v", err) - return err - } - plainName := filepath.Base(plainPath) - cName := n.EncryptName(plainName, dirIV) - // Write the encrypted name into gocryptfs.longname.[sha256].name - err = ioutil.WriteFile(filepath.Join(cDir, cHashedName+longNameSuffix), []byte(cName), 0600) - if err != nil { - toggledlog.Warn.Printf("WriteLongName: %v", err) + toggledlog.Warn.Printf("DeleteLongName: %v", err) + } + return err +} + +// WriteLongName encrypts plainName and writes it into "hashName.name". +// For the convenience of the caller, plainName may also be a path and will be +// converted internally. +func (n *NameTransform) WriteLongName(dirfd *os.File, hashName string, plainName string) (err error) { + plainName = filepath.Base(plainName) + + // Encrypt the basename + dirIV, err := ReadDirIVAt(dirfd) + if err != nil { + return err + } + cName := n.EncryptName(plainName, dirIV) + + // Write the encrypted name into hashName.name + fdRaw, err := syscall.Openat(int(dirfd.Fd()), hashName+LongNameSuffix, + syscall.O_WRONLY|syscall.O_CREAT|syscall.O_EXCL, 0600) + if err != nil { + toggledlog.Warn.Printf("WriteLongName: Openat: %v", err) + return err + } + fd := os.NewFile(uintptr(fdRaw), hashName+LongNameSuffix) + defer fd.Close() + _, err = fd.Write([]byte(cName)) + if err != nil { + toggledlog.Warn.Printf("WriteLongName: Write: %v", err) } return err } diff --git a/internal/nametransform/longnames_test.go b/internal/nametransform/longnames_test.go index 62073ec..8fa19fe 100644 --- a/internal/nametransform/longnames_test.go +++ b/internal/nametransform/longnames_test.go @@ -6,17 +6,17 @@ import ( func TestIsLongName(t *testing.T) { n := "gocryptfs.longname.LkwUdALvV_ANnzQN6ZZMYnxxfARD3IeZWCKnxGJjYmU=.name" - if IsLongName(n) != LongNameFilename { + if NameType(n) != LongNameFilename { t.Errorf("False negative") } n = "gocryptfs.longname.LkwUdALvV_ANnzQN6ZZMYnxxfARD3IeZWCKnxGJjYmU=" - if IsLongName(n) != LongNameContent { + if NameType(n) != LongNameContent { t.Errorf("False negative") } n = "LkwUdALvV_ANnzQN6ZZMYnxxfARD3IeZWCKnxGJjYmU=" - if IsLongName(n) != LongNameNone { + if NameType(n) != LongNameNone { t.Errorf("False positive") } } diff --git a/internal/nametransform/names_diriv.go b/internal/nametransform/names_diriv.go index 9336f5d..1beda3f 100644 --- a/internal/nametransform/names_diriv.go +++ b/internal/nametransform/names_diriv.go @@ -1,7 +1,7 @@ package nametransform import ( - "fmt" + "errors" "io/ioutil" "os" "path/filepath" @@ -21,25 +21,38 @@ const ( ) // ReadDirIV - read the "gocryptfs.diriv" file from "dir" (absolute ciphertext path) -// This function is exported because it allows for an efficient readdir implementation -func ReadDirIV(dir string) (iv []byte, readErr error) { - ivfile := filepath.Join(dir, DirIVFilename) - toggledlog.Debug.Printf("ReadDirIV: reading %s\n", ivfile) - iv, readErr = ioutil.ReadFile(ivfile) - if readErr != nil { - // The directory may have been concurrently deleted or moved. Failure to - // read the diriv is not an error in that case. - _, statErr := os.Stat(dir) - if os.IsNotExist(statErr) { - toggledlog.Debug.Printf("ReadDirIV: Dir %s was deleted under our feet", dir) - } else { - // This should not happen - toggledlog.Warn.Printf("ReadDirIV: Dir exists but diriv does not: %v\n", readErr) - } - return nil, readErr +// This function is exported because it allows for an efficient readdir implementation. +func ReadDirIV(dir string) (iv []byte, err error) { + dirfd, err := os.Open(dir) + if err != nil { + return nil, err } + defer dirfd.Close() + + return ReadDirIVAt(dirfd) +} + +// ReadDirIVAt reads "gocryptfs.diriv" from the directory that is opened as "dirfd". +// Using the dirfd makes it immune to concurrent renames of the directory. +func ReadDirIVAt(dirfd *os.File) (iv []byte, err error) { + fdRaw, err := syscall.Openat(int(dirfd.Fd()), DirIVFilename, syscall.O_RDONLY, 0) + if err != nil { + toggledlog.Warn.Printf("ReadDirIVAt: %v", err) + return nil, err + } + fd := os.NewFile(uintptr(fdRaw), DirIVFilename) + defer fd.Close() + + iv = make([]byte, dirIVLen+1) + n, err := fd.Read(iv) + if err != nil { + toggledlog.Warn.Printf("ReadDirIVAt: %v", err) + return nil, err + } + iv = iv[0:n] if len(iv) != dirIVLen { - return nil, fmt.Errorf("ReadDirIV: Invalid length %d\n", len(iv)) + toggledlog.Warn.Printf("ReadDirIVAt: wanted %d bytes, got %d", dirIVLen, len(iv)) + return nil, errors.New("invalid iv length") } return iv, nil } @@ -50,12 +63,15 @@ func ReadDirIV(dir string) (iv []byte, readErr error) { func WriteDirIV(dir string) error { iv := cryptocore.RandBytes(dirIVLen) file := filepath.Join(dir, DirIVFilename) - // 0444 permissions: the file is not secret but should not be written to - return ioutil.WriteFile(file, iv, 0444) + err := ioutil.WriteFile(file, iv, 0400) + if err != nil { + toggledlog.Warn.Printf("WriteDirIV: %v", err) + } + return err } // EncryptPathDirIV - encrypt relative plaintext path using EME with DirIV. -// Components that are longer than 255 bytes are hashed. +// Components that are longer than 255 bytes are hashed if be.longnames == true. func (be *NameTransform) EncryptPathDirIV(plainPath string, rootDir string) (cipherPath string, err error) { // Empty string means root directory if plainPath == "" {