nametransform: gather badname functions in badname.go
This commit is contained in:
parent
2efef1e270
commit
689b74835b
|
@ -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
|
||||||
|
}
|
|
@ -5,14 +5,11 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
"github.com/rfjakob/gocryptfs/internal/cryptocore"
|
"github.com/rfjakob/gocryptfs/internal/cryptocore"
|
||||||
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
|
||||||
"github.com/rfjakob/gocryptfs/internal/tlog"
|
"github.com/rfjakob/gocryptfs/internal/tlog"
|
||||||
"golang.org/x/sys/unix"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -95,78 +92,3 @@ func WriteDirIVAt(dirfd int) error {
|
||||||
}
|
}
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,8 +15,6 @@ import (
|
||||||
const (
|
const (
|
||||||
// Like ext4, we allow at most 255 bytes for a file name.
|
// Like ext4, we allow at most 255 bytes for a file name.
|
||||||
NameMax = 255
|
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.
|
// 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
|
// filename "cipherName", and failing that checks if it can be bypassed
|
||||||
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 {
|
if err != nil && n.HaveBadnamePatterns() {
|
||||||
for _, pattern := range n.badnamePatterns {
|
return n.decryptBadname(cipherName, iv)
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return res, err
|
return res, err
|
||||||
}
|
}
|
||||||
|
@ -117,6 +101,24 @@ func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 s
|
||||||
return cipherName64, nil
|
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
|
// B64EncodeToString returns a Base64-encoded string
|
||||||
func (n *NameTransform) B64EncodeToString(src []byte) string {
|
func (n *NameTransform) B64EncodeToString(src []byte) string {
|
||||||
return n.B64.EncodeToString(src)
|
return n.B64.EncodeToString(src)
|
||||||
|
@ -127,7 +129,11 @@ func (n *NameTransform) B64DecodeString(s string) ([]byte, error) {
|
||||||
return n.B64.DecodeString(s)
|
return n.B64.DecodeString(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HaveBadnamePatterns returns true if BadName patterns were provided
|
// Dir is like filepath.Dir but returns "" instead of ".".
|
||||||
func (n *NameTransform) HaveBadnamePatterns() bool {
|
func Dir(path string) string {
|
||||||
return len(n.badnamePatterns) > 0
|
d := filepath.Dir(path)
|
||||||
|
if d == "." {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return d
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue