libgocryptfs/internal/configfile/config_file.go
Jakob Unterwurzacher 61ef6b00a6 -devrandom: make flag a no-op
Commit f3c777d5ea added the `-devrandom` option:

    commit f3c777d5ea
    Author: @slackner
    Date:   Sun Nov 19 13:30:04 2017 +0100

    main: Add '-devrandom' commandline option

    Allows to use /dev/random for generating the master key instead of the
    default Go implementation. When the kernel random generator has been
    properly initialized both are considered equally secure, however:

    * Versions of Go prior to 1.9 just fall back to /dev/urandom if the
      getrandom() syscall would be blocking (Go Bug #19274)

    * Kernel versions prior to 3.17 do not support getrandom(), and there
      is no check if the random generator has been properly initialized
      before reading from /dev/urandom

    This is especially useful for embedded hardware with low-entroy. Please
    note that generation of the master key might block indefinitely if the
    kernel cannot harvest enough entropy.

We now require Go v1.13 and Kernel versions should have also moved on.
Make the flag a no-op.

https://github.com/rfjakob/gocryptfs/issues/596
2021-08-25 12:39:17 +02:00

320 lines
9.3 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/v2/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, false)
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
}