From 1e624a4cc3aafa57b5fa213c88bcd3689cefd1c3 Mon Sep 17 00:00:00 2001 From: Pavol Rusnak Date: Sat, 5 Sep 2020 22:42:15 +0200 Subject: Add support for FIDO2 tokens --- internal/configfile/config_file.go | 20 ++++++- internal/configfile/config_test.go | 8 +-- internal/configfile/feature_flags.go | 4 ++ internal/exitcodes/exitcodes.go | 2 + internal/fido2/fido2.go | 110 +++++++++++++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 6 deletions(-) create mode 100644 internal/fido2/fido2.go (limited to 'internal') 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 +} -- cgit v1.2.3