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.
This commit is contained in:
Jakob Unterwurzacher 2021-08-21 21:43:26 +02:00
parent 4764a9bde0
commit 97d8340bd8
6 changed files with 126 additions and 84 deletions

View File

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

View File

@ -88,40 +88,52 @@ type CreateArgs struct {
Fido2CredentialID []byte Fido2CredentialID []byte
Fido2HmacSalt []byte Fido2HmacSalt []byte
DeterministicNames bool DeterministicNames bool
XChaCha20Poly1305 bool
} }
// Create - create a new config with a random key encrypted with // Create - create a new config with a random key encrypted with
// "Password" and write it to "Filename". // "Password" and write it to "Filename".
// Uses scrypt with cost parameter "LogN". // Uses scrypt with cost parameter "LogN".
func Create(args *CreateArgs) error { func Create(args *CreateArgs) error {
var cf ConfFile cf := ConfFile{
cf.filename = args.Filename filename: args.Filename,
cf.Creator = args.Creator Creator: args.Creator,
cf.Version = contentenc.CurrentVersion Version: contentenc.CurrentVersion,
}
// Set feature flags // Feature flags
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagGCMIV128]) cf.setFeatureFlag(FlagHKDF)
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[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 { if args.PlaintextNames {
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagPlaintextNames]) cf.setFeatureFlag(FlagPlaintextNames)
} else { } else {
if !args.DeterministicNames { if !args.DeterministicNames {
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagDirIV]) cf.setFeatureFlag(FlagDirIV)
} }
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagEMENames]) cf.setFeatureFlag(FlagEMENames)
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagLongNames]) cf.setFeatureFlag(FlagLongNames)
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagRaw64]) cf.setFeatureFlag(FlagRaw64)
} }
if args.AESSIV { if args.AESSIV {
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagAESSIV]) cf.setFeatureFlag(FlagAESSIV)
} }
if len(args.Fido2CredentialID) > 0 { if len(args.Fido2CredentialID) > 0 {
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagFIDO2]) cf.setFeatureFlag(FlagFIDO2)
cf.FIDO2 = &FIDO2Params{ cf.FIDO2 = &FIDO2Params{
CredentialID: args.Fido2CredentialID, CredentialID: args.Fido2CredentialID,
HMACSalt: args.Fido2HmacSalt, 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 // Generate new random master key
var key []byte var key []byte
@ -193,50 +205,22 @@ func Load(filename string) (*ConfFile, error) {
return nil, err return nil, err
} }
if cf.Version != contentenc.CurrentVersion { if err := cf.Validate(); err != nil {
return nil, fmt.Errorf("Unsupported on-disk format %d", cf.Version) return nil, exitcodes.NewErr(err.Error(), exitcodes.DeprecatedFS)
}
// 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)
} }
// All good // All good
return &cf, nil 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 // DecryptMasterKey decrypts the masterkey stored in cf.EncryptedKey using
// password. // password.
func (cf *ConfFile) DecryptMasterKey(password []byte) (masterkey []byte, err error) { 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". // then rename over "filename".
// This way a password change atomically replaces the file. // This way a password change atomically replaces the file.
func (cf *ConfFile) WriteFile() error { func (cf *ConfFile) WriteFile() error {
if err := cf.Validate(); err != nil {
return err
}
tmp := cf.filename + ".tmp" tmp := cf.filename + ".tmp"
// 0400 permissions: gocryptfs.conf should be kept secret and never be written to. // 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) fd, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0400)

View File

@ -152,15 +152,14 @@ func TestIsFeatureFlagKnown(t *testing.T) {
testKnownFlags = append(testKnownFlags, f) testKnownFlags = append(testKnownFlags, f)
} }
var cf ConfFile
for _, f := range testKnownFlags { for _, f := range testKnownFlags {
if !cf.isFeatureFlagKnown(f) { if !isFeatureFlagKnown(f) {
t.Errorf("flag %q should be known", f) t.Errorf("flag %q should be known", f)
} }
} }
f := "StrangeFeatureFlag" f := "StrangeFeatureFlag"
if cf.isFeatureFlagKnown(f) { if isFeatureFlagKnown(f) {
t.Errorf("flag %q should be NOT known", f) t.Errorf("flag %q should be NOT known", f)
} }
} }

