Add support for FIDO2 tokens

This commit is contained in:
Pavol Rusnak 2020-09-05 22:42:15 +02:00 committed by Jakob Unterwurzacher
parent 6a9c49e9cf
commit 1e624a4cc3
10 changed files with 196 additions and 15 deletions

View File

@ -130,6 +130,10 @@ to your program, use `"--"`, which is accepted by most programs:
Stay in the foreground instead of forking away. Implies "-nosyslog". Stay in the foreground instead of forking away. Implies "-nosyslog".
For compatibility, "-f" is also accepted, but "-fg" is preferred. 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 #### -force_owner string
If given a string of the form "uid:gid" (where both "uid" and "gid" are 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 substituted with positive integers), presents all files as owned by the given

View File

@ -32,7 +32,7 @@ type argContainer struct {
// Mount options with opposites // Mount options with opposites
dev, nodev, suid, nosuid, exec, noexec, rw, ro bool dev, nodev, suid, nosuid, exec, noexec, rw, ro bool
masterkey, mountpoint, cipherdir, cpuprofile, 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 can be passed multiple times
extpass, badname, passfile multipleStrings extpass, badname, passfile multipleStrings
// For reverse mode, several ways to specify exclusions. All can be specified multiple times. // 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.fsname, "fsname", "", "Override the filesystem name")
flagSet.StringVar(&args.force_owner, "force_owner", "", "uid:gid pair to coerce ownership") 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.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 // Exclusion options
flagSet.Var(&args.exclude, "e", "Alias for -exclude") 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") tlog.Fatal.Printf("The options -extpass and -masterkey cannot be used at the same time")
os.Exit(exitcodes.Usage) 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 { if args.idle < 0 {
tlog.Fatal.Printf("Idle timeout cannot be less than 0") tlog.Fatal.Printf("Idle timeout cannot be less than 0")
os.Exit(exitcodes.Usage) os.Exit(exitcodes.Usage)

View File

@ -11,6 +11,7 @@ import (
"github.com/rfjakob/gocryptfs/internal/contentenc" "github.com/rfjakob/gocryptfs/internal/contentenc"
"github.com/rfjakob/gocryptfs/internal/cryptocore" "github.com/rfjakob/gocryptfs/internal/cryptocore"
"github.com/rfjakob/gocryptfs/internal/exitcodes" "github.com/rfjakob/gocryptfs/internal/exitcodes"
"github.com/rfjakob/gocryptfs/internal/fido2"
"github.com/rfjakob/gocryptfs/internal/readpassword" "github.com/rfjakob/gocryptfs/internal/readpassword"
"github.com/rfjakob/gocryptfs/internal/tlog" "github.com/rfjakob/gocryptfs/internal/tlog"
) )
@ -67,12 +68,14 @@ func main() {
encryptPaths *bool encryptPaths *bool
aessiv *bool aessiv *bool
sep0 *bool sep0 *bool
fido2 *string
} }
args.dumpmasterkey = flag.Bool("dumpmasterkey", false, "Decrypt and dump the master key") 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.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.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.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.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.Usage = usage
flag.Parse() flag.Parse()
s := sum(args.dumpmasterkey, args.decryptPaths, args.encryptPaths) s := sum(args.dumpmasterkey, args.decryptPaths, args.encryptPaths)
@ -97,20 +100,30 @@ func main() {
} }
defer fd.Close() defer fd.Close()
if *args.dumpmasterkey { if *args.dumpmasterkey {
dumpMasterKey(fn) dumpMasterKey(fn, *args.fido2)
} else { } else {
inspectCiphertext(fd, *args.aessiv) inspectCiphertext(fd, *args.aessiv)
} }
} }
func dumpMasterKey(fn string) { func dumpMasterKey(fn string, fido2Path string) {
tlog.Info.Enabled = false tlog.Info.Enabled = false
pw := readpassword.Once(nil, nil, "") cf, err := configfile.Load(fn)
masterkey, _, err := configfile.LoadAndDecrypt(fn, pw)
if err != nil { if err != nil {
fmt.Fprintln(os.Stderr, err) fmt.Fprintln(os.Stderr, err)
exitcodes.Exit(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)) fmt.Println(hex.EncodeToString(masterkey))
for i := range pw { for i := range pw {
pw[i] = 0 pw[i] = 0

View File

@ -9,7 +9,9 @@ import (
"syscall" "syscall"
"github.com/rfjakob/gocryptfs/internal/configfile" "github.com/rfjakob/gocryptfs/internal/configfile"
"github.com/rfjakob/gocryptfs/internal/cryptocore"
"github.com/rfjakob/gocryptfs/internal/exitcodes" "github.com/rfjakob/gocryptfs/internal/exitcodes"
"github.com/rfjakob/gocryptfs/internal/fido2"
"github.com/rfjakob/gocryptfs/internal/nametransform" "github.com/rfjakob/gocryptfs/internal/nametransform"
"github.com/rfjakob/gocryptfs/internal/readpassword" "github.com/rfjakob/gocryptfs/internal/readpassword"
"github.com/rfjakob/gocryptfs/internal/syscallcompat" "github.com/rfjakob/gocryptfs/internal/syscallcompat"
@ -67,14 +69,25 @@ func initDir(args *argContainer) {
} }
} }
// Choose password for config file // 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.") 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 creator := tlog.ProgramName + " " + GitVersion
err = configfile.Create(args.config, password, args.plaintextnames, 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 { if err != nil {
tlog.Fatal.Println(err) tlog.Fatal.Println(err)
os.Exit(exitcodes.WriteConf) os.Exit(exitcodes.WriteConf)

View File

@ -10,12 +10,13 @@ import (
"log" "log"
"syscall" "syscall"
"os"
"github.com/rfjakob/gocryptfs/internal/contentenc" "github.com/rfjakob/gocryptfs/internal/contentenc"
"github.com/rfjakob/gocryptfs/internal/cryptocore" "github.com/rfjakob/gocryptfs/internal/cryptocore"
"github.com/rfjakob/gocryptfs/internal/exitcodes" "github.com/rfjakob/gocryptfs/internal/exitcodes"
"github.com/rfjakob/gocryptfs/internal/tlog" "github.com/rfjakob/gocryptfs/internal/tlog"
) )
import "os"
const ( const (
// ConfDefaultName is the default configuration file name. // ConfDefaultName is the default configuration file name.
@ -28,6 +29,14 @@ const (
ConfReverseName = ".gocryptfs.reverse.conf" 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. // ConfFile is the content of a config file.
type ConfFile struct { type ConfFile struct {
// Creator is the gocryptfs version string. // 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 // mounting. This mechanism is analogous to the ext4 feature flags that are
// stored in the superblock. // stored in the superblock.
FeatureFlags []string FeatureFlags []string
// FIDO2 parameters
FIDO2 FIDO2Params
// Filename is the name of the config file. Not exported to JSON. // Filename is the name of the config file. Not exported to JSON.
filename string filename string
} }
@ -69,7 +80,7 @@ func randBytesDevRandom(n int) []byte {
// "password" and write it to "filename". // "password" and write it to "filename".
// Uses scrypt with cost parameter logN. // Uses scrypt with cost parameter logN.
func Create(filename string, password []byte, plaintextNames bool, 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 var cf ConfFile
cf.filename = filename cf.filename = filename
cf.Creator = creator cf.Creator = creator
@ -89,6 +100,11 @@ func Create(filename string, password []byte, plaintextNames bool,
if aessiv { if aessiv {
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagAESSIV]) 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 // Generate new random master key
var key []byte var key []byte

View File

@ -62,7 +62,7 @@ func TestLoadV2StrangeFeature(t *testing.T) {
} }
func TestCreateConfDefault(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -83,14 +83,14 @@ func TestCreateConfDefault(t *testing.T) {
} }
func TestCreateConfDevRandom(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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
func TestCreateConfPlaintextnames(t *testing.T) { 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -111,7 +111,7 @@ func TestCreateConfPlaintextnames(t *testing.T) {
// Reverse mode uses AESSIV // Reverse mode uses AESSIV
func TestCreateConfFileAESSIV(t *testing.T) { 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 { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -25,6 +25,9 @@ const (
// Note that this flag does not change the password hashing algorithm // Note that this flag does not change the password hashing algorithm
// which always is scrypt. // which always is scrypt.
FlagHKDF 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 // knownFlags stores the known feature flags and their string representation
@ -37,6 +40,7 @@ var knownFlags = map[flagIota]string{
FlagAESSIV: "AESSIV", FlagAESSIV: "AESSIV",
FlagRaw64: "Raw64", FlagRaw64: "Raw64",
FlagHKDF: "HKDF", FlagHKDF: "HKDF",
FlagFIDO2: "FIDO2",
} }
// Filesystems that do not have these feature flags set are deprecated. // Filesystems that do not have these feature flags set are deprecated.

View File

@ -70,6 +70,8 @@ const (
ExcludeError = 29 ExcludeError = 29
// DevNull means that /dev/null could not be opened // DevNull means that /dev/null could not be opened
DevNull = 30 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 // Err wraps an error with an associated numeric exit code

110
internal/fido2/fido2.go Normal file
View File

@ -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
}

16
main.go
View File

@ -17,6 +17,7 @@ import (
"github.com/rfjakob/gocryptfs/internal/configfile" "github.com/rfjakob/gocryptfs/internal/configfile"
"github.com/rfjakob/gocryptfs/internal/contentenc" "github.com/rfjakob/gocryptfs/internal/contentenc"
"github.com/rfjakob/gocryptfs/internal/exitcodes" "github.com/rfjakob/gocryptfs/internal/exitcodes"
"github.com/rfjakob/gocryptfs/internal/fido2"
"github.com/rfjakob/gocryptfs/internal/readpassword" "github.com/rfjakob/gocryptfs/internal/readpassword"
"github.com/rfjakob/gocryptfs/internal/speed" "github.com/rfjakob/gocryptfs/internal/speed"
"github.com/rfjakob/gocryptfs/internal/stupidgcm" "github.com/rfjakob/gocryptfs/internal/stupidgcm"
@ -50,7 +51,16 @@ func loadConfig(args *argContainer) (masterkey []byte, cf *configfile.ConfFile,
if masterkey != nil { if masterkey != nil {
return masterkey, cf, 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") tlog.Info.Println("Decrypting master key")
masterkey, err = cf.DecryptMasterKey(pw) masterkey, err = cf.DecryptMasterKey(pw)
for i := range pw { for i := range pw {
@ -78,6 +88,10 @@ func changePassword(args *argContainer) {
if len(masterkey) == 0 { if len(masterkey) == 0 {
log.Panic("empty masterkey") 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.") tlog.Info.Println("Please enter your new password.")
newPw := readpassword.Twice([]string(args.extpass), []string(args.passfile)) newPw := readpassword.Twice([]string(args.extpass), []string(args.passfile))
logN := confFile.ScryptObject.LogN() logN := confFile.ScryptObject.LogN()