From a652be805e1562948aff4dc232bd1c516ff01d00 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Thu, 21 Oct 2021 14:47:29 +0200 Subject: [PATCH 01/43] 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. --- internal/configfile/validate.go | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/internal/configfile/validate.go b/internal/configfile/validate.go index 511f704..1611ab0 100644 --- a/internal/configfile/validate.go +++ b/internal/configfile/validate.go @@ -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): + if 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(FlagDirIV) { return fmt.Errorf("PlaintextNames conflicts with DirIV feature flag") } @@ -59,7 +60,8 @@ 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(FlagEMENames) { // All combinations of DirIV, LongNames, Raw64 allowed } } From dc32710045f6f46913ae336b6fb77bf90b6bdb85 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Thu, 21 Oct 2021 09:37:04 +0200 Subject: [PATCH 02/43] 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 --- internal/fusefrontend/xattr_unit_test.go | 2 +- internal/nametransform/badname.go | 2 +- internal/nametransform/longnames_test.go | 41 ++++++++++++++++++++++++ internal/nametransform/names.go | 28 ++++++++++++---- mount.go | 2 +- 5 files changed, 65 insertions(+), 10 deletions(-) diff --git a/internal/fusefrontend/xattr_unit_test.go b/internal/fusefrontend/xattr_unit_test.go index 5bffd5e..86c87a7 100644 --- a/internal/fusefrontend/xattr_unit_test.go +++ b/internal/fusefrontend/xattr_unit_test.go @@ -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{ diff --git a/internal/nametransform/badname.go b/internal/nametransform/badname.go index eed0061..6e77561 100644 --- a/internal/nametransform/badname.go +++ b/internal/nametransform/badname.go @@ -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)] diff --git a/internal/nametransform/longnames_test.go b/internal/nametransform/longnames_test.go index 4210492..7a4e915 100644 --- a/internal/nametransform/longnames_test.go +++ b/internal/nametransform/longnames_test.go @@ -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) + } + } + } +} diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index d766d2f..939d31e 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -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.MaxInt + 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 diff --git a/mount.go b/mount.go index dfabbc9..004c646 100644 --- a/mount.go +++ b/mount.go @@ -324,7 +324,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, 0, args.raw64, []string(args.badname), frontendArgs.DeterministicNames) // After the crypto backend is initialized, // we can purge the master key from memory. From d583bdb79e6f05bce2451a7e220e553209da4c1d Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Thu, 21 Oct 2021 09:58:37 +0200 Subject: [PATCH 03/43] configfile: add LongNameMax support Feature flag + numeric paramater https://github.com/rfjakob/gocryptfs/issues/499 --- internal/configfile/config_file.go | 9 +++++++++ internal/configfile/config_test.go | 24 ++++++++++++++++++++++++ internal/configfile/feature_flags.go | 4 ++++ 3 files changed, 37 insertions(+) diff --git a/internal/configfile/config_file.go b/internal/configfile/config_file.go index 828f034..2d11346 100644 --- a/internal/configfile/config_file.go +++ b/internal/configfile/config_file.go @@ -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) diff --git a/internal/configfile/config_test.go b/internal/configfile/config_test.go index b8012d3..3407464 100644 --- a/internal/configfile/config_test.go +++ b/internal/configfile/config_test.go @@ -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"} diff --git a/internal/configfile/feature_flags.go b/internal/configfile/feature_flags.go index e28abd6..e45b20c 100644 --- a/internal/configfile/feature_flags.go +++ b/internal/configfile/feature_flags.go @@ -16,6 +16,9 @@ const ( FlagGCMIV128 // FlagLongNames allows file names longer than 176 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", From d14c9340d6fb473e9837e91db8b6e869c37ad8e5 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Thu, 21 Oct 2021 15:58:19 +0200 Subject: [PATCH 04/43] cli: add -longnamemax Fixes https://github.com/rfjakob/gocryptfs/issues/499 --- Documentation/MANPAGE.md | 22 ++++++++++++ README.md | 6 ++++ cli_args.go | 8 +++++ cli_args_test.go | 11 +++--- init_dir.go | 4 ++- internal/configfile/validate.go | 15 ++++++-- mount.go | 4 ++- tests/cli/longnamemax_test.go | 61 +++++++++++++++++++++++++++++++++ 8 files changed, 121 insertions(+), 10 deletions(-) create mode 100644 tests/cli/longnamemax_test.go diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index 01e4b5a..b9c72dd 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -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. diff --git a/README.md b/README.md index 1a02857..35c2bda 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,12 @@ 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. diff --git a/cli_args.go b/cli_args.go index b415b21..e925345 100644 --- a/cli_args.go +++ b/cli_args.go @@ -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 @@ -215,6 +217,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 +296,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 } diff --git a/cli_args_test.go b/cli_args_test.go index 6c923f4..74255f2 100644 --- a/cli_args_test.go +++ b/cli_args_test.go @@ -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 { diff --git a/init_dir.go b/init_dir.go index ab4c3df..9658bab 100644 --- a/init_dir.go +++ b/init_dir.go @@ -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) diff --git a/internal/configfile/validate.go b/internal/configfile/validate.go index 1611ab0..ab8917d 100644 --- a/internal/configfile/validate.go +++ b/internal/configfile/validate.go @@ -47,10 +47,10 @@ func (cf *ConfFile) Validate() error { } // Filename encryption { - if cf.IsFeatureFlagSet(FlagPlaintextNames) && cf.IsFeatureFlagSet(FlagEMENames) { - return fmt.Errorf("Can't have both PlaintextNames and EMENames feature flags") - } 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") } @@ -60,10 +60,19 @@ func (cf *ConfFile) Validate() error { if cf.IsFeatureFlagSet(FlagRaw64) { return fmt.Errorf("PlaintextNames conflicts with Raw64 feature flag") } + 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 } diff --git a/mount.go b/mount.go index 004c646..434edad 100644 --- a/mount.go +++ b/mount.go @@ -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, 0, + 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. diff --git a/tests/cli/longnamemax_test.go b/tests/cli/longnamemax_test.go new file mode 100644 index 0000000..fc429f6 --- /dev/null +++ b/tests/cli/longnamemax_test.go @@ -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) + } + } +} From 87a6bb370acc3690e89a8b0d5109fcb0dab0a374 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Thu, 21 Oct 2021 16:42:00 +0200 Subject: [PATCH 05/43] 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. --- internal/nametransform/names.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 939d31e..e07ccfb 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -44,7 +44,7 @@ func New(e *eme.EMECipher, longNames bool, longNameMax uint8, raw64 bool, badnam if raw64 { b64 = base64.RawURLEncoding } - var effectiveLongNameMax int = math.MaxInt + var effectiveLongNameMax int = math.MaxInt32 if longNames { if longNameMax == 0 { effectiveLongNameMax = NameMax From d530fbd400c88fa54c856d958963d77669ac9cf2 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Mon, 1 Nov 2021 14:44:32 +0100 Subject: [PATCH 06/43] 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 --- Documentation/MANPAGE.md | 7 ++++--- cli_args.go | 2 +- internal/configfile/feature_flags.go | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index b9c72dd..ffed44e 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -318,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`. diff --git a/cli_args.go b/cli_args.go index e925345..6b97744 100644 --- a/cli_args.go +++ b/cli_args.go @@ -163,7 +163,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") diff --git a/internal/configfile/feature_flags.go b/internal/configfile/feature_flags.go index e45b20c..d6627a5 100644 --- a/internal/configfile/feature_flags.go +++ b/internal/configfile/feature_flags.go @@ -14,7 +14,7 @@ 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. From 8722b894a6fb86945b96e5c3b2cc778853e21d17 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 4 Dec 2021 12:34:29 +0100 Subject: [PATCH 07/43] MANPAGE: -extpass: document dash duplication bug Closes https://github.com/rfjakob/gocryptfs/issues/621 --- Documentation/MANPAGE.md | 27 +++++++++++++++++++++++++++ cli_args.go | 4 ++++ 2 files changed, 31 insertions(+) diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index ffed44e..fa2f5b5 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -475,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. @@ -684,6 +688,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 --n` 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) diff --git a/cli_args.go b/cli_args.go index 6b97744..781dc5c 100644 --- a/cli_args.go +++ b/cli_args.go @@ -114,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 { From 39e736c099876710c1951b313bc09aa686a69e29 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 4 Dec 2021 12:37:12 +0100 Subject: [PATCH 08/43] MANPAGE: fix typo --- Documentation/MANPAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index fa2f5b5..503d145 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -703,7 +703,7 @@ Unfortunately, this means that in gocryptfs -extpass myapp -extpass -X -gocryptfs transforms the `-X` to `--X`, and it will call `myapp --n` as the extpass program. +gocryptfs transforms the `-X` to `--X`, and it will call `myapp --X` as the extpass program. Please use From de22cb1e5dfba8f8b97a5da671bfb2b1a845afb7 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Wed, 8 Dec 2021 12:45:23 +0100 Subject: [PATCH 09/43] crossbuild.bash: use shell function instead of variable This will allow easy expansion of build steps. --- crossbuild.bash | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/crossbuild.bash b/crossbuild.bash index 43bfd32..25eeb91 100755 --- a/crossbuild.bash +++ b/crossbuild.bash @@ -1,29 +1,31 @@ -#!/bin/bash -eu +#!/bin/bash # # Build on all supported architectures & operating systems +function build { + # Discard resulting binary by writing to /dev/null + go build -tags without_openssl -o /dev/null +} + +set -eux + cd "$(dirname "$0")" 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 # 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 From 7d60315cd53080e223051a4f16eb3ace3b86e095 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Wed, 8 Dec 2021 18:49:21 +0100 Subject: [PATCH 10/43] 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 --- tests/cli/cli_test.go | 2 +- tests/defaults/main_test.go | 4 ++-- tests/matrix/concurrency_test.go | 2 +- tests/plaintextnames/plaintextnames_test.go | 2 +- tests/root_test/root_test.go | 2 +- tests/sharedstorage/sharedstorage_test.go | 4 ++-- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/cli/cli_test.go b/tests/cli/cli_test.go index d63625b..c433e92 100644 --- a/tests/cli/cli_test.go +++ b/tests/cli/cli_test.go @@ -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) diff --git a/tests/defaults/main_test.go b/tests/defaults/main_test.go index 5310003..43019ba 100644 --- a/tests/defaults/main_test.go +++ b/tests/defaults/main_test.go @@ -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) } @@ -447,7 +447,7 @@ func TestForceOwner(t *testing.T) { // 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.Creat(foo, 0666) + fd, err := syscall.Open(foo, syscall.O_CREAT|syscall.O_WRONLY|syscall.O_EXCL, 0666) if err != nil { t.Fatal(err) } diff --git a/tests/matrix/concurrency_test.go b/tests/matrix/concurrency_test.go index d133c45..d824316 100644 --- a/tests/matrix/concurrency_test.go +++ b/tests/matrix/concurrency_test.go @@ -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 } diff --git a/tests/plaintextnames/plaintextnames_test.go b/tests/plaintextnames/plaintextnames_test.go index c6d6145..f2dc7e7 100644 --- a/tests/plaintextnames/plaintextnames_test.go +++ b/tests/plaintextnames/plaintextnames_test.go @@ -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) } diff --git a/tests/root_test/root_test.go b/tests/root_test/root_test.go index 3794e69..f0f5a1b 100644 --- a/tests/root_test/root_test.go +++ b/tests/root_test/root_test.go @@ -120,7 +120,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) } diff --git a/tests/sharedstorage/sharedstorage_test.go b/tests/sharedstorage/sharedstorage_test.go index a08deed..88f3304 100644 --- a/tests/sharedstorage/sharedstorage_test.go +++ b/tests/sharedstorage/sharedstorage_test.go @@ -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) From ec186c13cebeea48112aa9a863c278ffc23db197 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Wed, 8 Dec 2021 18:50:42 +0100 Subject: [PATCH 11/43] root_test, getdents-debug: restrict to linux This does not work neither make sense on MacOS. --- contrib/getdents-debug/getdents/getdents.go | 2 ++ tests/root_test/root_test.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/contrib/getdents-debug/getdents/getdents.go b/contrib/getdents-debug/getdents/getdents.go index a3cdac4..d10ca12 100644 --- a/contrib/getdents-debug/getdents/getdents.go +++ b/contrib/getdents-debug/getdents/getdents.go @@ -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 ) diff --git a/tests/root_test/root_test.go b/tests/root_test/root_test.go index f0f5a1b..fe38cfa 100644 --- a/tests/root_test/root_test.go +++ b/tests/root_test/root_test.go @@ -1,3 +1,5 @@ +//+build linux + // Package root_test contains tests that need root // permissions to run package root_test From a48d6c30410753eb16ae94e6bfc54ca7abb7d128 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Wed, 8 Dec 2021 18:53:15 +0100 Subject: [PATCH 12/43] 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 --- crossbuild.bash | 10 ++++++++-- test.bash | 8 ++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/crossbuild.bash b/crossbuild.bash index 25eeb91..a9fc6ae 100755 --- a/crossbuild.bash +++ b/crossbuild.bash @@ -7,9 +7,13 @@ function build { go build -tags without_openssl -o /dev/null } -set -eux +function compile_tests { + for i in $(go list ./...) ; do + go test -c -tags without_openssl -o /dev/null "$i" > /dev/null + done +} -cd "$(dirname "$0")" +set -eux export GO111MODULE=on export CGO_ENABLED=0 @@ -22,6 +26,8 @@ GOOS=linux GOARCH=arm64 build # MacOS on Intel 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, diff --git a/test.bash b/test.bash index efc6456..6289072 100755 --- a/test.bash +++ b/test.bash @@ -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" From bd1ecf537983ee5207e78f9bcd6a6ae50c1b4c62 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Thu, 9 Dec 2021 17:55:05 +0100 Subject: [PATCH 13/43] 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 --- internal/fusefrontend/node_xattr_darwin.go | 12 ++++++------ internal/syscallcompat/sys_darwin.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/fusefrontend/node_xattr_darwin.go b/internal/fusefrontend/node_xattr_darwin.go index 8c8a153..a539847 100644 --- a/internal/fusefrontend/node_xattr_darwin.go +++ b/internal/fusefrontend/node_xattr_darwin.go @@ -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) } diff --git a/internal/syscallcompat/sys_darwin.go b/internal/syscallcompat/sys_darwin.go index 075563f..de795a4 100644 --- a/internal/syscallcompat/sys_darwin.go +++ b/internal/syscallcompat/sys_darwin.go @@ -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 From a1f01419e2e6e5d637ec7afa31c0a1d95e0ea923 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 11 Dec 2021 15:35:01 +0100 Subject: [PATCH 14/43] tlog: respect NO_COLOR Fixes https://github.com/rfjakob/gocryptfs/issues/617 --- Documentation/MANPAGE.md | 7 +++++++ internal/tlog/log.go | 4 +++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index 503d145..25434c9 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -673,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 ========== diff --git a/internal/tlog/log.go b/internal/tlog/log.go index 5535ef3..3f27539 100644 --- a/internal/tlog/log.go +++ b/internal/tlog/log.go @@ -106,7 +106,9 @@ 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())) { ColorReset = "\033[0m" ColorGrey = "\033[2m" ColorRed = "\033[31m" From eb42e541828336e9b19e1bc5e087a419835b0c85 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 11 Dec 2021 15:37:13 +0100 Subject: [PATCH 15/43] 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. --- internal/tlog/log.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/tlog/log.go b/internal/tlog/log.go index 3f27539..62d791d 100644 --- a/internal/tlog/log.go +++ b/internal/tlog/log.go @@ -108,7 +108,9 @@ var Fatal *toggledLogger func init() { // 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())) { + 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" From 64be5de75f42e415198ff5e77de509680b69e0e1 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 19 Dec 2021 14:43:56 +0100 Subject: [PATCH 16/43] fusefrontend: allow slashes in xattr names xattr names have fewer restrictions than file names, relax the validation. Fixes https://github.com/rfjakob/gocryptfs/issues/627 --- internal/fusefrontend/node_xattr.go | 4 --- internal/fusefrontend/root_node.go | 4 +-- internal/nametransform/names.go | 34 +++++++++++++------ internal/nametransform/names_test.go | 23 +++++++++++++ internal/nametransform/xattr.go | 47 +++++++++++++++++++++++++++ tests/xattr/xattr_integration_test.go | 14 ++++++++ 6 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 internal/nametransform/xattr.go diff --git a/internal/fusefrontend/node_xattr.go b/internal/fusefrontend/node_xattr.go index 09ee5ef..44bc502 100644 --- a/internal/fusefrontend/node_xattr.go +++ b/internal/fusefrontend/node_xattr.go @@ -15,10 +15,6 @@ import ( // -1 as uint32 const minus1 = ^uint32(0) -// xattr names are encrypted like file names, but with a fixed IV. -// Padded with "_xx" for length 16. -var xattrNameIV = []byte("xattr_name_iv_xx") - // We store encrypted xattrs under this prefix plus the base64-encoded // encrypted original name. var xattrStorePrefix = "user.gocryptfs." diff --git a/internal/fusefrontend/root_node.go b/internal/fusefrontend/root_node.go index 7221be6..39cdef7 100644 --- a/internal/fusefrontend/root_node.go +++ b/internal/fusefrontend/root_node.go @@ -268,7 +268,7 @@ func (rn *RootNode) decryptXattrValue(cData []byte) (data []byte, err error) { // encryptXattrName transforms "user.foo" to "user.gocryptfs.a5sAd4XAa47f5as6dAf" func (rn *RootNode) encryptXattrName(attr string) (string, error) { // xattr names are encrypted like file names, but with a fixed IV. - cAttr, err := rn.nameTransform.EncryptName(attr, xattrNameIV) + cAttr, err := rn.nameTransform.EncryptXattrName(attr) if err != nil { return "", err } @@ -282,7 +282,7 @@ func (rn *RootNode) decryptXattrName(cAttr string) (attr string, err error) { } // Strip "user.gocryptfs." prefix cAttr = cAttr[len(xattrStorePrefix):] - attr, err = rn.nameTransform.DecryptName(cAttr, xattrNameIV) + attr, err = rn.nameTransform.DecryptXattrName(cAttr) if err != nil { return "", err } diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index e07ccfb..ebe0fb6 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -66,7 +66,14 @@ func New(e *eme.EMECipher, longNames bool, longNameMax uint8, raw64 bool, badnam func (n *NameTransform) DecryptName(cipherName string, iv []byte) (string, error) { res, err := n.decryptName(cipherName, iv) if err != nil && n.HaveBadnamePatterns() { - return n.decryptBadname(cipherName, iv) + res, err = n.decryptBadname(cipherName, iv) + } + if err != nil { + return "", err + } + if err := IsValidName(res); err != nil { + tlog.Warn.Printf("DecryptName %q: invalid name after decryption: %v", cipherName, err) + return "", syscall.EBADMSG } return res, err } @@ -79,30 +86,29 @@ func (n *NameTransform) decryptName(cipherName string, iv []byte) (string, error return "", err } if len(bin) == 0 { - tlog.Warn.Printf("DecryptName: empty input") + 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)) + 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) + tlog.Warn.Printf("decryptName %q: unPad16 error: %v", cipherName, err) return "", syscall.EBADMSG } plain := string(bin) - if err := IsValidName(plain); err != nil { - tlog.Warn.Printf("DecryptName %q: invalid name after decryption: %v", cipherName, err) - return "", syscall.EBADMSG - } return plain, err } -// EncryptName encrypts "plainName", returns a base64-encoded "cipherName64", +// 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) { @@ -110,11 +116,19 @@ func (n *NameTransform) EncryptName(plainName string, iv []byte) (cipherName64 s tlog.Warn.Printf("EncryptName %q: invalid plainName: %v", plainName, err) return "", syscall.EBADMSG } + 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, nil + return cipherName64 } // EncryptAndHashName encrypts "name" and hashes it to a longname if it is diff --git a/internal/nametransform/names_test.go b/internal/nametransform/names_test.go index b4e98d4..3c26c43 100644 --- a/internal/nametransform/names_test.go +++ b/internal/nametransform/names_test.go @@ -75,3 +75,26 @@ func TestIsValidName(t *testing.T) { } } } + +func TestIsValidXattrName(t *testing.T) { + cases := []struct { + in string + want bool + }{ + {"", false}, + {".", true}, + {"..", true}, + {"...", true}, + {"asdasd/asdasd", true}, + {"asdasd\000asdasd", false}, + {"hello", true}, + {strings.Repeat("x", 255), true}, + {strings.Repeat("x", 256), true}, + } + for _, c := range cases { + have := isValidXattrName(c.in) + if (have == nil) != c.want { + t.Errorf("isValidXattrName(%q): want %v have %v", c.in, c.want, have) + } + } +} diff --git a/internal/nametransform/xattr.go b/internal/nametransform/xattr.go new file mode 100644 index 0000000..0aa0fd8 --- /dev/null +++ b/internal/nametransform/xattr.go @@ -0,0 +1,47 @@ +package nametransform + +import ( + "fmt" + "strings" + "syscall" + + "github.com/rfjakob/gocryptfs/v2/internal/tlog" +) + +// xattr names are encrypted like file names, but with a fixed IV. +// Padded with "_xx" for length 16. +var xattrNameIV = []byte("xattr_name_iv_xx") + +func isValidXattrName(name string) error { + if name == "" { + return fmt.Errorf("empty input") + } + if strings.Contains(name, "\000") { + return fmt.Errorf("contains forbidden null byte") + } + return nil +} + +// EncryptXattrName encrypts an extended attribute (xattr) name. +// xattr names are encrypted like file names, but with a fixed IV, and fewer +// naming restriction. +func (n *NameTransform) EncryptXattrName(plainName string) (cipherName64 string, err error) { + if err := isValidXattrName(plainName); err != nil { + tlog.Warn.Printf("EncryptXattrName %q: invalid plainName: %v", plainName, err) + return "", syscall.EBADMSG + } + return n.encryptName(plainName, xattrNameIV), nil +} + +// 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) DecryptXattrName(cipherName string) (plainName string, err error) { + if plainName, err = n.decryptName(cipherName, xattrNameIV); err != nil { + return "", err + } + if err := isValidXattrName(plainName); err != nil { + tlog.Warn.Printf("DecryptXattrName %q: invalid name after decryption: %v", cipherName, err) + return "", syscall.EBADMSG + } + return plainName, err +} diff --git a/tests/xattr/xattr_integration_test.go b/tests/xattr/xattr_integration_test.go index efe903b..c968a47 100644 --- a/tests/xattr/xattr_integration_test.go +++ b/tests/xattr/xattr_integration_test.go @@ -369,3 +369,17 @@ func TestAcl(t *testing.T) { t.Error(err) } } + +// TestSlashInName checks that slashes in xattr names are allowed +// https://github.com/rfjakob/gocryptfs/issues/627 +func TestSlashInName(t *testing.T) { + fn := test_helpers.DefaultPlainDir + "/" + t.Name() + err := ioutil.WriteFile(fn, []byte("12345"), 0700) + if err != nil { + t.Fatalf("creating empty file failed: %v", err) + } + err = setGetRmList3(fn, "user.foo@https://bar", []byte("val")) + if err != nil { + t.Error(err) + } +} From 8d8b76dcac1340c3aa86d9883f99bfffb3eccd60 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 19 Dec 2021 14:46:43 +0100 Subject: [PATCH 17/43] go.mod: upgrade go-fuse We want https://github.com/hanwen/go-fuse/commit/934a183ed91446d218b5471c4df9f93db039f6e "fuse: prefer fusermount3 over fusermount; add debug output" Fixes https://github.com/rfjakob/gocryptfs/issues/626 --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 29f7c2c..4b1aff3 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/rfjakob/gocryptfs/v2 go 1.16 require ( - github.com/hanwen/go-fuse/v2 v2.1.1-0.20210825171523-3ab5d95a30ae + github.com/hanwen/go-fuse/v2 v2.1.1-0.20211219085202-934a183ed914 github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115 github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd // indirect github.com/jacobsa/oglemock v0.0.0-20150831005832-e94d794d06ff // indirect diff --git a/go.sum b/go.sum index be0be5f..a186360 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/hanwen/go-fuse/v2 v2.1.1-0.20210825171523-3ab5d95a30ae h1:4CB6T4YTUVvnro5ba8ju1QCbOuyGAeF3vvKlo50EJ4k= github.com/hanwen/go-fuse/v2 v2.1.1-0.20210825171523-3ab5d95a30ae/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc= +github.com/hanwen/go-fuse/v2 v2.1.1-0.20211219085202-934a183ed914 h1:hGXMxS1wTE4y+f7iBqFArrJ6X8QozHnEdnVzGZI9Ywc= +github.com/hanwen/go-fuse/v2 v2.1.1-0.20211219085202-934a183ed914/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc= github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115 h1:YuDUUFNM21CAbyPOpOP8BicaTD/0klJEKt5p8yuw+uY= github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115/go.mod h1:LadVJg0XuawGk+8L1rYnIED8451UyNxEMdTWCEt5kmU= github.com/jacobsa/oglematchers v0.0.0-20150720000706-141901ea67cd h1:9GCSedGjMcLZCrusBZuo4tyKLpKUPenUUqi34AkuFmA= From 5749e70c7cc4365d6de6a4d5afb73d71d0c10003 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 19 Dec 2021 14:50:52 +0100 Subject: [PATCH 18/43] nametransform: fix oversight in comment --- internal/nametransform/xattr.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/nametransform/xattr.go b/internal/nametransform/xattr.go index 0aa0fd8..721c4e4 100644 --- a/internal/nametransform/xattr.go +++ b/internal/nametransform/xattr.go @@ -33,7 +33,7 @@ func (n *NameTransform) EncryptXattrName(plainName string) (cipherName64 string, return n.encryptName(plainName, xattrNameIV), nil } -// DecryptName calls decryptName to try and decrypt a base64-encoded encrypted +// DecryptXattrName calls decryptName to try and decrypt a base64-encoded encrypted // filename "cipherName", and failing that checks if it can be bypassed func (n *NameTransform) DecryptXattrName(cipherName string) (plainName string, err error) { if plainName, err = n.decryptName(cipherName, xattrNameIV); err != nil { From 1eaf1211a259a38cdf3e7dad2e00e140409bef9a Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Mon, 3 Jan 2022 14:27:09 +0100 Subject: [PATCH 19/43] tests/cli: Check for leftover socket file This fails at the moment: $ go test ./tests/cli/ --- FAIL: TestMountPasswordEmpty (0.01s) cli_test.go:430: socket file "/tmp/gocryptfs-test-parent-1026/3413782690/TestMountPasswordEmpty.753166857.sock" left behind https://github.com/rfjakob/gocryptfs/issues/634 --- tests/cli/cli_test.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/cli/cli_test.go b/tests/cli/cli_test.go index c433e92..915759d 100644 --- a/tests/cli/cli_test.go +++ b/tests/cli/cli_test.go @@ -398,15 +398,37 @@ func TestShadows(t *testing.T) { } // TestMountPasswordIncorrect makes sure the correct exit code is used when the password -// was incorrect while mounting +// was incorrect while mounting. +// Also checks that we don't leave a socket file behind. func TestMountPasswordIncorrect(t *testing.T) { cDir := test_helpers.InitFS(t) // Create filesystem with password "test" + ctlSock := cDir + ".sock" pDir := cDir + ".mnt" - err := test_helpers.Mount(cDir, pDir, false, "-extpass", "echo WRONG", "-wpanic=false") + err := test_helpers.Mount(cDir, pDir, false, "-extpass", "echo WRONG", "-wpanic=false", "-ctlsock", ctlSock) exitCode := test_helpers.ExtractCmdExitCode(err) if exitCode != exitcodes.PasswordIncorrect { t.Errorf("want=%d, got=%d", exitcodes.PasswordIncorrect, exitCode) } + if _, err := os.Stat(ctlSock); err == nil { + t.Errorf("socket file %q left behind", ctlSock) + } +} + +// TestMountPasswordEmpty makes sure the correct exit code is used when the password +// was empty while mounting. +// Also checks that we don't leave a socket file behind (https://github.com/rfjakob/gocryptfs/issues/634). +func TestMountPasswordEmpty(t *testing.T) { + cDir := test_helpers.InitFS(t) // Create filesystem with password "test" + ctlSock := cDir + ".sock" + pDir := cDir + ".mnt" + err := test_helpers.Mount(cDir, pDir, false, "-extpass", "true", "-wpanic=false", "-ctlsock", ctlSock) + exitCode := test_helpers.ExtractCmdExitCode(err) + if exitCode != exitcodes.ReadPassword { + t.Errorf("want=%d, got=%d", exitcodes.ReadPassword, exitCode) + } + if _, err := os.Stat(ctlSock); err == nil { + t.Errorf("socket file %q left behind", ctlSock) + } } // TestPasswdPasswordIncorrect makes sure the correct exit code is used when the password From 4b251f3ce1f0a0472ed10a00aeef70c69ba03a5d Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Mon, 3 Jan 2022 15:18:59 +0100 Subject: [PATCH 20/43] readpassword: bubble up errors instead of exiting the process This allows cleanups to happen in the caller, like removing the control socket. Fixes https://github.com/rfjakob/gocryptfs/issues/634 --- gocryptfs-xray/xray_main.go | 6 +- init_dir.go | 6 +- internal/readpassword/extpass_test.go | 73 +++++++++++++----------- internal/readpassword/passfile.go | 28 +++++----- internal/readpassword/passfile_test.go | 76 +++++++++---------------- internal/readpassword/read.go | 77 +++++++++++++------------- internal/readpassword/stdin_test.go | 33 ++++++++--- main.go | 14 ++++- masterkey.go | 8 ++- 9 files changed, 171 insertions(+), 150 deletions(-) diff --git a/gocryptfs-xray/xray_main.go b/gocryptfs-xray/xray_main.go index 35f409f..534a400 100644 --- a/gocryptfs-xray/xray_main.go +++ b/gocryptfs-xray/xray_main.go @@ -148,7 +148,11 @@ func dumpMasterKey(fn string, fido2Path string) { } pw = fido2.Secret(fido2Path, cf.FIDO2.CredentialID, cf.FIDO2.HMACSalt) } else { - pw = readpassword.Once(nil, nil, "") + pw, err = readpassword.Once(nil, nil, "") + if err != nil { + tlog.Fatal.Println(err) + os.Exit(exitcodes.ReadPassword) + } } masterkey, err := cf.DecryptMasterKey(pw) // Purge password from memory diff --git a/init_dir.go b/init_dir.go index 9658bab..5ade692 100644 --- a/init_dir.go +++ b/init_dir.go @@ -87,7 +87,11 @@ func initDir(args *argContainer) { password = fido2.Secret(args.fido2, fido2CredentialID, fido2HmacSalt) } else { // normal password entry - password = readpassword.Twice([]string(args.extpass), []string(args.passfile)) + password, err = readpassword.Twice([]string(args.extpass), []string(args.passfile)) + if err != nil { + tlog.Fatal.Println(err) + os.Exit(exitcodes.ReadPassword) + } fido2CredentialID = nil fido2HmacSalt = nil } diff --git a/internal/readpassword/extpass_test.go b/internal/readpassword/extpass_test.go index 5c23ab3..d38f42f 100644 --- a/internal/readpassword/extpass_test.go +++ b/internal/readpassword/extpass_test.go @@ -2,7 +2,6 @@ package readpassword import ( "os" - "os/exec" "testing" "github.com/rfjakob/gocryptfs/v2/internal/tlog" @@ -16,68 +15,76 @@ func TestMain(m *testing.M) { func TestExtpass(t *testing.T) { p1 := "ads2q4tw41reg52" - p2 := string(readPasswordExtpass([]string{"echo " + p1})) - if p1 != p2 { - t.Errorf("p1=%q != p2=%q", p1, p2) + p2, err := readPasswordExtpass([]string{"echo " + p1}) + if err != nil { + t.Fatal(err) + } + if p1 != string(p2) { + t.Errorf("p1=%q != p2=%q", p1, string(p2)) } } func TestOnceExtpass(t *testing.T) { p1 := "lkadsf0923rdfi48rqwhdsf" - p2 := string(Once([]string{"echo " + p1}, nil, "")) - if p1 != p2 { - t.Errorf("p1=%q != p2=%q", p1, p2) + p2, err := Once([]string{"echo " + p1}, nil, "") + if err != nil { + t.Fatal(err) + } + if p1 != string(p2) { + t.Errorf("p1=%q != p2=%q", p1, string(p2)) } } // extpass with two arguments func TestOnceExtpass2(t *testing.T) { p1 := "foo" - p2 := string(Once([]string{"echo", p1}, nil, "")) - if p1 != p2 { - t.Errorf("p1=%q != p2=%q", p1, p2) + p2, err := Once([]string{"echo", p1}, nil, "") + if err != nil { + t.Fatal(err) + } + if p1 != string(p2) { + t.Errorf("p1=%q != p2=%q", p1, string(p2)) } } // extpass with three arguments func TestOnceExtpass3(t *testing.T) { p1 := "foo bar baz" - p2 := string(Once([]string{"echo", "foo", "bar", "baz"}, nil, "")) - if p1 != p2 { - t.Errorf("p1=%q != p2=%q", p1, p2) + p2, err := Once([]string{"echo", "foo", "bar", "baz"}, nil, "") + if err != nil { + t.Fatal(err) + } + if p1 != string(p2) { + t.Errorf("p1=%q != p2=%q", p1, string(p2)) } } func TestOnceExtpassSpaces(t *testing.T) { p1 := "mypassword" - p2 := string(Once([]string{"cat", "passfile_test_files/file with spaces.txt"}, nil, "")) - if p1 != p2 { - t.Errorf("p1=%q != p2=%q", p1, p2) + p2, err := Once([]string{"cat", "passfile_test_files/file with spaces.txt"}, nil, "") + if err != nil { + t.Fatal(err) + } + if p1 != string(p2) { + t.Errorf("p1=%q != p2=%q", p1, string(p2)) } } func TestTwiceExtpass(t *testing.T) { p1 := "w5w44t3wfe45srz434" - p2 := string(Once([]string{"echo " + p1}, nil, "")) - if p1 != p2 { - t.Errorf("p1=%q != p2=%q", p1, p2) + p2, err := Once([]string{"echo " + p1}, nil, "") + if err != nil { + t.Fatal(err) + } + if p1 != string(p2) { + t.Errorf("p1=%q != p2=%q", p1, string(p2)) } } -// When extpass returns an empty string, we should crash. -// -// The TEST_SLAVE magic is explained at -// https://talks.golang.org/2014/testing.slide#23 . +// Empty extpass should fail func TestExtpassEmpty(t *testing.T) { - if os.Getenv("TEST_SLAVE") == "1" { - readPasswordExtpass([]string{"echo"}) - return + _, err := readPasswordExtpass([]string{"echo"}) + if err == nil { + t.Fatal("empty password should have failed") } - cmd := exec.Command(os.Args[0], "-test.run=TestExtpassEmpty$") - cmd.Env = append(os.Environ(), "TEST_SLAVE=1") - err := cmd.Run() - if err != nil { - return - } - t.Fatal("empty password should have failed") } diff --git a/internal/readpassword/passfile.go b/internal/readpassword/passfile.go index 29fde6c..60902b7 100644 --- a/internal/readpassword/passfile.go +++ b/internal/readpassword/passfile.go @@ -2,28 +2,31 @@ package readpassword import ( "bytes" + "fmt" "os" - "github.com/rfjakob/gocryptfs/v2/internal/exitcodes" "github.com/rfjakob/gocryptfs/v2/internal/tlog" ) // readPassFileConcatenate reads the first line from each file name and // concatenates the results. The result does not contain any newlines. -func readPassFileConcatenate(passfileSlice []string) (result []byte) { +func readPassFileConcatenate(passfileSlice []string) (result []byte, err error) { for _, e := range passfileSlice { - result = append(result, readPassFile(e)...) + add, err := readPassFile(e) + if err != nil { + return nil, err + } + result = append(result, add...) } - return result + return result, nil } // readPassFile reads the first line from the passed file name. -func readPassFile(passfile string) []byte { +func readPassFile(passfile string) ([]byte, error) { tlog.Info.Printf("passfile: reading from file %q", passfile) f, err := os.Open(passfile) if err != nil { - tlog.Fatal.Printf("fatal: passfile: could not open %q: %v", passfile, err) - os.Exit(exitcodes.ReadPassword) + return nil, fmt.Errorf("fatal: passfile: could not open %q: %v", passfile, err) } defer f.Close() // +1 for an optional trailing newline, @@ -31,23 +34,20 @@ func readPassFile(passfile string) []byte { buf := make([]byte, maxPasswordLen+2) n, err := f.Read(buf) if err != nil { - tlog.Fatal.Printf("fatal: passfile: could not read from %q: %v", passfile, err) - os.Exit(exitcodes.ReadPassword) + return nil, fmt.Errorf("fatal: passfile: could not read from %q: %v", passfile, err) } buf = buf[:n] // Split into first line and "trailing garbage" lines := bytes.SplitN(buf, []byte("\n"), 2) if len(lines[0]) == 0 { - tlog.Fatal.Printf("fatal: passfile: empty first line in %q", passfile) - os.Exit(exitcodes.ReadPassword) + return nil, fmt.Errorf("fatal: passfile: empty first line in %q", passfile) } if len(lines[0]) > maxPasswordLen { - tlog.Fatal.Printf("fatal: passfile: max password length (%d bytes) exceeded", maxPasswordLen) - os.Exit(exitcodes.ReadPassword) + return nil, fmt.Errorf("fatal: passfile: max password length (%d bytes) exceeded", maxPasswordLen) } if len(lines) > 1 && len(lines[1]) > 0 { tlog.Warn.Printf("warning: passfile: ignoring trailing garbage (%d bytes) after first line", len(lines[1])) } - return lines[0] + return lines[0], nil } diff --git a/internal/readpassword/passfile_test.go b/internal/readpassword/passfile_test.go index dbfe159..425e4e5 100644 --- a/internal/readpassword/passfile_test.go +++ b/internal/readpassword/passfile_test.go @@ -1,8 +1,6 @@ package readpassword import ( - "os" - "os/exec" "testing" ) @@ -17,76 +15,49 @@ func TestPassfile(t *testing.T) { {"file with spaces.txt", "mypassword"}, } for _, tc := range testcases { - pw := readPassFile("passfile_test_files/" + tc.file) + pw, err := readPassFile("passfile_test_files/" + tc.file) + if err != nil { + t.Fatal(err) + } if string(pw) != tc.want { t.Errorf("Wrong result: want=%q have=%q", tc.want, pw) } // Calling readPassFileConcatenate with only one element should give the // same result - pw = readPassFileConcatenate([]string{"passfile_test_files/" + tc.file}) + pw, err = readPassFileConcatenate([]string{"passfile_test_files/" + tc.file}) + if err != nil { + t.Fatal(err) + } if string(pw) != tc.want { t.Errorf("Wrong result: want=%q have=%q", tc.want, pw) } } } -// readPassFile() should exit instead of returning an empty string. -// -// The TEST_SLAVE magic is explained at -// https://talks.golang.org/2014/testing.slide#23 , mirror: -// http://web.archive.org/web/20200426174352/https://talks.golang.org/2014/testing.slide#23 +// readPassFile() should fail instead of returning an empty string. func TestPassfileEmpty(t *testing.T) { - if os.Getenv("TEST_SLAVE") == "1" { - readPassFile("passfile_test_files/empty.txt") - return + _, err := readPassFile("passfile_test_files/empty.txt") + if err == nil { + t.Fatal("should have failed") } - cmd := exec.Command(os.Args[0], "-test.run=TestPassfileEmpty$") - cmd.Env = append(os.Environ(), "TEST_SLAVE=1") - err := cmd.Run() - if err != nil { - return - } - t.Fatal("should have exited") } // File containing just a newline. -// readPassFile() should exit instead of returning an empty string. -// -// The TEST_SLAVE magic is explained at -// https://talks.golang.org/2014/testing.slide#23 , mirror: -// http://web.archive.org/web/20200426174352/https://talks.golang.org/2014/testing.slide#23 +// readPassFile() should fal instead of returning an empty string. func TestPassfileNewline(t *testing.T) { - if os.Getenv("TEST_SLAVE") == "1" { - readPassFile("passfile_test_files/newline.txt") - return + _, err := readPassFile("passfile_test_files/newline.txt") + if err == nil { + t.Fatal("should have failed") } - cmd := exec.Command(os.Args[0], "-test.run=TestPassfileNewline$") - cmd.Env = append(os.Environ(), "TEST_SLAVE=1") - err := cmd.Run() - if err != nil { - return - } - t.Fatal("should have exited") } // File containing "\ngarbage". -// readPassFile() should exit instead of returning an empty string. -// -// The TEST_SLAVE magic is explained at -// https://talks.golang.org/2014/testing.slide#23 , mirror: -// http://web.archive.org/web/20200426174352/https://talks.golang.org/2014/testing.slide#23 +// readPassFile() should return an error. func TestPassfileEmptyFirstLine(t *testing.T) { - if os.Getenv("TEST_SLAVE") == "1" { - readPassFile("passfile_test_files/empty_first_line.txt") - return + _, err := readPassFile("passfile_test_files/empty_first_line.txt") + if err == nil { + t.Fatal("should have failed") } - cmd := exec.Command(os.Args[0], "-test.run=TestPassfileEmptyFirstLine$") - cmd.Env = append(os.Environ(), "TEST_SLAVE=1") - err := cmd.Run() - if err != nil { - return - } - t.Fatal("should have exited") } // TestPassFileConcatenate tests readPassFileConcatenate @@ -95,8 +66,11 @@ func TestPassFileConcatenate(t *testing.T) { "passfile_test_files/file with spaces.txt", "passfile_test_files/mypassword_garbage.txt", } - res := string(readPassFileConcatenate(files)) - if res != "mypasswordmypassword" { + res, err := readPassFileConcatenate(files) + if err != nil { + t.Fatal(err) + } + if string(res) != "mypasswordmypassword" { t.Errorf("wrong result: %q", res) } } diff --git a/internal/readpassword/read.go b/internal/readpassword/read.go index c0dce43..498d09b 100644 --- a/internal/readpassword/read.go +++ b/internal/readpassword/read.go @@ -11,7 +11,6 @@ import ( "golang.org/x/crypto/ssh/terminal" - "github.com/rfjakob/gocryptfs/v2/internal/exitcodes" "github.com/rfjakob/gocryptfs/v2/internal/tlog" ) @@ -22,7 +21,7 @@ const ( // Once tries to get a password from the user, either from the terminal, extpass, passfile // or stdin. Leave "prompt" empty to use the default "Password: " prompt. -func Once(extpass []string, passfile []string, prompt string) []byte { +func Once(extpass []string, passfile []string, prompt string) ([]byte, error) { if len(passfile) != 0 { return readPassFileConcatenate(passfile) } @@ -40,7 +39,7 @@ func Once(extpass []string, passfile []string, prompt string) []byte { // Twice is the same as Once but will prompt twice if we get the password from // the terminal. -func Twice(extpass []string, passfile []string) []byte { +func Twice(extpass []string, passfile []string) ([]byte, error) { if len(passfile) != 0 { return readPassFileConcatenate(passfile) } @@ -50,54 +49,59 @@ func Twice(extpass []string, passfile []string) []byte { if !terminal.IsTerminal(int(os.Stdin.Fd())) { return readPasswordStdin("Password") } - p1 := readPasswordTerminal("Password: ") - p2 := readPasswordTerminal("Repeat: ") + p1, err := readPasswordTerminal("Password: ") + if err != nil { + return nil, err + } + p2, err := readPasswordTerminal("Repeat: ") + if err != nil { + return nil, err + } if !bytes.Equal(p1, p2) { - tlog.Fatal.Println("Passwords do not match") - os.Exit(exitcodes.ReadPassword) + return nil, fmt.Errorf("Passwords do not match") } // Wipe the password duplicate from memory for i := range p2 { p2[i] = 0 } - return p1 + return p1, nil } // readPasswordTerminal reads a line from the terminal. // Exits on read error or empty result. -func readPasswordTerminal(prompt string) []byte { +func readPasswordTerminal(prompt string) ([]byte, error) { fd := int(os.Stdin.Fd()) fmt.Fprintf(os.Stderr, prompt) // terminal.ReadPassword removes the trailing newline p, err := terminal.ReadPassword(fd) if err != nil { - tlog.Fatal.Printf("Could not read password from terminal: %v\n", err) - os.Exit(exitcodes.ReadPassword) + return nil, fmt.Errorf("Could not read password from terminal: %v\n", err) } fmt.Fprintf(os.Stderr, "\n") if len(p) == 0 { - tlog.Fatal.Println("Password is empty") - os.Exit(exitcodes.PasswordEmpty) + return nil, fmt.Errorf("Password is empty") } - return p + return p, nil } // readPasswordStdin reads a line from stdin. // It exits with a fatal error on read error or empty result. -func readPasswordStdin(prompt string) []byte { +func readPasswordStdin(prompt string) ([]byte, error) { tlog.Info.Printf("Reading %s from stdin", prompt) - p := readLineUnbuffered(os.Stdin) - if len(p) == 0 { - tlog.Fatal.Printf("Got empty %s from stdin", prompt) - os.Exit(exitcodes.ReadPassword) + p, err := readLineUnbuffered(os.Stdin) + if err != nil { + return nil, err } - return p + if len(p) == 0 { + return nil, fmt.Errorf("Got empty %s from stdin", prompt) + } + return p, nil } // readPasswordExtpass executes the "extpass" program and returns the first line // of the output. // Exits on read error or empty result. -func readPasswordExtpass(extpass []string) []byte { +func readPasswordExtpass(extpass []string) ([]byte, error) { var parts []string if len(extpass) == 1 { parts = strings.Split(extpass[0], " ") @@ -109,50 +113,47 @@ func readPasswordExtpass(extpass []string) []byte { cmd.Stderr = os.Stderr pipe, err := cmd.StdoutPipe() if err != nil { - tlog.Fatal.Printf("extpass pipe setup failed: %v", err) - os.Exit(exitcodes.ReadPassword) + return nil, fmt.Errorf("extpass pipe setup failed: %v", err) } err = cmd.Start() if err != nil { - tlog.Fatal.Printf("extpass cmd start failed: %v", err) - os.Exit(exitcodes.ReadPassword) + return nil, fmt.Errorf("extpass cmd start failed: %v", err) + } + p, err := readLineUnbuffered(pipe) + if err != nil { + return nil, err } - p := readLineUnbuffered(pipe) pipe.Close() err = cmd.Wait() if err != nil { - tlog.Fatal.Printf("extpass program returned an error: %v", err) - os.Exit(exitcodes.ReadPassword) + return nil, fmt.Errorf("extpass program returned an error: %v", err) } if len(p) == 0 { - tlog.Fatal.Println("extpass: password is empty") - os.Exit(exitcodes.ReadPassword) + return nil, fmt.Errorf("extpass: password is empty") } - return p + return p, nil } // readLineUnbuffered reads single bytes from "r" util it gets "\n" or EOF. // The returned string does NOT contain the trailing "\n". -func readLineUnbuffered(r io.Reader) (l []byte) { +func readLineUnbuffered(r io.Reader) (l []byte, err error) { b := make([]byte, 1) for { if len(l) > maxPasswordLen { - tlog.Fatal.Printf("fatal: maximum password length of %d bytes exceeded", maxPasswordLen) - os.Exit(exitcodes.ReadPassword) + return nil, fmt.Errorf("fatal: maximum password length of %d bytes exceeded", maxPasswordLen) } n, err := r.Read(b) if err == io.EOF { - return l + return l, nil } if err != nil { - tlog.Fatal.Printf("readLineUnbuffered: %v", err) - os.Exit(exitcodes.ReadPassword) + return nil, fmt.Errorf("readLineUnbuffered: %v", err) } if n == 0 { continue } if b[0] == '\n' { - return l + return l, nil } l = append(l, b...) } diff --git a/internal/readpassword/stdin_test.go b/internal/readpassword/stdin_test.go index 01dd701..5d5d846 100644 --- a/internal/readpassword/stdin_test.go +++ b/internal/readpassword/stdin_test.go @@ -8,12 +8,20 @@ import ( ) // Provide password via stdin, terminated by "\n". +// +// The TEST_SLAVE magic is explained at +// https://talks.golang.org/2014/testing.slide#23 , mirror: +// http://web.archive.org/web/20200426174352/https://talks.golang.org/2014/testing.slide#23 func TestStdin(t *testing.T) { p1 := "g55434t55wef" if os.Getenv("TEST_SLAVE") == "1" { - p2 := string(readPasswordStdin("foo")) - if p1 != p2 { - fmt.Fprintf(os.Stderr, "%q != %q", p1, p2) + p2, err := readPasswordStdin("foo") + if err != nil { + fmt.Fprint(os.Stderr, err) + os.Exit(1) + } + if p1 != string(p2) { + fmt.Fprintf(os.Stderr, "%q != %q", p1, string(p2)) os.Exit(1) } return @@ -41,12 +49,20 @@ func TestStdin(t *testing.T) { // Provide password via stdin, terminated by EOF (pipe close). This should not // hang. +// +// The TEST_SLAVE magic is explained at +// https://talks.golang.org/2014/testing.slide#23 , mirror: +// http://web.archive.org/web/20200426174352/https://talks.golang.org/2014/testing.slide#23 func TestStdinEof(t *testing.T) { p1 := "asd45as5f4a36" if os.Getenv("TEST_SLAVE") == "1" { - p2 := string(readPasswordStdin("foo")) - if p1 != p2 { - fmt.Fprintf(os.Stderr, "%q != %q", p1, p2) + p2, err := readPasswordStdin("foo") + if err != nil { + fmt.Fprint(os.Stderr, err) + os.Exit(1) + } + if p1 != string(p2) { + fmt.Fprintf(os.Stderr, "%q != %q", p1, string(p2)) os.Exit(1) } return @@ -76,7 +92,10 @@ func TestStdinEof(t *testing.T) { // Provide empty password via stdin func TestStdinEmpty(t *testing.T) { if os.Getenv("TEST_SLAVE") == "1" { - readPasswordStdin("foo") + _, err := readPasswordStdin("foo") + if err != nil { + os.Exit(1) + } } cmd := exec.Command(os.Args[0], "-test.run=TestStdinEmpty$") cmd.Env = append(os.Environ(), "TEST_SLAVE=1") diff --git a/main.go b/main.go index 0ac7423..d1bf0c3 100644 --- a/main.go +++ b/main.go @@ -55,11 +55,15 @@ func loadConfig(args *argContainer) (masterkey []byte, cf *configfile.ConfFile, if cf.IsFeatureFlagSet(configfile.FlagFIDO2) { if args.fido2 == "" { tlog.Fatal.Printf("Masterkey encrypted using FIDO2 token; need to use the --fido2 option.") - os.Exit(exitcodes.Usage) + return nil, nil, exitcodes.NewErr("", exitcodes.Usage) } pw = fido2.Secret(args.fido2, cf.FIDO2.CredentialID, cf.FIDO2.HMACSalt) } else { - pw = readpassword.Once([]string(args.extpass), []string(args.passfile), "") + pw, err = readpassword.Once([]string(args.extpass), []string(args.passfile), "") + if err != nil { + tlog.Fatal.Println(err) + return nil, nil, exitcodes.NewErr("", exitcodes.ReadPassword) + } } tlog.Info.Println("Decrypting master key") masterkey, err = cf.DecryptMasterKey(pw) @@ -93,7 +97,11 @@ func changePassword(args *argContainer) { os.Exit(exitcodes.Usage) } tlog.Info.Println("Please enter your new password.") - newPw := readpassword.Twice([]string(args.extpass), []string(args.passfile)) + newPw, err := readpassword.Twice([]string(args.extpass), []string(args.passfile)) + if err != nil { + tlog.Fatal.Println(err) + os.Exit(exitcodes.ReadPassword) + } logN := confFile.ScryptObject.LogN() if args._explicitScryptn { logN = args.scryptn diff --git a/masterkey.go b/masterkey.go index 10009cb..d488441 100644 --- a/masterkey.go +++ b/masterkey.go @@ -39,8 +39,12 @@ func unhexMasterKey(masterkey string, fromStdin bool) []byte { func handleArgsMasterkey(args *argContainer) (masterkey []byte) { // "-masterkey=stdin" if args.masterkey == "stdin" { - in := string(readpassword.Once(nil, nil, "Masterkey")) - return unhexMasterKey(in, true) + in, err := readpassword.Once(nil, nil, "Masterkey") + if err != nil { + tlog.Fatal.Println(err) + os.Exit(exitcodes.ReadPassword) + } + return unhexMasterKey(string(in), true) } // "-masterkey=941a6029-3adc6a1c-..." if args.masterkey != "" { From 700ae685cc7cb99b396caeaeee4e39eeac20f1c7 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Tue, 4 Jan 2022 15:21:20 +0100 Subject: [PATCH 21/43] tests: improve SEEK_DATA test for MacOS (1) Create a 1 GiB file instead of 1 TiB, because apparently, on MacOS, the file (sometimes?) is not created sparse, and fills up users' disks: https://github.com/rfjakob/gocryptfs/issues/625 (2) On darwin, SEEK_DATA is not the same as on Linux ( https://github.com/golang/go/commit/2f8b555de27198775f9606e001ef19b76efdb415 ) so use the value provided by the unix package. --- tests/defaults/main_test.go | 16 ++++++++-------- tests/reverse/correctness_test.go | 14 ++++++-------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/tests/defaults/main_test.go b/tests/defaults/main_test.go index 43019ba..0f31a72 100644 --- a/tests/defaults/main_test.go +++ b/tests/defaults/main_test.go @@ -14,6 +14,8 @@ import ( "syscall" "testing" + "golang.org/x/sys/unix" + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" ) @@ -267,15 +269,15 @@ func TestCpWarnings(t *testing.T) { } } -// TestSeekData tests that fs.FileLseeker is implemented +// TestSeekData tests that SEEK_DATA works func TestSeekData(t *testing.T) { fn := filepath.Join(test_helpers.DefaultPlainDir, t.Name()) f, err := os.Create(fn) if err != nil { t.Fatal(err) } - var oneTiB int64 = 1024 * 1024 * 1024 * 1024 - if _, err = f.Seek(oneTiB, 0); err != nil { + var dataOffset int64 = 1024 * 1024 * 1024 // 1 GiB + if _, err = f.Seek(dataOffset, 0); err != nil { t.Fatal(err) } if _, err = f.Write([]byte("foo")); err != nil { @@ -283,18 +285,16 @@ func TestSeekData(t *testing.T) { } f.Close() - const SEEK_DATA = 3 - f, err = os.Open(fn) if err != nil { t.Fatal(err) } - off, err := f.Seek(1024*1024, SEEK_DATA) + off, err := f.Seek(1024*1024, unix.SEEK_DATA) if err != nil { t.Fatal(err) } - if off < oneTiB-1024*1024 { - t.Errorf("off=%d, expected=%d\n", off, oneTiB) + if off < dataOffset-1024*1024 { + t.Errorf("off=%d, expected=%d\n", off, dataOffset) } f.Close() } diff --git a/tests/reverse/correctness_test.go b/tests/reverse/correctness_test.go index 090a468..4a5224c 100644 --- a/tests/reverse/correctness_test.go +++ b/tests/reverse/correctness_test.go @@ -259,7 +259,7 @@ func TestStatfs(t *testing.T) { } } -// TestSeekData tests that fs.FileLseeker is implemented +// TestSeekData tests that SEEK_DATA works func TestSeekData(t *testing.T) { if !plaintextnames { t.Skip() @@ -270,8 +270,8 @@ func TestSeekData(t *testing.T) { if err != nil { t.Fatal(err) } - var oneTiB int64 = 1024 * 1024 * 1024 * 1024 - if _, err = f.Seek(oneTiB, 0); err != nil { + var dataOffset int64 = 1 * 1024 * 1024 * 1024 // 1 GiB + if _, err = f.Seek(dataOffset, 0); err != nil { t.Fatal(err) } if _, err = f.Write([]byte("foo")); err != nil { @@ -279,19 +279,17 @@ func TestSeekData(t *testing.T) { } f.Close() - const SEEK_DATA = 3 - fn2 := filepath.Join(dirB, t.Name()) f, err = os.Open(fn2) if err != nil { t.Fatal(err) } - off, err := f.Seek(1024*1024, SEEK_DATA) + off, err := f.Seek(1024*1024, unix.SEEK_DATA) if err != nil { t.Fatal(err) } - if off < oneTiB-1024*1024 { - t.Errorf("off=%d, expected=%d\n", off, oneTiB) + if off < dataOffset-1024*1024 { + t.Errorf("off=%d, expected=%d\n", off, dataOffset) } f.Close() } From c23a7f225984af1aa9fd0113f93be837a13d9b08 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Tue, 4 Jan 2022 15:24:52 +0100 Subject: [PATCH 22/43] test.bash: disable parallelism in verbose mode This way we get live output, and hopefully see clearer where things hang if they do. Also, don't pass on flags to "go vet", the verbose output is pretty useless. https://github.com/rfjakob/gocryptfs/issues/625 --- test.bash | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/test.bash b/test.bash index 6289072..f94f4d4 100755 --- a/test.bash +++ b/test.bash @@ -1,14 +1,23 @@ #!/bin/bash -if [[ -z $TMPDIR ]]; then +set -eu + +VERBOSE=0 +for i in "$@" ; do + if [[ $i == "-v" ]] ; then + VERBOSE=1 + set -x + break + fi +done + +if [[ -z ${TMPDIR:-} ]]; then TMPDIR=/var/tmp export TMPDIR else echo "Using TMPDIR=$TMPDIR" fi -set -eu - cd "$(dirname "$0")" export GO111MODULE=on MYNAME=$(basename "$0") @@ -53,7 +62,7 @@ if ! go tool | grep vet > /dev/null ; then elif [[ -d vendor ]] ; then echo "vendor directory exists, skipping 'go tool vet'" else - go vet "$@" ./... + go vet ./... fi if command -v shellcheck > /dev/null ; then @@ -63,10 +72,18 @@ else echo "shellcheck not installed - skipping" fi -# We don't want all the subprocesses -# holding the lock file open -# vvvvv -go test -count 1 ./... "$@" 200>&- +EXTRA_ARGS="" +if [[ $VERBOSE -eq 1 ]]; then + # Disabling parallelism disables per-package output buffering, hence enabling + # live streaming of result output. And seeing where things hang. + EXTRA_ARGS="-p 1" +fi + +# We don't want all the subprocesses +# holding the lock file open +# vvvvv +# shellcheck disable=SC2086 +go test -count 1 $EXTRA_ARGS ./... "$@" 200>&- # ^^^^^^^^ # Disable result caching From 5f955423b736d56b5b741fbd1b853c83044aa0fe Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Mon, 10 Jan 2022 20:05:36 +0100 Subject: [PATCH 23/43] fusefrontend: fix -force_owner not affecting MKNOD Fixes https://github.com/rfjakob/gocryptfs/issues/629 --- internal/fusefrontend/node.go | 6 ++++++ tests/defaults/main_test.go | 15 ++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/internal/fusefrontend/node.go b/internal/fusefrontend/node.go index 182cda5..ead77c9 100644 --- a/internal/fusefrontend/node.go +++ b/internal/fusefrontend/node.go @@ -277,7 +277,13 @@ func (n *Node) Mknod(ctx context.Context, name string, mode, rdev uint32, out *f errno = fs.ToErrno(err) return } + inode = n.newChild(ctx, st, out) + + if rn.args.ForceOwner != nil { + out.Owner = *rn.args.ForceOwner + } + return inode, 0 } diff --git a/tests/defaults/main_test.go b/tests/defaults/main_test.go index 0f31a72..7633e8b 100644 --- a/tests/defaults/main_test.go +++ b/tests/defaults/main_test.go @@ -427,10 +427,11 @@ func TestFsync(t *testing.T) { } // force_owner was broken by the v2.0 rewrite: -// The owner was only forced for GETATTR, but not for CREATE or LOOKUP. +// The owner was only forced for GETATTR, but not for CREATE, LOOKUP, MKNOD. // // https://github.com/rfjakob/gocryptfs/issues/609 // https://github.com/rfjakob/gocryptfs/pull/610 +// https://github.com/rfjakob/gocryptfs/issues/629 func TestForceOwner(t *testing.T) { cDir := test_helpers.InitFS(t) os.Chmod(cDir, 0777) // Mount needs to be accessible for us @@ -479,6 +480,18 @@ func TestForceOwner(t *testing.T) { t.Errorf("GETATTR returned uid or gid != 1234: %#v", st) } + // Test MKNOD + sock := pDir + "/sock" + if err := syscall.Mknod(sock, syscall.S_IFSOCK|0600, 0); err != nil { + t.Fatal(err) + } + if err := syscall.Stat(sock, &st); err != nil { + t.Fatal(err) + } + if st.Uid != 1234 || st.Gid != 1234 { + t.Errorf("MKNOD 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") From 3ca2b1983dbab14e3769efd126098cbca6fb2ffd Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 22 Jan 2022 11:46:08 +0100 Subject: [PATCH 24/43] tests: enable -fusedebug if FUSEDEBUG env is set --- tests/test_helpers/mount_unmount.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_helpers/mount_unmount.go b/tests/test_helpers/mount_unmount.go index 8ff2564..afd33f1 100644 --- a/tests/test_helpers/mount_unmount.go +++ b/tests/test_helpers/mount_unmount.go @@ -37,7 +37,10 @@ type mountInfo struct { func Mount(c string, p string, showOutput bool, extraArgs ...string) error { args := []string{"-q", "-wpanic", "-nosyslog", "-fg", fmt.Sprintf("-notifypid=%d", os.Getpid())} args = append(args, extraArgs...) - //args = append(args, "-fusedebug") + if _, isset := os.LookupEnv("FUSEDEBUG"); isset { + fmt.Println("FUSEDEBUG is set, enabling -fusedebug") + args = append(args, "-fusedebug") + } //args = append(args, "-d") args = append(args, c, p) From b7cac4ffd07733ab6bcd0d33345de3306a8a5a59 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 22 Jan 2022 12:28:27 +0100 Subject: [PATCH 25/43] fusefrontend: support RENAME_WHITEOUT, RENAME_EXCHANGE Both new internal test and xfstests generic/013 are happy. https://github.com/rfjakob/gocryptfs/issues/641 --- internal/fusefrontend/node.go | 23 +++++++++++------------ internal/syscallcompat/sys_darwin.go | 4 +++- internal/syscallcompat/sys_linux.go | 4 +++- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/internal/fusefrontend/node.go b/internal/fusefrontend/node.go index ead77c9..688cc0d 100644 --- a/internal/fusefrontend/node.go +++ b/internal/fusefrontend/node.go @@ -391,23 +391,17 @@ func (n *Node) Symlink(ctx context.Context, target, name string, out *fuse.Entry return inode, 0 } -// xfstests generic/013 now also exercises RENAME_EXCHANGE and RENAME_WHITEOUT, -// uncovering lots of problems with longnames -// -// Reject those flags with syscall.EINVAL. // If we can handle the flags, this function returns 0. func rejectRenameFlags(flags uint32) syscall.Errno { - // Normal rename, we can handle that - if flags == 0 { + switch flags { + case 0, syscallcompat.RENAME_NOREPLACE, syscallcompat.RENAME_EXCHANGE, syscallcompat.RENAME_WHITEOUT: return 0 - } - // We also can handle RENAME_NOREPLACE - if flags == syscallcompat.RENAME_NOREPLACE { + case syscallcompat.RENAME_NOREPLACE | syscallcompat.RENAME_WHITEOUT: return 0 + default: + tlog.Warn.Printf("rejectRenameFlags: unknown flag combination 0x%x", flags) + return syscall.EINVAL } - // We cannot handle RENAME_EXCHANGE and RENAME_WHITEOUT yet. - // Needs extra code for .name files. - return syscall.EINVAL } // Rename - FUSE call. @@ -472,6 +466,11 @@ func (n *Node) Rename(ctx context.Context, name string, newParent fs.InodeEmbedd } return fs.ToErrno(err) } + if flags&syscallcompat.RENAME_EXCHANGE != 0 || flags&syscallcompat.RENAME_WHITEOUT != 0 { + // These flags mean that there is now a new file at cName and we + // should NOT delete its longname file. + return 0 + } if nametransform.IsLongContent(cName) { nametransform.DeleteLongNameAt(dirfd, cName) } diff --git a/internal/syscallcompat/sys_darwin.go b/internal/syscallcompat/sys_darwin.go index de795a4..7b8773c 100644 --- a/internal/syscallcompat/sys_darwin.go +++ b/internal/syscallcompat/sys_darwin.go @@ -21,8 +21,10 @@ const ( // O_PATH is only defined on Linux O_PATH = 0 - // RENAME_NOREPLACE is only defined on Linux + // Only defined on Linux RENAME_NOREPLACE = 0 + RENAME_WHITEOUT = 0 + RENAME_EXCHANGE = 0 // KAUTH_UID_NONE and KAUTH_GID_NONE are special values to // revert permissions to the process credentials. diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go index 961d1c9..a64b27e 100644 --- a/internal/syscallcompat/sys_linux.go +++ b/internal/syscallcompat/sys_linux.go @@ -28,8 +28,10 @@ const ( // O_PATH is only defined on Linux O_PATH = unix.O_PATH - // RENAME_NOREPLACE is only defined on Linux + // Only defined on Linux RENAME_NOREPLACE = unix.RENAME_NOREPLACE + RENAME_WHITEOUT = unix.RENAME_WHITEOUT + RENAME_EXCHANGE = unix.RENAME_EXCHANGE ) var preallocWarn sync.Once From 3bac814ea9f8468499c65c9b2b0a6f023e23d35e Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 22 Jan 2022 12:40:24 +0100 Subject: [PATCH 26/43] tests: add TestRenameWhiteout, TestRenameExchange f --- tests/defaults/overlayfs_test.go | 109 +++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 tests/defaults/overlayfs_test.go diff --git a/tests/defaults/overlayfs_test.go b/tests/defaults/overlayfs_test.go new file mode 100644 index 0000000..1a3298d --- /dev/null +++ b/tests/defaults/overlayfs_test.go @@ -0,0 +1,109 @@ +// +build linux + +package defaults + +import ( + "io/ioutil" + "os" + "strings" + "testing" + + "golang.org/x/sys/unix" + + "github.com/rfjakob/gocryptfs/v2/internal/syscallcompat" + "github.com/rfjakob/gocryptfs/v2/tests/test_helpers" +) + +// https://github.com/rfjakob/gocryptfs/issues/641 +// +// I was trying to run the Docker daemon with the recommended overlay2 storage driver, and encrypt its `/var/lib/docker` directory using gocryptfs. overlay2 was giving me the following errors: +// ``` +// Jan 21 19:09:43 friedhelm.rankenste.in kernel: overlayfs: upper fs does not support tmpfile. +// Jan 21 19:09:43 friedhelm.rankenste.in kernel: overlayfs: upper fs does not support RENAME_WHITEOUT. +// Jan 21 19:09:43 friedhelm.rankenste.in kernel: overlayfs: upper fs missing required features. +// ``` + +func TestRenameWhiteout(t *testing.T) { + short := t.Name() + ".short" + long := t.Name() + strings.Repeat(".long", 200/len(".long")) + + names := [][]string{ + // short to short + {short + "s2s", short + "s2s2"}, + // short to long + {short + "s2l", long + "s2l2"}, + // long to short + {long + "l2s", short + "l2s2"}, + // long to long + {long + "l2l", short + "l2l2"}, + } + + for _, flags := range []uint{syscallcompat.RENAME_WHITEOUT, syscallcompat.RENAME_WHITEOUT | syscallcompat.RENAME_NOREPLACE} { + for _, n := range names { + pSrc := test_helpers.DefaultPlainDir + "/" + n[0] + pDst := test_helpers.DefaultPlainDir + "/" + n[1] + if err := ioutil.WriteFile(pSrc, nil, 0200); err != nil { + t.Fatalf("creating empty file failed: %v", err) + } + err := unix.Renameat2(-1, pSrc, -1, pDst, flags) + if err != nil { + t.Error(err) + } + // readdir should not choke on leftover or missing .name files + _, err = os.ReadDir(test_helpers.DefaultPlainDir) + if err != nil { + t.Error(err) + } + // pSrc should now be a character device 0 file + var st unix.Stat_t + err = unix.Stat(pSrc, &st) + if err != nil { + t.Error(err) + } + if !(st.Mode&unix.S_IFMT == unix.S_IFCHR) { + t.Error("not a device file") + } + if st.Rdev != 0 { + t.Errorf("want device 0, have %d", st.Rdev) + } + unix.Unlink(pSrc) + unix.Unlink(pDst) + } + } +} + +func TestRenameExchange(t *testing.T) { + short := t.Name() + ".short" + long := t.Name() + strings.Repeat(".long", 200/len(".long")) + + names := [][]string{ + // short to short + {short + "s2s", short + "s2s2"}, + // short to long + {short + "s2l", long + "s2l2"}, + // long to short + {long + "l2s", short + "l2s2"}, + // long to long + {long + "l2l", short + "l2l2"}, + } + + for _, n := range names { + pSrc := test_helpers.DefaultPlainDir + "/" + n[0] + pDst := test_helpers.DefaultPlainDir + "/" + n[1] + if err := ioutil.WriteFile(pSrc, nil, 0200); err != nil { + t.Fatalf("creating empty file failed: %v", err) + } + if err := ioutil.WriteFile(pDst, nil, 0200); err != nil { + t.Fatalf("creating empty file failed: %v", err) + } + err := unix.Renameat2(-1, pSrc, -1, pDst, unix.RENAME_EXCHANGE) + if err != nil { + t.Error(err) + } + // readdir should not choke on leftover or missing .name files + _, err = os.ReadDir(test_helpers.DefaultPlainDir) + if err != nil { + t.Error(err) + } + } +} From b859bc96efa91eff01f7dc0c14d90633b7e4ba92 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 22 Jan 2022 12:40:54 +0100 Subject: [PATCH 27/43] fusefrontend: fix "duplicate case" darwin build failure $ ./crossbuild.bash [...] + GOOS=darwin + GOARCH=amd64 + build + go build -tags without_openssl -o /dev/null internal/fusefrontend/node.go:397:2: duplicate case syscallcompat.RENAME_NOREPLACE (value 0) in switch previous case at internal/fusefrontend/node.go:397:7 internal/fusefrontend/node.go:397:2: duplicate case syscallcompat.RENAME_EXCHANGE (value 0) in switch previous case at internal/fusefrontend/node.go:397:7 internal/fusefrontend/node.go:397:2: duplicate case syscallcompat.RENAME_WHITEOUT (value 0) in switch previous case at internal/fusefrontend/node.go:397:7 internal/fusefrontend/node.go:399:38: duplicate case syscallcompat.RENAME_NOREPLACE | syscallcompat.RENAME_WHITEOUT (value 0) in switch previous case at internal/fusefrontend/node.go:397:7 --- internal/syscallcompat/sys_darwin.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/syscallcompat/sys_darwin.go b/internal/syscallcompat/sys_darwin.go index 7b8773c..06f09f0 100644 --- a/internal/syscallcompat/sys_darwin.go +++ b/internal/syscallcompat/sys_darwin.go @@ -21,10 +21,11 @@ const ( // O_PATH is only defined on Linux O_PATH = 0 - // Only defined on Linux - RENAME_NOREPLACE = 0 - RENAME_WHITEOUT = 0 - RENAME_EXCHANGE = 0 + // Only exists on Linux. Define here to fix build failure, even though + // we will never see the flags. + RENAME_NOREPLACE = 1 + RENAME_EXCHANGE = 2 + RENAME_WHITEOUT = 4 // KAUTH_UID_NONE and KAUTH_GID_NONE are special values to // revert permissions to the process credentials. From 696f11499bc47ef4d9709ca75fd5ecff56076da2 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 22 Jan 2022 14:06:39 +0100 Subject: [PATCH 28/43] tests: add skipped O_TMPFILE test Looks like the FUSE protocol does support O_TMPFILE yet. https://github.com/rfjakob/gocryptfs/issues/641 --- tests/defaults/overlayfs_test.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/defaults/overlayfs_test.go b/tests/defaults/overlayfs_test.go index 1a3298d..2bc4387 100644 --- a/tests/defaults/overlayfs_test.go +++ b/tests/defaults/overlayfs_test.go @@ -107,3 +107,13 @@ func TestRenameExchange(t *testing.T) { } } } + +// Looks like the FUSE protocol does support O_TMPFILE yet +func TestOTmpfile(t *testing.T) { + p := test_helpers.DefaultPlainDir + "/" + t.Name() + fd, err := unix.Openat(-1, p, unix.O_TMPFILE, 0600) + if err != nil { + t.Skip(err) + } + unix.Close(fd) +} From 47358938ec111f27465beba20a8e0c16c3b98bda Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 22 Jan 2022 16:07:59 +0100 Subject: [PATCH 29/43] tests: fix build failure on Go 1.15 and older These don't have os.ReadDir yet. Error was: Error: vet: tests/defaults/overlayfs_test.go:104:15: ReadDir not declared by package os --- tests/defaults/overlayfs_test.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/defaults/overlayfs_test.go b/tests/defaults/overlayfs_test.go index 2bc4387..7b41de4 100644 --- a/tests/defaults/overlayfs_test.go +++ b/tests/defaults/overlayfs_test.go @@ -50,7 +50,12 @@ func TestRenameWhiteout(t *testing.T) { t.Error(err) } // readdir should not choke on leftover or missing .name files - _, err = os.ReadDir(test_helpers.DefaultPlainDir) + dir, err := os.Open(test_helpers.DefaultPlainDir) + if err != nil { + t.Fatal(err) + } + defer dir.Close() + _, err = dir.Readdir(0) if err != nil { t.Error(err) } @@ -101,7 +106,12 @@ func TestRenameExchange(t *testing.T) { t.Error(err) } // readdir should not choke on leftover or missing .name files - _, err = os.ReadDir(test_helpers.DefaultPlainDir) + dir, err := os.Open(test_helpers.DefaultPlainDir) + if err != nil { + t.Fatal(err) + } + defer dir.Close() + _, err = dir.Readdir(0) if err != nil { t.Error(err) } From b636f79f8981f2b782a26dbf074ed457157d8413 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 22 Jan 2022 16:19:33 +0100 Subject: [PATCH 30/43] MANPAGE: add missing -acl section Looks like https://github.com/rfjakob/gocryptfs/commit/86d8336b43418c028c34c37f06fcbd43ab0d44a1 forgot to add the option to the manpage. --- Documentation/MANPAGE.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index 25434c9..8f65db8 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -177,6 +177,10 @@ MOUNT OPTIONS Available options for mounting are listed below. Usually, you don't need any. Defaults are fine. +#### -acl +Enable ACL enforcement. When you want to use ACLs, you must enable this +option. + #### -allow_other By default, the Linux kernel prevents any other user (even root) to access a mounted FUSE filesystem. Settings this option allows access for From ba75aa1ab0dcef8ad8c8fbb11e8895413ad26787 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Thu, 27 Jan 2022 15:44:09 +0100 Subject: [PATCH 31/43] root_test: add TestOverlay ; syscallcompat: add QuirkNoUserXattr --- internal/syscallcompat/quirks.go | 2 ++ internal/syscallcompat/quirks_linux.go | 4 ++++ tests/root_test/root_test.go | 31 ++++++++++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/internal/syscallcompat/quirks.go b/internal/syscallcompat/quirks.go index 110c00d..858f16d 100644 --- a/internal/syscallcompat/quirks.go +++ b/internal/syscallcompat/quirks.go @@ -13,6 +13,8 @@ const ( // On MacOS ExFAT, all empty files share inode number 1: // https://github.com/rfjakob/gocryptfs/issues/585 QuirkDuplicateIno1 + // QuirkNoUserXattr means that user.* xattrs are not supported + QuirkNoUserXattr ) func logQuirk(s string) { diff --git a/internal/syscallcompat/quirks_linux.go b/internal/syscallcompat/quirks_linux.go index bcdcf07..5ef2d8a 100644 --- a/internal/syscallcompat/quirks_linux.go +++ b/internal/syscallcompat/quirks_linux.go @@ -27,5 +27,9 @@ func DetectQuirks(cipherdir string) (q uint64) { q |= QuirkBrokenFalloc } + if uint32(st.Type) == unix.TMPFS_MAGIC { + logQuirk("tmpfs detected, no extended attributes except acls will work.") + } + return q } diff --git a/tests/root_test/root_test.go b/tests/root_test/root_test.go index fe38cfa..462aaeb 100644 --- a/tests/root_test/root_test.go +++ b/tests/root_test/root_test.go @@ -362,3 +362,34 @@ func TestBtrfsQuirks(t *testing.T) { t.Errorf("wrong quirk: %v", quirk) } } + +func TestOverlay(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("must run as root") + } + cDir := test_helpers.InitFS(t) + if syscallcompat.DetectQuirks(cDir)|syscallcompat.QuirkNoUserXattr != 0 { + t.Logf("No user xattrs! overlay mount will likely fail.") + } + os.Chmod(cDir, 0755) + pDir := cDir + ".mnt" + test_helpers.MountOrFatal(t, cDir, pDir, "-allow_other", "-extpass=echo test") + defer test_helpers.UnmountPanic(pDir) + + for _, d := range []string{"lower", "upper", "work", "merged"} { + err := os.Mkdir(pDir+"/"+d, 0700) + if err != nil { + t.Fatal(err) + } + } + ovlMnt := pDir + "/merged" + cmd := exec.Command("mount", "-t", "overlay", "overlay", + "-o", "lowerdir="+pDir+"/lower,upperdir="+pDir+"/upper,workdir="+pDir+"/work", + ovlMnt) + out, err := cmd.CombinedOutput() + if err != nil { + t.Log(string(out)) + t.Fatal(err) + } + defer syscall.Unmount(ovlMnt, 0) +} From a2b54cfccd7d67529f87309c84ab7c00a8b6a4b0 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Thu, 27 Jan 2022 18:35:45 +0100 Subject: [PATCH 32/43] root_test: fix leftover loop mount After running "make root_test" a few times df would look like this, no good: $ df Filesystem 1K-blocks Used Available Use% Mounted on [...] /dev/loop11 8729 8525 0 100% /tmp/gocryptfs-test-parent-0/4081611019/TestDiskFull.ext4.mnt /dev/loop12 8729 8525 0 100% /tmp/gocryptfs-test-parent-0/1959939106/TestDiskFull.ext4.mnt /dev/loop13 8729 8525 0 100% /tmp/gocryptfs-test-parent-0/2455888382/TestDiskFull.ext4.mnt /dev/loop14 8729 8525 0 100% /tmp/gocryptfs-test-parent-0/2002998275/TestDiskFull.ext4.mnt /dev/loop15 8729 8525 0 100% /var/tmp/gocryptfs-test-parent-0/806736609/TestDiskFull.ext4.mnt /dev/loop16 8729 8525 0 100% /tmp/gocryptfs-test-parent-0/4050106930/TestDiskFull.ext4.mnt /dev/loop17 8729 8525 0 100% /tmp/gocryptfs-test-parent-0/1661931756/TestDiskFull.ext4.mnt /dev/loop18 8729 8525 0 100% /tmp/gocryptfs-test-parent-0/617990718/TestDiskFull.ext4.mnt /dev/loop19 8729 8525 0 100% /tmp/gocryptfs-test-parent-0/3194420338/TestDiskFull.ext4.mnt /dev/loop20 8729 8525 0 100% /tmp/gocryptfs-test-parent-0/2180745159/TestDiskFull.ext4.mnt Turns out the unmount failed with EBUSY, so use lazy unmount. --- tests/root_test/root_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/root_test/root_test.go b/tests/root_test/root_test.go index 462aaeb..7d1e296 100644 --- a/tests/root_test/root_test.go +++ b/tests/root_test/root_test.go @@ -179,7 +179,13 @@ func TestDiskFull(t *testing.T) { t.Fatal(err) } defer syscall.Unlink(ext4img) - defer syscall.Unmount(ext4mnt, 0) + defer func() { + const MNT_DETACH = 2 + err := syscall.Unmount(ext4mnt, MNT_DETACH) + if err != nil { + t.Log(err) + } + }() // gocryptfs -init cipherdir := ext4mnt + "/a" From ad2904f9ed84225150c45037c00ca5e6b767e63f Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 19 Mar 2022 15:18:39 +0100 Subject: [PATCH 33/43] MANPAGE: document that -scryptn also applies to -passwd Closes https://github.com/rfjakob/gocryptfs/issues/646 --- Documentation/MANPAGE.md | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index 8f65db8..998c8e0 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -157,14 +157,6 @@ mounted using gocryptfs v1.2 and higher. Default true. Reverse mode shows a read-only encrypted view of a plaintext directory. Implies "-aessiv". -#### -scryptn int -scrypt cost parameter expressed as scryptn=log2(N). Possible values are -10 to 28, representing N=2^10 to N=2^28. - -Setting this to a lower -value speeds up mounting and reduces its memory needs, but makes -the password susceptible to brute-force attacks. The default is 16. - #### -xchacha Use XChaCha20-Poly1305 file content encryption. This should be much faster than AES-GCM on CPUs that lack AES acceleration. @@ -569,6 +561,16 @@ Quiet - silence informational messages. Applies to: all actions. +#### -scryptn int +scrypt cost parameter expressed as scryptn=log2(N). Possible values are +10 to 28, representing N=2^10 to N=2^28. + +Setting this to a lower +value speeds up mounting and reduces its memory needs, but makes +the password susceptible to brute-force attacks. The default is 16. + +Applies to: `-init`, `-passwd` + #### -trace string Write execution trace to file. View the trace using "go tool trace FILE". From c9e4e4f74150d2734496e90a4c442a17b79f52c1 Mon Sep 17 00:00:00 2001 From: Val Date: Thu, 31 Mar 2022 14:41:36 -0400 Subject: [PATCH 34/43] Fix reverse gocryptfs.conf access on macOS Unlike the FUSE implementation on Linux, macFUSE doesn't cache the file attributes from the `LOOKUP` call, so it calls `GETATTR` prior to accessing a file. In the case of the `VirtualConfNode` (reverse config file passthrough), this resulted in the default `GETATTR` implementation returning an empty result, ultimately resulting in a "permission denied" error. 14:44:14.095207 rx 3: GETATTR n2 14:44:14.095229 tx 3: OK, {tA=1s {M0100000 SZ=0 L=0 0:0 0 0:8954996 A 0.000000 M 0.000000 C 0.000000}} 14:44:14.099943 rx 4: ACCESS n2 {u=501 g=20 r} 14:44:14.099990 tx 4: 13=permission denied By impementing `Getattr` (from `fs.NodeGetattrer`) on `VirtualConfNode` this solves the issue. --- internal/fusefrontend_reverse/virtualconf.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/internal/fusefrontend_reverse/virtualconf.go b/internal/fusefrontend_reverse/virtualconf.go index 8620f6d..e8cc080 100644 --- a/internal/fusefrontend_reverse/virtualconf.go +++ b/internal/fusefrontend_reverse/virtualconf.go @@ -10,6 +10,7 @@ import ( ) var _ = (fs.NodeOpener)((*VirtualConfNode)(nil)) +var _ = (fs.NodeGetattrer)((*VirtualConfNode)(nil)) type VirtualConfNode struct { fs.Inode @@ -27,6 +28,17 @@ func (n *VirtualConfNode) Open(ctx context.Context, flags uint32) (fh fs.FileHan return } +func (n *VirtualConfNode) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + var st syscall.Stat_t + err := syscall.Stat(n.path, &st) + if err != nil { + return fs.ToErrno(err) + } + out.FromStat(&st) + return 0 +} + + // Check that we have implemented the fs.File* interfaces var _ = (fs.FileReader)((*VirtualConfFile)(nil)) var _ = (fs.FileReleaser)((*VirtualConfFile)(nil)) From e9ecff7f07aeb1efe0edec7b4b050ce3c0ef75f8 Mon Sep 17 00:00:00 2001 From: Yuta Hayashibe Date: Wed, 4 May 2022 18:06:20 +0900 Subject: [PATCH 35/43] Fix typos --- cli_args.go | 2 +- contrib/atomicrename/main.go | 2 +- contrib/findholes/holes/holes.go | 2 +- internal/cryptocore/randsize_test.go | 2 +- internal/fusefrontend/file.go | 4 ++-- internal/inomap/inomap.go | 4 ++-- internal/nametransform/badname.go | 2 +- internal/stupidgcm/openssl.go | 2 +- internal/stupidgcm/prefer.go | 2 +- tests/cli/cli_test.go | 2 +- tests/example_filesystems/example_filesystems_test.go | 2 +- 11 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cli_args.go b/cli_args.go index 781dc5c..91bd56e 100644 --- a/cli_args.go +++ b/cli_args.go @@ -334,7 +334,7 @@ func countOpFlags(args *argContainer) int { return count } -// isFlagPassed finds out if the flag was explictely passed on the command line. +// isFlagPassed finds out if the flag was explicitly passed on the command line. // https://stackoverflow.com/a/54747682/1380267 func isFlagPassed(flagSet *flag.FlagSet, name string) bool { found := false diff --git a/contrib/atomicrename/main.go b/contrib/atomicrename/main.go index 67088b0..394753b 100644 --- a/contrib/atomicrename/main.go +++ b/contrib/atomicrename/main.go @@ -26,7 +26,7 @@ func usage() { them in random order over a single "dst" file while reading the "dst" file concurrently in a loop. -Progress and errors are reported as they occour in addition to a summary +Progress and errors are reported as they occur in addition to a summary printed at the end. cifs and fuse filesystems are known to fail, local filesystems and nfs seem ok. diff --git a/contrib/findholes/holes/holes.go b/contrib/findholes/holes/holes.go index 307c624..95c9d2b 100644 --- a/contrib/findholes/holes/holes.go +++ b/contrib/findholes/holes/holes.go @@ -153,7 +153,7 @@ func Verify(fd int, segments []Segment) (err error) { case SegmentEOF: continue default: - log.Panicf("BUG: unkown segment type %d", s.Type) + log.Panicf("BUG: unknown segment type %d", s.Type) } for off := s.Offset; off < segments[i+1].Offset; off++ { res, err := syscall.Seek(fd, off, whence) diff --git a/internal/cryptocore/randsize_test.go b/internal/cryptocore/randsize_test.go index 1db4745..ed91d4f 100644 --- a/internal/cryptocore/randsize_test.go +++ b/internal/cryptocore/randsize_test.go @@ -10,7 +10,7 @@ import ( ) /* -The troughput we get from /dev/urandom / getentropy depends a lot on the used +The throughput we get from /dev/urandom / getentropy depends a lot on the used block size. Results on my Pentium G630 running Linux 4.11: BenchmarkRandSize/16-2 3000000 571 ns/op 27.98 MB/s diff --git a/internal/fusefrontend/file.go b/internal/fusefrontend/file.go index 3ce1b1e..2f111fd 100644 --- a/internal/fusefrontend/file.go +++ b/internal/fusefrontend/file.go @@ -207,8 +207,8 @@ func (f *File) doRead(dst []byte, off uint64, length uint64) ([]byte, syscall.Er plaintext, err := f.contentEnc.DecryptBlocks(ciphertext, firstBlockNo, fileID) f.rootNode.contentEnc.CReqPool.Put(ciphertext) if err != nil { - curruptBlockNo := firstBlockNo + f.contentEnc.PlainOffToBlockNo(uint64(len(plaintext))) - tlog.Warn.Printf("doRead %d: corrupt block #%d: %v", f.qIno.Ino, curruptBlockNo, err) + corruptBlockNo := firstBlockNo + f.contentEnc.PlainOffToBlockNo(uint64(len(plaintext))) + tlog.Warn.Printf("doRead %d: corrupt block #%d: %v", f.qIno.Ino, corruptBlockNo, err) return nil, syscall.EIO } diff --git a/internal/inomap/inomap.go b/internal/inomap/inomap.go index 997ea9b..070fab4 100644 --- a/internal/inomap/inomap.go +++ b/internal/inomap/inomap.go @@ -9,7 +9,7 @@ // Each (Dev, Tag) tuple gets a namespace id assigned. The original inode // number is then passed through in the lower 48 bits. // -// If namespace ids are exhaused, or the original id is larger than 48 bits, +// If namespace ids are exhausted, or the original id is larger than 48 bits, // the whole (Dev, Tag, Ino) tuple gets mapped in the spill map, and the // spill bit is set to 1. package inomap @@ -114,7 +114,7 @@ func (m *InoMap) Translate(in QIno) (out uint64) { // TranslateStat translates (device, ino) pair contained in "st" into a unique // inode number and overwrites the ino in "st" with it. -// Convience wrapper around Translate(). +// Convenience wrapper around Translate(). func (m *InoMap) TranslateStat(st *syscall.Stat_t) { in := QInoFromStat(st) st.Ino = m.Translate(in) diff --git a/internal/nametransform/badname.go b/internal/nametransform/badname.go index 6e77561..28bc028 100644 --- a/internal/nametransform/badname.go +++ b/internal/nametransform/badname.go @@ -34,7 +34,7 @@ func (be *NameTransform) EncryptAndHashBadName(name string, iv []byte, dirfd int //file found, return result return lastFoundName, nil } - //BadName Mode: check if the name was tranformed without change (badname suffix and undecryptable cipher name) + //BadName Mode: check if the name was transformed without change (badname suffix and undecryptable cipher name) err = syscallcompat.Fstatat(dirfd, name[:len(name)-len(BadnameSuffix)], &st, unix.AT_SYMLINK_NOFOLLOW) if err == nil { filesFound++ diff --git a/internal/stupidgcm/openssl.go b/internal/stupidgcm/openssl.go index ae0ee5c..b46fba8 100644 --- a/internal/stupidgcm/openssl.go +++ b/internal/stupidgcm/openssl.go @@ -117,7 +117,7 @@ func slicePointerOrNull(s []byte) (ptr *C.uchar) { } // This functions exists to benchmark the C call overhead from Go. -// See BenchmarkCCall for resuts. +// See BenchmarkCCall for results. func noopCFunction() { C.noop_c_function() } diff --git a/internal/stupidgcm/prefer.go b/internal/stupidgcm/prefer.go index bb613c3..fe8c613 100644 --- a/internal/stupidgcm/prefer.go +++ b/internal/stupidgcm/prefer.go @@ -40,7 +40,7 @@ func PreferOpenSSLXchacha20poly1305() bool { if runtime.GOARCH == "amd64" { return false } - // On arm64 and arm, OpenSSL is faster. Probably everwhere else too. + // On arm64 and arm, OpenSSL is faster. Probably everywhere else too. return true } diff --git a/tests/cli/cli_test.go b/tests/cli/cli_test.go index 915759d..fc2bfed 100644 --- a/tests/cli/cli_test.go +++ b/tests/cli/cli_test.go @@ -951,7 +951,7 @@ func TestInitNotEmpty(t *testing.T) { } // TestSharedstorage checks that `-sharedstorage` shows stable inode numbers to -// userpsace despite having hard link tracking disabled +// userspace despite having hard link tracking disabled func TestSharedstorage(t *testing.T) { dir := test_helpers.InitFS(t) mnt := dir + ".mnt" diff --git a/tests/example_filesystems/example_filesystems_test.go b/tests/example_filesystems/example_filesystems_test.go index 37820b5..a5f0595 100644 --- a/tests/example_filesystems/example_filesystems_test.go +++ b/tests/example_filesystems/example_filesystems_test.go @@ -333,7 +333,7 @@ func TestExampleFSv13reverse(t *testing.T) { } dirA = tmpFsPath + dirA // Mount using password - // We pass "-wpanic=false" because the '..' and '.' tests deliverately trigger warnings + // We pass "-wpanic=false" because the '..' and '.' tests deliberately trigger warnings test_helpers.MountOrFatal(t, dirA, dirB, "-reverse", "-extpass", "echo test", "-wpanic=false", opensslOpt) c := dirB + "/gocryptfs.conf" if !test_helpers.VerifyExistence(t, c) { From 702a2e19ccaba962449f3844d57f32e56711422e Mon Sep 17 00:00:00 2001 From: Abirdcfly Date: Wed, 10 Aug 2022 13:18:07 +0800 Subject: [PATCH 36/43] fix minor unreachable code caused by t.Fatal Signed-off-by: Abirdcfly --- internal/stupidgcm/common_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/stupidgcm/common_test.go b/internal/stupidgcm/common_test.go index 1b32cfa..e5b14a4 100644 --- a/internal/stupidgcm/common_test.go +++ b/internal/stupidgcm/common_test.go @@ -52,11 +52,11 @@ func testEncryptDecrypt(t *testing.T, c1 cipher.AEAD, c2 cipher.AEAD) { // Ciphertext must be identical to Go GCM if !bytes.Equal(c1out, c2out) { - t.Fatalf("Compare failed for encryption, size %d", i) t.Log("c1out:") t.Log("\n" + hex.Dump(c1out)) t.Log("c2out:") t.Log("\n" + hex.Dump(c2out)) + t.Fatalf("Compare failed for encryption, size %d", i) } c1out2, sErr := c1.Open(dst, iv, c1out[len(dst):], authData) @@ -115,11 +115,11 @@ func testInplaceSeal(t *testing.T, c1 cipher.AEAD, c2 cipher.AEAD) { // Ciphertext must be identical to Go GCM if !bytes.Equal(c1out, c2out) { - t.Fatalf("Compare failed for encryption, size %d", i) t.Log("sOut:") t.Log("\n" + hex.Dump(c1out)) t.Log("gOut:") t.Log("\n" + hex.Dump(c2out)) + t.Fatalf("Compare failed for encryption, size %d", i) } } } From 5582d8370cce68bbad1f7e44f1fdc52fdc9d9d71 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Mon, 22 Aug 2022 14:00:04 +0200 Subject: [PATCH 37/43] ctlsock: raise timeout to 10 seconds There was at least one user who hit the earlier 1 second timeout. Raise to 10 seconds which ought to be enough for anyone. Fixes https://github.com/rfjakob/gocryptfs/issues/683 --- ctlsock/ctlsock.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ctlsock/ctlsock.go b/ctlsock/ctlsock.go index 1c440b5..47df9d0 100644 --- a/ctlsock/ctlsock.go +++ b/ctlsock/ctlsock.go @@ -21,9 +21,13 @@ type CtlSock struct { Conn net.Conn } +// There was at least one user who hit the earlier 1 second timeout. Raise to 10 +// seconds which ought to be enough for anyone. +const ctlsockTimeout = 10 * time.Second + // New opens the socket at `socketPath` and stores it in a `CtlSock` object. func New(socketPath string) (*CtlSock, error) { - conn, err := net.DialTimeout("unix", socketPath, 1*time.Second) + conn, err := net.DialTimeout("unix", socketPath, ctlsockTimeout) if err != nil { return nil, err } @@ -32,7 +36,7 @@ func New(socketPath string) (*CtlSock, error) { // Query sends a request to the control socket returns the response. func (c *CtlSock) Query(req *RequestStruct) (*ResponseStruct, error) { - c.Conn.SetDeadline(time.Now().Add(time.Second)) + c.Conn.SetDeadline(time.Now().Add(ctlsockTimeout)) msg, err := json.Marshal(req) if err != nil { return nil, err From bf29c9f99d90343d608c86b536c36abece158591 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 28 Aug 2022 11:02:28 +0200 Subject: [PATCH 38/43] tests: add TestLongnamemax100Reverse Fails right now as reported in https://github.com/rfjakob/gocryptfs/pull/655 --- FAIL: TestLongnamemax100Reverse (0.09s) longnamemax_test.go:104: l=64: should see a longname now longnamemax_test.go:104: l=65: should see a longname now longnamemax_test.go:104: l=66: should see a longname now longnamemax_test.go:104: l=67: should see a longname now longnamemax_test.go:104: l=68: should see a longname now longnamemax_test.go:104: l=69: should see a longname now longnamemax_test.go:104: l=70: should see a longname now longnamemax_test.go:104: l=71: should see a longname now longnamemax_test.go:104: l=72: should see a longname now longnamemax_test.go:104: l=73: should see a longname now longnamemax_test.go:104: l=74: should see a longname now longnamemax_test.go:104: l=75: should see a longname now longnamemax_test.go:104: l=76: should see a longname now longnamemax_test.go:104: l=77: should see a longname now longnamemax_test.go:104: l=78: should see a longname now longnamemax_test.go:104: l=79: should see a longname now longnamemax_test.go:104: l=80: should see a longname now longnamemax_test.go:104: l=81: should see a longname now longnamemax_test.go:104: l=82: should see a longname now longnamemax_test.go:104: l=83: should see a longname now longnamemax_test.go:104: l=84: should see a longname now longnamemax_test.go:104: l=85: should see a longname now longnamemax_test.go:104: l=86: should see a longname now longnamemax_test.go:104: l=87: should see a longname now longnamemax_test.go:104: l=88: should see a longname now longnamemax_test.go:104: l=89: should see a longname now longnamemax_test.go:104: l=90: should see a longname now longnamemax_test.go:104: l=91: should see a longname now longnamemax_test.go:104: l=92: should see a longname now longnamemax_test.go:104: l=93: should see a longname now longnamemax_test.go:104: l=94: should see a longname now longnamemax_test.go:104: l=95: should see a longname now longnamemax_test.go:104: l=96: should see a longname now longnamemax_test.go:104: l=97: should see a longname now longnamemax_test.go:104: l=98: should see a longname now longnamemax_test.go:104: l=99: should see a longname now longnamemax_test.go:104: l=100: should see a longname now longnamemax_test.go:104: l=101: should see a longname now longnamemax_test.go:104: l=102: should see a longname now longnamemax_test.go:104: l=103: should see a longname now longnamemax_test.go:104: l=104: should see a longname now longnamemax_test.go:104: l=105: should see a longname now longnamemax_test.go:104: l=106: should see a longname now longnamemax_test.go:104: l=107: should see a longname now longnamemax_test.go:104: l=108: should see a longname now longnamemax_test.go:104: l=109: should see a longname now longnamemax_test.go:104: l=110: should see a longname now longnamemax_test.go:104: l=111: should see a longname now longnamemax_test.go:104: l=112: should see a longname now longnamemax_test.go:104: l=113: should see a longname now longnamemax_test.go:104: l=114: should see a longname now longnamemax_test.go:104: l=115: should see a longname now longnamemax_test.go:104: l=116: should see a longname now longnamemax_test.go:104: l=117: should see a longname now longnamemax_test.go:104: l=118: should see a longname now longnamemax_test.go:104: l=119: should see a longname now longnamemax_test.go:104: l=120: should see a longname now longnamemax_test.go:104: l=121: should see a longname now longnamemax_test.go:104: l=122: should see a longname now longnamemax_test.go:104: l=123: should see a longname now longnamemax_test.go:104: l=124: should see a longname now longnamemax_test.go:104: l=125: should see a longname now longnamemax_test.go:104: l=126: should see a longname now longnamemax_test.go:104: l=127: should see a longname now longnamemax_test.go:104: l=128: should see a longname now longnamemax_test.go:104: l=129: should see a longname now longnamemax_test.go:104: l=130: should see a longname now longnamemax_test.go:104: l=131: should see a longname now longnamemax_test.go:104: l=132: should see a longname now longnamemax_test.go:104: l=133: should see a longname now longnamemax_test.go:104: l=134: should see a longname now longnamemax_test.go:104: l=135: should see a longname now longnamemax_test.go:104: l=136: should see a longname now longnamemax_test.go:104: l=137: should see a longname now longnamemax_test.go:104: l=138: should see a longname now longnamemax_test.go:104: l=139: should see a longname now longnamemax_test.go:104: l=140: should see a longname now longnamemax_test.go:104: l=141: should see a longname now longnamemax_test.go:104: l=142: should see a longname now longnamemax_test.go:104: l=143: should see a longname now longnamemax_test.go:104: l=144: should see a longname now longnamemax_test.go:104: l=145: should see a longname now longnamemax_test.go:104: l=146: should see a longname now longnamemax_test.go:104: l=147: should see a longname now longnamemax_test.go:104: l=148: should see a longname now longnamemax_test.go:104: l=149: should see a longname now longnamemax_test.go:104: l=150: should see a longname now longnamemax_test.go:104: l=151: should see a longname now longnamemax_test.go:104: l=152: should see a longname now longnamemax_test.go:104: l=153: should see a longname now longnamemax_test.go:104: l=154: should see a longname now longnamemax_test.go:104: l=155: should see a longname now longnamemax_test.go:104: l=156: should see a longname now longnamemax_test.go:104: l=157: should see a longname now longnamemax_test.go:104: l=158: should see a longname now longnamemax_test.go:104: l=159: should see a longname now longnamemax_test.go:104: l=160: should see a longname now longnamemax_test.go:104: l=161: should see a longname now longnamemax_test.go:104: l=162: should see a longname now longnamemax_test.go:104: l=163: should see a longname now longnamemax_test.go:104: l=164: should see a longname now longnamemax_test.go:104: l=165: should see a longname now longnamemax_test.go:104: l=166: should see a longname now longnamemax_test.go:104: l=167: should see a longname now longnamemax_test.go:104: l=168: should see a longname now longnamemax_test.go:104: l=169: should see a longname now longnamemax_test.go:104: l=170: should see a longname now longnamemax_test.go:104: l=171: should see a longname now longnamemax_test.go:104: l=172: should see a longname now longnamemax_test.go:104: l=173: should see a longname now longnamemax_test.go:104: l=174: should see a longname now longnamemax_test.go:104: l=175: should see a longname now FAIL https://github.com/rfjakob/gocryptfs/pull/655 --- tests/cli/longnamemax_test.go | 48 ++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/cli/longnamemax_test.go b/tests/cli/longnamemax_test.go index fc429f6..e44a84e 100644 --- a/tests/cli/longnamemax_test.go +++ b/tests/cli/longnamemax_test.go @@ -16,7 +16,7 @@ import ( // Create & test fs with -longnamemax=100 func TestLongnamemax100(t *testing.T) { - cDir := test_helpers.InitFS(nil, "-longnamemax", "100") + cDir := test_helpers.InitFS(t, "-longnamemax", "100") pDir := cDir + ".mnt" // Check config file sanity @@ -59,3 +59,49 @@ func TestLongnamemax100(t *testing.T) { } } } + +// Create & test fs with -reverse -longnamemax=100 +func TestLongnamemax100Reverse(t *testing.T) { + backingDir := test_helpers.InitFS(t, "-reverse", "-longnamemax", "100") + mntDir := backingDir + ".mnt" + + // Check config file sanity + _, c, err := configfile.LoadAndDecrypt(backingDir+"/"+configfile.ConfReverseName, 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(backingDir, mntDir, "-reverse", "-extpass", "echo test") + defer test_helpers.UnmountPanic(mntDir) + + for l := 1; l <= 255; l++ { + path := backingDir + "/" + strings.Repeat("x", l) + if err := ioutil.WriteFile(path, nil, 0600); err != nil { + t.Fatal(err) + } + matches, err := filepath.Glob(mntDir + "/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) + } + } +} From 1bff80b46cf3cdd2d6934ebf905fca96dde7af97 Mon Sep 17 00:00:00 2001 From: NekoGirlSAIKOU Date: Mon, 25 Apr 2022 01:35:30 +0800 Subject: [PATCH 39/43] Fix invalid -longnamemax for reverse mode --- internal/fusefrontend_reverse/ctlsock_interface.go | 2 +- internal/fusefrontend_reverse/node_dir_ops.go | 2 +- internal/fusefrontend_reverse/root_node.go | 14 +++++++++++--- internal/nametransform/names.go | 4 ++++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/internal/fusefrontend_reverse/ctlsock_interface.go b/internal/fusefrontend_reverse/ctlsock_interface.go index 9266cbf..1bfe1c9 100644 --- a/internal/fusefrontend_reverse/ctlsock_interface.go +++ b/internal/fusefrontend_reverse/ctlsock_interface.go @@ -26,7 +26,7 @@ func (rn *RootNode) EncryptPath(plainPath string) (string, error) { if err != nil { return "", err } - if rn.args.LongNames && len(encryptedPart) > unix.NAME_MAX { + if rn.args.LongNames && (len(encryptedPart) > unix.NAME_MAX || len(encryptedPart) > rn.nameTransform.GetLongNameMax()) { encryptedPart = rn.nameTransform.HashLongName(encryptedPart) } cipherPath = filepath.Join(cipherPath, encryptedPart) diff --git a/internal/fusefrontend_reverse/node_dir_ops.go b/internal/fusefrontend_reverse/node_dir_ops.go index 05e2f49..fdd15ce 100644 --- a/internal/fusefrontend_reverse/node_dir_ops.go +++ b/internal/fusefrontend_reverse/node_dir_ops.go @@ -73,7 +73,7 @@ func (n *Node) Readdir(ctx context.Context) (stream fs.DirStream, errno syscall. entries[i].Name = "___GOCRYPTFS_INVALID_NAME___" continue } - if len(cName) > unix.NAME_MAX { + if len(cName) > unix.NAME_MAX || len(cName) > rn.nameTransform.GetLongNameMax() { cName = rn.nameTransform.HashLongName(cName) dotNameFile := fuse.DirEntry{ Mode: virtualFileMode, diff --git a/internal/fusefrontend_reverse/root_node.go b/internal/fusefrontend_reverse/root_node.go index e15ddb0..cb8b95f 100644 --- a/internal/fusefrontend_reverse/root_node.go +++ b/internal/fusefrontend_reverse/root_node.go @@ -42,6 +42,9 @@ type RootNode struct { // rootDev stores the device number of the backing directory. Used for // --one-file-system. rootDev uint64 + // If a file name length is shorter than shortNameMax, there is no need to + // hash it. + shortNameMax int } // NewRootNode returns an encrypted FUSE overlay filesystem. @@ -50,6 +53,7 @@ type RootNode struct { func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransform.NameTransform) *RootNode { var rootDev uint64 var st syscall.Stat_t + var shortNameMax int if err := syscall.Stat(args.Cipherdir, &st); err != nil { tlog.Warn.Printf("Could not stat backing directory %q: %v", args.Cipherdir, err) if args.OneFileSystem { @@ -60,12 +64,16 @@ func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransf rootDev = uint64(st.Dev) } + shortNameMax = n.GetLongNameMax() * 3 / 4 + shortNameMax = shortNameMax - shortNameMax % 16 - 1 + rn := &RootNode{ args: args, nameTransform: n, contentEnc: c, inoMap: inomap.New(rootDev), rootDev: rootDev, + shortNameMax: shortNameMax, } if len(args.Exclude) > 0 || len(args.ExcludeWildcard) > 0 || len(args.ExcludeFrom) > 0 { rn.excluder = prepareExcluder(args) @@ -87,16 +95,16 @@ func (rn *RootNode) findLongnameParent(fd int, diriv []byte, longname string) (p return } for _, entry := range entries { - if len(entry.Name) <= shortNameMax { + if len(entry.Name) <= rn.shortNameMax { continue } cFullName, err = rn.nameTransform.EncryptName(entry.Name, diriv) if err != nil { continue } - if len(cFullName) <= unix.NAME_MAX { + if len(cFullName) <= unix.NAME_MAX && len(cFullName) <= rn.nameTransform.GetLongNameMax() { // Entry should have been skipped by the shortNameMax check above - log.Panic("logic error or wrong shortNameMax constant?") + log.Panic("logic error or wrong shortNameMax?") } hName := rn.nameTransform.HashLongName(cFullName) if longname == hName { diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index ebe0fb6..0488d13 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -167,3 +167,7 @@ func Dir(path string) string { } return d } + +func (n *NameTransform) GetLongNameMax() int { + return n.longNameMax +} \ No newline at end of file From 4808adc761783e93d6b6d91a9ed4727089f66688 Mon Sep 17 00:00:00 2001 From: NekoGirlSAIKOU Date: Mon, 25 Apr 2022 21:27:08 +0800 Subject: [PATCH 40/43] Add comment to pass Codacy Static Code Analysis --- internal/nametransform/names.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 0488d13..4d56d71 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -168,6 +168,8 @@ func Dir(path string) string { return d } +// GetLongNameMax will return curent `longNameMax`. File name longer than +// this should be hashed. func (n *NameTransform) GetLongNameMax() int { return n.longNameMax } \ No newline at end of file From 003a7fa2e53ac15d2c94a34102ae12b69b23c586 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 28 Aug 2022 11:11:36 +0200 Subject: [PATCH 41/43] make format --- contrib/getdents-debug/getdents/getdents.go | 3 ++- internal/cryptocore/randsize_test.go | 1 + internal/fusefrontend_reverse/root_node.go | 2 +- internal/fusefrontend_reverse/virtualconf.go | 1 - internal/nametransform/names.go | 2 +- internal/stupidgcm/chacha.go | 1 + internal/stupidgcm/chacha_test.go | 1 + internal/stupidgcm/common.go | 1 + internal/stupidgcm/common_test.go | 1 + internal/stupidgcm/gcm.go | 1 + internal/stupidgcm/gcm_test.go | 1 + internal/stupidgcm/locking.go | 1 + internal/stupidgcm/openssl.go | 1 + internal/stupidgcm/without_openssl.go | 1 + internal/stupidgcm/xchacha.go | 1 + internal/stupidgcm/xchacha_test.go | 1 + internal/syscallcompat/getdents_linux.go | 1 + internal/syscallcompat/getdents_test.go | 1 + race.go | 1 + tests/defaults/overlayfs_test.go | 1 + tests/root_test/root_test.go | 3 ++- tests/xattr/xattr_fd_test.go | 3 ++- 22 files changed, 24 insertions(+), 6 deletions(-) diff --git a/contrib/getdents-debug/getdents/getdents.go b/contrib/getdents-debug/getdents/getdents.go index d10ca12..273c240 100644 --- a/contrib/getdents-debug/getdents/getdents.go +++ b/contrib/getdents-debug/getdents/getdents.go @@ -1,4 +1,5 @@ -//+build linux +//go:build linux +// +build linux /* Small tool to try to debug unix.Getdents problems on CIFS mounts diff --git a/internal/cryptocore/randsize_test.go b/internal/cryptocore/randsize_test.go index ed91d4f..7f19820 100644 --- a/internal/cryptocore/randsize_test.go +++ b/internal/cryptocore/randsize_test.go @@ -1,3 +1,4 @@ +//go:build go1.7 // +build go1.7 // ^^^^^^^^^^^^ we use the "sub-benchmark" feature that was added in Go 1.7 diff --git a/internal/fusefrontend_reverse/root_node.go b/internal/fusefrontend_reverse/root_node.go index cb8b95f..8a2afd9 100644 --- a/internal/fusefrontend_reverse/root_node.go +++ b/internal/fusefrontend_reverse/root_node.go @@ -65,7 +65,7 @@ func NewRootNode(args fusefrontend.Args, c *contentenc.ContentEnc, n *nametransf } shortNameMax = n.GetLongNameMax() * 3 / 4 - shortNameMax = shortNameMax - shortNameMax % 16 - 1 + shortNameMax = shortNameMax - shortNameMax%16 - 1 rn := &RootNode{ args: args, diff --git a/internal/fusefrontend_reverse/virtualconf.go b/internal/fusefrontend_reverse/virtualconf.go index e8cc080..3643fad 100644 --- a/internal/fusefrontend_reverse/virtualconf.go +++ b/internal/fusefrontend_reverse/virtualconf.go @@ -38,7 +38,6 @@ func (n *VirtualConfNode) Getattr(ctx context.Context, fh fs.FileHandle, out *fu return 0 } - // Check that we have implemented the fs.File* interfaces var _ = (fs.FileReader)((*VirtualConfFile)(nil)) var _ = (fs.FileReleaser)((*VirtualConfFile)(nil)) diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 4d56d71..7a983a0 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -172,4 +172,4 @@ func Dir(path string) string { // this should be hashed. func (n *NameTransform) GetLongNameMax() int { return n.longNameMax -} \ No newline at end of file +} diff --git a/internal/stupidgcm/chacha.go b/internal/stupidgcm/chacha.go index e09ed0b..de0c2e8 100644 --- a/internal/stupidgcm/chacha.go +++ b/internal/stupidgcm/chacha.go @@ -1,3 +1,4 @@ +//go:build !without_openssl // +build !without_openssl package stupidgcm diff --git a/internal/stupidgcm/chacha_test.go b/internal/stupidgcm/chacha_test.go index 5f803aa..542ff15 100644 --- a/internal/stupidgcm/chacha_test.go +++ b/internal/stupidgcm/chacha_test.go @@ -1,3 +1,4 @@ +//go:build !without_openssl // +build !without_openssl package stupidgcm diff --git a/internal/stupidgcm/common.go b/internal/stupidgcm/common.go index bb100eb..d88dc62 100644 --- a/internal/stupidgcm/common.go +++ b/internal/stupidgcm/common.go @@ -1,3 +1,4 @@ +//go:build !without_openssl // +build !without_openssl package stupidgcm diff --git a/internal/stupidgcm/common_test.go b/internal/stupidgcm/common_test.go index e5b14a4..7f38e90 100644 --- a/internal/stupidgcm/common_test.go +++ b/internal/stupidgcm/common_test.go @@ -1,3 +1,4 @@ +//go:build cgo && !without_openssl // +build cgo,!without_openssl package stupidgcm diff --git a/internal/stupidgcm/gcm.go b/internal/stupidgcm/gcm.go index 00819dd..2e5aac4 100644 --- a/internal/stupidgcm/gcm.go +++ b/internal/stupidgcm/gcm.go @@ -1,3 +1,4 @@ +//go:build !without_openssl // +build !without_openssl package stupidgcm diff --git a/internal/stupidgcm/gcm_test.go b/internal/stupidgcm/gcm_test.go index 73668fa..c730a87 100644 --- a/internal/stupidgcm/gcm_test.go +++ b/internal/stupidgcm/gcm_test.go @@ -1,3 +1,4 @@ +//go:build !without_openssl // +build !without_openssl // We compare against Go's built-in GCM implementation. Since stupidgcm only diff --git a/internal/stupidgcm/locking.go b/internal/stupidgcm/locking.go index 68ab509..04cf232 100644 --- a/internal/stupidgcm/locking.go +++ b/internal/stupidgcm/locking.go @@ -1,3 +1,4 @@ +//go:build !without_openssl // +build !without_openssl package stupidgcm diff --git a/internal/stupidgcm/openssl.go b/internal/stupidgcm/openssl.go index b46fba8..8c950f8 100644 --- a/internal/stupidgcm/openssl.go +++ b/internal/stupidgcm/openssl.go @@ -1,3 +1,4 @@ +//go:build !without_openssl // +build !without_openssl package stupidgcm diff --git a/internal/stupidgcm/without_openssl.go b/internal/stupidgcm/without_openssl.go index 42604de..fcef793 100644 --- a/internal/stupidgcm/without_openssl.go +++ b/internal/stupidgcm/without_openssl.go @@ -1,3 +1,4 @@ +//go:build without_openssl // +build without_openssl package stupidgcm diff --git a/internal/stupidgcm/xchacha.go b/internal/stupidgcm/xchacha.go index ca740e4..3c121ba 100644 --- a/internal/stupidgcm/xchacha.go +++ b/internal/stupidgcm/xchacha.go @@ -1,3 +1,4 @@ +//go:build !without_openssl // +build !without_openssl // Copyright 2018 The Go Authors. All rights reserved. diff --git a/internal/stupidgcm/xchacha_test.go b/internal/stupidgcm/xchacha_test.go index fdea8b5..676a023 100644 --- a/internal/stupidgcm/xchacha_test.go +++ b/internal/stupidgcm/xchacha_test.go @@ -1,3 +1,4 @@ +//go:build !without_openssl // +build !without_openssl package stupidgcm diff --git a/internal/syscallcompat/getdents_linux.go b/internal/syscallcompat/getdents_linux.go index cedb463..da3868f 100644 --- a/internal/syscallcompat/getdents_linux.go +++ b/internal/syscallcompat/getdents_linux.go @@ -1,3 +1,4 @@ +//go:build linux // +build linux package syscallcompat diff --git a/internal/syscallcompat/getdents_test.go b/internal/syscallcompat/getdents_test.go index a6f41ca..eb670d6 100644 --- a/internal/syscallcompat/getdents_test.go +++ b/internal/syscallcompat/getdents_test.go @@ -1,3 +1,4 @@ +//go:build linux // +build linux package syscallcompat diff --git a/race.go b/race.go index a17501a..c185364 100644 --- a/race.go +++ b/race.go @@ -1,3 +1,4 @@ +//go:build race // +build race package main diff --git a/tests/defaults/overlayfs_test.go b/tests/defaults/overlayfs_test.go index 7b41de4..8cb773d 100644 --- a/tests/defaults/overlayfs_test.go +++ b/tests/defaults/overlayfs_test.go @@ -1,3 +1,4 @@ +//go:build linux // +build linux package defaults diff --git a/tests/root_test/root_test.go b/tests/root_test/root_test.go index 7d1e296..c4ed7db 100644 --- a/tests/root_test/root_test.go +++ b/tests/root_test/root_test.go @@ -1,4 +1,5 @@ -//+build linux +//go:build linux +// +build linux // Package root_test contains tests that need root // permissions to run diff --git a/tests/xattr/xattr_fd_test.go b/tests/xattr/xattr_fd_test.go index 76fc3ab..f3586cf 100644 --- a/tests/xattr/xattr_fd_test.go +++ b/tests/xattr/xattr_fd_test.go @@ -1,4 +1,5 @@ -//+build linux +//go:build linux +// +build linux // Darwin does not support Fgetxattr and friends! From 6677d8f1d50a2e51947dac137d126d37614a6255 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 28 Aug 2022 12:03:34 +0200 Subject: [PATCH 42/43] Replace remaining golang.org/x/crypto/ssh/terminal ref with golang.org/x/term Fixes https://github.com/rfjakob/gocryptfs/issues/681 Fixes 2a25c3a8fda1f0918fd76687561b1a9c615298b9 --- go.mod | 2 +- go.sum | 6 ++---- internal/readpassword/read.go | 10 +++++----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/go.mod b/go.mod index 4b1aff3..754a667 100644 --- a/go.mod +++ b/go.mod @@ -16,5 +16,5 @@ require ( golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 - golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b + golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 ) diff --git a/go.sum b/go.sum index a186360..4167388 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/hanwen/go-fuse/v2 v2.1.1-0.20210825171523-3ab5d95a30ae h1:4CB6T4YTUVvnro5ba8ju1QCbOuyGAeF3vvKlo50EJ4k= -github.com/hanwen/go-fuse/v2 v2.1.1-0.20210825171523-3ab5d95a30ae/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc= github.com/hanwen/go-fuse/v2 v2.1.1-0.20211219085202-934a183ed914 h1:hGXMxS1wTE4y+f7iBqFArrJ6X8QozHnEdnVzGZI9Ywc= github.com/hanwen/go-fuse/v2 v2.1.1-0.20211219085202-934a183ed914/go.mod h1:B1nGE/6RBFyBRC1RRnf23UpwCdyJ31eukw34oAKukAc= github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115 h1:YuDUUFNM21CAbyPOpOP8BicaTD/0klJEKt5p8yuw+uY= @@ -43,8 +41,8 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 h1:c8PlLMqBbOHoqtjteWm5/kbe6rNY2pbRfbIMVnepueo= golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= +golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/readpassword/read.go b/internal/readpassword/read.go index 498d09b..3ad3bb4 100644 --- a/internal/readpassword/read.go +++ b/internal/readpassword/read.go @@ -9,7 +9,7 @@ import ( "os/exec" "strings" - "golang.org/x/crypto/ssh/terminal" + "golang.org/x/term" "github.com/rfjakob/gocryptfs/v2/internal/tlog" ) @@ -31,7 +31,7 @@ func Once(extpass []string, passfile []string, prompt string) ([]byte, error) { if prompt == "" { prompt = "Password" } - if !terminal.IsTerminal(int(os.Stdin.Fd())) { + if !term.IsTerminal(int(os.Stdin.Fd())) { return readPasswordStdin(prompt) } return readPasswordTerminal(prompt + ": ") @@ -46,7 +46,7 @@ func Twice(extpass []string, passfile []string) ([]byte, error) { if len(extpass) != 0 { return readPasswordExtpass(extpass) } - if !terminal.IsTerminal(int(os.Stdin.Fd())) { + if !term.IsTerminal(int(os.Stdin.Fd())) { return readPasswordStdin("Password") } p1, err := readPasswordTerminal("Password: ") @@ -72,8 +72,8 @@ func Twice(extpass []string, passfile []string) ([]byte, error) { func readPasswordTerminal(prompt string) ([]byte, error) { fd := int(os.Stdin.Fd()) fmt.Fprintf(os.Stderr, prompt) - // terminal.ReadPassword removes the trailing newline - p, err := terminal.ReadPassword(fd) + // term.ReadPassword removes the trailing newline + p, err := term.ReadPassword(fd) if err != nil { return nil, fmt.Errorf("Could not read password from terminal: %v\n", err) } From 4bd1a8db4cda06fa3f2f2842fda5ae489b37d8c7 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sun, 28 Aug 2022 12:04:44 +0200 Subject: [PATCH 43/43] README: Update Changelog for v2.3 --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 35c2bda..cdfac89 100644 --- a/README.md +++ b/README.md @@ -196,11 +196,15 @@ RM: 2,367 Changelog --------- -#### vNEXT +#### v2.3, 2022-08-28 * 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. +* Support for [`NO_COLOR`](https://no-color.org/) env variable ([#617](https://github.com/rfjakob/gocryptfs/issues/617)) +* Fix `-force_owner` not not affecting socket files ([#629](https://github.com/rfjakob/gocryptfs/issues/629) +* MacOS: fix inaccessible `gocryptfs.conf` in reverse mode ([commit](https://github.com/rfjakob/gocryptfs/commit/c9e4e4f74150d2734496e90a4c442a17b79f52c1)) +* Raise ctlsock operation timeout from 1 to 10 seconds ([#683](https://github.com/rfjakob/gocryptfs/issues/683)) #### v2.2.1, 2021-10-20 * Fix `-force_owner` only taking effect after 2 seconds ([#609](https://github.com/rfjakob/gocryptfs/issues/609)).