diff --git a/internal/fusefrontend/node_helpers.go b/internal/fusefrontend/node_helpers.go index b2f1d4a..f2d1e5e 100644 --- a/internal/fusefrontend/node_helpers.go +++ b/internal/fusefrontend/node_helpers.go @@ -121,7 +121,15 @@ func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno sy var iv []byte dirfd, iv = rn.dirCache.Lookup(n) if dirfd > 0 { - cName, err := rn.nameTransform.EncryptAndHashName(child, iv) + var cName string + var err error + if rn.nameTransform.HaveBadnamePatterns() { + //BadName allowed, try to determine filenames + cName, err = rn.nameTransform.EncryptAndHashBadName(child, iv, dirfd) + } else { + cName, err = rn.nameTransform.EncryptAndHashName(child, iv) + } + if err != nil { return -1, "", fs.ToErrno(err) } diff --git a/internal/fusefrontend/root_node.go b/internal/fusefrontend/root_node.go index a830cc4..35b7be0 100644 --- a/internal/fusefrontend/root_node.go +++ b/internal/fusefrontend/root_node.go @@ -245,7 +245,11 @@ func (rn *RootNode) openBackingDir(relPath string) (dirfd int, cName string, err syscall.Close(dirfd) return -1, "", err } - cName, err = rn.nameTransform.EncryptAndHashName(name, iv) + 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 diff --git a/internal/nametransform/diriv.go b/internal/nametransform/diriv.go index 1d27aa5..d62b3fb 100644 --- a/internal/nametransform/diriv.go +++ b/internal/nametransform/diriv.go @@ -6,11 +6,13 @@ import ( "io" "os" "path/filepath" + "strings" "syscall" "github.com/rfjakob/gocryptfs/internal/cryptocore" "github.com/rfjakob/gocryptfs/internal/syscallcompat" "github.com/rfjakob/gocryptfs/internal/tlog" + "golang.org/x/sys/unix" ) const ( @@ -112,6 +114,54 @@ func (be *NameTransform) EncryptAndHashName(name string, iv []byte) (string, err return cName, nil } +// EncryptAndHashBadName tries to find the "name" substring, which (encrypted and hashed) +// leads to an unique existing file +// Returns ENOENT if cipher file does not exist or is not unique +func (be *NameTransform) EncryptAndHashBadName(name string, iv []byte, dirfd int) (cName string, err error) { + var st unix.Stat_t + var filesFound int + lastFoundName, err := be.EncryptAndHashName(name, iv) + if !strings.HasSuffix(name, BadNameFlag) || err != nil { + //Default mode: same behaviour on error or no BadNameFlag on "name" + return lastFoundName, err + } + //Default mode: Check if File extists without modifications + err = syscallcompat.Fstatat(dirfd, lastFoundName, &st, unix.AT_SYMLINK_NOFOLLOW) + if err == nil { + //file found, return result + return lastFoundName, nil + } + //BadName Mode: check if the name was tranformed without change (badname suffix and undecryptable cipher name) + err = syscallcompat.Fstatat(dirfd, name[:len(name)-len(BadNameFlag)], &st, unix.AT_SYMLINK_NOFOLLOW) + if err == nil { + filesFound++ + lastFoundName = name[:len(name)-len(BadNameFlag)] + } + // search for the longest badname pattern match + for charpos := len(name) - len(BadNameFlag); charpos > 0; charpos-- { + //only use original cipher name and append assumed suffix (without badname flag) + cNamePart, err := be.EncryptName(name[:charpos], iv) + if err != nil { + //expand suffix on error + continue + } + if be.longNames && len(cName) > NameMax { + cNamePart = be.HashLongName(cName) + } + cNameBadReverse := cNamePart + name[charpos:len(name)-len(BadNameFlag)] + err = syscallcompat.Fstatat(dirfd, cNameBadReverse, &st, unix.AT_SYMLINK_NOFOLLOW) + if err == nil { + filesFound++ + lastFoundName = cNameBadReverse + } + } + if filesFound == 1 { + return lastFoundName, nil + } + // more than 1 possible file found, ignore + return "", syscall.ENOENT +} + // Dir is like filepath.Dir but returns "" instead of ".". func Dir(path string) string { d := filepath.Dir(path) diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index ca28230..f730184 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -15,6 +15,8 @@ import ( const ( // Like ext4, we allow at most 255 bytes for a file name. NameMax = 255 + //BadNameFlag is appended to filenames in plain mode if a ciphername is inavlid but is shown + BadNameFlag = " GOCRYPTFS_BAD_NAME" ) // NameTransformer is an interface used to transform filenames. @@ -22,11 +24,13 @@ type NameTransformer interface { DecryptName(cipherName string, iv []byte) (string, error) EncryptName(plainName string, iv []byte) (string, error) EncryptAndHashName(name string, iv []byte) (string, error) + EncryptAndHashBadName(name string, iv []byte, dirfd int) (string, error) // HashLongName - take the hash of a long string "name" and return // "gocryptfs.longname.[sha256]" // // This function does not do any I/O. HashLongName(name string) string + HaveBadnamePatterns() bool WriteLongNameAt(dirfd int, hashName string, plainName string) error B64EncodeToString(src []byte) string B64DecodeString(s string) ([]byte, error) @@ -70,10 +74,10 @@ func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error for charpos := len(cipherName) - 1; charpos >= nameMin; charpos-- { res, err = n.decryptName(cipherName[:charpos], iv) if err == nil { - return res + cipherName[charpos:] + " GOCRYPTFS_BAD_NAME", nil + return res + cipherName[charpos:] + BadNameFlag, nil } } - return cipherName + " GOCRYPTFS_BAD_NAME", nil + return cipherName + BadNameFlag, nil } } } @@ -135,3 +139,8 @@ func (n *NameTransform) B64EncodeToString(src []byte) string { func (n *NameTransform) B64DecodeString(s string) ([]byte, error) { return n.B64.DecodeString(s) } + +// HaveBadnamePatterns returns true if BadName patterns were provided +func (n *NameTransform) HaveBadnamePatterns() bool { + return len(n.BadnamePatterns) > 0 +} diff --git a/tests/cli/cli_test.go b/tests/cli/cli_test.go index 9248f5d..08c1b83 100644 --- a/tests/cli/cli_test.go +++ b/tests/cli/cli_test.go @@ -16,6 +16,7 @@ import ( "github.com/rfjakob/gocryptfs/internal/configfile" "github.com/rfjakob/gocryptfs/internal/exitcodes" + "github.com/rfjakob/gocryptfs/internal/nametransform" "github.com/rfjakob/gocryptfs/tests/test_helpers" ) @@ -698,18 +699,29 @@ func TestSymlinkedCipherdir(t *testing.T) { // TestBadname tests the `-badname` option func TestBadname(t *testing.T) { + //Supported structure of badname: + //"Visible" shows the success of function DecryptName (cipher -> plain) + //"Access" shows the success of function EncryptAndHashBadName (plain -> cipher) + //Case Visible Access Description + //Case 1 x x Access file without BadName suffix (default mode) + //Case 2 x x Access file with BadName suffix which has a valid cipher file (will only be possible if file was created without badname option) + //Case 3 Access file with valid ciphername + BadName suffix (impossible since this would not be produced by DecryptName) + //Case 4 x x Access file with decryptable part of name and Badname suffix (default badname case) + //Case 5 x x Access file with undecryptable name and BadName suffix (e. g. when part of the cipher name was cut) + //Case 6 x Access file with multiple possible matches. + //Case 7 Access file with BadName suffix and non-matching pattern + dir := test_helpers.InitFS(t) mnt := dir + ".mnt" validFileName := "file" - invalidSuffix := ".invalid_file" + invalidSuffix := "_invalid_file" + var contentCipher [7][]byte + //first mount without badname (see case 2) + test_helpers.MountOrFatal(t, dir, mnt, "-extpass=echo test", "-wpanic=false") - // use static suffix for testing - test_helpers.MountOrFatal(t, dir, mnt, "-badname=*", "-extpass=echo test") - defer test_helpers.UnmountPanic(mnt) - - // write one valid filename (empty content) file := mnt + "/" + validFileName - err := ioutil.WriteFile(file, nil, 0600) + // Case 1: write one valid filename (empty content) + err := ioutil.WriteFile(file, []byte("Content Case 1."), 0600) if err != nil { t.Fatal(err) } @@ -720,7 +732,6 @@ func TestBadname(t *testing.T) { t.Fatal(err) } defer fread.Close() - encryptedfilename := "" ciphernames, err := fread.Readdirnames(0) if err != nil { @@ -733,14 +744,64 @@ func TestBadname(t *testing.T) { break } } + //Generate valid cipherdata for all cases + for i := 0; i < len(contentCipher); i++ { + err := ioutil.WriteFile(file, []byte(fmt.Sprintf("Content Case %d.", i+1)), 0600) + if err != nil { + t.Fatal(err) + } + //save the cipher data for file operations in cipher dir + contentCipher[i], err = ioutil.ReadFile(dir + "/" + encryptedfilename) + if err != nil { + t.Fatal(err) + } + } - // write invalid file which should be decodable - err = ioutil.WriteFile(dir+"/"+encryptedfilename+invalidSuffix, nil, 0600) + //re-write content for case 1 + err = ioutil.WriteFile(file, []byte("Content Case 1."), 0600) if err != nil { t.Fatal(err) } - // write invalid file which is not decodable (cropping the encrpyted file name) - err = ioutil.WriteFile(dir+"/"+encryptedfilename[:len(encryptedfilename)-2]+invalidSuffix, nil, 0600) + + // Case 2: File with invalid suffix in plain name but valid cipher file + file = mnt + "/" + validFileName + nametransform.BadNameFlag + err = ioutil.WriteFile(file, []byte("Content Case 2."), 0600) + if err != nil { + t.Fatal(err) + } + // unmount... + test_helpers.UnmountPanic(mnt) + + // ...and remount with -badname. + test_helpers.MountOrFatal(t, dir, mnt, "-badname=*valid*", "-extpass=echo test", "-wpanic=false") + defer test_helpers.UnmountPanic(mnt) + + // Case 3 is impossible: only BadnameSuffix would mean the cipher name is valid + + // Case 4: write invalid file which should be decodable + err = ioutil.WriteFile(dir+"/"+encryptedfilename+invalidSuffix, contentCipher[3], 0600) + if err != nil { + t.Fatal(err) + } + //Case 5: write invalid file which is not decodable (cropping the encrpyted file name) + err = ioutil.WriteFile(dir+"/"+encryptedfilename[:len(encryptedfilename)-2]+invalidSuffix, contentCipher[4], 0600) + if err != nil { + t.Fatal(err) + } + + // Case 6: Multiple possible matches + // generate two files with invalid cipher names which can both match the badname pattern + err = ioutil.WriteFile(dir+"/mzaZRF9_0IU-_5vv2wPC"+invalidSuffix, contentCipher[5], 0600) + if err != nil { + t.Fatal(err) + } + err = ioutil.WriteFile(dir+"/mzaZRF9_0IU-_5vv2wP"+invalidSuffix, contentCipher[5], 0600) + if err != nil { + t.Fatal(err) + } + + // Case 7: Non-Matching badname pattern + err = ioutil.WriteFile(dir+"/"+encryptedfilename+"wrongPattern", contentCipher[6], 0600) if err != nil { t.Fatal(err) } @@ -755,22 +816,74 @@ func TestBadname(t *testing.T) { if err != nil { t.Fatal(err) } - foundDecodable := false - foundUndecodable := false + + searchstrings := []string{ + validFileName, + validFileName + nametransform.BadNameFlag, + "", + validFileName + invalidSuffix + nametransform.BadNameFlag, + encryptedfilename[:len(encryptedfilename)-2] + invalidSuffix + nametransform.BadNameFlag, + "", + validFileName + "wrongPattern" + nametransform.BadNameFlag} + results := []bool{false, false, true, false, false, true, true} + var filecontent string + var filebytes []byte for _, name := range names { - if strings.Contains(name, validFileName+invalidSuffix+" GOCRYPTFS_BAD_NAME") { - foundDecodable = true - } else if strings.Contains(name, encryptedfilename[:len(encryptedfilename)-2]+invalidSuffix+" GOCRYPTFS_BAD_NAME") { - foundUndecodable = true + if name == searchstrings[0] { + //Case 1: Test access + filebytes, err = ioutil.ReadFile(mnt + "/" + name) + if err != nil { + t.Fatal(err) + } + filecontent = string(filebytes) + if filecontent == "Content Case 1." { + results[0] = true + } + + } else if name == searchstrings[1] { + //Case 2: Test Access + filebytes, err = ioutil.ReadFile(mnt + "/" + name) + if err != nil { + t.Fatal(err) + } + filecontent = string(filebytes) + if filecontent == "Content Case 2." { + results[1] = true + } + } else if name == searchstrings[3] { + //Case 4: Test Access + filebytes, err = ioutil.ReadFile(mnt + "/" + name) + if err != nil { + t.Fatal(err) + } + filecontent = string(filebytes) + if filecontent == "Content Case 4." { + results[3] = true + } + } else if name == searchstrings[4] { + //Case 5: Test Access + filebytes, err = ioutil.ReadFile(mnt + "/" + name) + if err != nil { + t.Fatal(err) + } + filecontent = string(filebytes) + if filecontent == "Content Case 5." { + results[4] = true + } + } else if name == searchstrings[6] { + //Case 7 + results[6] = false } + //Case 3 is always passed + //Case 6 is highly obscure: + //The last part of a valid cipher name must match the badname pattern AND + //the remaining cipher name must still be decryptable. Test case not programmable in a general case } - if !foundDecodable { - t.Errorf("did not find invalid name %s in %v", validFileName+invalidSuffix+" GOCRYPTFS_BAD_NAME", names) - } - - if !foundUndecodable { - t.Errorf("did not find invalid name %s in %v", encryptedfilename[:len(encryptedfilename)-2]+invalidSuffix+" GOCRYPTFS_BAD_NAME", names) + for i := 0; i < len(results); i++ { + if !results[i] { + t.Errorf("Case %d failed: '%s' in [%s]", i+1, searchstrings[i], strings.Join(names, ",")) + } } }