aboutsummaryrefslogtreecommitdiff
path: root/internal/nametransform
diff options
context:
space:
mode:
Diffstat (limited to 'internal/nametransform')
-rw-r--r--internal/nametransform/diriv.go2
-rw-r--r--internal/nametransform/names.go43
-rw-r--r--internal/nametransform/nfc_test.go29
-rw-r--r--internal/nametransform/pad16.go12
4 files changed, 72 insertions, 14 deletions
diff --git a/internal/nametransform/diriv.go b/internal/nametransform/diriv.go
index 7929c40..5dd4940 100644
--- a/internal/nametransform/diriv.go
+++ b/internal/nametransform/diriv.go
@@ -67,7 +67,7 @@ func fdReadDirIV(fd *os.File) (iv []byte, err error) {
func WriteDirIVAt(dirfd int) error {
iv := cryptocore.RandBytes(DirIVLen)
// 0400 permissions: gocryptfs.diriv should never be modified after creation.
- // Don't use "ioutil.WriteFile", it causes trouble on NFS:
+ // Don't use "os.WriteFile", it causes trouble on NFS:
// https://github.com/rfjakob/gocryptfs/commit/7d38f80a78644c8ec4900cc990bfb894387112ed
fd, err := syscallcompat.Openat(dirfd, DirIVFilename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, dirivPerms)
if err != nil {
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))
+ }
+}
diff --git a/internal/nametransform/pad16.go b/internal/nametransform/pad16.go
index 833be0e..2c2466a 100644
--- a/internal/nametransform/pad16.go
+++ b/internal/nametransform/pad16.go
@@ -32,10 +32,10 @@ func pad16(orig []byte) (padded []byte) {
func unPad16(padded []byte) ([]byte, error) {
oldLen := len(padded)
if oldLen == 0 {
- return nil, errors.New("Empty input")
+ return nil, errors.New("empty input")
}
if oldLen%aes.BlockSize != 0 {
- return nil, errors.New("Unaligned size")
+ return nil, errors.New("unaligned size")
}
// The last byte is always a padding byte
padByte := padded[oldLen-1]
@@ -43,20 +43,20 @@ func unPad16(padded []byte) ([]byte, error) {
padLen := int(padByte)
// Padding must be at least 1 byte
if padLen == 0 {
- return nil, errors.New("Padding cannot be zero-length")
+ return nil, errors.New("padding cannot be zero-length")
}
// Padding more than 16 bytes make no sense
if padLen > aes.BlockSize {
- return nil, fmt.Errorf("Padding too long, padLen=%d > 16", padLen)
+ return nil, fmt.Errorf("padding too long, padLen=%d > 16", padLen)
}
// Padding cannot be as long as (or longer than) the whole string,
if padLen >= oldLen {
- return nil, fmt.Errorf("Padding too long, oldLen=%d >= padLen=%d", oldLen, padLen)
+ return nil, fmt.Errorf("padding too long, oldLen=%d >= padLen=%d", oldLen, padLen)
}
// All padding bytes must be identical
for i := oldLen - padLen; i < oldLen; i++ {
if padded[i] != padByte {
- return nil, fmt.Errorf("Padding byte at i=%d is invalid", i)
+ return nil, fmt.Errorf("padding byte at i=%d is invalid", i)
}
}
newLen := oldLen - padLen