fusefrontend: allow slashes in xattr names

xattr names have fewer restrictions than file names,
relax the validation.

Fixes https://github.com/rfjakob/gocryptfs/issues/627
This commit is contained in:
Jakob Unterwurzacher 2021-12-19 14:43:56 +01:00
parent eb42e54182
commit 64be5de75f
6 changed files with 110 additions and 16 deletions

View File

@ -15,10 +15,6 @@ import (
// -1 as uint32 // -1 as uint32
const minus1 = ^uint32(0) 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 // We store encrypted xattrs under this prefix plus the base64-encoded
// encrypted original name. // encrypted original name.
var xattrStorePrefix = "user.gocryptfs." var xattrStorePrefix = "user.gocryptfs."

View File

@ -268,7 +268,7 @@ func (rn *RootNode) decryptXattrValue(cData []byte) (data []byte, err error) {
// encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf" // encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf"
func (rn *RootNode) encryptXattrName(attr string) (string, error) { func (rn *RootNode) encryptXattrName(attr string) (string, error) {
// xattr names are encrypted like file names, but with a fixed IV. // 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 { if err != nil {
return "", err return "", err
} }
@ -282,7 +282,7 @@ func (rn *RootNode) decryptXattrName(cAttr string) (attr string, err error) {
} }
// Strip "user.gocryptfs." prefix // Strip "user.gocryptfs." prefix
cAttr = cAttr[len(xattrStorePrefix):] cAttr = cAttr[len(xattrStorePrefix):]
attr, err = rn.nameTransform.DecryptName(cAttr, xattrNameIV) attr, err = rn.nameTransform.DecryptXattrName(cAttr)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@ -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) { func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error) {
res, err := n.decryptName(cipherName, iv) res, err := n.decryptName(cipherName, iv)
if err != nil && n.HaveBadnamePatterns() { 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 return res, err
} }
@ -79,30 +86,29 @@ func (n *NameTransform) decryptName(cipherName string, iv []byte) (string, error
return "", err return "", err
} }
if len(bin) == 0 { if len(bin) == 0 {
tlog.Warn.Printf("DecryptName: empty input") tlog.Warn.Printf("decryptName: empty input")
return "", syscall.EBADMSG return "", syscall.EBADMSG
} }
if len(bin)%aes.BlockSize != 0 { 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 return "", syscall.EBADMSG
} }
bin = n.emeCipher.Decrypt(iv, bin) bin = n.emeCipher.Decrypt(iv, bin)
bin, err = unPad16(bin) bin, err = unPad16(bin)
if err != nil { 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 return "", syscall.EBADMSG
} }
plain := string(bin) 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 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). // 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 // This function is exported because in some cases, fusefrontend needs access
// to the full (not hashed) name if longname is used. // to the full (not hashed) name if longname is used.
func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 string, err error) { 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) tlog.Warn.Printf("EncryptName %q: invalid plainName: %v", plainName, err)
return "", syscall.EBADMSG 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 := []byte(plainName)
bin = pad16(bin) bin = pad16(bin)
bin = n.emeCipher.Encrypt(iv, bin) bin = n.emeCipher.Encrypt(iv, bin)
cipherName64 = n.B64.EncodeToString(bin) cipherName64 = n.B64.EncodeToString(bin)
return cipherName64, nil return cipherName64
} }
// EncryptAndHashName encrypts "name" and hashes it to a longname if it is // EncryptAndHashName encrypts "name" and hashes it to a longname if it is

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -369,3 +369,17 @@ func TestAcl(t *testing.T) {
t.Error(err) 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)
}
}