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".
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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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/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()