View File

@ -11,7 +11,8 @@ const (
// This flag is mandatory since gocryptfs v1.0. // This flag is mandatory since gocryptfs v1.0.
FlagEMENames FlagEMENames
// FlagGCMIV128 indicates 128-bit GCM IVs. // 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 FlagGCMIV128
// FlagLongNames allows file names longer than 176 bytes. // FlagLongNames allows file names longer than 176 bytes.
FlagLongNames FlagLongNames
@ -46,20 +47,8 @@ var knownFlags = map[flagIota]string{
FlagXChaCha20Poly1305: "XChaCha20Poly1305", 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. // isFeatureFlagKnown verifies that we understand a feature flag.
func (cf *ConfFile) isFeatureFlagKnown(flag string) bool { func isFeatureFlagKnown(flag string) bool {
for _, knownFlag := range knownFlags { for _, knownFlag := range knownFlags {
if knownFlag == flag { if knownFlag == flag {
return true return true

View File

@ -1,6 +1,7 @@
package configfile package configfile
import ( import (
"fmt"
"log" "log"
"math" "math"
"os" "os"
@ -62,8 +63,10 @@ func NewScryptKDF(logN int) ScryptKDF {
// DeriveKey returns a new key from a supplied password. // DeriveKey returns a new key from a supplied password.
func (s *ScryptKDF) DeriveKey(pw []byte) []byte { 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) k, err := scrypt.Key(pw, s.Salt, s.N, s.R, s.P, s.KeyLen)
if err != nil { if err != nil {
log.Panicf("DeriveKey failed: %v", err) log.Panicf("DeriveKey failed: %v", err)
@ -81,26 +84,22 @@ func (s *ScryptKDF) LogN() int {
// If not, it exists with an error message. // If not, it exists with an error message.
// This makes sure we do not get weak parameters passed through a // This makes sure we do not get weak parameters passed through a
// rougue gocryptfs.conf. // rougue gocryptfs.conf.
func (s *ScryptKDF) validateParams() { func (s *ScryptKDF) validateParams() error {
minN := 1 << scryptMinLogN minN := 1 << scryptMinLogN
if s.N < minN { if s.N < minN {
tlog.Fatal.Println("Fatal: scryptn below 10 is too low to make sense") return fmt.Errorf("Fatal: scryptn below 10 is too low to make sense")
os.Exit(exitcodes.ScryptParams)
} }
if s.R < scryptMinR { if s.R < scryptMinR {
tlog.Fatal.Printf("Fatal: scrypt parameter R below minimum: value=%d, min=%d", s.R, scryptMinR) return fmt.Errorf("Fatal: scrypt parameter R below minimum: value=%d, min=%d", s.R, scryptMinR)
os.Exit(exitcodes.ScryptParams)
} }
if s.P < scryptMinP { if s.P < scryptMinP {
tlog.Fatal.Printf("Fatal: scrypt parameter P below minimum: value=%d, min=%d", s.P, scryptMinP) return fmt.Errorf("Fatal: scrypt parameter P below minimum: value=%d, min=%d", s.P, scryptMinP)
os.Exit(exitcodes.ScryptParams)
} }
if len(s.Salt) < scryptMinSaltLen { if len(s.Salt) < scryptMinSaltLen {
tlog.Fatal.Printf("Fatal: scrypt salt length below minimum: value=%d, min=%d", len(s.Salt), scryptMinSaltLen) return fmt.Errorf("Fatal: scrypt salt length below minimum: value=%d, min=%d", len(s.Salt), scryptMinSaltLen)
os.Exit(exitcodes.ScryptParams)
} }
if s.KeyLen < cryptocore.KeyLen { if s.KeyLen < cryptocore.KeyLen {
tlog.Fatal.Printf("Fatal: scrypt parameter KeyLen below minimum: value=%d, min=%d", s.KeyLen, cryptocore.KeyLen) return fmt.Errorf("Fatal: scrypt parameter KeyLen below minimum: value=%d, min=%d", s.KeyLen, cryptocore.KeyLen)
os.Exit(exitcodes.ScryptParams)
} }
return nil
} }

View File

@ -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
}