aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPavol Rusnak2020-09-05 22:42:15 +0200
committerJakob Unterwurzacher2020-09-12 18:06:54 +0200
commit1e624a4cc3aafa57b5fa213c88bcd3689cefd1c3 (patch)
treea6e4f51ecb2dc0bac4276b2f65b39a3b426bc4ee /internal
parent6a9c49e9cf23c85622dd4b181cdc615abc72d6bb (diff)
Add support for FIDO2 tokens
Diffstat (limited to 'internal')
-rw-r--r--internal/configfile/config_file.go20
-rw-r--r--internal/configfile/config_test.go8
-rw-r--r--internal/configfile/feature_flags.go4
-rw-r--r--internal/exitcodes/exitcodes.go2
-rw-r--r--internal/fido2/fido2.go110
5 files changed, 138 insertions, 6 deletions
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
+}