a611810ff4
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.
147 lines
4.6 KiB
Go
147 lines
4.6 KiB
Go
// Package nametransform encrypts and decrypts filenames.
|
|
package nametransform
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"encoding/base64"
|
|
"path/filepath"
|
|
"syscall"
|
|
|
|
"github.com/rfjakob/eme"
|
|
|
|
"github.com/rfjakob/gocryptfs/internal/tlog"
|
|
)
|
|
|
|
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.
|
|
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)
|
|
}
|
|
|
|
// NameTransform is used to transform filenames.
|
|
type NameTransform struct {
|
|
emeCipher *eme.EMECipher
|
|
longNames bool
|
|
// B64 = either base64.URLEncoding or base64.RawURLEncoding, depending
|
|
// on the Raw64 feature flag
|
|
B64 *base64.Encoding
|
|
// Patterns to bypass decryption
|
|
BadnamePatterns []string
|
|
}
|
|
|
|
// New returns a new NameTransform instance.
|
|
func New(e *eme.EMECipher, longNames bool, raw64 bool) *NameTransform {
|
|
b64 := base64.URLEncoding
|
|
if raw64 {
|
|
b64 = base64.RawURLEncoding
|
|
}
|
|
return &NameTransform{
|
|
emeCipher: e,
|
|
longNames: longNames,
|
|
B64: b64,
|
|
}
|
|
}
|
|
|
|
// DecryptName calls decryptName to try and decrypt a base64-encoded encrypted
|
|
// 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
|
|
}
|
|
}
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
// decryptName decrypts a base64-encoded encrypted filename "cipherName" using the
|
|
// initialization vector "iv".
|
|
func (n *NameTransform) decryptName(cipherName string, iv []byte) (string, error) {
|
|
bin, err := n.B64.DecodeString(cipherName)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(bin) == 0 {
|
|
tlog.Warn.Printf("DecryptName: empty input")
|
|
return "", syscall.EBADMSG
|
|
}
|
|
if len(bin)%aes.BlockSize != 0 {
|
|
tlog.Debug.Printf("DecryptName %q: decoded length %d is not a multiple of 16", cipherName, len(bin))
|
|
return "", syscall.EBADMSG
|
|
}
|
|
bin = n.emeCipher.Decrypt(iv, bin)
|
|
bin, err = unPad16(bin)
|
|
if err != nil {
|
|
tlog.Warn.Printf("DecryptName %q: unPad16 error: %v", cipherName, err)
|
|
return "", syscall.EBADMSG
|
|
}
|
|
plain := string(bin)
|
|
if err := IsValidName(plain); err != nil {
|
|
tlog.Warn.Printf("DecryptName %q: invalid name after decryption: %v", cipherName, err)
|
|
return "", syscall.EBADMSG
|
|
}
|
|
return plain, err
|
|
}
|
|
|
|
// EncryptName encrypts "plainName", returns a base64-encoded "cipherName64",
|
|
// encrypted using EME (https://github.com/rfjakob/eme).
|
|
//
|
|
// This function is exported because in some cases, fusefrontend needs access
|
|
// to the full (not hashed) name if longname is used.
|
|
func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 string, err error) {
|
|
if err := IsValidName(plainName); err != nil {
|
|
tlog.Warn.Printf("EncryptName %q: invalid plainName: %v", plainName, err)
|
|
return "", syscall.EBADMSG
|
|
}
|
|
bin := []byte(plainName)
|
|
bin = pad16(bin)
|
|
bin = n.emeCipher.Encrypt(iv, bin)
|
|
cipherName64 = n.B64.EncodeToString(bin)
|
|
return cipherName64, nil
|
|
}
|
|
|
|
// B64EncodeToString returns a Base64-encoded string
|
|
func (n *NameTransform) B64EncodeToString(src []byte) string {
|
|
return n.B64.EncodeToString(src)
|
|
}
|
|
|
|
// B64DecodeString decodes a Base64-encoded 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
|
|
}
|