From 689b74835bd38ebaf87ba0e205c10b9594e51863 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Mon, 21 Jun 2021 12:08:18 +0200 Subject: [PATCH] nametransform: gather badname functions in badname.go --- internal/nametransform/badname.go | 91 +++++++++++++++++++++++++++++++ internal/nametransform/diriv.go | 78 -------------------------- internal/nametransform/names.go | 48 +++++++++------- 3 files changed, 118 insertions(+), 99 deletions(-) create mode 100644 internal/nametransform/badname.go diff --git a/internal/nametransform/badname.go b/internal/nametransform/badname.go new file mode 100644 index 0000000..5cc9799 --- /dev/null +++ b/internal/nametransform/badname.go @@ -0,0 +1,91 @@ +package nametransform + +import ( + "crypto/aes" + "path/filepath" + "strings" + "syscall" + + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/internal/syscallcompat" +) + +const ( + // BadNameFlag is appended to filenames in plaintext view if a corrupt + // ciphername is shown due to a matching `-badname` pattern + BadNameFlag = " GOCRYPTFS_BAD_NAME" +) + +// 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 +} + +func (n *NameTransform) decryptBadname(cipherName string, iv []byte) (string, error) { + for _, pattern := range n.badnamePatterns { + match, err := filepath.Match(pattern, cipherName) + // Pattern should have been validated already + if err == nil && match { + // Find longest decryptable substring + // At least 16 bytes due to AES --> at least 22 characters in base64 + nameMin := n.B64.EncodedLen(aes.BlockSize) + for charpos := len(cipherName) - 1; charpos >= nameMin; charpos-- { + res, err := n.decryptName(cipherName[:charpos], iv) + if err == nil { + return res + cipherName[charpos:] + BadNameFlag, nil + } + } + return cipherName + BadNameFlag, nil + } + } + return "", syscall.EBADMSG +} + +// HaveBadnamePatterns returns true if `-badname` patterns were provided +func (n *NameTransform) HaveBadnamePatterns() bool { + return len(n.badnamePatterns) > 0 +} diff --git a/internal/nametransform/diriv.go b/internal/nametransform/diriv.go index d62b3fb..b10c899 100644 --- a/internal/nametransform/diriv.go +++ b/internal/nametransform/diriv.go @@ -5,14 +5,11 @@ import ( "fmt" "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 ( @@ -95,78 +92,3 @@ func WriteDirIVAt(dirfd int) error { } return nil } - -// encryptAndHashName encrypts "name" and hashes it to a longname if it is -// too long. -// Returns ENAMETOOLONG if "name" is longer than 255 bytes. -func (be *NameTransform) EncryptAndHashName(name string, iv []byte) (string, error) { - // Prevent the user from creating files longer than 255 chars. - if len(name) > NameMax { - return "", syscall.ENAMETOOLONG - } - cName, err := be.EncryptName(name, iv) - if err != nil { - return "", err - } - if be.longNames && len(cName) > NameMax { - return be.HashLongName(cName), nil - } - 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) - if d == "." { - return "" - } - return d -} diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 2ee52e4..566f0c7 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -15,8 +15,6 @@ 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" ) // NameTransform is used to transform filenames. @@ -51,22 +49,8 @@ func New(e *eme.EMECipher, longNames bool, raw64 bool, badname []string) *NameTr // filename "cipherName", and failing that checks if it can be bypassed func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error) { res, err := n.decryptName(cipherName, iv) - if err != nil { - for _, pattern := range n.badnamePatterns { - match, err := filepath.Match(pattern, cipherName) - if err == nil && match { // Pattern should have been validated already - // Find longest decryptable substring - // At least 16 bytes due to AES --> at least 22 characters in base64 - nameMin := n.B64.EncodedLen(aes.BlockSize) - for charpos := len(cipherName) - 1; charpos >= nameMin; charpos-- { - res, err = n.decryptName(cipherName[:charpos], iv) - if err == nil { - return res + cipherName[charpos:] + BadNameFlag, nil - } - } - return cipherName + BadNameFlag, nil - } - } + if err != nil && n.HaveBadnamePatterns() { + return n.decryptBadname(cipherName, iv) } return res, err } @@ -117,6 +101,24 @@ func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 s return cipherName64, nil } +// EncryptAndHashName encrypts "name" and hashes it to a longname if it is +// too long. +// Returns ENAMETOOLONG if "name" is longer than 255 bytes. +func (be *NameTransform) EncryptAndHashName(name string, iv []byte) (string, error) { + // Prevent the user from creating files longer than 255 chars. + if len(name) > NameMax { + return "", syscall.ENAMETOOLONG + } + cName, err := be.EncryptName(name, iv) + if err != nil { + return "", err + } + if be.longNames && len(cName) > NameMax { + return be.HashLongName(cName), nil + } + return cName, nil +} + // B64EncodeToString returns a Base64-encoded string func (n *NameTransform) B64EncodeToString(src []byte) string { return n.B64.EncodeToString(src) @@ -127,7 +129,11 @@ 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 +// Dir is like filepath.Dir but returns "" instead of ".". +func Dir(path string) string { + d := filepath.Dir(path) + if d == "." { + return "" + } + return d }