Compare commits

...

19 Commits

Author SHA1 Message Date
Jakob Unterwurzacher eb42e54182 tlog: only enable color if both stderr and stdout are a terminal
This

    gocryptfs -init /does/not/exist 2> err.log

used to write escape codes into err.log. Stop doing that.
2021-12-11 15:37:13 +01:00
Jakob Unterwurzacher a1f01419e2 tlog: respect NO_COLOR
Fixes https://github.com/rfjakob/gocryptfs/issues/617
2021-12-11 15:35:01 +01:00
Jakob Unterwurzacher bd1ecf5379 darwin: use O_NOFOLLOW for xattr opens
Running the tests we have lots of these:

Openat: O_NOFOLLOW missing: flags = 0x4
-wpanic turns this warning into a panic: Openat: O_NOFOLLOW missing: flags = 0x4
panic: -wpanic turns this warning into a panic: Openat: O_NOFOLLOW missing: flags = 0x4

goroutine 114 [running]:
log.(*Logger).Panic(0x14000118280, {0x14000313ca8, 0x1, 0x1})
	log/log.go:224 +0x90
github.com/rfjakob/gocryptfs/v2/internal/tlog.(*toggledLogger).Printf(0x14000076780, {0x1009dc2e8, 0x27}, {0x14000313d18, 0x1, 0x1})
	github.com/rfjakob/gocryptfs/v2/internal/tlog/log.go:78 +0x168
github.com/rfjakob/gocryptfs/v2/internal/syscallcompat.Openat(0x9, {0x1009d0747, 0x1}, 0x4, 0x0)
	github.com/rfjakob/gocryptfs/v2/internal/syscallcompat/sys_common.go:59 +0xf0
github.com/rfjakob/gocryptfs/v2/internal/fusefrontend.(*Node).getXAttr(0x14000142000, {0x1400001c140, 0x3a})
	github.com/rfjakob/gocryptfs/v2/internal/fusefrontend/node_xattr_darwin.go:30 +0x8c
github.com/rfjakob/gocryptfs/v2/internal/fusefrontend.(*Node).Getxattr(0x14000142000, {0x100a7eba0, 0x1400000c2e8}, {0x14000016348, 0x14}, {0x14000326000, 0x20, 0x4000})
	github.com/rfjakob/gocryptfs/v2/internal/fusefrontend/node_xattr.go:65 +0x1ac
github.com/hanwen/go-fuse/v2/fs.(*rawBridge).GetXAttr(0x1400008e140, 0x140001901e0, 0x140001133c0, {0x14000016348, 0x14}, {0x14000326000, 0x20, 0x4000})
	github.com/hanwen/go-fuse/v2@v2.1.1-0.20210825171523-3ab5d95a30ae/fs/bridge.go:685 +0x114
github.com/hanwen/go-fuse/v2/fuse.doGetXAttr(0x14000144000, 0x14000113200)
	github.com/hanwen/go-fuse/v2@v2.1.1-0.20210825171523-3ab5d95a30ae/fuse/opcode.go:270 +0x224
github.com/hanwen/go-fuse/v2/fuse.(*Server).handleRequest(0x14000144000, 0x14000113200)
	github.com/hanwen/go-fuse/v2@v2.1.1-0.20210825171523-3ab5d95a30ae/fuse/server.go:499 +0x214
created by github.com/hanwen/go-fuse/v2/fuse.(*Server).loop
	github.com/hanwen/go-fuse/v2@v2.1.1-0.20210825171523-3ab5d95a30ae/fuse/server.go:470 +0xac

https://github.com/rfjakob/gocryptfs/issues/625
2021-12-09 17:55:05 +01:00
Jakob Unterwurzacher a48d6c3041 test.bash, crossbuild: catch MacOS test build failures
Regression test for https://github.com/rfjakob/gocryptfs/issues/623

Fixes https://github.com/rfjakob/gocryptfs/issues/623
2021-12-08 18:53:15 +01:00
Jakob Unterwurzacher ec186c13ce root_test, getdents-debug: restrict to linux
This does not work neither make sense on MacOS.
2021-12-08 18:50:42 +01:00
Jakob Unterwurzacher 7d60315cd5 tests: convert Creat() calls to Open()
Creat() is equivalent to Open(..., O_CREAT|O_WRONLY|O_TRUNC, ...)
and MacOS does not have syscall.Creat().

https://github.com/rfjakob/gocryptfs/issues/623
2021-12-08 18:49:21 +01:00
Jakob Unterwurzacher de22cb1e5d crossbuild.bash: use shell function instead of variable
This will allow easy expansion of build steps.
2021-12-08 12:45:23 +01:00
Jakob Unterwurzacher 39e736c099 MANPAGE: fix typo 2021-12-04 12:37:12 +01:00
Jakob Unterwurzacher 8722b894a6 MANPAGE: -extpass: document dash duplication bug
Closes https://github.com/rfjakob/gocryptfs/issues/621
2021-12-04 12:34:29 +01:00
Jakob Unterwurzacher d530fbd400 docs: names longer than 175 bytes (not 176) are stored in longnames
Quoting fusefrontend_reverse/node_helpers.go :

	// File names are padded to 16-byte multiples, encrypted and
	// base64-encoded. We can encode at most 176 bytes to stay below the 255
	// bytes limit:
	// * base64(176 bytes) = 235 bytes
	// * base64(192 bytes) = 256 bytes (over 255!)
	// But the PKCS#7 padding is at least one byte. This means we can only use
	// 175 bytes for the file name.

