diff --git a/internal/fusefrontend/node_xattr.go b/internal/fusefrontend/node_xattr.go index 09ee5ef..44bc502 100644 --- a/internal/fusefrontend/node_xattr.go +++ b/internal/fusefrontend/node_xattr.go @@ -15,10 +15,6 @@ import ( // -1 as uint32 const minus1 = ^uint32(0) -// xattr names are encrypted like file names, but with a fixed IV. -// Padded with "_xx" for length 16. -var xattrNameIV = []byte("xattr_name_iv_xx") - // We store encrypted xattrs under this prefix plus the base64-encoded // encrypted original name. var xattrStorePrefix = "user.gocryptfs." diff --git a/internal/fusefrontend/root_node.go b/internal/fusefrontend/root_node.go index 7221be6..39cdef7 100644 --- a/internal/fusefrontend/root_node.go +++ b/internal/fusefrontend/root_node.go @@ -268,7 +268,7 @@ func (rn *RootNode) decryptXattrValue(cData []byte) (data []byte, err error) { // 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) + cAttr, err := rn.nameTransform.EncryptXattrName(attr) if err != nil { return "", err } @@ -282,7 +282,7 @@ func (rn *RootNode) decryptXattrName(cAttr string) (attr string, err error) { } // Strip "user.gocryptfs." prefix cAttr = cAttr[len(xattrStorePrefix):] - attr, err = rn.nameTransform.DecryptName(cAttr, xattrNameIV) + attr, err = rn.nameTransform.DecryptXattrName(cAttr) if err != nil { return "", err } diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index e07ccfb..ebe0fb6 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -66,7 +66,14 @@ func New(e *eme.EMECipher, longNames bool, longNameMax uint8, raw64 bool, badnam func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error) { res, err := n.decryptName(cipherName, iv) if err != nil && n.HaveBadnamePatterns() { - return n.decryptBadname(cipherName, iv) + res, err = n.decryptBadname(cipherName, iv) + } + if err != nil { + return "", err + } + if err := IsValidName(res); err != nil { + tlog.Warn.Printf("DecryptName %q: invalid name after decryption: %v", cipherName, err) + return "", syscall.EBADMSG } return res, err } @@ -79,30 +86,29 @@ func (n *NameTransform) decryptName(cipherName string, iv []byte) (string, error return "", err } if len(bin) == 0 { - tlog.Warn.Printf("DecryptName: empty input") + tlog.Warn.Printf("decryptName: empty input") return "", syscall.EBADMSG } if len(bin)%aes.BlockSize != 0 { - tlog.Debug.Printf("DecryptName %q: decoded length %d is not a multiple of 16", cipherName, len(bin)) + tlog.Debug.Printf("decryptName %q: decoded length %d is not a multiple of 16", cipherName, len(bin)) return "", syscall.EBADMSG } bin = n.emeCipher.Decrypt(iv, bin) bin, err = unPad16(bin) if err != nil { - tlog.Warn.Printf("DecryptName %q: unPad16 error: %v", cipherName, err) + tlog.Warn.Printf("decryptName %q: unPad16 error: %v", cipherName, err) return "", syscall.EBADMSG } plain := string(bin) - if err := IsValidName(plain); err != nil { - tlog.Warn.Printf("DecryptName %q: invalid name after decryption: %v", cipherName, err) - return "", syscall.EBADMSG - } return plain, err } -// EncryptName encrypts "plainName", returns a base64-encoded "cipherName64", +// EncryptName encrypts a file name "plainName" and returns a base64-encoded "cipherName64", // encrypted using EME (https://github.com/rfjakob/eme). // +// plainName is checked for null bytes, slashes etc. and such names are rejected +// with an error. +// // This function is exported because in some cases, fusefrontend needs access // to the full (not hashed) name if longname is used. func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 string, err error) { @@ -110,11 +116,19 @@ func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 s tlog.Warn.Printf("EncryptName %q: invalid plainName: %v", plainName, err) return "", syscall.EBADMSG } + return n.encryptName(plainName, iv), nil +} + +// encryptName encrypts "plainName" and returns a base64-encoded "cipherName64", +// encrypted using EME (https://github.com/rfjakob/eme). +// +// No checks for null bytes etc are performed against plainName. +func (n *NameTransform) encryptName(plainName string, iv []byte) (cipherName64 string) { bin := []byte(plainName) bin = pad16(bin) bin = n.emeCipher.Encrypt(iv, bin) cipherName64 = n.B64.EncodeToString(bin) - return cipherName64, nil + return cipherName64 } // EncryptAndHashName encrypts "name" and hashes it to a longname if it is diff --git a/internal/nametransform/names_test.go b/internal/nametransform/names_test.go index b4e98d4..3c26c43 100644 --- a/internal/nametransform/names_test.go +++ b/internal/nametransform/names_test.go @@ -75,3 +75,26 @@ func TestIsValidName(t *testing.T) { } } } + +func TestIsValidXattrName(t *testing.T) { + cases := []struct { + in string + want bool + }{ + {"", false}, + {".", true}, + {"..", true}, + {"...", true}, + {"asdasd/asdasd", true}, + {"asdasd\000asdasd", false}, + {"hello", true}, + {strings.Repeat("x", 255), true}, + {strings.Repeat("x", 256), true}, + } + for _, c := range cases { + have := isValidXattrName(c.in) + if (have == nil) != c.want { + t.Errorf("isValidXattrName(%q): want %v have %v", c.in, c.want, have) + } + } +} diff --git a/internal/nametransform/xattr.go b/internal/nametransform/xattr.go new file mode 100644 index 0000000..0aa0fd8 --- /dev/null +++ b/internal/nametransform/xattr.go @@ -0,0 +1,47 @@ +package nametransform + +import ( + "fmt" + "strings" + "syscall" + + "github.com/rfjakob/gocryptfs/v2/internal/tlog" +) + +// xattr names are encrypted like file names, but with a fixed IV. +// Padded with "_xx" for length 16. +var xattrNameIV = []byte("xattr_name_iv_xx") + +func isValidXattrName(name string) error { + if name == "" { + return fmt.Errorf("empty input") + } + if strings.Contains(name, "\000") { + return fmt.Errorf("contains forbidden null byte") + } + return nil +} + +// EncryptXattrName encrypts an extended attribute (xattr) name. +// xattr names are encrypted like file names, but with a fixed IV, and fewer +// naming restriction. +func (n *NameTransform) EncryptXattrName(plainName string) (cipherName64 string, err error) { + if err := isValidXattrName(plainName); err != nil { + tlog.Warn.Printf("EncryptXattrName %q: invalid plainName: %v", plainName, err) + return "", syscall.EBADMSG + } + return n.encryptName(plainName, xattrNameIV), nil +} + +// DecryptName calls decryptName to try and decrypt a base64-encoded encrypted +// filename "cipherName", and failing that checks if it can be bypassed +func (n *NameTransform) DecryptXattrName(cipherName string) (plainName string, err error) { + if plainName, err = n.decryptName(cipherName, xattrNameIV); err != nil { + return "", err + } + if err := isValidXattrName(plainName); err != nil { + tlog.Warn.Printf("DecryptXattrName %q: invalid name after decryption: %v", cipherName, err) + return "", syscall.EBADMSG + } + return plainName, err +} diff --git a/tests/xattr/xattr_integration_test.go b/tests/xattr/xattr_integration_test.go index efe903b..c968a47 100644 --- a/tests/xattr/xattr_integration_test.go +++ b/tests/xattr/xattr_integration_test.go @@ -369,3 +369,17 @@ func TestAcl(t *testing.T) { t.Error(err) } } + +// TestSlashInName checks that slashes in xattr names are allowed +// https://github.com/rfjakob/gocryptfs/issues/627 +func TestSlashInName(t *testing.T) { + fn := test_helpers.DefaultPlainDir + "/" + t.Name() + err := ioutil.WriteFile(fn, []byte("12345"), 0700) + if err != nil { + t.Fatalf("creating empty file failed: %v", err) + } + err = setGetRmList3(fn, "user.foo@https://bar", []byte("val")) + if err != nil { + t.Error(err) + } +}