diff options
| author | Jakob Unterwurzacher | 2026-01-23 21:32:49 +0100 |
|---|---|---|
| committer | Jakob Unterwurzacher | 2026-01-24 19:47:17 +0100 |
| commit | f5f6d99efa23422ba8aa3f0ef81a267c989e1145 (patch) | |
| tree | afa8fedaa2511fbea733f6c76c68ee20a33bf233 | |
| parent | c9cf6f1f8a5b90c9cb70ed19f8c8426dc2655c9d (diff) | |
macos: normalize unicode file names in forward mode
Summary: Store as NFC, read as NFD.
This commit resolves https://github.com/rfjakob/gocryptfs/issues/850
by addressing Unicode normalization mismatches on macOS between NFC
(used by CLI tools) and NFD (used by GUI apps). The solution is inspired
by Cryptomator's approach ( https://github.com/cryptomator/cryptomator/issues/264 ).
Forward mode on MacOS now enforces NFC for storage but presents NFD
as recommended by https://developer.apple.com/library/archive/qa/qa1173/_index.html
and https://github.com/macfuse/macfuse/wiki/File-Names-(Unicode-Normalization-Forms) .
See https://github.com/rfjakob/gocryptfs/pull/949 for more info.
This commit does nothing for reverse mode as it is not clear if
anything can be done. Reverse mode can not influence how the
file names are stored, hence mapping normalized names back to
what is actually on disk seems difficult.
| -rw-r--r-- | go.mod | 1 | ||||
| -rw-r--r-- | go.sum | 2 | ||||
| -rw-r--r-- | internal/nametransform/names.go | 38 |
3 files changed, 34 insertions, 7 deletions
@@ -13,4 +13,5 @@ require ( golang.org/x/crypto v0.45.0 golang.org/x/sys v0.38.0 golang.org/x/term v0.37.0 + golang.org/x/text v0.31.0 ) @@ -38,6 +38,8 @@ golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 3313a7c..d007592 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,10 @@ 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. + nfd2nfc bool } // New returns a new NameTransform instance. @@ -55,34 +62,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 +143,13 @@ 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. + // To prevent trouble when decrypting MacOS-created gocryptfs filesystems + // on other OS, we convert to NFC form. + plainName = norm.NFC.String(plainName) + } return n.encryptName(plainName, iv), nil } |