Noticed by @bailey27 at https://github.com/rfjakob/gocryptfs/issues/499#issuecomment-955790427
2021-11-01 14:44:32 +01:00
Jakob Unterwurzacher 87a6bb370a nametransform: fix math.MaxInt build failure on older Go
Failure is:

  # github.com/rfjakob/gocryptfs/v2/internal/nametransform
  internal/nametransform/names.go:47:33: undefined: math.MaxInt

math.MaxInt was only introduced in Go 1.17. Use MaxInt32 instead,
which is good enough, even on amd64. It only has to be larger than
any name we might encounter.
2021-10-21 16:44:05 +02:00
Jakob Unterwurzacher d14c9340d6 cli: add -longnamemax
Fixes https://github.com/rfjakob/gocryptfs/issues/499
2021-10-21 15:58:19 +02:00
Jakob Unterwurzacher d583bdb79e configfile: add LongNameMax support
Feature flag + numeric paramater

https://github.com/rfjakob/gocryptfs/issues/499
2021-10-21 14:55:30 +02:00
Jakob Unterwurzacher dc32710045 nametransform: add longNameMax parameter
Determines when to start hashing long names instead
of hardcoded 255. Will be used to alleviate "name too long"
issues some users see on cloud storage.

https://github.com/rfjakob/gocryptfs/issues/499
2021-10-21 14:55:30 +02:00
Jakob Unterwurzacher a652be805e configfile: replace broken switch/case logic with if
Because switch only matches once, we could have missed invalid
cases.

Replace the switch statements with a straight if rake.
2021-10-21 14:55:30 +02:00
Jakob Unterwurzacher 4ba0ced3c7 README: update changelog for v2.2.1 2021-10-20 15:16:31 +02:00
Jakob Unterwurzacher b0bddc5ed0 github actions: fix allow_other failure
Jobs currently fail like this:

/usr/bin/fusermount: option allow_other only allowed if 'user_allow_other' is set in /etc/fuse.conf
fs.Mount failed: fusermount exited with code 256
--- FAIL: TestForceOwner (0.05s)
    main_test.go:438: mount failed: exit status 19
FAIL
FAIL	github.com/rfjakob/gocryptfs/v2/tests/defaults	1.584s
2021-10-15 22:01:52 +02:00
Charles Duffy 8ec872e330 fusefrontend: honor ForceOwner for LOOKUP and CREATE operations 2021-10-15 17:35:12 +02:00
Jakob Unterwurzacher 3b881b0174 tests: add TestForceOwner
https://github.com/rfjakob/gocryptfs/issues/609
https://github.com/rfjakob/gocryptfs/pull/610
2021-10-15 17:35:12 +02:00
30 changed files with 416 additions and 61 deletions

View File

@ -36,6 +36,9 @@ jobs:
# CI platform specific setup steps happen here
- run: sudo apt-get install -qq fuse3 libssl-dev
# Fix "/usr/bin/fusermount: option allow_other only allowed if 'user_allow_other' is set in /etc/fuse.conf"
- run: echo user_allow_other | sudo tee -a /etc/fuse.conf
# Build & upload static binary
- run: ./build-without-openssl.bash
- uses: actions/upload-artifact@v2

View File

