Badname file content access

This proposal is the counterpart of the modifications from the `-badname`
parameter. It modifies the plain -> cipher mapping for filenames when using
`-badname` parameter. The new function `EncryptAndHashBadName` tries to find a
cipher filename for the given plain name with the following steps:

1. If `badname` is disabled or direct mapping is successful: Map directly
(default and current behaviour)

2. If a file with badname flag has a valid cipher file, this is returned
(=File just ends with the badname flag)

3. If a file with a badname flag exists where only the badname flag was added,
this is returned (=File cipher name could not be decrypted by function
`DecryptName` and just the badname flag was added)

4. Search for all files which cipher file name extists when cropping more and
more characters from the end. If only 1 file is found, return this

5. Return an error otherwise

This allows file access in the file browsers but most important it allows that
you rename files with undecryptable cipher names in the plain directories.
Renaming those files will then generate a proper cipher filename One
backdraft: When mounting the cipher dir with -badname parameter, you can never
create (or rename to) files whose file name ends with the badname file flag
(at the moment this is " GOCRYPTFS_BAD_NAME"). This will cause an error.

I modified the CLI test function to cover additional test cases. Test [Case
7](https://github.com/DerDonut/gocryptfs/blob/badnamecontent/tests/cli/cli_test.go#L712)
cannot be performed since the cli tests are executed in panic mode. The
testing is stopped on error. Since the function`DecryptName` produces internal
errors when hitting non-decryptable file names, this test was omitted.

This implementation is a proposal where I tried to change the minimum amount
of existing code. Another possibility would be instead of creating the new
function `EncryptAndHashBadName` to modify the signature of the existing
function `EncryptAndHashName(name string, iv []byte)` to
`EncryptAndHashName(name string, iv []byte, dirfd int)` and integrate the
functionality into this function directly. You may allow calling with dirfd=-1
or other invalid values an then performing the current functionality.
This commit is contained in:
DerDonut 2021-06-17 08:11:33 +02:00 committed by Jakob Unterwurzacher
parent cdddd1d711
commit a611810ff4
5 changed files with 212 additions and 28 deletions

View File

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

View File

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

View File

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

View File

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

View File

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