From d14c9340d6fb473e9837e91db8b6e869c37ad8e5 Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Thu, 21 Oct 2021 15:58:19 +0200 Subject: [PATCH] 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) + } + } +}