diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/nametransform/names.go | 43 | ||||
| -rw-r--r-- | internal/nametransform/nfc_test.go | 29 |
2 files changed, 65 insertions, 7 deletions
diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 3313a7c..0389c95 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -7,9 +7,12 @@ import ( "errors" "math" "path/filepath" + "runtime" "strings" "syscall" + "golang.org/x/text/unicode/norm" + "github.com/rfjakob/eme" "github.com/rfjakob/gocryptfs/v2/internal/tlog" @@ -32,6 +35,12 @@ type NameTransform struct { // Patterns to bypass decryption badnamePatterns []string deterministicNames bool + // Convert filenames to NFC before encrypting, + // and to NFD when decrypting. + // For MacOS compatibility. + // Automatically enabled on MacOS, off otherwise, + // except in tests (see nfc_test.go). + nfd2nfc bool } // New returns a new NameTransform instance. @@ -55,34 +64,44 @@ func New(e *eme.EMECipher, longNames bool, longNameMax uint8, raw64 bool, badnam effectiveLongNameMax = int(longNameMax) } } + nfd2nfc := runtime.GOOS == "darwin" + if nfd2nfc { + tlog.Info.Printf("Running on MacOS, enabling Unicode normalization") + } return &NameTransform{ emeCipher: e, longNameMax: effectiveLongNameMax, B64: b64, badnamePatterns: badname, deterministicNames: deterministicNames, + nfd2nfc: nfd2nfc, } } // 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) +func (n *NameTransform) DecryptName(cipherName string, iv []byte) (plainName string, err error) { + plainName, err = n.decryptName(cipherName, iv) if err != nil && n.HaveBadnamePatterns() { - res, err = n.decryptBadname(cipherName, iv) + plainName, err = n.decryptBadname(cipherName, iv) } if err != nil { return "", err } - if err := IsValidName(res); err != nil { + if err := IsValidName(plainName); err != nil { tlog.Warn.Printf("DecryptName %q: invalid name after decryption: %v", cipherName, err) return "", syscall.EBADMSG } - return res, err + if n.nfd2nfc { + // MacOS expects file names in NFD form. Present them as NFD. + // They are converted back to NFC in EncryptName. + plainName = norm.NFD.String(plainName) + } + return plainName, err } -// decryptName decrypts a base64-encoded encrypted filename "cipherName" using the -// initialization vector "iv". +// decryptName decrypts a base64-encoded encrypted file- or xattr-name "cipherName" +// using the initialization vector "iv". func (n *NameTransform) decryptName(cipherName string, iv []byte) (string, error) { // From https://pkg.go.dev/encoding/base64#Encoding.Strict : // > Note that the input is still malleable, as new line characters @@ -126,6 +145,16 @@ func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 s tlog.Warn.Printf("EncryptName %q: invalid plainName: %v", plainName, err) return "", syscall.EBADMSG } + if n.nfd2nfc { + // MacOS GUI apps expect Unicode in NFD form. + // But MacOS CLI apps, Linux and Windows use NFC form. + // We normalize to NFC for two reasons: + // 1) Make sharing gocryptfs filesystems from MacOS to other systems + // less painful + // 2) Enable DecryptName to normalize to NFD, which works for both + // GUI and CLI on MacOS. + plainName = norm.NFC.String(plainName) + } return n.encryptName(plainName, iv), nil } diff --git a/internal/nametransform/nfc_test.go b/internal/nametransform/nfc_test.go new file mode 100644 index 0000000..aad1d7d --- /dev/null +++ b/internal/nametransform/nfc_test.go @@ -0,0 +1,29 @@ +package nametransform + +import ( + "strconv" + "testing" + + "golang.org/x/text/unicode/norm" +) + +func TestNFD2NFC(t *testing.T) { + n := newLognamesTestInstance(NameMax) + n.nfd2nfc = true + iv := make([]byte, DirIVLen) + srcNFC := "Österreich Café" + srcNFD := norm.NFD.String(srcNFC) + + // cipherName should get normalized to NFC + cipherName, _ := n.EncryptName(srcNFD, iv) + // Decrypt without changing normalization + decryptedRaw, _ := n.decryptName(cipherName, iv) + if srcNFC != decryptedRaw { + t.Errorf("want %s have %s", strconv.QuoteToASCII(srcNFC), strconv.QuoteToASCII(decryptedRaw)) + } + // Decrypt with normalizing to NFD + decrypted, _ := n.DecryptName(cipherName, iv) + if srcNFD != decrypted { + t.Errorf("want %s have %s", strconv.QuoteToASCII(srcNFD), strconv.QuoteToASCII(decrypted)) + } +} |
