diff --git a/internal/configfile/config_file.go b/internal/configfile/config_file.go index 089af07..d50b71b 100644 --- a/internal/configfile/config_file.go +++ b/internal/configfile/config_file.go @@ -55,7 +55,7 @@ type ConfFile struct { // stored in the superblock. FeatureFlags []string // FIDO2 parameters - FIDO2 FIDO2Params + FIDO2 *FIDO2Params `json:",omitempty"` // Filename is the name of the config file. Not exported to JSON. filename string } @@ -101,8 +101,10 @@ func Create(filename string, password []byte, plaintextNames bool, } if len(fido2CredentialID) > 0 { cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagFIDO2]) - cf.FIDO2.CredentialID = fido2CredentialID - cf.FIDO2.HMACSalt = fido2HmacSalt + cf.FIDO2 = &FIDO2Params{ + CredentialID: fido2CredentialID, + HMACSalt: fido2HmacSalt, + } } { // Generate new random master key diff --git a/internal/cryptocore/cryptocore.go b/internal/cryptocore/cryptocore.go index 8ea11d4..56e0baa 100644 --- a/internal/cryptocore/cryptocore.go +++ b/internal/cryptocore/cryptocore.go @@ -35,6 +35,19 @@ const ( BackendAESSIV AEADTypeEnum = 5 ) +func (a AEADTypeEnum) String() string { + switch a { + case BackendOpenSSL: + return "BackendOpenSSL" + case BackendGoGCM: + return "BackendGoGCM" + case BackendAESSIV: + return "BackendAESSIV" + default: + return fmt.Sprintf("%d", a) + } +} + // CryptoCore is the low level crypto implementation. type CryptoCore struct { // EME is used for filename encryption. diff --git a/internal/nametransform/badname.go b/internal/nametransform/badname.go new file mode 100644 index 0000000..53fd0c4 --- /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" + + "../syscallcompat" +) + +const ( + // BadnameSuffix is appended to filenames in plaintext view if a corrupt + // ciphername is shown due to a matching `-badname` pattern + BadnameSuffix = " 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, BadnameSuffix) || 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(BadnameSuffix)], &st, unix.AT_SYMLINK_NOFOLLOW) + if err == nil { + filesFound++ + lastFoundName = name[:len(name)-len(BadnameSuffix)] + } + // search for the longest badname pattern match + for charpos := len(name) - len(BadnameSuffix); 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(BadnameSuffix)] + 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:] + BadnameSuffix, nil + } + } + return cipherName + BadnameSuffix, 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 67635b3..ae46c9a 100644 --- a/internal/nametransform/diriv.go +++ b/internal/nametransform/diriv.go @@ -5,7 +5,6 @@ import ( "fmt" "io" "os" - "path/filepath" "syscall" "../cryptocore" @@ -87,30 +86,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 -} - -// 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 fc7c7ff..08ea41e 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -15,21 +15,6 @@ const ( NameMax = 255 ) -// NameTransformer is an interface used to transform filenames. -type NameTransformer interface { - DecryptName(cipherName string, iv []byte) (string, error) - EncryptName(plainName string, iv []byte) (string, error) - EncryptAndHashName(name string, iv []byte) (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 - WriteLongNameAt(dirfd int, hashName string, plainName string) error - B64EncodeToString(src []byte) string - B64DecodeString(s string) ([]byte, error) -} - // NameTransform is used to transform filenames. type NameTransform struct { emeCipher *eme.EMECipher @@ -38,19 +23,20 @@ type NameTransform struct { // on the Raw64 feature flag B64 *base64.Encoding // Patterns to bypass decryption - BadnamePatterns []string + badnamePatterns []string } // New returns a new NameTransform instance. -func New(e *eme.EMECipher, longNames bool, raw64 bool) *NameTransform { +func New(e *eme.EMECipher, longNames bool, raw64 bool, badname []string) *NameTransform { b64 := base64.URLEncoding if raw64 { b64 = base64.RawURLEncoding } return &NameTransform{ - emeCipher: e, - longNames: longNames, - B64: b64, + emeCipher: e, + longNames: longNames, + B64: b64, + badnamePatterns: badname, } } @@ -58,22 +44,8 @@ func New(e *eme.EMECipher, longNames bool, raw64 bool) *NameTransform { // 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:] + " GOCRYPTFS_BAD_NAME", nil - } - } - return cipherName + " GOCRYPTFS_BAD_NAME", nil - } - } + if err != nil && n.HaveBadnamePatterns() { + return n.decryptBadname(cipherName, iv) } return res, err } @@ -119,6 +91,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) @@ -128,3 +118,12 @@ func (n *NameTransform) B64EncodeToString(src []byte) string { func (n *NameTransform) B64DecodeString(s string) ([]byte, error) { return n.B64.DecodeString(s) } + +// 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/pathiv/pathiv.go b/internal/pathiv/pathiv.go deleted file mode 100644 index db0f7af..0000000 --- a/internal/pathiv/pathiv.go +++ /dev/null @@ -1,57 +0,0 @@ -package pathiv - -import ( - "crypto/sha256" - "encoding/binary" - - "../nametransform" -) - -// Purpose identifies for which purpose the IV will be used. This is mixed into the -// derivation. -type Purpose string - -const ( - // PurposeDirIV means the value will be used as a directory IV - PurposeDirIV Purpose = "DIRIV" - // PurposeFileID means the value will be used as the file ID in the file header - PurposeFileID Purpose = "FILEID" - // PurposeSymlinkIV means the value will be used as the IV for symlink encryption - PurposeSymlinkIV Purpose = "SYMLINKIV" - // PurposeBlock0IV means the value will be used as the IV of ciphertext block #0. - PurposeBlock0IV Purpose = "BLOCK0IV" -) - -// Derive derives an IV from an encrypted path by hashing it with sha256 -func Derive(path string, purpose Purpose) []byte { - // Use null byte as separator as it cannot occur in the path - extended := []byte(path + "\000" + string(purpose)) - hash := sha256.Sum256(extended) - return hash[:nametransform.DirIVLen] -} - -// FileIVs contains both IVs that are needed to create a file. -type FileIVs struct { - ID []byte - Block0IV []byte -} - -// DeriveFile derives both IVs that are needed to create a file and returns them -// in a container struct. -func DeriveFile(path string) (fileIVs FileIVs) { - fileIVs.ID = Derive(path, PurposeFileID) - fileIVs.Block0IV = Derive(path, PurposeBlock0IV) - return fileIVs -} - -// BlockIV returns the block IV for block number "blockNo". "block0iv" is the block -// IV of block #0. -func BlockIV(block0iv []byte, blockNo uint64) []byte { - iv := make([]byte, len(block0iv)) - copy(iv, block0iv) - // Add blockNo to one half of the iv - lowBytes := iv[8:] - lowInt := binary.BigEndian.Uint64(lowBytes) - binary.BigEndian.PutUint64(lowBytes, lowInt+blockNo) - return iv -} diff --git a/internal/pathiv/pathiv_test.go b/internal/pathiv/pathiv_test.go deleted file mode 100644 index 96745bd..0000000 --- a/internal/pathiv/pathiv_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package pathiv - -import ( - "bytes" - "encoding/hex" - "testing" -) - -// TestBlockIV makes sure we don't change the block iv derivation algorithm "BlockIV()" -// inadvertedly. -func TestBlockIV(t *testing.T) { - b0 := make([]byte, 16) - b0x := BlockIV(b0, 0) - if !bytes.Equal(b0, b0x) { - t.Errorf("b0x should be equal to b0") - } - b27 := BlockIV(b0, 0x27) - expected, _ := hex.DecodeString("00000000000000000000000000000027") - if !bytes.Equal(b27, expected) { - t.Errorf("\nhave=%s\nwant=%s", hex.EncodeToString(b27), hex.EncodeToString(expected)) - } - bff := bytes.Repeat([]byte{0xff}, 16) - b28 := BlockIV(bff, 0x28) - expected, _ = hex.DecodeString("ffffffffffffffff0000000000000027") - if !bytes.Equal(b28, expected) { - t.Errorf("\nhave=%s\nwant=%s", hex.EncodeToString(b28), hex.EncodeToString(expected)) - } -} diff --git a/volume.go b/volume.go index 393f2df..17e7658 100644 --- a/volume.go +++ b/volume.go @@ -74,7 +74,8 @@ func registerNewVolume(rootCipherDir string, masterkey []byte, cf *configfile.Co forcedecode := false newVolume.cryptoCore = cryptocore.New(masterkey, cryptoBackend, contentenc.DefaultIVBits, true, forcedecode) newVolume.contentEnc = contentenc.New(newVolume.cryptoCore, contentenc.DefaultBS, forcedecode) - newVolume.nameTransform = nametransform.New(newVolume.cryptoCore.EMECipher, true, true) + var badname []string + newVolume.nameTransform = nametransform.New(newVolume.cryptoCore.EMECipher, true, true, badname) //copying rootCipherDir var grcd strings.Builder