d023cd6c95
The rewritten openssl backend does not support this flag anymore, and it was inherently dangerour. Drop it (ignored for compatibility)
320 lines
9.2 KiB
Go
320 lines
9.2 KiB
Go
// Package configfile reads and writes gocryptfs.conf does the key
|
|
// wrapping.
|
|
package configfile
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"syscall"
|
|
|
|
"os"
|
|
|
|
"github.com/rfjakob/gocryptfs/v2/internal/contentenc"
|
|
"github.com/rfjakob/gocryptfs/v2/internal/cryptocore"
|
|
"github.com/rfjakob/gocryptfs/v2/internal/exitcodes"
|
|
"github.com/rfjakob/gocryptfs/v2/internal/tlog"
|
|
)
|
|
|
|
const (
|
|
// ConfDefaultName is the default configuration file name.
|
|
// The dot "." is not used in base64url (RFC4648), hence
|
|
// we can never clash with an encrypted file.
|
|
ConfDefaultName = "gocryptfs.conf"
|
|
// ConfReverseName is the default configuration file name in reverse mode,
|
|
// the config file gets stored next to the plain-text files. Make it hidden
|
|
// (start with dot) to not annoy the user.
|
|
ConfReverseName = ".gocryptfs.reverse.conf"
|
|
)
|
|
|
|
// FIDO2Params is a structure for storing FIDO2 parameters.
|
|
type FIDO2Params struct {
|
|
// FIDO2 credential
|
|
CredentialID []byte
|
|
// FIDO2 hmac-secret salt
|
|
HMACSalt []byte
|
|
}
|
|
|
|
// ConfFile is the content of a config file.
|
|
type ConfFile struct {
|
|
// Creator is the gocryptfs version string.
|
|
// This only documents the config file for humans who look at it. The actual
|
|
// technical info is contained in FeatureFlags.
|
|
Creator string
|
|
// EncryptedKey holds an encrypted AES key, unlocked using a password
|
|
// hashed with scrypt
|
|
EncryptedKey []byte
|
|
// ScryptObject stores parameters for scrypt hashing (key derivation)
|
|
ScryptObject ScryptKDF
|
|
// Version is the On-Disk-Format version this filesystem uses
|
|
Version uint16
|
|
// FeatureFlags is a list of feature flags this filesystem has enabled.
|
|
// If gocryptfs encounters a feature flag it does not support, it will refuse
|
|
// mounting. This mechanism is analogous to the ext4 feature flags that are
|
|
// stored in the superblock.
|
|
FeatureFlags []string
|
|
// FIDO2 parameters
|
|
FIDO2 *FIDO2Params `json:",omitempty"`
|
|
// Filename is the name of the config file. Not exported to JSON.
|
|
filename string
|
|
}
|
|
|
|
// 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
|
|
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 {
|
|
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.setFeatureFlag(FlagPlaintextNames)
|
|
} else {
|
|
if !args.DeterministicNames {
|
|
cf.setFeatureFlag(FlagDirIV)
|
|
}
|
|
cf.setFeatureFlag(FlagEMENames)
|
|
cf.setFeatureFlag(FlagLongNames)
|
|
cf.setFeatureFlag(FlagRaw64)
|
|
}
|
|
if args.AESSIV {
|
|
cf.setFeatureFlag(FlagAESSIV)
|
|
}
|
|
if len(args.Fido2CredentialID) > 0 {
|
|
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
|
|
key := cryptocore.RandBytes(cryptocore.KeyLen)
|
|
tlog.PrintMasterkeyReminder(key)
|
|
// 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, args.Password, args.LogN)
|
|
for i := range key {
|
|
key[i] = 0
|
|
}
|
|
// key runs out of scope here
|
|
}
|
|
// Write file to disk
|
|
return cf.WriteFile()
|
|
}
|
|
|
|
// LoadAndDecrypt - read config file from disk and decrypt the
|
|
// contained key using "password".
|
|
// Returns the decrypted key and the ConfFile object
|
|
//
|
|
// If "password" is empty, the config file is read
|
|
// but the key is not decrypted (returns nil in its place).
|
|
func LoadAndDecrypt(filename string, password []byte) ([]byte, *ConfFile, error) {
|
|
cf, err := Load(filename)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if len(password) == 0 {
|
|
// We have validated the config file, but without a password we cannot
|
|
// decrypt the master key. Return only the parsed config.
|
|
return nil, cf, nil
|
|
// TODO: Make this an error in gocryptfs v1.7. All code should now call
|
|
// Load() instead of calling LoadAndDecrypt() with an empty password.
|
|
}
|
|
|
|
// Decrypt the masterkey using the password
|
|
key, err := cf.DecryptMasterKey(password)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return key, cf, err
|
|
}
|
|
|
|
// Load loads and parses the config file at "filename".
|
|
func Load(filename string) (*ConfFile, error) {
|
|
var cf ConfFile
|
|
cf.filename = filename
|
|
|
|
// Read from disk
|
|
js, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(js) == 0 {
|
|
return nil, fmt.Errorf("Config file is empty")
|
|
}
|
|
|
|
// Unmarshal
|
|
err = json.Unmarshal(js, &cf)
|
|
if err != nil {
|
|
tlog.Warn.Printf("Failed to unmarshal config file")
|
|
return nil, err
|
|
}
|
|
|
|
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) {
|
|
// Generate derived key from password
|
|
scryptHash := cf.ScryptObject.DeriveKey(password)
|
|
|
|
// Unlock master key using password-based key
|
|
useHKDF := cf.IsFeatureFlagSet(FlagHKDF)
|
|
ce := getKeyEncrypter(scryptHash, useHKDF)
|
|
|
|
tlog.Warn.Enabled = false // Silence DecryptBlock() error messages on incorrect password
|
|
masterkey, err = ce.DecryptBlock(cf.EncryptedKey, 0, nil)
|
|
tlog.Warn.Enabled = true
|
|
|
|
// Purge scrypt-derived key
|
|
for i := range scryptHash {
|
|
scryptHash[i] = 0
|
|
}
|
|
scryptHash = nil
|
|
ce.Wipe()
|
|
ce = nil
|
|
|
|
if err != nil {
|
|
tlog.Warn.Printf("failed to unlock master key: %s", err.Error())
|
|
return nil, exitcodes.NewErr("Password incorrect.", exitcodes.PasswordIncorrect)
|
|
}
|
|
return masterkey, nil
|
|
}
|
|
|
|
// EncryptKey - encrypt "key" using an scrypt hash generated from "password"
|
|
// and store it in cf.EncryptedKey.
|
|
// Uses scrypt with cost parameter logN and stores the scrypt parameters in
|
|
// cf.ScryptObject.
|
|
func (cf *ConfFile) EncryptKey(key []byte, password []byte, logN int) {
|
|
// Generate scrypt-derived key from password
|
|
cf.ScryptObject = NewScryptKDF(logN)
|
|
scryptHash := cf.ScryptObject.DeriveKey(password)
|
|
|
|
// Lock master key using password-based key
|
|
useHKDF := cf.IsFeatureFlagSet(FlagHKDF)
|
|
ce := getKeyEncrypter(scryptHash, useHKDF)
|
|
cf.EncryptedKey = ce.EncryptBlock(key, 0, nil)
|
|
|
|
// Purge scrypt-derived key
|
|
for i := range scryptHash {
|
|
scryptHash[i] = 0
|
|
}
|
|
scryptHash = nil
|
|
ce.Wipe()
|
|
ce = nil
|
|
}
|
|
|
|
// WriteFile - write out config in JSON format to file "filename.tmp"
|
|
// 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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
js, err := json.MarshalIndent(cf, "", "\t")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// For convenience for the user, add a newline at the end.
|
|
js = append(js, '\n')
|
|
_, err = fd.Write(js)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
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
|
|
tlog.Warn.Printf("Warning: fsync failed: %v", err)
|
|
// Try sync instead
|
|
syscall.Sync()
|
|
}
|
|
err = fd.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = os.Rename(tmp, cf.filename)
|
|
return err
|
|
}
|
|
|
|
// getKeyEncrypter is a helper function that returns the right ContentEnc
|
|
// instance for the "useHKDF" setting.
|
|
func getKeyEncrypter(scryptHash []byte, useHKDF bool) *contentenc.ContentEnc {
|
|
IVLen := 96
|
|
// gocryptfs v1.2 and older used 96-bit IVs for master key encryption.
|
|
// v1.3 adds the "HKDF" feature flag, which also enables 128-bit nonces.
|
|
if useHKDF {
|
|
IVLen = contentenc.DefaultIVBits
|
|
}
|
|
cc := cryptocore.New(scryptHash, cryptocore.BackendGoGCM, IVLen, useHKDF)
|
|
ce := contentenc.New(cc, 4096)
|
|
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
|
|
}
|