diff options
| author | Pavol Rusnak | 2020-09-05 22:42:15 +0200 | 
|---|---|---|
| committer | Jakob Unterwurzacher | 2020-09-12 18:06:54 +0200 | 
| commit | 1e624a4cc3aafa57b5fa213c88bcd3689cefd1c3 (patch) | |
| tree | a6e4f51ecb2dc0bac4276b2f65b39a3b426bc4ee | |
| parent | 6a9c49e9cf23c85622dd4b181cdc615abc72d6bb (diff) | |
Add support for FIDO2 tokens
| -rw-r--r-- | Documentation/MANPAGE.md | 4 | ||||
| -rw-r--r-- | cli_args.go | 7 | ||||
| -rw-r--r-- | gocryptfs-xray/xray_main.go | 21 | ||||
| -rw-r--r-- | init_dir.go | 19 | ||||
| -rw-r--r-- | internal/configfile/config_file.go | 20 | ||||
| -rw-r--r-- | internal/configfile/config_test.go | 8 | ||||
| -rw-r--r-- | internal/configfile/feature_flags.go | 4 | ||||
| -rw-r--r-- | internal/exitcodes/exitcodes.go | 2 | ||||
| -rw-r--r-- | internal/fido2/fido2.go | 110 | ||||
| -rw-r--r-- | main.go | 16 | 
10 files changed, 196 insertions, 15 deletions
| diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md index 43876d3..40c2f26 100644 --- a/Documentation/MANPAGE.md +++ b/Documentation/MANPAGE.md @@ -130,6 +130,10 @@ to your program, use `"--"`, which is accepted by most programs:  Stay in the foreground instead of forking away. Implies "-nosyslog".  For compatibility, "-f" is also accepted, but "-fg" is preferred. +#### -fido2 DEVICE_PATH +Use a FIDO2 token to initialize and unlock the filesystem. +Use "fido2-token -L" to obtain the FIDO2 token device path. +  #### -force_owner string  If given a string of the form "uid:gid" (where both "uid" and "gid" are  substituted with positive integers), presents all files as owned by the given diff --git a/cli_args.go b/cli_args.go index e4073fa..11bb96e 100644 --- a/cli_args.go +++ b/cli_args.go @@ -32,7 +32,7 @@ type argContainer struct {  	// Mount options with opposites  	dev, nodev, suid, nosuid, exec, noexec, rw, ro bool  	masterkey, mountpoint, cipherdir, cpuprofile, -	memprofile, ko, ctlsock, fsname, force_owner, trace string +	memprofile, ko, ctlsock, fsname, force_owner, trace, fido2 string  	// -extpass, -badname, -passfile can be passed multiple times  	extpass, badname, passfile multipleStrings  	// For reverse mode, several ways to specify exclusions. All can be specified multiple times. @@ -189,6 +189,7 @@ func parseCliOpts() (args argContainer) {  	flagSet.StringVar(&args.fsname, "fsname", "", "Override the filesystem name")  	flagSet.StringVar(&args.force_owner, "force_owner", "", "uid:gid pair to coerce ownership")  	flagSet.StringVar(&args.trace, "trace", "", "Write execution trace to file") +	flagSet.StringVar(&args.fido2, "fido2", "", "Protect the masterkey using a FIDO2 token instead of a password")  	// Exclusion options  	flagSet.Var(&args.exclude, "e", "Alias for -exclude") @@ -279,6 +280,10 @@ func parseCliOpts() (args argContainer) {  		tlog.Fatal.Printf("The options -extpass and -masterkey cannot be used at the same time")  		os.Exit(exitcodes.Usage)  	} +	if !args.extpass.Empty() && args.fido2 != "" { +		tlog.Fatal.Printf("The options -extpass and -fido2 cannot be used at the same time") +		os.Exit(exitcodes.Usage) +	}  	if args.idle < 0 {  		tlog.Fatal.Printf("Idle timeout cannot be less than 0")  		os.Exit(exitcodes.Usage) diff --git a/gocryptfs-xray/xray_main.go b/gocryptfs-xray/xray_main.go index 7e928e7..dec803b 100644 --- a/gocryptfs-xray/xray_main.go +++ b/gocryptfs-xray/xray_main.go @@ -11,6 +11,7 @@ import (  	"github.com/rfjakob/gocryptfs/internal/contentenc"  	"github.com/rfjakob/gocryptfs/internal/cryptocore"  	"github.com/rfjakob/gocryptfs/internal/exitcodes" +	"github.com/rfjakob/gocryptfs/internal/fido2"  	"github.com/rfjakob/gocryptfs/internal/readpassword"  	"github.com/rfjakob/gocryptfs/internal/tlog"  ) @@ -67,12 +68,14 @@ func main() {  		encryptPaths  *bool  		aessiv        *bool  		sep0          *bool +		fido2         *string  	}  	args.dumpmasterkey = flag.Bool("dumpmasterkey", false, "Decrypt and dump the master key")  	args.decryptPaths = flag.Bool("decrypt-paths", false, "Decrypt file paths using gocryptfs control socket")  	args.encryptPaths = flag.Bool("encrypt-paths", false, "Encrypt file paths using gocryptfs control socket")  	args.sep0 = flag.Bool("0", false, "Use \\0 instead of \\n as separator")  	args.aessiv = flag.Bool("aessiv", false, "Assume AES-SIV mode instead of AES-GCM") +	args.fido2 = flag.String("fido2", "", "Protect the masterkey using a FIDO2 token instead of a password")  	flag.Usage = usage  	flag.Parse()  	s := sum(args.dumpmasterkey, args.decryptPaths, args.encryptPaths) @@ -97,20 +100,30 @@ func main() {  	}  	defer fd.Close()  	if *args.dumpmasterkey { -		dumpMasterKey(fn) +		dumpMasterKey(fn, *args.fido2)  	} else {  		inspectCiphertext(fd, *args.aessiv)  	}  } -func dumpMasterKey(fn string) { +func dumpMasterKey(fn string, fido2Path string) {  	tlog.Info.Enabled = false -	pw := readpassword.Once(nil, nil, "") -	masterkey, _, err := configfile.LoadAndDecrypt(fn, pw) +	cf, err := configfile.Load(fn)  	if err != nil {  		fmt.Fprintln(os.Stderr, err)  		exitcodes.Exit(err)  	} +	var pw []byte +	if cf.IsFeatureFlagSet(configfile.FlagFIDO2) { +		if fido2Path == "" { +			tlog.Fatal.Printf("Masterkey encrypted using FIDO2 token; need to use the --fido2 option.") +			os.Exit(exitcodes.Usage) +		} +		pw = fido2.Secret(fido2Path, cf.FIDO2.CredentialID, cf.FIDO2.HMACSalt) +	} else { +		pw = readpassword.Once(nil, nil, "") +	} +	masterkey, err := cf.DecryptMasterKey(pw)  	fmt.Println(hex.EncodeToString(masterkey))  	for i := range pw {  		pw[i] = 0 diff --git a/init_dir.go b/init_dir.go index 19fabcf..68268a0 100644 --- a/init_dir.go +++ b/init_dir.go @@ -9,7 +9,9 @@ import (  	"syscall"  	"github.com/rfjakob/gocryptfs/internal/configfile" +	"github.com/rfjakob/gocryptfs/internal/cryptocore"  	"github.com/rfjakob/gocryptfs/internal/exitcodes" +	"github.com/rfjakob/gocryptfs/internal/fido2"  	"github.com/rfjakob/gocryptfs/internal/nametransform"  	"github.com/rfjakob/gocryptfs/internal/readpassword"  	"github.com/rfjakob/gocryptfs/internal/syscallcompat" @@ -67,14 +69,25 @@ func initDir(args *argContainer) {  		}  	}  	// Choose password for config file -	if args.extpass.Empty() { +	if args.extpass.Empty() && args.fido2 == "" {  		tlog.Info.Printf("Choose a password for protecting your files.")  	}  	{ -		password := readpassword.Twice([]string(args.extpass), []string(args.passfile)) +		var password []byte +		var fido2CredentialID, fido2HmacSalt []byte +		if args.fido2 != "" { +			fido2CredentialID = fido2.Register(args.fido2, filepath.Base(args.cipherdir)) +			fido2HmacSalt = cryptocore.RandBytes(32) +			password = fido2.Secret(args.fido2, fido2CredentialID, fido2HmacSalt) +		} else { +			// normal password entry +			password = readpassword.Twice([]string(args.extpass), []string(args.passfile)) +			fido2CredentialID = nil +			fido2HmacSalt = nil +		}  		creator := tlog.ProgramName + " " + GitVersion  		err = configfile.Create(args.config, password, args.plaintextnames, -			args.scryptn, creator, args.aessiv, args.devrandom) +			args.scryptn, creator, args.aessiv, args.devrandom, fido2CredentialID, fido2HmacSalt)  		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 c27ecd4..e4921f7 100644 --- a/internal/configfile/config_file.go +++ b/internal/configfile/config_file.go @@ -10,12 +10,13 @@ import (  	"log"  	"syscall" +	"os" +  	"github.com/rfjakob/gocryptfs/internal/contentenc"  	"github.com/rfjakob/gocryptfs/internal/cryptocore"  	"github.com/rfjakob/gocryptfs/internal/exitcodes"  	"github.com/rfjakob/gocryptfs/internal/tlog"  ) -import "os"  const (  	// ConfDefaultName is the default configuration file name. @@ -28,6 +29,14 @@ const (  	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. @@ -46,6 +55,8 @@ type ConfFile struct {  	// mounting. This mechanism is analogous to the ext4 feature flags that are  	// stored in the superblock.  	FeatureFlags []string +	// FIDO2 parameters +	FIDO2 FIDO2Params  	// Filename is the name of the config file. Not exported to JSON.  	filename string  } @@ -69,7 +80,7 @@ func randBytesDevRandom(n int) []byte {  // "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) error { +	logN int, creator string, aessiv bool, devrandom bool, fido2CredentialID []byte, fido2HmacSalt []byte) error {  	var cf ConfFile  	cf.filename = filename  	cf.Creator = creator @@ -89,6 +100,11 @@ func Create(filename string, password []byte, plaintextNames bool,  	if aessiv {  		cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagAESSIV])  	} +	if len(fido2CredentialID) > 0 { +		cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagFIDO2]) +		cf.FIDO2.CredentialID = fido2CredentialID +		cf.FIDO2.HMACSalt = fido2HmacSalt +	}  	{  		// Generate new random master key  		var key []byte diff --git a/internal/configfile/config_test.go b/internal/configfile/config_test.go index 832867c..ce35531 100644 --- a/internal/configfile/config_test.go +++ b/internal/configfile/config_test.go @@ -62,7 +62,7 @@ func TestLoadV2StrangeFeature(t *testing.T) {  }  func TestCreateConfDefault(t *testing.T) { -	err := Create("config_test/tmp.conf", testPw, false, 10, "test", false, false) +	err := Create("config_test/tmp.conf", testPw, false, 10, "test", false, false, nil, nil)  	if err != nil {  		t.Fatal(err)  	} @@ -83,14 +83,14 @@ func TestCreateConfDefault(t *testing.T) {  }  func TestCreateConfDevRandom(t *testing.T) { -	err := Create("config_test/tmp.conf", testPw, false, 10, "test", false, true) +	err := Create("config_test/tmp.conf", testPw, false, 10, "test", false, true, nil, nil)  	if err != nil {  		t.Fatal(err)  	}  }  func TestCreateConfPlaintextnames(t *testing.T) { -	err := Create("config_test/tmp.conf", testPw, true, 10, "test", false, false) +	err := Create("config_test/tmp.conf", testPw, true, 10, "test", false, false, nil, nil)  	if err != nil {  		t.Fatal(err)  	} @@ -111,7 +111,7 @@ func TestCreateConfPlaintextnames(t *testing.T) {  // Reverse mode uses AESSIV  func TestCreateConfFileAESSIV(t *testing.T) { -	err := Create("config_test/tmp.conf", testPw, false, 10, "test", true, false) +	err := Create("config_test/tmp.conf", testPw, false, 10, "test", true, false, nil, nil)  	if err != nil {  		t.Fatal(err)  	} diff --git a/internal/configfile/feature_flags.go b/internal/configfile/feature_flags.go index 2d609f2..5964a53 100644 --- a/internal/configfile/feature_flags.go +++ b/internal/configfile/feature_flags.go @@ -25,6 +25,9 @@ const (  	// Note that this flag does not change the password hashing algorithm  	// which always is scrypt.  	FlagHKDF +	// FlagFIDO2 means that "-fido2" was used when creating the filesystem. +	// The masterkey is protected using a FIDO2 token instead of a password. +	FlagFIDO2  )  // knownFlags stores the known feature flags and their string representation @@ -37,6 +40,7 @@ var knownFlags = map[flagIota]string{  	FlagAESSIV:         "AESSIV",  	FlagRaw64:          "Raw64",  	FlagHKDF:           "HKDF", +	FlagFIDO2:          "FIDO2",  }  // Filesystems that do not have these feature flags set are deprecated. diff --git a/internal/exitcodes/exitcodes.go b/internal/exitcodes/exitcodes.go index b876333..508ba38 100644 --- a/internal/exitcodes/exitcodes.go +++ b/internal/exitcodes/exitcodes.go @@ -70,6 +70,8 @@ const (  	ExcludeError = 29  	// DevNull means that /dev/null could not be opened  	DevNull = 30 +	// FIDO2Error - an error was encountered while interacting with a FIDO2 token +	FIDO2Error = 31  )  // Err wraps an error with an associated numeric exit code diff --git a/internal/fido2/fido2.go b/internal/fido2/fido2.go new file mode 100644 index 0000000..ea8ffd8 --- /dev/null +++ b/internal/fido2/fido2.go @@ -0,0 +1,110 @@ +package fido2 + +import ( +	"bytes" +	"encoding/base64" +	"fmt" +	"io" +	"os" +	"os/exec" +	"strings" + +	"github.com/rfjakob/gocryptfs/internal/cryptocore" +	"github.com/rfjakob/gocryptfs/internal/exitcodes" +	"github.com/rfjakob/gocryptfs/internal/tlog" +) + +type fidoCommand int + +const ( +	cred          fidoCommand = iota +	assert        fidoCommand = iota +	assertWithPIN fidoCommand = iota +) + +const relyingPartyID = "gocryptfs" + +func callFidoCommand(command fidoCommand, device string, stdin []string) ([]string, error) { +	var cmd *exec.Cmd +	switch command { +	case cred: +		cmd = exec.Command("fido2-cred", "-M", "-h", "-v", device) +	case assert: +		cmd = exec.Command("fido2-assert", "-G", "-h", device) +	case assertWithPIN: +		cmd = exec.Command("fido2-assert", "-G", "-h", "-v", device) +	} +	tlog.Debug.Printf("callFidoCommand: executing %q with args %v", cmd.Path, cmd.Args) +	cmd.Stderr = os.Stderr +	in, err := cmd.StdinPipe() +	if err != nil { +		return nil, err +	} +	for _, s := range stdin { +		// This does not deadlock because the pipe buffer is big enough (64kiB on Linux) +		io.WriteString(in, s+"\n") +	} +	in.Close() +	out, err := cmd.Output() +	if err != nil { +		return nil, fmt.Errorf("%s failed with %v", cmd.Args[0], err) +	} +	return strings.Split(string(out), "\n"), nil +} + +// Register registers a credential using a FIDO2 token +func Register(device string, userName string) (credentialID []byte) { +	tlog.Info.Printf("FIDO2 Register: interact with your device ...") +	cdh := base64.StdEncoding.EncodeToString(cryptocore.RandBytes(32)) +	userID := base64.StdEncoding.EncodeToString(cryptocore.RandBytes(32)) +	stdin := []string{cdh, relyingPartyID, userName, userID} +	out, err := callFidoCommand(cred, device, stdin) +	if err != nil { +		tlog.Fatal.Println(err) +		os.Exit(exitcodes.FIDO2Error) +	} +	credentialID, err = base64.StdEncoding.DecodeString(out[4]) +	if err != nil { +		tlog.Fatal.Println(err) +		os.Exit(exitcodes.FIDO2Error) +	} +	return credentialID +} + +// Secret generates a HMAC secret using a FIDO2 token +func Secret(device string, credentialID []byte, salt []byte) (secret []byte) { +	tlog.Info.Printf("FIDO2 Secret: interact with your device ...") +	cdh := base64.StdEncoding.EncodeToString(cryptocore.RandBytes(32)) +	crid := base64.StdEncoding.EncodeToString(credentialID) +	hmacsalt := base64.StdEncoding.EncodeToString(salt) +	stdin := []string{cdh, relyingPartyID, crid, hmacsalt} +	// try asserting without PIN first +	out, err := callFidoCommand(assert, device, stdin) +	if err != nil { +		// if that fails, let's assert with PIN +		out, err = callFidoCommand(assertWithPIN, device, stdin) +		if err != nil { +			tlog.Fatal.Println(err) +			os.Exit(exitcodes.FIDO2Error) +		} +	} +	secret, err = base64.StdEncoding.DecodeString(out[4]) +	if err != nil { +		tlog.Fatal.Println(err) +		os.Exit(exitcodes.FIDO2Error) +	} + +	// sanity checks +	secretLen := len(secret) +	if secretLen < 32 { +		tlog.Fatal.Printf("FIDO2 HMACSecret too short (%d)!\n", secretLen) +		os.Exit(exitcodes.FIDO2Error) +	} +	zero := make([]byte, secretLen) +	if bytes.Equal(zero, secret) { +		tlog.Fatal.Printf("FIDO2 HMACSecret is all zero!") +		os.Exit(exitcodes.FIDO2Error) +	} + +	return secret +} @@ -17,6 +17,7 @@ import (  	"github.com/rfjakob/gocryptfs/internal/configfile"  	"github.com/rfjakob/gocryptfs/internal/contentenc"  	"github.com/rfjakob/gocryptfs/internal/exitcodes" +	"github.com/rfjakob/gocryptfs/internal/fido2"  	"github.com/rfjakob/gocryptfs/internal/readpassword"  	"github.com/rfjakob/gocryptfs/internal/speed"  	"github.com/rfjakob/gocryptfs/internal/stupidgcm" @@ -50,7 +51,16 @@ func loadConfig(args *argContainer) (masterkey []byte, cf *configfile.ConfFile,  	if masterkey != nil {  		return masterkey, cf, nil  	} -	pw := readpassword.Once([]string(args.extpass), []string(args.passfile), "") +	var pw []byte +	if cf.IsFeatureFlagSet(configfile.FlagFIDO2) { +		if args.fido2 == "" { +			tlog.Fatal.Printf("Masterkey encrypted using FIDO2 token; need to use the --fido2 option.") +			os.Exit(exitcodes.Usage) +		} +		pw = fido2.Secret(args.fido2, cf.FIDO2.CredentialID, cf.FIDO2.HMACSalt) +	} else { +		pw = readpassword.Once([]string(args.extpass), []string(args.passfile), "") +	}  	tlog.Info.Println("Decrypting master key")  	masterkey, err = cf.DecryptMasterKey(pw)  	for i := range pw { @@ -78,6 +88,10 @@ func changePassword(args *argContainer) {  		if len(masterkey) == 0 {  			log.Panic("empty masterkey")  		} +		if confFile.IsFeatureFlagSet(configfile.FlagFIDO2) { +			tlog.Fatal.Printf("Password change is not supported on FIDO2-enabled filesystems.") +			os.Exit(exitcodes.Usage) +		}  		tlog.Info.Println("Please enter your new password.")  		newPw := readpassword.Twice([]string(args.extpass), []string(args.passfile))  		logN := confFile.ScryptObject.LogN() | 
