aboutsummaryrefslogtreecommitdiff
path: root/internal/nametransform/names.go
blob: d0075929f622c757b9b51b3c75a023013e01aaa8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
// Package nametransform encrypts and decrypts filenames.
package nametransform

import (
	"crypto/aes"
	"encoding/base64"
	"errors"
	"math"
	"path/filepath"
	"runtime"
	"strings"
	"syscall"

	"golang.org/x/text/unicode/norm"

	"github.com/rfjakob/eme"

	"github.com/rfjakob/gocryptfs/v2/internal/tlog"
)

const (
	// Like ext4, we allow at most 255 bytes for a file name.
	NameMax = 255
)

// NameTransform is used to transform filenames.
type NameTransform struct {
	emeCipher *eme.EMECipher
	// Names longer than `longNameMax` are hashed. Set to MaxInt when
	// longnames are disabled.
	longNameMax int
	// B64 = either base64.URLEncoding or base64.RawURLEncoding, depending
	// on the Raw64 feature flag
	B64 *base64.Encoding
	// 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.
//
// If `longNames` is set, names longer than `longNameMax` are hashed to
// `gocryptfs.longname.[sha256]`.
// Pass `longNameMax = 0` to use the default value (255).
func New(e *eme.EMECipher, longNames bool, longNameMax uint8, raw64 bool, badname []string, deterministicNames bool) *NameTransform {
	tlog.Debug.Printf("nametransform.New: longNameMax=%v, raw64=%v, badname=%q",
		longNameMax, raw64, badname)
	b64 := base64.URLEncoding
	if raw64 {
		b64 = base64.RawURLEncoding
	}
	b64 = b64.Strict() // Reject non-zero padding bits
	var effectiveLongNameMax int = math.MaxInt32
	if longNames {
		if longNameMax == 0 {
			effectiveLongNameMax = NameMax
		} else {
			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) (plainName string, err error) {
	plainName, err = n.decryptName(cipherName, iv)
	if err != nil && n.HaveBadnamePatterns() {
		plainName, err = n.decryptBadname(cipherName, iv)
	}
	if err != nil {
		return "", err
	}
	if err := IsValidName(plainName); err != nil {
		tlog.Warn.Printf("DecryptName %q: invalid name after decryption: %v", cipherName, err)
		return "", syscall.EBADMSG
	}
	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 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
	// > (CR and LF) are still ignored.
	// Check for CR and LF ourselves.
	if strings.ContainsAny(cipherName, "\r\n") {
		return "", errors.New("characters CR or LF in base64")
	}
	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)
	return plain, err
}

// EncryptName encrypts a file name "plainName" and returns a base64-encoded "cipherName64",
// encrypted using EME (https://github.com/rfjakob/eme).
//
// plainName is checked for null bytes, slashes etc. and such names are rejected
// with an error.
//
// 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
	}
	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
}

// encryptName encrypts "plainName" and returns a base64-encoded "cipherName64",
// encrypted using EME (https://github.com/rfjakob/eme).
//
// No checks for null bytes etc are performed against plainName.
func (n *NameTransform) encryptName(plainName string, iv []byte) (cipherName64 string) {
	bin := []byte(plainName)
	bin = pad16(bin)
	bin = n.emeCipher.Encrypt(iv, bin)
	cipherName64 = n.B64.EncodeToString(bin)
	return cipherName64
}

// 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 len(cName) > be.longNameMax {
		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)
}

// B64DecodeString decodes a Base64-encoded 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
}

// GetLongNameMax will return curent `longNameMax`. File name longer than
// this should be hashed.
func (n *NameTransform) GetLongNameMax() int {
	return n.longNameMax
}