From 97d8340bd81ddd60baac598d3e25ebfb4decb50c Mon Sep 17 00:00:00 2001 From: Jakob Unterwurzacher Date: Sat, 21 Aug 2021 21:43:26 +0200 Subject: [PATCH] configfile: add Validate() function, support FlagXChaCha20Poly1305 We used to do validation using lists of mandatory feature flags. With the introduction of XChaCha20Poly1305, this became too simplistic, as it uses a different IV length, hence disabling GCMIV128. Add a dedicated function, Validate(), with open-coded validation logic. The validation and creation logic also gets XChaCha20Poly1305 support, and gocryptfs -init -xchacha now writes the flag into gocryptfs.conf. --- init_dir.go | 3 +- internal/configfile/config_file.go | 93 ++++++++++++---------------- internal/configfile/config_test.go | 5 +- internal/configfile/feature_flags.go | 17 +---- internal/configfile/scrypt.go | 25 ++++---- internal/configfile/validate.go | 67 ++++++++++++++++++++ 6 files changed, 126 insertions(+), 84 deletions(-) create mode 100644 internal/configfile/validate.go diff --git a/init_dir.go b/init_dir.go index 8f11351..c6539c8 100644 --- a/init_dir.go +++ b/init_dir.go @@ -96,7 +96,8 @@ func initDir(args *argContainer) { Devrandom: args.devrandom, Fido2CredentialID: fido2CredentialID, Fido2HmacSalt: fido2HmacSalt, - DeterministicNames: args.deterministic_names}) + DeterministicNames: args.deterministic_names, + XChaCha20Poly1305: args.xchacha}) if err != nil { tlog.Fatal.Println(err) os.Exit(exitcodes.WriteConf) diff --git a/internal/configfile/config_file.go b/internal/configfile/config_file.go index d457db6..dba6c47 100644 --- a/internal/configfile/config_file.go +++ b/internal/configfile/config_file.go @@ -88,40 +88,52 @@ type CreateArgs struct { Fido2CredentialID []byte Fido2HmacSalt []byte DeterministicNames bool + XChaCha20Poly1305 bool } // Create - create a new config with a random key encrypted with // "Password" and write it to "Filename". // Uses scrypt with cost parameter "LogN". func Create(args *CreateArgs) error { - var cf ConfFile - cf.filename = args.Filename - cf.Creator = args.Creator - cf.Version = contentenc.CurrentVersion - - // Set feature flags - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagGCMIV128]) - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagHKDF]) + cf := ConfFile{ + filename: args.Filename, + Creator: args.Creator, + Version: contentenc.CurrentVersion, + } + // Feature flags + cf.setFeatureFlag(FlagHKDF) + if args.XChaCha20Poly1305 { + cf.setFeatureFlag(FlagXChaCha20Poly1305) + } else { + // 128-bit IVs are mandatory for AES-GCM (default is 96!) and AES-SIV, + // XChaCha20Poly1305 uses even an even longer IV of 192 bits. + cf.setFeatureFlag(FlagGCMIV128) + } if args.PlaintextNames { - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagPlaintextNames]) + cf.setFeatureFlag(FlagPlaintextNames) } else { if !args.DeterministicNames { - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagDirIV]) + cf.setFeatureFlag(FlagDirIV) } - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagEMENames]) - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagLongNames]) - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagRaw64]) + cf.setFeatureFlag(FlagEMENames) + cf.setFeatureFlag(FlagLongNames) + cf.setFeatureFlag(FlagRaw64) } if args.AESSIV { - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagAESSIV]) + cf.setFeatureFlag(FlagAESSIV) } if len(args.Fido2CredentialID) > 0 { - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagFIDO2]) + cf.setFeatureFlag(FlagFIDO2) cf.FIDO2 = &FIDO2Params{ CredentialID: args.Fido2CredentialID, HMACSalt: args.Fido2HmacSalt, } } + // Catch bugs and invalid cli flag combinations early + cf.ScryptObject = NewScryptKDF(args.LogN) + if err := cf.Validate(); err != nil { + return err + } { // Generate new random master key var key []byte @@ -193,50 +205,22 @@ func Load(filename string) (*ConfFile, error) { return nil, err } - if cf.Version != contentenc.CurrentVersion { - return nil, fmt.Errorf("Unsupported on-disk format %d", cf.Version) - } - - // Check that all set feature flags are known - for _, flag := range cf.FeatureFlags { - if !cf.isFeatureFlagKnown(flag) { - return nil, fmt.Errorf("Unsupported feature flag %q", flag) - } - } - - // Check that all required feature flags are set - var requiredFlags []flagIota - if cf.IsFeatureFlagSet(FlagPlaintextNames) { - requiredFlags = requiredFlagsPlaintextNames - } else { - requiredFlags = requiredFlagsNormal - } - deprecatedFs := false - for _, i := range requiredFlags { - if !cf.IsFeatureFlagSet(i) { - fmt.Fprintf(os.Stderr, "Required feature flag %q is missing\n", knownFlags[i]) - deprecatedFs = true - } - } - if deprecatedFs { - fmt.Fprintf(os.Stderr, tlog.ColorYellow+` - The filesystem was created by gocryptfs v0.6 or earlier. This version of - gocryptfs can no longer mount the filesystem. - Please download gocryptfs v0.11 and upgrade your filesystem, - see https://github.com/rfjakob/gocryptfs/v2/wiki/Upgrading for instructions. - - If you have trouble upgrading, join the discussion at - https://github.com/rfjakob/gocryptfs/v2/issues/29 . - -`+tlog.ColorReset) - - return nil, exitcodes.NewErr("Deprecated filesystem", exitcodes.DeprecatedFS) + if err := cf.Validate(); err != nil { + return nil, exitcodes.NewErr(err.Error(), exitcodes.DeprecatedFS) } // All good return &cf, nil } +func (cf *ConfFile) setFeatureFlag(flag flagIota) { + if cf.IsFeatureFlagSet(flag) { + // Already set, ignore + return + } + cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[flag]) +} + // DecryptMasterKey decrypts the masterkey stored in cf.EncryptedKey using // password. func (cf *ConfFile) DecryptMasterKey(password []byte) (masterkey []byte, err error) { @@ -293,6 +277,9 @@ func (cf *ConfFile) EncryptKey(key []byte, password []byte, logN int) { // then rename over "filename". // This way a password change atomically replaces the file. func (cf *ConfFile) WriteFile() error { + if err := cf.Validate(); err != nil { + return err + } tmp := cf.filename + ".tmp" // 0400 permissions: gocryptfs.conf should be kept secret and never be written to. fd, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400) diff --git a/internal/configfile/config_test.go b/internal/configfile/config_test.go index 6986888..021b6c1 100644 --- a/internal/configfile/config_test.go +++ b/internal/configfile/config_test.go @@ -152,15 +152,14 @@ func TestIsFeatureFlagKnown(t *testing.T) { testKnownFlags = append(testKnownFlags, f) } - var cf ConfFile for _, f := range testKnownFlags { - if !cf.isFeatureFlagKnown(f) { + if !isFeatureFlagKnown(f) { t.Errorf("flag %q should be known", f) } } f := "StrangeFeatureFlag" - if cf.isFeatureFlagKnown(f) { + if isFeatureFlagKnown(f) { t.Errorf("flag %q should be NOT known", f) } } diff --git a/internal/configfile/feature_flags.go b/internal/configfile/feature_flags.go index be5616f..e28abd6 100644 --- a/internal/configfile/feature_flags.go +++ b/internal/configfile/feature_flags.go @@ -11,7 +11,8 @@ const ( // This flag is mandatory since gocryptfs v1.0. FlagEMENames // FlagGCMIV128 indicates 128-bit GCM IVs. - // This flag is mandatory since gocryptfs v1.0. + // This flag is mandatory since gocryptfs v1.0, + // except when XChaCha20Poly1305 is used. FlagGCMIV128 // FlagLongNames allows file names longer than 176 bytes. FlagLongNames @@ -46,20 +47,8 @@ var knownFlags = map[flagIota]string{ FlagXChaCha20Poly1305: "XChaCha20Poly1305", } -// Filesystems that do not have these feature flags set are deprecated. -var requiredFlagsNormal = []flagIota{ - FlagEMENames, - FlagGCMIV128, -} - -// Filesystems without filename encryption obviously don't have or need the -// filename related feature flags. -var requiredFlagsPlaintextNames = []flagIota{ - FlagGCMIV128, -} - // isFeatureFlagKnown verifies that we understand a feature flag. -func (cf *ConfFile) isFeatureFlagKnown(flag string) bool { +func isFeatureFlagKnown(flag string) bool { for _, knownFlag := range knownFlags { if knownFlag == flag { return true diff --git a/internal/configfile/scrypt.go b/internal/configfile/scrypt.go index 7ac822e..0ce8777 100644 --- a/internal/configfile/scrypt.go +++ b/internal/configfile/scrypt.go @@ -1,6 +1,7 @@ package configfile import ( + "fmt" "log" "math" "os" @@ -62,8 +63,10 @@ func NewScryptKDF(logN int) ScryptKDF { // DeriveKey returns a new key from a supplied password. func (s *ScryptKDF) DeriveKey(pw []byte) []byte { - s.validateParams() - + if err := s.validateParams(); err != nil { + tlog.Fatal.Println(err.Error()) + os.Exit(exitcodes.ScryptParams) + } k, err := scrypt.Key(pw, s.Salt, s.N, s.R, s.P, s.KeyLen) if err != nil { log.Panicf("DeriveKey failed: %v", err) @@ -81,26 +84,22 @@ func (s *ScryptKDF) LogN() int { // If not, it exists with an error message. // This makes sure we do not get weak parameters passed through a // rougue gocryptfs.conf. -func (s *ScryptKDF) validateParams() { +func (s *ScryptKDF) validateParams() error { minN := 1 << scryptMinLogN if s.N < minN { - tlog.Fatal.Println("Fatal: scryptn below 10 is too low to make sense") - os.Exit(exitcodes.ScryptParams) + return fmt.Errorf("Fatal: scryptn below 10 is too low to make sense") } if s.R < scryptMinR { - tlog.Fatal.Printf("Fatal: scrypt parameter R below minimum: value=%d, min=%d", s.R, scryptMinR) - os.Exit(exitcodes.ScryptParams) + return fmt.Errorf("Fatal: scrypt parameter R below minimum: value=%d, min=%d", s.R, scryptMinR) } if s.P < scryptMinP { - tlog.Fatal.Printf("Fatal: scrypt parameter P below minimum: value=%d, min=%d", s.P, scryptMinP) - os.Exit(exitcodes.ScryptParams) + return fmt.Errorf("Fatal: scrypt parameter P below minimum: value=%d, min=%d", s.P, scryptMinP) } if len(s.Salt) < scryptMinSaltLen { - tlog.Fatal.Printf("Fatal: scrypt salt length below minimum: value=%d, min=%d", len(s.Salt), scryptMinSaltLen) - os.Exit(exitcodes.ScryptParams) + return fmt.Errorf("Fatal: scrypt salt length below minimum: value=%d, min=%d", len(s.Salt), scryptMinSaltLen) } if s.KeyLen < cryptocore.KeyLen { - tlog.Fatal.Printf("Fatal: scrypt parameter KeyLen below minimum: value=%d, min=%d", s.KeyLen, cryptocore.KeyLen) - os.Exit(exitcodes.ScryptParams) + return fmt.Errorf("Fatal: scrypt parameter KeyLen below minimum: value=%d, min=%d", s.KeyLen, cryptocore.KeyLen) } + return nil } diff --git a/internal/configfile/validate.go b/internal/configfile/validate.go new file mode 100644 index 0000000..511f704 --- /dev/null +++ b/internal/configfile/validate.go @@ -0,0 +1,67 @@ +package configfile + +import ( + "fmt" + + "github.com/rfjakob/gocryptfs/v2/internal/contentenc" +) + +// Validate that the combination of settings makes sense and is supported +func (cf *ConfFile) Validate() error { + if cf.Version != contentenc.CurrentVersion { + return fmt.Errorf("Unsupported on-disk format %d", cf.Version) + } + // scrypt params ok? + if err := cf.ScryptObject.validateParams(); err != nil { + return err + } + // All feature flags that are in the config file are known? + for _, flag := range cf.FeatureFlags { + if !isFeatureFlagKnown(flag) { + return fmt.Errorf("Unknown feature flag %q", flag) + } + } + // File content encryption + { + switch { + case 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(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(FlagGCMIV128) { + return fmt.Errorf("AES-GCM requires GCMIV128 feature flag") + } + } + } + // Filename encryption + { + switch { + case cf.IsFeatureFlagSet(FlagPlaintextNames) && cf.IsFeatureFlagSet(FlagEMENames): + return fmt.Errorf("Can't have both PlaintextNames and EMENames feature flags") + case cf.IsFeatureFlagSet(FlagPlaintextNames): + if cf.IsFeatureFlagSet(FlagDirIV) { + return fmt.Errorf("PlaintextNames conflicts with DirIV feature flag") + } + if cf.IsFeatureFlagSet(FlagLongNames) { + return fmt.Errorf("PlaintextNames conflicts with LongNames feature flag") + } + if cf.IsFeatureFlagSet(FlagRaw64) { + return fmt.Errorf("PlaintextNames conflicts with Raw64 feature flag") + } + case cf.IsFeatureFlagSet(FlagEMENames): + // All combinations of DirIV, LongNames, Raw64 allowed + } + } + return nil +}