@ -123,6 +123,28 @@ and https://github.com/rfjakob/gocryptfs/issues/596 for background info.
Use HKDF to derive separate keys for content and name encryption from
the master key. Default true.
#### -longnamemax
integer value, allowed range 62...255
Hash file names that (in encrypted form) exceed this length. The default
is 255, which aligns with the usual name length limit on Linux and
provides best performance.
However, online storage may impose lower limits on file name and/or
path length. In this case, setting -longnamemax to a lower value
can be helpful.
The lower the value, the more extra `.name` files
must be created, which slows down directory listings.
Values below 62 are not allowed as then the hashed name
would be longer than the original name.
Example:
-longnamemax 100
#### -plaintextnames
Do not encrypt file names and symlink targets.
@ -296,9 +318,10 @@ the directories. Example:
gocryptfs -ko noexec /tmp/foo /tmp/bar
#### -longnames
Store names longer than 176 bytes in extra files (default true)
This flag is useful when recovering old gocryptfs filesystems using
"-masterkey". It is ignored (stays at the default) otherwise.
Store names that are longer than 175 bytes in extra files (default true).
This flag is only useful when recovering very old gocryptfs filesystems (gocryptfs v0.8 and earlier)
using "-masterkey". It is ignored (stays at the default) otherwise.
#### -nodev
See `-dev, -nodev`.
@ -452,6 +475,10 @@ to your program, use `"--"`, which is accepted by most programs:
Applies to: all actions that ask for a password.
BUG: In `-extpass -X`, the `-X` will be interpreted as `--X`. Please use
`-extpass=-X` to prevent that. See **Dash duplication** in the **BUGS** section
for details.
#### -fido2 DEVICE_PATH
Use a FIDO2 token to initialize and unlock the filesystem.
Use "fido2-token -L" to obtain the FIDO2 token device path.
@ -646,6 +673,13 @@ the `-nofail` option for details).
/tmp/cipher /tmp/plain fuse./usr/local/bin/gocryptfs nofail,allow_other,passfile=/tmp/password 0 0
ENVIRONMENT VARIABLES
=====================
### NO_COLOR
If `NO_COLOR` is set (regardless of value), colored output is disabled (see https://no-color.org/).
EXIT CODES
==========
@ -661,6 +695,29 @@ other: please check the error message
See also: https://github.com/rfjakob/gocryptfs/blob/master/internal/exitcodes/exitcodes.go
BUGS
====
### Dash duplication
gocryptfs v2.1 switched to the `pflag` library for command-line parsing
to support flags and positional arguments in any order. To stay compatible
with single-dash long options like `-extpass`, an ugly hack was added:
The command line is preprocessed, and all single-dash options are converted to
double-dash.
Unfortunately, this means that in
gocryptfs -extpass myapp -extpass -X
gocryptfs transforms the `-X` to `--X`, and it will call `myapp --X` as the extpass program.
Please use
gocryptfs -extpass myapp -extpass=-X
to work around this bug.
SEE ALSO
========
mount(2) fuse(8) fallocate(2) encfs(1) gitignore(5)

View File

@ -196,6 +196,20 @@ RM: 2,367
Changelog
---------
#### vNEXT
* Add **`-longnamemax`** flag to `-init` ([#499](https://github.com/rfjakob/gocryptfs/issues/499)).
Can be used to work around file or path length restrictions on online storage.
See the [man page](https://github.com/rfjakob/gocryptfs/blob/master/Documentation/MANPAGE.md#-longnamemax)
for details.
#### v2.2.1, 2021-10-20
* Fix `-force_owner` only taking effect after 2 seconds ([#609](https://github.com/rfjakob/gocryptfs/issues/609)).
This was a regression introduced in v2.0.
* MacOS: Fix build.bash failure with error `date: illegal option -- -` when `SOURCE_DATE_EPOCH` is set
([#570](https://github.com/rfjakob/gocryptfs/issues/570))
* `-init`: suggest xchacha on CPUs without AES acceleration ([commit](https://github.com/rfjakob/gocryptfs/commit/e8e35982845f36e714b915350eaf6855487aa0e8))
* `-info`: add contentEncryption to output
#### v2.2.0, 2021-09-25
* **`-deterministic-names`: new option for `-init`**, both for reverse and forward mode.
Disables file name randomisation & `gocryptfs.diriv` files

View File

@ -45,6 +45,8 @@ type argContainer struct {
notifypid, scryptn int
// Idle time before autounmount
idle time.Duration
// -longnamemax (hash encrypted names that are longer than this)
longnamemax uint8
// Helper variables that are NOT cli options all start with an underscore
// _configCustom is true when the user sets a custom config file name.
_configCustom bool
@ -112,6 +114,10 @@ func prefixOArgs(osArgs []string) ([]string, error) {
// into "--debug" (spf13/pflag style).
// gocryptfs v2.1 switched from `flag` to `pflag`, but we obviously want to stay
// cli-compatible, and this is the hack to do it.
//
// BUG: In `-extpass -X`, the `-X` gets transformed `--X`.
// See "Dash duplication" in the man page and
// https://github.com/rfjakob/gocryptfs/issues/621 .
func convertToDoubleDash(osArgs []string) (out []string) {
out = append(out, osArgs...)
for i, v := range out {
@ -161,7 +167,7 @@ func parseCliOpts(osArgs []string) (args argContainer) {
flagSet.BoolVar(&args.quiet, "quiet", false, "Quiet - silence informational messages")
flagSet.BoolVar(&args.nosyslog, "nosyslog", false, "Do not redirect output to syslog when running in the background")
flagSet.BoolVar(&args.wpanic, "wpanic", false, "When encountering a warning, panic and exit immediately")
flagSet.BoolVar(&args.longnames, "longnames", true, "Store names longer than 176 bytes in extra files")
flagSet.BoolVar(&args.longnames, "longnames", true, "Store names longer than 175 bytes in extra files")
flagSet.BoolVar(&args.allow_other, "allow_other", false, "Allow other users to access the filesystem. "+
"Only works if user_allow_other is set in /etc/fuse.conf.")
flagSet.BoolVar(&args.reverse, "reverse", false, "Reverse mode")
@ -215,6 +221,8 @@ func parseCliOpts(osArgs []string) (args argContainer) {
flagSet.StringSliceVar(&args.badname, "badname", nil, "Glob pattern invalid file names that should be shown")
flagSet.StringSliceVar(&args.passfile, "passfile", nil, "Read password from file")
flagSet.Uint8Var(&args.longnamemax, "longnamemax", 255, "Hash encrypted names that are longer than this")
flagSet.IntVar(&args.notifypid, "notifypid", 0, "Send USR1 to the specified process after "+
"successful mount - used internally for daemonization")
const scryptn = "scryptn"
@ -292,6 +300,10 @@ func parseCliOpts(osArgs []string) (args argContainer) {
os.Exit(exitcodes.Usage)
}
}
if args.longnamemax > 0 && args.longnamemax < 62 {
tlog.Fatal.Printf("-longnamemax: value %d is outside allowed range 62 ... 255", args.longnamemax)
os.Exit(exitcodes.Usage)
}
return args
}

View File

@ -116,11 +116,12 @@ func TestConvertToDoubleDash(t *testing.T) {
func TestParseCliOpts(t *testing.T) {
defaultArgs := argContainer{
longnames: true,
raw64: true,
hkdf: true,
openssl: stupidgcm.PreferOpenSSLAES256GCM(), // depends on CPU and build flags
scryptn: 16,
longnames: true,
longnamemax: 255,
raw64: true,
hkdf: true,
openssl: stupidgcm.PreferOpenSSLAES256GCM(), // depends on CPU and build flags
scryptn: 16,
}
type testcaseContainer struct {

View File

@ -1,3 +1,5 @@
//+build linux
/*
Small tool to try to debug unix.Getdents problems on CIFS mounts
( https://github.com/rfjakob/gocryptfs/issues/483 )

View File

@ -1,29 +1,37 @@
#!/bin/bash -eu
#!/bin/bash
#
# Build on all supported architectures & operating systems
cd "$(dirname "$0")"
function build {
# Discard resulting binary by writing to /dev/null
go build -tags without_openssl -o /dev/null
}
function compile_tests {
for i in $(go list ./...) ; do
go test -c -tags without_openssl -o /dev/null "$i" > /dev/null
done
}
set -eux
export GO111MODULE=on
# Discard resulting binary by writing to /dev/null
B="go build -tags without_openssl -o /dev/null"
set -x
export CGO_ENABLED=0
GOOS=linux GOARCH=amd64 $B
GOOS=linux GOARCH=amd64 build
# See https://github.com/golang/go/wiki/GoArm
GOOS=linux GOARCH=arm GOARM=7 $B
GOOS=linux GOARCH=arm64 $B
GOOS=linux GOARCH=arm GOARM=7 build
GOOS=linux GOARCH=arm64 build
# MacOS on Intel
GOOS=darwin GOARCH=amd64 $B
GOOS=darwin GOARCH=amd64 build
# Catch tests that don't work on MacOS (takes a long time so we only run it once)
time GOOS=darwin GOARCH=amd64 compile_tests
# MacOS on Apple Silicon M1.
# Go 1.16 added support for the M1 and added ios/arm64,
# so we use this to check if we should attempt a build.
if go tool dist list | grep ios/arm64 ; then
GOOS=darwin GOARCH=arm64 $B
GOOS=darwin GOARCH=arm64 build
fi

View File

@ -102,7 +102,9 @@ func initDir(args *argContainer) {
Fido2CredentialID: fido2CredentialID,
Fido2HmacSalt: fido2HmacSalt,
DeterministicNames: args.deterministic_names,
XChaCha20Poly1305: args.xchacha})
XChaCha20Poly1305: args.xchacha,
LongNameMax: args.longnamemax,
})
if err != nil {
tlog.Fatal.Println(err)
os.Exit(exitcodes.WriteConf)

View File

@ -55,6 +55,8 @@ type ConfFile struct {
FeatureFlags []string
// FIDO2 parameters
FIDO2 *FIDO2Params `json:",omitempty"`
// LongNameMax corresponds to the -longnamemax flag
LongNameMax uint8 `json:",omitempty"`
// Filename is the name of the config file. Not exported to JSON.
filename string
}
@ -71,6 +73,7 @@ type CreateArgs struct {
Fido2HmacSalt []byte
DeterministicNames bool
XChaCha20Poly1305 bool
LongNameMax uint8
}
// Create - create a new config with a random key encrypted with
@ -97,6 +100,12 @@ func Create(args *CreateArgs) error {
if !args.DeterministicNames {
cf.setFeatureFlag(FlagDirIV)
}
// 0 means to *use* the default (which means we don't have to save it), and
// 255 *is* the default, which means we don't have to save it either.
if args.LongNameMax != 0 && args.LongNameMax != 255 {
cf.LongNameMax = args.LongNameMax
cf.setFeatureFlag(FlagLongNameMax)
}
cf.setFeatureFlag(FlagEMENames)
cf.setFeatureFlag(FlagLongNames)
cf.setFeatureFlag(FlagRaw64)

View File

@ -131,6 +131,30 @@ func TestCreateConfFileAESSIV(t *testing.T) {
}
}
func TestCreateConfLongNameMax(t *testing.T) {
args := &CreateArgs{
Filename: "config_test/tmp.conf",
Password: testPw,
LogN: 10,
Creator: "test",
LongNameMax: 100,
}
err := Create(args)
if err != nil {
t.Fatal(err)
}
_, c, err := LoadAndDecrypt("config_test/tmp.conf", testPw)
if err != nil {
t.Fatal(err)
}
if !c.IsFeatureFlagSet(FlagLongNameMax) {
t.Error("FlagLongNameMax should be set but is not")
}
if c.LongNameMax != args.LongNameMax {
t.Errorf("wrong LongNameMax value: want=%d have=%d", args.LongNameMax, c.LongNameMax)
}
}
func TestIsFeatureFlagKnown(t *testing.T) {
// Test a few hardcoded values
testKnownFlags := []string{"DirIV", "PlaintextNames", "EMENames", "GCMIV128", "LongNames", "AESSIV"}

View File

@ -14,8 +14,11 @@ const (
// This flag is mandatory since gocryptfs v1.0,
// except when XChaCha20Poly1305 is used.
FlagGCMIV128
// FlagLongNames allows file names longer than 176 bytes.
// FlagLongNames allows file names longer than 175 bytes.
FlagLongNames
// FlagLongNameMax sets a custom name length limit, names longer than that
// will be hashed.
FlagLongNameMax
// FlagAESSIV selects an AES-SIV based crypto backend.
FlagAESSIV
// FlagRaw64 enables raw (unpadded) base64 encoding for file names
@ -40,6 +43,7 @@ var knownFlags = map[flagIota]string{
FlagEMENames: "EMENames",
FlagGCMIV128: "GCMIV128",
FlagLongNames: "LongNames",
FlagLongNameMax: "LongNameMax",
FlagAESSIV: "AESSIV",
FlagRaw64: "Raw64",
FlagHKDF: "HKDF",

View File

@ -23,22 +23,23 @@ func (cf *ConfFile) Validate() error {
}
// File content encryption
{
switch {
case cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) && cf.IsFeatureFlagSet(FlagAESSIV):
if cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) && cf.IsFeatureFlagSet(FlagAESSIV) {
return fmt.Errorf("Can't have both XChaCha20Poly1305 and AESSIV feature flags")
case cf.IsFeatureFlagSet(FlagAESSIV):
if !cf.IsFeatureFlagSet(FlagGCMIV128) {
return fmt.Errorf("AESSIV requires GCMIV128 feature flag")
}
case cf.IsFeatureFlagSet(FlagXChaCha20Poly1305):
}
if cf.IsFeatureFlagSet(FlagAESSIV) && !cf.IsFeatureFlagSet(FlagGCMIV128) {
return fmt.Errorf("AESSIV requires GCMIV128 feature flag")
}
if cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) {
if cf.IsFeatureFlagSet(FlagGCMIV128) {
return fmt.Errorf("XChaCha20Poly1305 conflicts with GCMIV128 feature flag")
}
if !cf.IsFeatureFlagSet(FlagHKDF) {
return fmt.Errorf("XChaCha20Poly1305 requires HKDF feature flag")
}
}
// The absence of other flags means AES-GCM (oldest algorithm)
case !cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) && !cf.IsFeatureFlagSet(FlagAESSIV):
if !cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) && !cf.IsFeatureFlagSet(FlagAESSIV) {
if !cf.IsFeatureFlagSet(FlagGCMIV128) {
return fmt.Errorf("AES-GCM requires GCMIV128 feature flag")
}
@ -46,10 +47,10 @@ func (cf *ConfFile) Validate() error {
}
// Filename encryption
{
switch {
case cf.IsFeatureFlagSet(FlagPlaintextNames) && cf.IsFeatureFlagSet(FlagEMENames):
return fmt.Errorf("Can't have both PlaintextNames and EMENames feature flags")
case cf.IsFeatureFlagSet(FlagPlaintextNames):
if cf.IsFeatureFlagSet(FlagPlaintextNames) {
if cf.IsFeatureFlagSet(FlagEMENames) {
return fmt.Errorf("PlaintextNames conflicts with EMENames feature flag")
}
if cf.IsFeatureFlagSet(FlagDirIV) {
return fmt.Errorf("PlaintextNames conflicts with DirIV feature flag")
}
@ -59,9 +60,19 @@ func (cf *ConfFile) Validate() error {
if cf.IsFeatureFlagSet(FlagRaw64) {
return fmt.Errorf("PlaintextNames conflicts with Raw64 feature flag")
}
case cf.IsFeatureFlagSet(FlagEMENames):
if cf.IsFeatureFlagSet(FlagLongNameMax) {
return fmt.Errorf("PlaintextNames conflicts with LongNameMax feature flag")
}
}
if cf.IsFeatureFlagSet(FlagEMENames) {
// All combinations of DirIV, LongNames, Raw64 allowed
}
if cf.LongNameMax != 0 && !cf.IsFeatureFlagSet(FlagLongNameMax) {
return fmt.Errorf("LongNameMax=%d but the LongNameMax feature flag is NOT set", cf.LongNameMax)
}
if cf.LongNameMax == 0 && cf.IsFeatureFlagSet(FlagLongNameMax) {
return fmt.Errorf("LongNameMax=0 but the LongNameMax feature flag IS set")
}
}
return nil
}

View File

@ -41,6 +41,10 @@ func (n *Node) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (ch
n.translateSize(dirfd, cName, &out.Attr)
rn := n.rootNode()
if rn.args.ForceOwner != nil {
out.Owner = *rn.args.ForceOwner
}
if rn.args.SharedStorage {
// If we already have a child node that matches what we found on disk*
// (as reflected in `ch`), return it here.

View File

@ -103,6 +103,12 @@ func (n *Node) Create(ctx context.Context, name string, flags uint32, mode uint3
if errno != 0 {
return
}
inode = n.newChild(ctx, st, out)
if rn.args.ForceOwner != nil {
out.Owner = *rn.args.ForceOwner
}
return inode, fh, fuseFlags, errno
}

View File

@ -27,7 +27,7 @@ func (n *Node) getXAttr(cAttr string) (out []byte, errno syscall.Errno) {
defer syscall.Close(dirfd)
// O_NONBLOCK to not block on FIFOs.
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
if err != nil {
return nil, fs.ToErrno(err)
}
@ -49,10 +49,10 @@ func (n *Node) setXAttr(context *fuse.Context, cAttr string, cData []byte, flags
defer syscall.Close(dirfd)
// O_NONBLOCK to not block on FIFOs.
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK, 0)
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
// Directories cannot be opened read-write. Retry.
if err == syscall.EISDIR {
fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK, 0)
fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
}
if err != nil {
fs.ToErrno(err)
@ -71,10 +71,10 @@ func (n *Node) removeXAttr(cAttr string) (errno syscall.Errno) {
defer syscall.Close(dirfd)
// O_NONBLOCK to not block on FIFOs.
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK, 0)
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_WRONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
// Directories cannot be opened read-write. Retry.
if err == syscall.EISDIR {
fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK, 0)
fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
}
if err != nil {
return fs.ToErrno(err)
@ -93,7 +93,7 @@ func (n *Node) listXAttr() (out []string, errno syscall.Errno) {
defer syscall.Close(dirfd)
// O_NONBLOCK to not block on FIFOs.
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK, 0)
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
if err != nil {
return nil, fs.ToErrno(err)
}

View File

@ -19,7 +19,7 @@ func newTestFS(args Args) *RootNode {
key := make([]byte, cryptocore.KeyLen)
cCore := cryptocore.New(key, cryptocore.BackendGoGCM, contentenc.DefaultIVBits, true)
cEnc := contentenc.New(cCore, contentenc.DefaultBS)
n := nametransform.New(cCore.EMECipher, true, true, nil, false)
n := nametransform.New(cCore.EMECipher, true, 0, true, nil, false)
rn := NewRootNode(args, cEnc, n)
oneSec := time.Second
options := &fs.Options{

View File

@ -48,7 +48,7 @@ func (be *NameTransform) EncryptAndHashBadName(name string, iv []byte, dirfd int
//expand suffix on error
continue
}
if be.longNames && len(cName) > NameMax {
if len(cName) > be.longNameMax {
cNamePart = be.HashLongName(cName)
}
cNameBadReverse := cNamePart + name[charpos:len(name)-len(BadnameSuffix)]

View File

@ -1,7 +1,11 @@
package nametransform
import (
"strings"
"testing"
"github.com/rfjakob/gocryptfs/v2/internal/contentenc"
"github.com/rfjakob/gocryptfs/v2/internal/cryptocore"
)
func TestIsLongName(t *testing.T) {
@ -28,3 +32,40 @@ func TestRemoveLongNameSuffix(t *testing.T) {
t.Error(".name suffix not removed")
}
}
func newLognamesTestInstance(longNameMax uint8) *NameTransform {
key := make([]byte, cryptocore.KeyLen)
cCore := cryptocore.New(key, cryptocore.BackendGoGCM, contentenc.DefaultIVBits, true)
return New(cCore.EMECipher, true, longNameMax, true, nil, false)
}
func TestLongNameMax(t *testing.T) {
iv := make([]byte, 16)
for max := 0; max <= NameMax; max++ {
n := newLognamesTestInstance(uint8(max))
if max == 0 {
// effective value is 255
max = NameMax
}
for l := 0; l <= NameMax+10; l++ {
name := strings.Repeat("x", l)
out, err := n.EncryptAndHashName(name, iv)
if l == 0 || l > NameMax {
if err == nil {
t.Errorf("should have rejected a name of length %d, but did not", l)
}
continue
}
cName, _ := n.EncryptName(name, iv)
rawLen := len(cName)
want := LongNameNone
if rawLen > max {
want = LongNameContent
}
have := NameType(out)
if have != want {
t.Errorf("l=%d max=%d: wanted %v, got %v\nname=%q\nout=%q", l, max, want, have, name, out)
}
}
}
}

View File

@ -4,6 +4,7 @@ package nametransform
import (
"crypto/aes"
"encoding/base64"
"math"
"path/filepath"
"syscall"
@ -20,7 +21,9 @@ const (
// NameTransform is used to transform filenames.
type NameTransform struct {
emeCipher *eme.EMECipher
longNames bool
// 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
@ -30,17 +33,28 @@ type NameTransform struct {
}
// New returns a new NameTransform instance.
func New(e *eme.EMECipher, longNames bool, raw64 bool, badname []string, deterministicNames bool) *NameTransform {
tlog.Debug.Printf("nametransform.New: longNames=%v, raw64=%v, badname=%q",
longNames, raw64, badname)
//
// 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
}
var effectiveLongNameMax int = math.MaxInt32
if longNames {
if longNameMax == 0 {
effectiveLongNameMax = NameMax
} else {
effectiveLongNameMax = int(longNameMax)
}
}
return &NameTransform{
emeCipher: e,
longNames: longNames,
longNameMax: effectiveLongNameMax,
B64: b64,
badnamePatterns: badname,
deterministicNames: deterministicNames,
@ -115,7 +129,7 @@ func (be *NameTransform) EncryptAndHashName(name string, iv []byte) (string, err
if err != nil {
return "", err
}
if be.longNames && len(cName) > NameMax {
if len(cName) > be.longNameMax {
return be.HashLongName(cName), nil
}
return cName, nil

View File

@ -14,7 +14,7 @@ import (
)
const (
// O_DIRECT means oncached I/O on Linux. No direct equivalent on MacOS and defined
// O_DIRECT means uncached I/O on Linux. No direct equivalent on MacOS and defined
// to zero there.
O_DIRECT = 0

View File

@ -106,7 +106,11 @@ var Warn *toggledLogger
var Fatal *toggledLogger
func init() {
if term.IsTerminal(int(os.Stdout.Fd())) {
// Enable color output if we are connected to a terminal and NO_COLOR is
// unset ( https://no-color.org/ ).
if _, nocolor := os.LookupEnv("NO_COLOR"); !nocolor &&
term.IsTerminal(int(os.Stdout.Fd())) &&
term.IsTerminal(int(os.Stderr.Fd())) {
ColorReset = "\033[0m"
ColorGrey = "\033[2m"
ColorRed = "\033[31m"

View File

@ -292,6 +292,8 @@ func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys f
// Settings from the config file override command line args
frontendArgs.PlaintextNames = confFile.IsFeatureFlagSet(configfile.FlagPlaintextNames)
frontendArgs.DeterministicNames = !confFile.IsFeatureFlagSet(configfile.FlagDirIV)
// Things that don't have to be in frontendArgs are only in args
args.longnamemax = confFile.LongNameMax
args.raw64 = confFile.IsFeatureFlagSet(configfile.FlagRaw64)
args.hkdf = confFile.IsFeatureFlagSet(configfile.FlagHKDF)
// Note: this will always return the non-openssl variant
@ -324,7 +326,7 @@ func initFuseFrontend(args *argContainer) (rootNode fs.InodeEmbedder, wipeKeys f
// Init crypto backend
cCore := cryptocore.New(masterkey, cryptoBackend, IVBits, args.hkdf)
cEnc := contentenc.New(cCore, contentenc.DefaultBS)
nameTransform := nametransform.New(cCore.EMECipher, frontendArgs.LongNames,
nameTransform := nametransform.New(cCore.EMECipher, frontendArgs.LongNames, args.longnamemax,
args.raw64, []string(args.badname), frontendArgs.DeterministicNames)
// After the crypto backend is initialized,
// we can purge the master key from memory.

View File

@ -93,4 +93,12 @@ if find . -type f -name \*.go -print0 | xargs -0 grep -E 'syscall.(Setegid|Seteu
exit 1
fi
if find . -type f -name \*.go -print0 | xargs -0 grep '\.Creat('; then
# MacOS does not have syscall.Creat(). Creat() is equivalent to Open(..., O_CREAT|O_WRONLY|O_TRUNC, ...),
# but usually you want O_EXCL instead of O_TRUNC because it is safer, so that's what we suggest
# instead.
echo "$MYNAME: Please use Open(..., O_CREAT|O_WRONLY|O_EXCL, ...) instead of Creat()! https://github.com/rfjakob/gocryptfs/issues/623"
exit 1
fi
) 200> "$LOCKFILE"

View File

@ -982,7 +982,7 @@ func TestMountCreat(t *testing.T) {
for i := 0; i < concurrency; i++ {
go func(i int) {
path := fmt.Sprintf("%s/%d", mnt, i)
fd, err := syscall.Creat(path, 0600)
fd, err := syscall.Open(path, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_TRUNC, 0600)
syscall.Close(fd)
if err != nil {
t.Errorf("Creat %q: %v", path, err)

View File

@ -0,0 +1,61 @@
package cli
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
"github.com/rfjakob/gocryptfs/v2/internal/configfile"
"github.com/rfjakob/gocryptfs/v2/tests/test_helpers"
)
// Create & test fs with -longnamemax=100
func TestLongnamemax100(t *testing.T) {
cDir := test_helpers.InitFS(nil, "-longnamemax", "100")
pDir := cDir + ".mnt"
// Check config file sanity
_, c, err := configfile.LoadAndDecrypt(cDir+"/"+configfile.ConfDefaultName, testPw)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if !c.IsFeatureFlagSet(configfile.FlagLongNameMax) {
t.Error("FlagLongNameMax should be on")
}
if c.LongNameMax != 100 {
t.Errorf("LongNameMax=%d, want 100", c.LongNameMax)
}
// Check that it takes effect
test_helpers.MountOrExit(cDir, pDir, "-extpass", "echo test")
defer test_helpers.UnmountPanic(pDir)
for l := 1; l <= 255; l++ {
path := pDir + "/" + strings.Repeat("x", l)
if err := ioutil.WriteFile(path, nil, 0600); err != nil {
t.Fatal(err)
}
matches, err := filepath.Glob(cDir + "/gocryptfs.longname.*")
if err != nil {
t.Fatal(err)
}
err = syscall.Unlink(path)
if err != nil {
t.Fatal(err)
}
// As determined experimentally, a name of length >= 64 causes a longname
// to be created.
if l <= 63 && len(matches) != 0 {
t.Errorf("l=%d: should not see a longname yet", l)
}
if l >= 64 && len(matches) != 2 {
t.Errorf("l=%d: should see a longname now", l)
}
}
}

View File

@ -401,7 +401,7 @@ func TestMaxlen(t *testing.T) {
func TestFsync(t *testing.T) {
fileName := test_helpers.DefaultPlainDir + "/" + t.Name() + ".file"
fileFD, err := syscall.Creat(fileName, 0600)
fileFD, err := syscall.Open(fileName, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_TRUNC, 0600)
if err != nil {
t.Fatal(err)
}
@ -425,3 +425,69 @@ func TestFsync(t *testing.T) {
t.Fatal(err)
}
}
// force_owner was broken by the v2.0 rewrite:
// The owner was only forced for GETATTR, but not for CREATE or LOOKUP.
//
// https://github.com/rfjakob/gocryptfs/issues/609
// https://github.com/rfjakob/gocryptfs/pull/610
func TestForceOwner(t *testing.T) {
cDir := test_helpers.InitFS(t)
os.Chmod(cDir, 0777) // Mount needs to be accessible for us
pDir := cDir + ".mnt"
test_helpers.MountOrFatal(t, cDir, pDir, "-force_owner=1234:1234", "-extpass=echo test")
defer test_helpers.UnmountPanic(pDir)
// We need an unrestricted umask
oldmask := syscall.Umask(0)
defer syscall.Umask(oldmask)
foo := pDir + "/foo"
// In the answer to a FUSE CREATE, gocryptfs sends file information including
// the owner. This is cached by the kernel and will be used for the next
// stat() call.
fd, err := syscall.Open(foo, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_EXCL, 0666)
if err != nil {
t.Fatal(err)
}
syscall.Close(fd)
var st syscall.Stat_t
if err := syscall.Stat(foo, &st); err != nil {
t.Fatal(err)
}
if st.Uid != 1234 || st.Gid != 1234 {
t.Errorf("CREATE returned uid or gid != 1234: %#v", st)
}
// We can clear the kernel stat() cache by writing to the file
fd, err = syscall.Open(foo, syscall.O_WRONLY, 0)
if err != nil {
t.Fatal(err)
}
if _, err := syscall.Write(fd, []byte("hello world")); err != nil {
t.Fatal(err)
}
syscall.Close(fd)
// This stat() triggers a new GETATTR
if err := syscall.Stat(foo, &st); err != nil {
t.Fatal(err)
}
if st.Uid != 1234 || st.Gid != 1234 {
t.Errorf("GETATTR returned uid or gid != 1234: %#v", st)
}
// Remount to clear cache
test_helpers.UnmountPanic(pDir)
test_helpers.MountOrFatal(t, cDir, pDir, "-force_owner=1234:1234", "-extpass=echo test")
// This stat() triggers a new LOOKUP
if err := syscall.Stat(foo, &st); err != nil {
t.Fatal(err)
}
if st.Uid != 1234 || st.Gid != 1234 {
t.Errorf("LOOKUP returned uid or gid != 1234: %#v", st)
}
}

View File

@ -144,7 +144,7 @@ func TestInoReuse(t *testing.T) {
wg.Add(1)
go func() {
for i := 0; i < 1000; i++ {
fd, err := syscall.Creat(fn, 0600)
fd, err := syscall.Open(fn, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_TRUNC, 0600)
if err == syscall.EISDIR {
continue
}

View File

@ -116,7 +116,7 @@ func TestInoReuseEvil(t *testing.T) {
}
// create a new file that will likely get the same inode number
pPath2 := pPath + "2"
fd, err := syscall.Creat(pPath2, 0600)
fd, err := syscall.Open(pPath2, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_TRUNC, 0600)
if err != nil {
t.Fatal(err)
}

View File

@ -1,3 +1,5 @@
//+build linux
// Package root_test contains tests that need root
// permissions to run
package root_test
@ -120,7 +122,7 @@ func writeTillFull(t *testing.T, path string) (int, syscall.Errno) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
fd, err := syscall.Creat(path, 0600)
fd, err := syscall.Open(path, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_TRUNC, 0600)
if err != nil {
return 0, err.(syscall.Errno)
}

View File

@ -77,7 +77,7 @@ func TestDirUnlink(t *testing.T) {
if err := unix.Rmdir(tc.mnt2 + "/foo"); err != nil {
t.Fatal(err)
}
if fd, err := unix.Creat(tc.mnt2+"/foo", 0600); err != nil {
if fd, err := unix.Open(tc.mnt2+"/foo", unix.O_CREAT|unix.O_WRONLY|unix.O_TRUNC, 0600); err != nil {
t.Fatal(err)
} else {
unix.Close(fd)
@ -104,7 +104,7 @@ func TestStaleHardlinks(t *testing.T) {
defer tc.cleanup()
link0 := tc.mnt1 + "/link0"
if fd, err := unix.Creat(link0, 0600); err != nil {
if fd, err := unix.Open(link0, unix.O_CREAT|unix.O_WRONLY|unix.O_TRUNC, 0600); err != nil {
t.Fatal(err)
} else {
unix.Close(fd)