diff --git a/directory.go b/directory.go index 8ff2659..83fb477 100644 --- a/directory.go +++ b/directory.go @@ -60,7 +60,7 @@ func gcf_list_dir(sessionID int, dirName string) (*C.char, *C.int, C.int) { var cachedIV []byte if !OpenedVolumes[sessionID].plainTextNames { // Read the DirIV from disk - cachedIV, err = nametransform.ReadDirIVAt(fd) + cachedIV, err = volume.nameTransform.ReadDirIVAt(fd) if err != nil { return nil, nil, 0 } diff --git a/helpers.go b/helpers.go index 51b3437..bf60ec7 100644 --- a/helpers.go +++ b/helpers.go @@ -50,7 +50,7 @@ func (volume *Volume) openBackingDir(relPath string) (dirfd int, cName string, e // Walk the directory tree parts := strings.Split(relPath, "/") for i, name := range parts { - iv, err := nametransform.ReadDirIVAt(dirfd) + iv, err := volume.nameTransform.ReadDirIVAt(dirfd) if err != nil { syscall.Close(dirfd) return -1, "", err @@ -112,7 +112,7 @@ func (volume *Volume) prepareAtSyscall(path string) (dirfd int, cName string, er // Cache store if !volume.plainTextNames { // TODO: openBackingDir already calls ReadDirIVAt(). Avoid duplicate work? - iv, err := nametransform.ReadDirIVAt(dirfd) + iv, err := volume.nameTransform.ReadDirIVAt(dirfd) if err != nil { syscall.Close(dirfd) return -1, "", err diff --git a/internal/configfile/config_file.go b/internal/configfile/config_file.go index d50b71b..ffc41de 100644 --- a/internal/configfile/config_file.go +++ b/internal/configfile/config_file.go @@ -5,9 +5,7 @@ package configfile import ( "encoding/json" "fmt" - "io" "io/ioutil" - "log" "syscall" "os" @@ -60,64 +58,66 @@ type ConfFile struct { filename string } -// randBytesDevRandom gets "n" random bytes from /dev/random or panics -func randBytesDevRandom(n int) []byte { - f, err := os.Open("/dev/random") - if err != nil { - log.Panic("Failed to open /dev/random: " + err.Error()) - } - defer f.Close() - b := make([]byte, n) - _, err = io.ReadFull(f, b) - if err != nil { - log.Panic("Failed to read random bytes: " + err.Error()) - } - return b +// CreateArgs exists because the argument list to Create became too long. +type CreateArgs struct { + Filename string + Password []byte + PlaintextNames bool + LogN int + Creator string + AESSIV bool + 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(filename string, password []byte, plaintextNames bool, - logN int, creator string, aessiv bool, devrandom bool, fido2CredentialID []byte, fido2HmacSalt []byte) error { - var cf ConfFile - cf.filename = filename - cf.Creator = creator - cf.Version = contentenc.CurrentVersion - - // Set feature flags - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagGCMIV128]) - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagHKDF]) - if plaintextNames { - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagPlaintextNames]) +// "Password" and write it to "Filename". +// Uses scrypt with cost parameter "LogN". +func Create(args *CreateArgs) error { + cf := ConfFile{ + filename: args.Filename, + Creator: args.Creator, + Version: contentenc.CurrentVersion, + } + // Feature flags + cf.setFeatureFlag(FlagHKDF) + if args.XChaCha20Poly1305 { + cf.setFeatureFlag(FlagXChaCha20Poly1305) } else { - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagDirIV]) - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagEMENames]) - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagLongNames]) - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagRaw64]) + // 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 aessiv { - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagAESSIV]) - } - if len(fido2CredentialID) > 0 { - cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagFIDO2]) - cf.FIDO2 = &FIDO2Params{ - CredentialID: fido2CredentialID, - HMACSalt: fido2HmacSalt, + if args.PlaintextNames { + cf.setFeatureFlag(FlagPlaintextNames) + } else { + if !args.DeterministicNames { + cf.setFeatureFlag(FlagDirIV) } + cf.setFeatureFlag(FlagEMENames) + cf.setFeatureFlag(FlagLongNames) + cf.setFeatureFlag(FlagRaw64) + } + if args.AESSIV { + cf.setFeatureFlag(FlagAESSIV) + } + // Catch bugs and invalid cli flag combinations early + cf.ScryptObject = NewScryptKDF(args.LogN) + if err := cf.Validate(); err != nil { + return err + } + // 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 - if devrandom { - key = randBytesDevRandom(cryptocore.KeyLen) - } else { - key = cryptocore.RandBytes(cryptocore.KeyLen) - } + key := cryptocore.RandBytes(cryptocore.KeyLen) // Encrypt it using the password // This sets ScryptObject and EncryptedKey // Note: this looks at the FeatureFlags, so call it AFTER setting them. - cf.EncryptKey(key, password, logN, false) + cf.EncryptKey(key, args.Password, args.LogN, false) for i := range key { key[i] = 0 } @@ -175,39 +175,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 { - 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, giveHash bool) (masterkey, scryptHash []byte, err error) { @@ -296,6 +279,9 @@ func (cf *ConfFile) GetMasterkey(password, givenScryptHash, returnedScryptHashBu // 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) @@ -315,7 +301,7 @@ func (cf *ConfFile) WriteFile() error { err = fd.Sync() if err != nil { // This can happen on network drives: FRITZ.NAS mounted on MacOS returns - // "operation not supported": https://github.com/rfjakob/gocryptfs/issues/390 + // "operation not supported": https://github.com/rfjakob/gocryptfs/v2/issues/390 // Try sync instead syscall.Sync() } @@ -340,3 +326,18 @@ func getKeyEncrypter(scryptHash []byte, useHKDF bool) *contentenc.ContentEnc { ce := contentenc.New(cc, 4096, false) return ce } + +// ContentEncryption tells us which content encryption algorithm is selected +func (cf *ConfFile) ContentEncryption() (algo cryptocore.AEADTypeEnum, err error) { + if err := cf.Validate(); err != nil { + return cryptocore.AEADTypeEnum{}, err + } + if cf.IsFeatureFlagSet(FlagXChaCha20Poly1305) { + return cryptocore.BackendXChaCha20Poly1305, nil + } + if cf.IsFeatureFlagSet(FlagAESSIV) { + return cryptocore.BackendAESSIV, nil + } + // If neither AES-SIV nor XChaCha are selected, we must be using AES-GCM + return cryptocore.BackendGoGCM, nil +} diff --git a/internal/configfile/feature_flags.go b/internal/configfile/feature_flags.go index 5964a53..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 @@ -28,36 +29,26 @@ const ( // FlagFIDO2 means that "-fido2" was used when creating the filesystem. // The masterkey is protected using a FIDO2 token instead of a password. FlagFIDO2 + // FlagXChaCha20Poly1305 means we use XChaCha20-Poly1305 file content encryption + FlagXChaCha20Poly1305 ) // knownFlags stores the known feature flags and their string representation var knownFlags = map[flagIota]string{ - FlagPlaintextNames: "PlaintextNames", - FlagDirIV: "DirIV", - FlagEMENames: "EMENames", - FlagGCMIV128: "GCMIV128", - FlagLongNames: "LongNames", - FlagAESSIV: "AESSIV", - FlagRaw64: "Raw64", - FlagHKDF: "HKDF", - FlagFIDO2: "FIDO2", -} - -// Filesystems that do not have these feature flags set are deprecated. -var requiredFlagsNormal = []flagIota{ - FlagDirIV, - FlagEMENames, - FlagGCMIV128, -} - -// Filesystems without filename encryption obviously don't have or need the -// filename related feature flags. -var requiredFlagsPlaintextNames = []flagIota{ - FlagGCMIV128, + FlagPlaintextNames: "PlaintextNames", + FlagDirIV: "DirIV", + FlagEMENames: "EMENames", + FlagGCMIV128: "GCMIV128", + FlagLongNames: "LongNames", + FlagAESSIV: "AESSIV", + FlagRaw64: "Raw64", + FlagHKDF: "HKDF", + FlagFIDO2: "FIDO2", + FlagXChaCha20Poly1305: "XChaCha20Poly1305", } // 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 c1edf4b..8178a92 100644 --- a/internal/configfile/scrypt.go +++ b/internal/configfile/scrypt.go @@ -1,6 +1,7 @@ package configfile import ( + "fmt" "log" "math" "os" @@ -61,8 +62,9 @@ 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 { + 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) @@ -80,21 +82,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 { - os.Exit(exitcodes.ScryptParams) + return fmt.Errorf("Fatal: scryptn below 10 is too low to make sense") } if 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 { - 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 { - 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 { - 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..6bd4f3b --- /dev/null +++ b/internal/configfile/validate.go @@ -0,0 +1,67 @@ +package configfile + +import ( + "fmt" + + "../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 +} diff --git a/internal/contentenc/content.go b/internal/contentenc/content.go index c9b3c60..6b9fd41 100644 --- a/internal/contentenc/content.go +++ b/internal/contentenc/content.go @@ -13,9 +13,6 @@ import ( "../stupidgcm" ) -// NonceMode determines how nonces are created. -type NonceMode int - const ( //value from FUSE doc MAX_KERNEL_WRITE = 128 * 1024 @@ -27,15 +24,6 @@ const ( // master key in the config file is encrypted with a 96-bit IV for // gocryptfs v1.2 and earlier. v1.3 switched to 128 bit. DefaultIVBits = 128 - - _ = iota // skip zero - // RandomNonce chooses a random nonce. - RandomNonce NonceMode = iota - // ReverseDeterministicNonce chooses a deterministic nonce, suitable for - // use in reverse mode. - ReverseDeterministicNonce NonceMode = iota - // ExternalNonce derives a nonce from external sources. - ExternalNonce NonceMode = iota ) // ContentEnc is used to encipher and decipher file content. @@ -171,7 +159,7 @@ func (be *ContentEnc) DecryptBlock(ciphertext []byte, blockNo uint64, fileID []b nonce := ciphertext[:be.cryptoCore.IVLen] if bytes.Equal(nonce, be.allZeroNonce) { // Bug in tmpfs? - // https://github.com/rfjakob/gocryptfs/issues/56 + // https://github.com/rfjakob/gocryptfs/v2/issues/56 // http://www.spinics.net/lists/kernel/msg2370127.html return nil, errors.New("all-zero nonce") } diff --git a/internal/cryptocore/cryptocore.go b/internal/cryptocore/cryptocore.go index 56e0baa..03e0416 100644 --- a/internal/cryptocore/cryptocore.go +++ b/internal/cryptocore/cryptocore.go @@ -6,10 +6,11 @@ import ( "crypto/aes" "crypto/cipher" "crypto/sha512" - "fmt" "log" "runtime" + "golang.org/x/crypto/chacha20poly1305" + "github.com/rfjakob/eme" "../siv_aead" @@ -17,37 +18,35 @@ import ( ) const ( - // KeyLen is the cipher key length in bytes. 32 for AES-256. + // KeyLen is the cipher key length in bytes. All backends use 32 bytes. KeyLen = 32 - // AuthTagLen is the length of a GCM auth tag in bytes. + // AuthTagLen is the length of a authentication tag in bytes. + // All backends use 16 bytes. AuthTagLen = 16 ) // AEADTypeEnum indicates the type of AEAD backend in use. -type AEADTypeEnum int - -const ( - // BackendOpenSSL specifies the OpenSSL backend. - BackendOpenSSL AEADTypeEnum = 3 - // BackendGoGCM specifies the Go based GCM backend. - BackendGoGCM AEADTypeEnum = 4 - // BackendAESSIV specifies an AESSIV backend. - BackendAESSIV AEADTypeEnum = 5 -) - -func (a AEADTypeEnum) String() string { - switch a { - case BackendOpenSSL: - return "BackendOpenSSL" - case BackendGoGCM: - return "BackendGoGCM" - case BackendAESSIV: - return "BackendAESSIV" - default: - return fmt.Sprintf("%d", a) - } +type AEADTypeEnum struct { + Name string + NonceSize int } +// BackendOpenSSL specifies the OpenSSL backend. +// "AES-GCM-256-OpenSSL" in gocryptfs -speed. +var BackendOpenSSL AEADTypeEnum = AEADTypeEnum{"AES-GCM-256-OpenSSL", 16} + +// BackendGoGCM specifies the Go based GCM backend. +// "AES-GCM-256-Go" in gocryptfs -speed. +var BackendGoGCM AEADTypeEnum = AEADTypeEnum{"AES-GCM-256-Go", 16} + +// BackendAESSIV specifies an AESSIV backend. +// "AES-SIV-512-Go" in gocryptfs -speed. +var BackendAESSIV AEADTypeEnum = AEADTypeEnum{"AES-SIV-512-Go", siv_aead.NonceSize} + +// BackendXChaCha20Poly1305 specifies XChaCha20-Poly1305-Go. +// "XChaCha20-Poly1305-Go" in gocryptfs -speed. +var BackendXChaCha20Poly1305 AEADTypeEnum = AEADTypeEnum{"XChaCha20-Poly1305-Go", chacha20poly1305.NonceSizeX} + // CryptoCore is the low level crypto implementation. type CryptoCore struct { // EME is used for filename encryption. @@ -58,7 +57,8 @@ type CryptoCore struct { AEADBackend AEADTypeEnum // GCM needs unique IVs (nonces) IVGenerator *nonceGenerator - IVLen int + // IVLen in bytes + IVLen int } // New returns a new CryptoCore object or panics. @@ -71,10 +71,11 @@ type CryptoCore struct { // a config file) or the masterkey (when finally mounting the filesystem). func New(key []byte, aeadType AEADTypeEnum, IVBitLen int, useHKDF bool, forceDecode bool) *CryptoCore { if len(key) != KeyLen { - log.Panic(fmt.Sprintf("Unsupported key length %d", len(key))) + log.Panicf("Unsupported key length of %d bytes", len(key)) + } + if IVBitLen != 96 && IVBitLen != 128 && IVBitLen != chacha20poly1305.NonceSizeX*8 { + log.Panicf("Unsupported IV length of %d bits", IVBitLen) } - // We want the IV size in bytes - IVLen := IVBitLen / 8 // Initialize EME for filename encryption. var emeCipher *eme.EMECipher @@ -103,12 +104,14 @@ func New(key []byte, aeadType AEADTypeEnum, IVBitLen int, useHKDF bool, forceDec if useHKDF { gcmKey = hkdfDerive(key, hkdfInfoGCMContent, KeyLen) } else { + // Filesystems created by gocryptfs v0.7 through v1.2 don't use HKDF. + // Example: tests/example_filesystems/v0.9 gcmKey = append([]byte{}, key...) } switch aeadType { case BackendOpenSSL: - if IVLen != 16 { - log.Panic("stupidgcm only supports 128-bit IVs") + if IVBitLen != 128 { + log.Panicf("stupidgcm only supports 128-bit IVs, you wanted %d", IVBitLen) } aeadCipher = stupidgcm.New(gcmKey, forceDecode) case BackendGoGCM: @@ -116,7 +119,7 @@ func New(key []byte, aeadType AEADTypeEnum, IVBitLen int, useHKDF bool, forceDec if err != nil { log.Panic(err) } - aeadCipher, err = cipher.NewGCMWithNonceSize(goGcmBlockCipher, IVLen) + aeadCipher, err = cipher.NewGCMWithNonceSize(goGcmBlockCipher, IVBitLen/8) if err != nil { log.Panic(err) } @@ -125,9 +128,9 @@ func New(key []byte, aeadType AEADTypeEnum, IVBitLen int, useHKDF bool, forceDec gcmKey[i] = 0 } } else if aeadType == BackendAESSIV { - if IVLen != 16 { - // SIV supports any nonce size, but we only use 16. - log.Panic("AES-SIV must use 16-byte nonces") + if IVBitLen != 128 { + // SIV supports any nonce size, but we only use 128. + log.Panicf("AES-SIV must use 128-bit IVs, you wanted %d", IVBitLen) } // AES-SIV uses 1/2 of the key for authentication, 1/2 for // encryption, so we need a 64-bytes key for AES-256. Derive it from @@ -144,16 +147,34 @@ func New(key []byte, aeadType AEADTypeEnum, IVBitLen int, useHKDF bool, forceDec for i := range key64 { key64[i] = 0 } + } else if aeadType == BackendXChaCha20Poly1305 { + // We don't support legacy modes with XChaCha20-Poly1305 + if IVBitLen != chacha20poly1305.NonceSizeX*8 { + log.Panicf("XChaCha20-Poly1305 must use 192-bit IVs, you wanted %d", IVBitLen) + } + if !useHKDF { + log.Panic("XChaCha20-Poly1305 must use HKDF, but it is disabled") + } + derivedKey := hkdfDerive(key, hkdfInfoXChaChaPoly1305Content, chacha20poly1305.KeySize) + aeadCipher, err = chacha20poly1305.NewX(derivedKey) + if err != nil { + log.Panic(err) + } } else { - log.Panic("unknown backend cipher") + log.Panicf("unknown cipher backend %q", aeadType.Name) + } + + if aeadCipher.NonceSize()*8 != IVBitLen { + log.Panicf("Mismatched aeadCipher.NonceSize*8=%d and IVBitLen=%d bits", + aeadCipher.NonceSize()*8, IVBitLen) } return &CryptoCore{ EMECipher: emeCipher, AEADCipher: aeadCipher, AEADBackend: aeadType, - IVGenerator: &nonceGenerator{nonceLen: IVLen}, - IVLen: IVLen, + IVGenerator: &nonceGenerator{nonceLen: IVBitLen / 8}, + IVLen: IVBitLen / 8, } } diff --git a/internal/cryptocore/hkdf.go b/internal/cryptocore/hkdf.go index 87ca1b9..b56f507 100644 --- a/internal/cryptocore/hkdf.go +++ b/internal/cryptocore/hkdf.go @@ -10,9 +10,10 @@ import ( const ( // "info" data that HKDF mixes into the generated key to make it unique. // For convenience, we use a readable string. - hkdfInfoEMENames = "EME filename encryption" - hkdfInfoGCMContent = "AES-GCM file content encryption" - hkdfInfoSIVContent = "AES-SIV file content encryption" + hkdfInfoEMENames = "EME filename encryption" + hkdfInfoGCMContent = "AES-GCM file content encryption" + hkdfInfoSIVContent = "AES-SIV file content encryption" + hkdfInfoXChaChaPoly1305Content = "XChaCha20-Poly1305 file content encryption" ) // hkdfDerive derives "outLen" bytes from "masterkey" and "info" using diff --git a/internal/nametransform/diriv.go b/internal/nametransform/diriv.go index ae46c9a..1e1e887 100644 --- a/internal/nametransform/diriv.go +++ b/internal/nametransform/diriv.go @@ -22,7 +22,11 @@ const ( // ReadDirIVAt reads "gocryptfs.diriv" from the directory that is opened as "dirfd". // Using the dirfd makes it immune to concurrent renames of the directory. // Retries on EINTR. -func ReadDirIVAt(dirfd int) (iv []byte, err error) { +// If deterministicNames is set it returns an all-zero slice. +func (n *NameTransform) ReadDirIVAt(dirfd int) (iv []byte, err error) { + if n.deterministicNames { + return make([]byte, DirIVLen), nil + } fdRaw, err := syscallcompat.Openat(dirfd, DirIVFilename, syscall.O_RDONLY|syscall.O_NOFOLLOW, 0) if err != nil { @@ -63,7 +67,7 @@ func WriteDirIVAt(dirfd int) error { iv := cryptocore.RandBytes(DirIVLen) // 0400 permissions: gocryptfs.diriv should never be modified after creation. // Don't use "ioutil.WriteFile", it causes trouble on NFS: - // https://github.com/rfjakob/gocryptfs/commit/7d38f80a78644c8ec4900cc990bfb894387112ed + // https://github.com/rfjakob/gocryptfs/v2/commit/7d38f80a78644c8ec4900cc990bfb894387112ed fd, err := syscallcompat.Openat(dirfd, DirIVFilename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, dirivPerms) if err != nil { return err diff --git a/internal/nametransform/longnames.go b/internal/nametransform/longnames.go index 21e7714..4fc378e 100644 --- a/internal/nametransform/longnames.go +++ b/internal/nametransform/longnames.go @@ -124,7 +124,7 @@ func (n *NameTransform) WriteLongNameAt(dirfd int, hashName string, plainName st plainName = filepath.Base(plainName) // Encrypt the basename - dirIV, err := ReadDirIVAt(dirfd) + dirIV, err := n.ReadDirIVAt(dirfd) if err != nil { return err } diff --git a/internal/nametransform/names.go b/internal/nametransform/names.go index 08ea41e..b631dd4 100644 --- a/internal/nametransform/names.go +++ b/internal/nametransform/names.go @@ -23,20 +23,22 @@ type NameTransform struct { // on the Raw64 feature flag B64 *base64.Encoding // Patterns to bypass decryption - badnamePatterns []string + badnamePatterns []string + deterministicNames bool } // New returns a new NameTransform instance. -func New(e *eme.EMECipher, longNames bool, raw64 bool, badname []string) *NameTransform { +func New(e *eme.EMECipher, longNames bool, raw64 bool, badname []string, deterministicNames bool) *NameTransform { b64 := base64.URLEncoding if raw64 { b64 = base64.RawURLEncoding } return &NameTransform{ - emeCipher: e, - longNames: longNames, - B64: b64, - badnamePatterns: badname, + emeCipher: e, + longNames: longNames, + B64: b64, + badnamePatterns: badname, + deterministicNames: deterministicNames, } } diff --git a/internal/nametransform/perms.go b/internal/nametransform/perms.go index cfcd062..6b88afd 100644 --- a/internal/nametransform/perms.go +++ b/internal/nametransform/perms.go @@ -6,14 +6,14 @@ const ( // never chmod'ed or chown'ed. // // Group-readable so the FS can be mounted by several users in the same group - // (see https://github.com/rfjakob/gocryptfs/issues/387 ). + // (see https://github.com/rfjakob/gocryptfs/v2/issues/387 ). // // Note that gocryptfs.conf is still created with 0400 permissions so the // owner must explicitly chmod it to permit access. // // World-readable so an encrypted directory can be copied by the non-root // owner when gocryptfs is running as root - // ( https://github.com/rfjakob/gocryptfs/issues/539 ). + // ( https://github.com/rfjakob/gocryptfs/v2/issues/539 ). dirivPerms = 0444 // Permissions for gocryptfs.longname.[sha256].name files. diff --git a/internal/siv_aead/siv_aead.go b/internal/siv_aead/siv_aead.go index eabb5e2..482efd9 100644 --- a/internal/siv_aead/siv_aead.go +++ b/internal/siv_aead/siv_aead.go @@ -19,6 +19,11 @@ const ( // KeyLen is the required key length. The SIV algorithm supports other lengths, // but we only support 64. KeyLen = 64 + // NonceSize is the required nonce/IV length. + // SIV supports any nonce size, but in gocryptfs we exclusively use 16. + NonceSize = 16 + // Overhead is the number of bytes added for integrity checking + Overhead = 16 ) // New returns a new cipher.AEAD implementation. @@ -42,11 +47,11 @@ func new2(keyIn []byte) cipher.AEAD { func (s *sivAead) NonceSize() int { // SIV supports any nonce size, but in gocryptfs we exclusively use 16. - return 16 + return NonceSize } func (s *sivAead) Overhead() int { - return 16 + return Overhead } // Seal encrypts "in" using "nonce" and "authData" and appends the result to "dst" diff --git a/internal/stupidgcm/prefer.go b/internal/stupidgcm/prefer.go index bacd56a..94c1b6c 100644 --- a/internal/stupidgcm/prefer.go +++ b/internal/stupidgcm/prefer.go @@ -14,7 +14,7 @@ import ( // 2) Is ARM64 && has AES instructions && Go is v1.11 or higher // (commit https://github.com/golang/go/commit/4f1f503373cda7160392be94e3849b0c9b9ebbda) // -// See https://github.com/rfjakob/gocryptfs/wiki/CPU-Benchmarks +// See https://github.com/rfjakob/gocryptfs/v2/wiki/CPU-Benchmarks // for benchmarks. func PreferOpenSSL() bool { if BuiltWithoutOpenssl { @@ -26,7 +26,7 @@ func PreferOpenSSL() bool { return false } // On the Apple M1, Go stdlib is faster than OpenSSL, despite cpu.ARM64.HasAES - // reading false: https://github.com/rfjakob/gocryptfs/issues/556#issuecomment-848079309 + // reading false: https://github.com/rfjakob/gocryptfs/v2/issues/556#issuecomment-848079309 if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" { return false } diff --git a/internal/stupidgcm/stupidgcm.go b/internal/stupidgcm/stupidgcm.go index 82d0323..01db41b 100644 --- a/internal/stupidgcm/stupidgcm.go +++ b/internal/stupidgcm/stupidgcm.go @@ -224,7 +224,7 @@ func (g *StupidGCM) Open(dst, iv, in, authData []byte) ([]byte, error) { if res != 1 { // The error code must always be checked by the calling function, because the decrypted buffer // may contain corrupted data that we are returning in case the user forced reads - if g.forceDecode == true { + if g.forceDecode { return append(dst, buf...), ErrAuth } return nil, ErrAuth diff --git a/internal/stupidgcm/without_openssl.go b/internal/stupidgcm/without_openssl.go deleted file mode 100644 index d420da3..0000000 --- a/internal/stupidgcm/without_openssl.go +++ /dev/null @@ -1,52 +0,0 @@ -// +build without_openssl - -package stupidgcm - -import ( - "fmt" - "os" - - "../exitcodes" -) - -type StupidGCM struct{} - -const ( - // BuiltWithoutOpenssl indicates if openssl been disabled at compile-time - BuiltWithoutOpenssl = true -) - -func errExit() { - fmt.Fprintln(os.Stderr, "gocryptfs has been compiled without openssl support but you are still trying to use openssl") - os.Exit(exitcodes.OpenSSL) -} - -func New(_ []byte, _ bool) *StupidGCM { - errExit() - // Never reached - return &StupidGCM{} -} - -func (g *StupidGCM) NonceSize() int { - errExit() - return -1 -} - -func (g *StupidGCM) Overhead() int { - errExit() - return -1 -} - -func (g *StupidGCM) Seal(_, _, _, _ []byte) []byte { - errExit() - return nil -} - -func (g *StupidGCM) Open(_, _, _, _ []byte) ([]byte, error) { - errExit() - return nil, nil -} - -func (g *StupidGCM) Wipe() { - errExit() -} diff --git a/internal/syscallcompat/eintr.go b/internal/syscallcompat/eintr.go index cdde806..2e2bb18 100644 --- a/internal/syscallcompat/eintr.go +++ b/internal/syscallcompat/eintr.go @@ -12,7 +12,7 @@ import ( // https://github.com/golang/go/blob/d2a80f3fb5b44450e0b304ac5a718f99c053d82a/src/os/file_posix.go#L243 // // This is needed because CIFS throws lots of EINTR errors: -// https://github.com/rfjakob/gocryptfs/issues/483 +// https://github.com/rfjakob/gocryptfs/v2/issues/483 // // Don't use retryEINTR() with syscall.Close()! // See https://code.google.com/p/chromium/issues/detail?id=269623 . diff --git a/internal/syscallcompat/getdents_linux.go b/internal/syscallcompat/getdents_linux.go index df886b4..5fa2a82 100644 --- a/internal/syscallcompat/getdents_linux.go +++ b/internal/syscallcompat/getdents_linux.go @@ -19,7 +19,7 @@ const sizeofDirent = int(unsafe.Sizeof(unix.Dirent{})) // maxReclen sanity check: Reclen should never be larger than this. // Due to padding between entries, it is 280 even on 32-bit architectures. -// See https://github.com/rfjakob/gocryptfs/issues/197 for details. +// See https://github.com/rfjakob/gocryptfs/v2/issues/197 for details. const maxReclen = 280 type DirEntry struct { diff --git a/internal/syscallcompat/sys_common.go b/internal/syscallcompat/sys_common.go index 0123b25..ea9ac14 100644 --- a/internal/syscallcompat/sys_common.go +++ b/internal/syscallcompat/sys_common.go @@ -1,7 +1,6 @@ package syscallcompat import ( - "bytes" "syscall" "golang.org/x/sys/unix" @@ -27,21 +26,6 @@ func Readlinkat(dirfd int, path string) (string, error) { } } -// Faccessat exists both in Linux and in MacOS 10.10+, but the Linux version -// DOES NOT support any flags. Emulate AT_SYMLINK_NOFOLLOW like glibc does. -func Faccessat(dirfd int, path string, mode uint32) error { - var st unix.Stat_t - err := Fstatat(dirfd, path, &st, unix.AT_SYMLINK_NOFOLLOW) - if err != nil { - return err - } - if st.Mode&syscall.S_IFMT == syscall.S_IFLNK { - // Pretend that a symlink is always accessible - return nil - } - return unix.Faccessat(dirfd, path, mode, 0) -} - // Openat wraps the Openat syscall. // Retries on EINTR. func Openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) { @@ -57,15 +41,6 @@ func Openat(dirfd int, path string, flags int, mode uint32) (fd int, err error) return fd, err } -// Fchownat syscall. -func Fchownat(dirfd int, path string, uid int, gid int, flags int) (err error) { - // Why would we ever want to call this without AT_SYMLINK_NOFOLLOW? - if flags&unix.AT_SYMLINK_NOFOLLOW == 0 { - flags |= unix.AT_SYMLINK_NOFOLLOW - } - return unix.Fchownat(dirfd, path, uid, gid, flags) -} - // Fstatat syscall. // Retries on EINTR. func Fstatat(dirfd int, path string, stat *unix.Stat_t, flags int) (err error) { @@ -100,118 +75,3 @@ const XATTR_BUFSZ = XATTR_SIZE_MAX + 1024 // We try with a small buffer first - this one can be allocated on the stack. const XATTR_BUFSZ_SMALL = 500 - -// Fgetxattr is a wrapper around unix.Fgetxattr that handles the buffer sizing. -func Fgetxattr(fd int, attr string) (val []byte, err error) { - fn := func(buf []byte) (int, error) { - return unix.Fgetxattr(fd, attr, buf) - } - return getxattrSmartBuf(fn) -} - -// Lgetxattr is a wrapper around unix.Lgetxattr that handles the buffer sizing. -func Lgetxattr(path string, attr string) (val []byte, err error) { - fn := func(buf []byte) (int, error) { - return unix.Lgetxattr(path, attr, buf) - } - return getxattrSmartBuf(fn) -} - -func getxattrSmartBuf(fn func(buf []byte) (int, error)) ([]byte, error) { - // Fastpaths. Important for security.capabilities, which gets queried a lot. - buf := make([]byte, XATTR_BUFSZ_SMALL) - sz, err := fn(buf) - // Non-existing xattr - if err == unix.ENODATA { - return nil, err - } - // Underlying fs does not support security.capabilities (example: tmpfs) - if err == unix.EOPNOTSUPP { - return nil, err - } - // Small xattr - if err == nil && sz < len(buf) { - goto out - } - // Generic slowpath - // - // If the buffer is too small to fit the value, Linux and MacOS react - // differently: - // Linux: returns an ERANGE error and "-1" bytes. - // MacOS: truncates the value and returns "size" bytes. - // - // We choose the simple approach of buffer that is bigger than the limit on - // Linux, and return an error for everything that is bigger (which can - // only happen on MacOS). - buf = make([]byte, XATTR_BUFSZ) - sz, err = fn(buf) - if err == syscall.ERANGE { - // Do NOT return ERANGE - the user might retry ad inifinitum! - return nil, syscall.EOVERFLOW - } - if err != nil { - return nil, err - } - if sz >= XATTR_SIZE_MAX { - return nil, syscall.EOVERFLOW - } -out: - // Copy only the actually used bytes to a new (smaller) buffer - // so "buf" never leaves the function and can be allocated on the stack. - val := make([]byte, sz) - copy(val, buf) - return val, nil -} - -// Flistxattr is a wrapper for unix.Flistxattr that handles buffer sizing and -// parsing the returned blob to a string slice. -func Flistxattr(fd int) (attrs []string, err error) { - // See the buffer sizing comments in getxattrSmartBuf. - // TODO: smarter buffer sizing? - buf := make([]byte, XATTR_BUFSZ) - sz, err := unix.Flistxattr(fd, buf) - if err == syscall.ERANGE { - // Do NOT return ERANGE - the user might retry ad inifinitum! - return nil, syscall.EOVERFLOW - } - if err != nil { - return nil, err - } - if sz >= XATTR_SIZE_MAX { - return nil, syscall.EOVERFLOW - } - attrs = parseListxattrBlob(buf[:sz]) - return attrs, nil -} - -// Llistxattr is a wrapper for unix.Llistxattr that handles buffer sizing and -// parsing the returned blob to a string slice. -func Llistxattr(path string) (attrs []string, err error) { - // TODO: smarter buffer sizing? - buf := make([]byte, XATTR_BUFSZ) - sz, err := unix.Llistxattr(path, buf) - if err == syscall.ERANGE { - // Do NOT return ERANGE - the user might retry ad inifinitum! - return nil, syscall.EOVERFLOW - } - if err != nil { - return nil, err - } - if sz >= XATTR_SIZE_MAX { - return nil, syscall.EOVERFLOW - } - attrs = parseListxattrBlob(buf[:sz]) - return attrs, nil -} - -func parseListxattrBlob(buf []byte) (attrs []string) { - parts := bytes.Split(buf, []byte{0}) - for _, part := range parts { - if len(part) == 0 { - // Last part is empty, ignore - continue - } - attrs = append(attrs, string(part)) - } - return attrs -} diff --git a/internal/syscallcompat/sys_linux.go b/internal/syscallcompat/sys_linux.go index d0fa027..70beb5f 100644 --- a/internal/syscallcompat/sys_linux.go +++ b/internal/syscallcompat/sys_linux.go @@ -35,7 +35,7 @@ func EnospcPrealloc(fd int, off int64, len int64) (err error) { } if err == syscall.EOPNOTSUPP { // ZFS and ext3 do not support fallocate. Warn but continue anyway. - // https://github.com/rfjakob/gocryptfs/issues/22 + // https://github.com/rfjakob/gocryptfs/v2/issues/22 return nil } return err diff --git a/volume.go b/volume.go index 17e7658..52ff4d1 100644 --- a/volume.go +++ b/volume.go @@ -75,7 +75,7 @@ func registerNewVolume(rootCipherDir string, masterkey []byte, cf *configfile.Co newVolume.cryptoCore = cryptocore.New(masterkey, cryptoBackend, contentenc.DefaultIVBits, true, forcedecode) newVolume.contentEnc = contentenc.New(newVolume.cryptoCore, contentenc.DefaultBS, forcedecode) var badname []string - newVolume.nameTransform = nametransform.New(newVolume.cryptoCore.EMECipher, true, true, badname) + newVolume.nameTransform = nametransform.New(newVolume.cryptoCore.EMECipher, true, true, badname, false) //copying rootCipherDir var grcd strings.Builder @@ -156,7 +156,16 @@ func gcf_change_password(rootCipherDir string, oldPassword, givenScryptHash, new //export gcf_create_volume func gcf_create_volume(rootCipherDir string, password []byte, plaintextNames bool, logN int, creator string) bool { - err := configfile.Create(filepath.Join(rootCipherDir, configfile.ConfDefaultName), password, plaintextNames, logN, creator, false, false, nil, nil) + err := configfile.Create(&configfile.CreateArgs{ + Filename: filepath.Join(rootCipherDir, configfile.ConfDefaultName), + Password: password, + PlaintextNames: plaintextNames, + LogN: logN, + Creator: creator, + AESSIV: false, + DeterministicNames: false, + XChaCha20Poly1305: false, + }) if err == nil { if plaintextNames { return true