aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/fusefrontend/node_helpers.go10
-rw-r--r--internal/fusefrontend/root_node.go6
-rw-r--r--internal/nametransform/diriv.go50
-rw-r--r--internal/nametransform/names.go13
-rw-r--r--tests/cli/cli_test.go161
5 files changed, 212 insertions, 28 deletions
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: <ciphername><badname pattern><badname suffix>
+ //"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"
-
- // use static suffix for testing
- test_helpers.MountOrFatal(t, dir, mnt, "-badname=*", "-extpass=echo test")
- defer test_helpers.UnmountPanic(mnt)
+ 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")
- // 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
+ }
- if !foundDecodable {
- t.Errorf("did not find invalid name %s in %v", validFileName+invalidSuffix+" GOCRYPTFS_BAD_NAME", names)
+ } 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 !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, ","))
+ }
}
}