Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for FIDO2 tokens #505

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Documentation/MANPAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion cli_args.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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)
Expand Down
21 changes: 17 additions & 4 deletions gocryptfs-xray/xray_main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
19 changes: 16 additions & 3 deletions init_dir.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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, args.fido2 != "", fido2CredentialID, fido2HmacSalt)
if err != nil {
tlog.Fatal.Println(err)
os.Exit(exitcodes.WriteConf)
Expand Down
17 changes: 16 additions & 1 deletion internal/configfile/config_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,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.
Expand All @@ -46,6 +54,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
}
Expand All @@ -69,7 +79,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, fido2 bool, fido2CredentialID []byte, fido2HmacSalt []byte) error {
var cf ConfFile
cf.filename = filename
cf.Creator = creator
Expand All @@ -89,6 +99,11 @@ func Create(filename string, password []byte, plaintextNames bool,
if aessiv {
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagAESSIV])
}
if fido2 {
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagFIDO2])
cf.FIDO2.CredentialID = fido2CredentialID
cf.FIDO2.HMACSalt = fido2HmacSalt
}
{
// Generate new random master key
var key []byte
Expand Down
4 changes: 4 additions & 0 deletions internal/configfile/feature_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions internal/exitcodes/exitcodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
107 changes: 107 additions & 0 deletions internal/fido2/fido2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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)
}
in, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
for _, s := range stdin {
io.WriteString(in, s+"\n")
}
in.Close()
out, err := cmd.Output()
if err != nil {
return nil, err
}
return strings.Split(string(out), "\n"), nil
}

// Register registers a credential using a FIDO2 token
func Register(device string, userName string) (credentialID []byte) {
fmt.Println("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) {
fmt.Println("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 changes: 15 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down