fusefrontend: add xattr support
At the moment, only for reverse mode. https://github.com/rfjakob/gocryptfs/issues/217
This commit is contained in:
parent
f20974c4da
commit
1ed3d51df1
@ -156,6 +156,8 @@ Changelog
|
|||||||
vNEXT, in progress
|
vNEXT, in progress
|
||||||
* Add `-masterkey=stdin` functionality
|
* Add `-masterkey=stdin` functionality
|
||||||
([#218](https://github.com/rfjakob/gocryptfs/issues/218))
|
([#218](https://github.com/rfjakob/gocryptfs/issues/218))
|
||||||
|
* Support extended attributes (xattr) in forward mode
|
||||||
|
([#217](https://github.com/rfjakob/gocryptfs/issues/217))
|
||||||
|
|
||||||
v1.4.4, 2018-03-18
|
v1.4.4, 2018-03-18
|
||||||
* Overwrite secrets in memory with zeros as soon as possible
|
* Overwrite secrets in memory with zeros as soon as possible
|
||||||
|
@ -131,10 +131,11 @@ func (be *ContentEnc) DecryptBlocks(ciphertext []byte, firstBlockNo uint64, file
|
|||||||
|
|
||||||
// concatAD concatenates the block number and the file ID to a byte blob
|
// concatAD concatenates the block number and the file ID to a byte blob
|
||||||
// that can be passed to AES-GCM as associated data (AD).
|
// that can be passed to AES-GCM as associated data (AD).
|
||||||
// Result is: aData = blockNo.bigEndian + fileID.
|
// Result is: aData = [blockNo.bigEndian fileID].
|
||||||
func concatAD(blockNo uint64, fileID []byte) (aData []byte) {
|
func concatAD(blockNo uint64, fileID []byte) (aData []byte) {
|
||||||
if fileID != nil && len(fileID) != headerIDLen {
|
if fileID != nil && len(fileID) != headerIDLen {
|
||||||
// fileID is nil when decrypting the master key from the config file
|
// fileID is nil when decrypting the master key from the config file,
|
||||||
|
// and for symlinks and xattrs.
|
||||||
log.Panicf("wrong fileID length: %d", len(fileID))
|
log.Panicf("wrong fileID length: %d", len(fileID))
|
||||||
}
|
}
|
||||||
const lenUint64 = 8
|
const lenUint64 = 8
|
||||||
|
@ -592,23 +592,3 @@ func (fs *FS) Access(path string, mode uint32, context *fuse.Context) (code fuse
|
|||||||
}
|
}
|
||||||
return fuse.ToStatus(syscall.Access(cPath, mode))
|
return fuse.ToStatus(syscall.Access(cPath, mode))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetXAttr implements pathfs.Filesystem.
|
|
||||||
func (fs *FS) GetXAttr(name string, attr string, context *fuse.Context) ([]byte, fuse.Status) {
|
|
||||||
return nil, fuse.ENOSYS
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetXAttr implements pathfs.Filesystem.
|
|
||||||
func (fs *FS) SetXAttr(name string, attr string, data []byte, flags int, context *fuse.Context) fuse.Status {
|
|
||||||
return fuse.ENOSYS
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListXAttr implements pathfs.Filesystem.
|
|
||||||
func (fs *FS) ListXAttr(name string, context *fuse.Context) ([]string, fuse.Status) {
|
|
||||||
return nil, fuse.ENOSYS
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveXAttr implements pathfs.Filesystem.
|
|
||||||
func (fs *FS) RemoveXAttr(name string, attr string, context *fuse.Context) fuse.Status {
|
|
||||||
return fuse.ENOSYS
|
|
||||||
}
|
|
||||||
|
157
internal/fusefrontend/xattr.go
Normal file
157
internal/fusefrontend/xattr.go
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
// Package fusefrontend interfaces directly with the go-fuse library.
|
||||||
|
package fusefrontend
|
||||||
|
|
||||||
|
// FUSE operations on paths
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/hanwen/go-fuse/fuse"
|
||||||
|
xattr "github.com/rfjakob/pkg-xattr"
|
||||||
|
|
||||||
|
"github.com/rfjakob/gocryptfs/internal/tlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// xattr names are encrypted like file names, but with a fixed IV.
|
||||||
|
var xattrNameIV = []byte("xattr_name_iv_xx")
|
||||||
|
|
||||||
|
// Only allow the "user" namespace, block "trusted" and "security", as
|
||||||
|
// these may be interpreted by the system, and we don't want to cause
|
||||||
|
// trouble with our encrypted garbage.
|
||||||
|
var xattrUserPrefix = "user."
|
||||||
|
|
||||||
|
// We store encrypted xattrs under this prefix plus the base64-encoded
|
||||||
|
// encrypted original name.
|
||||||
|
var xattrStorePrefix = "user.gocryptfs."
|
||||||
|
|
||||||
|
// GetXAttr: read the value of extended attribute "attr".
|
||||||
|
// Implements pathfs.Filesystem.
|
||||||
|
func (fs *FS) GetXAttr(path string, attr string, context *fuse.Context) ([]byte, fuse.Status) {
|
||||||
|
if fs.isFiltered(path) {
|
||||||
|
return nil, fuse.EPERM
|
||||||
|
}
|
||||||
|
cAttr, err := fs.encryptXattrName(attr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
cPath, err := fs.getBackingPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
cData64, err := xattr.Get(cPath, cAttr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, unpackXattrErr(err)
|
||||||
|
}
|
||||||
|
// xattr data is decrypted like a symlink target
|
||||||
|
data, err := fs.decryptSymlinkTarget(string(cData64))
|
||||||
|
if err != nil {
|
||||||
|
tlog.Warn.Printf("GetXAttr: %v", err)
|
||||||
|
return nil, fuse.EIO
|
||||||
|
}
|
||||||
|
return []byte(data), fuse.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetXAttr implements pathfs.Filesystem.
|
||||||
|
func (fs *FS) SetXAttr(path string, attr string, data []byte, flags int, context *fuse.Context) fuse.Status {
|
||||||
|
if fs.isFiltered(path) {
|
||||||
|
return fuse.EPERM
|
||||||
|
}
|
||||||
|
if flags != 0 {
|
||||||
|
return fuse.EPERM
|
||||||
|
}
|
||||||
|
cPath, err := fs.getBackingPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
cAttr, err := fs.encryptXattrName(attr)
|
||||||
|
if err != nil {
|
||||||
|
return fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
// xattr data is encrypted like a symlink target
|
||||||
|
cData64 := []byte(fs.encryptSymlinkTarget(string(data)))
|
||||||
|
return unpackXattrErr(xattr.Set(cPath, cAttr, cData64))
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveXAttr implements pathfs.Filesystem.
|
||||||
|
func (fs *FS) RemoveXAttr(path string, attr string, context *fuse.Context) fuse.Status {
|
||||||
|
if fs.isFiltered(path) {
|
||||||
|
return fuse.EPERM
|
||||||
|
}
|
||||||
|
cPath, err := fs.getBackingPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
cAttr, err := fs.encryptXattrName(attr)
|
||||||
|
if err != nil {
|
||||||
|
return fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
return unpackXattrErr(xattr.Remove(cPath, cAttr))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListXAttr implements pathfs.Filesystem.
|
||||||
|
func (fs *FS) ListXAttr(path string, context *fuse.Context) ([]string, fuse.Status) {
|
||||||
|
if fs.isFiltered(path) {
|
||||||
|
return nil, fuse.EPERM
|
||||||
|
}
|
||||||
|
cPath, err := fs.getBackingPath(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fuse.ToStatus(err)
|
||||||
|
}
|
||||||
|
cNames, err := xattr.List(cPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, unpackXattrErr(err)
|
||||||
|
}
|
||||||
|
names := make([]string, 0, len(cNames))
|
||||||
|
for _, curName := range cNames {
|
||||||
|
if !strings.HasPrefix(curName, xattrStorePrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name, err := fs.decryptXattrName(curName)
|
||||||
|
if err != nil {
|
||||||
|
tlog.Warn.Printf("ListXAttr: invalid xattr name %q: %v", curName, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
return names, fuse.OK
|
||||||
|
}
|
||||||
|
|
||||||
|
// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf"
|
||||||
|
func (fs *FS) encryptXattrName(attr string) (cAttr string, err error) {
|
||||||
|
// Reject anything that does not start with "user."
|
||||||
|
if !strings.HasPrefix(attr, xattrUserPrefix) {
|
||||||
|
return "", syscall.EPERM
|
||||||
|
}
|
||||||
|
// xattr names are encrypted like file names, but with a fixed IV.
|
||||||
|
cAttr = xattrStorePrefix + fs.nameTransform.EncryptName(attr, xattrNameIV)
|
||||||
|
return cAttr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *FS) 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 = fs.nameTransform.DecryptName(cAttr, xattrNameIV)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return attr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// unpackXattrErr unpacks an error value that we got from xattr.Get/Set/etc
|
||||||
|
// and converts it to a fuse status.
|
||||||
|
func unpackXattrErr(err error) fuse.Status {
|
||||||
|
if err == nil {
|
||||||
|
return fuse.OK
|
||||||
|
}
|
||||||
|
err2, ok := err.(*xattr.Error)
|
||||||
|
if !ok {
|
||||||
|
tlog.Warn.Printf("unpackXattrErr: cannot unpack err=%v", err)
|
||||||
|
return fuse.EIO
|
||||||
|
}
|
||||||
|
return fuse.ToStatus(err2.Err)
|
||||||
|
}
|
41
internal/fusefrontend/xattr_unit_test.go
Normal file
41
internal/fusefrontend/xattr_unit_test.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package fusefrontend
|
||||||
|
|
||||||
|
// This file is named "xattr_unit_test.go" because there is also a
|
||||||
|
// "xattr_integration_test.go" in the test/xattr package.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/rfjakob/gocryptfs/internal/contentenc"
|
||||||
|
"github.com/rfjakob/gocryptfs/internal/cryptocore"
|
||||||
|
"github.com/rfjakob/gocryptfs/internal/nametransform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestFS() *FS {
|
||||||
|
// Init crypto backend
|
||||||
|
key := make([]byte, cryptocore.KeyLen)
|
||||||
|
cCore := cryptocore.New(key, cryptocore.BackendGoGCM, contentenc.DefaultIVBits, true, false)
|
||||||
|
cEnc := contentenc.New(cCore, contentenc.DefaultBS, false)
|
||||||
|
nameTransform := nametransform.New(cCore.EMECipher, true, true)
|
||||||
|
args := Args{}
|
||||||
|
return NewFS(args, cEnc, nameTransform)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncryptDecryptXattrName(t *testing.T) {
|
||||||
|
fs := newTestFS()
|
||||||
|
_, err := fs.encryptXattrName("xxxx")
|
||||||
|
if err != syscall.EPERM {
|
||||||
|
t.Fatalf("Names that don't start with 'user.' should fail")
|
||||||
|
}
|
||||||
|
attr1 := "user.foo123456789"
|
||||||
|
cAttr, err := fs.encryptXattrName(attr1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Logf("cAttr=%v", cAttr)
|
||||||
|
attr2, err := fs.decryptXattrName(cAttr)
|
||||||
|
if attr1 != attr2 {
|
||||||
|
t.Fatalf("Decrypt mismatch: %v != %v", attr1, attr2)
|
||||||
|
}
|
||||||
|
}
|
96
tests/xattr/xattr_integration_test.go
Normal file
96
tests/xattr/xattr_integration_test.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package defaults
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
xattr "github.com/rfjakob/pkg-xattr"
|
||||||
|
|
||||||
|
"github.com/rfjakob/gocryptfs/tests/test_helpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
// On modern Linux distributions, /tmp may be on tmpfs,
|
||||||
|
// which does not support user xattrs. Try /var/tmp instead.
|
||||||
|
var alternateTestParentDir = "/var/tmp/gocryptfs-xattr-test-parent"
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
if !xattr.Supported(test_helpers.TmpDir) {
|
||||||
|
test_helpers.SwitchTestParentDir(alternateTestParentDir)
|
||||||
|
}
|
||||||
|
if !xattr.Supported(test_helpers.TmpDir) {
|
||||||
|
fmt.Printf("xattrs not supported on %q", test_helpers.TmpDir)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
test_helpers.ResetTmpDir(true)
|
||||||
|
test_helpers.MountOrExit(test_helpers.DefaultCipherDir, test_helpers.DefaultPlainDir, "-zerokey")
|
||||||
|
r := m.Run()
|
||||||
|
test_helpers.UnmountPanic(test_helpers.DefaultPlainDir)
|
||||||
|
os.RemoveAll(test_helpers.TmpDir)
|
||||||
|
os.Exit(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXattrSetGetRm(t *testing.T) {
|
||||||
|
attr := "user.foo"
|
||||||
|
fn := test_helpers.DefaultPlainDir + "/TestXattrSetGetRm"
|
||||||
|
err := ioutil.WriteFile(fn, nil, 0700)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating empty file failed: %v", err)
|
||||||
|
}
|
||||||
|
// Set
|
||||||
|
val1 := []byte("123456789")
|
||||||
|
err = xattr.Set(fn, attr, val1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Read back
|
||||||
|
val2, err := xattr.Get(fn, attr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(val1, val2) {
|
||||||
|
t.Fatalf("wrong readback value: %v != %v", val1, val2)
|
||||||
|
}
|
||||||
|
// Remove
|
||||||
|
err = xattr.Remove(fn, attr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Read back
|
||||||
|
val3, err := xattr.Get(fn, attr)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("attr is still there after deletion!? val3=%v", val3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestXattrList(t *testing.T) {
|
||||||
|
fn := test_helpers.DefaultPlainDir + "/TestXattrList"
|
||||||
|
err := ioutil.WriteFile(fn, nil, 0700)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating empty file failed: %v", err)
|
||||||
|
}
|
||||||
|
val := []byte("xxxxxxxxyyyyyyyyyyyyyyyzzzzzzzzzzzzz")
|
||||||
|
num := 20
|
||||||
|
for i := 1; i <= num; i++ {
|
||||||
|
attr := fmt.Sprintf("user.TestXattrList.%02d", i)
|
||||||
|
err = xattr.Set(fn, attr, val)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names, err := xattr.List(fn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(names) != num {
|
||||||
|
t.Errorf("wrong number of names, want=%d have=%d", num, len(names))
|
||||||
|
}
|
||||||
|
for _, n := range names {
|
||||||
|
if !strings.HasPrefix(n, "user.TestXattrList.") {
|
||||||
|
t.Errorf("unexpected attr name: %q", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user