Skip to content

Commit 1e624a4

Browse files
prusnakrfjakob
authored andcommitted
Add support for FIDO2 tokens
1 parent 6a9c49e commit 1e624a4

File tree

10 files changed

+196
-15
lines changed

10 files changed

+196
-15
lines changed

Documentation/MANPAGE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,10 @@ to your program, use `"--"`, which is accepted by most programs:
130130
Stay in the foreground instead of forking away. Implies "-nosyslog".
131131
For compatibility, "-f" is also accepted, but "-fg" is preferred.
132132

133+
#### -fido2 DEVICE_PATH
134+
Use a FIDO2 token to initialize and unlock the filesystem.
135+
Use "fido2-token -L" to obtain the FIDO2 token device path.
136+
133137
#### -force_owner string
134138
If given a string of the form "uid:gid" (where both "uid" and "gid" are
135139
substituted with positive integers), presents all files as owned by the given

cli_args.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type argContainer struct {
3232
// Mount options with opposites
3333
dev, nodev, suid, nosuid, exec, noexec, rw, ro bool
3434
masterkey, mountpoint, cipherdir, cpuprofile,
35-
memprofile, ko, ctlsock, fsname, force_owner, trace string
35+
memprofile, ko, ctlsock, fsname, force_owner, trace, fido2 string
3636
// -extpass, -badname, -passfile can be passed multiple times
3737
extpass, badname, passfile multipleStrings
3838
// For reverse mode, several ways to specify exclusions. All can be specified multiple times.
@@ -189,6 +189,7 @@ func parseCliOpts() (args argContainer) {
189189
flagSet.StringVar(&args.fsname, "fsname", "", "Override the filesystem name")
190190
flagSet.StringVar(&args.force_owner, "force_owner", "", "uid:gid pair to coerce ownership")
191191
flagSet.StringVar(&args.trace, "trace", "", "Write execution trace to file")
192+
flagSet.StringVar(&args.fido2, "fido2", "", "Protect the masterkey using a FIDO2 token instead of a password")
192193

193194
// Exclusion options
194195
flagSet.Var(&args.exclude, "e", "Alias for -exclude")
@@ -279,6 +280,10 @@ func parseCliOpts() (args argContainer) {
279280
tlog.Fatal.Printf("The options -extpass and -masterkey cannot be used at the same time")
280281
os.Exit(exitcodes.Usage)
281282
}
283+
if !args.extpass.Empty() && args.fido2 != "" {
284+
tlog.Fatal.Printf("The options -extpass and -fido2 cannot be used at the same time")
285+
os.Exit(exitcodes.Usage)
286+
}
282287
if args.idle < 0 {
283288
tlog.Fatal.Printf("Idle timeout cannot be less than 0")
284289
os.Exit(exitcodes.Usage)

gocryptfs-xray/xray_main.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/rfjakob/gocryptfs/internal/contentenc"
1212
"github.com/rfjakob/gocryptfs/internal/cryptocore"
1313
"github.com/rfjakob/gocryptfs/internal/exitcodes"
14+
"github.com/rfjakob/gocryptfs/internal/fido2"
1415
"github.com/rfjakob/gocryptfs/internal/readpassword"
1516
"github.com/rfjakob/gocryptfs/internal/tlog"
1617
)
@@ -67,12 +68,14 @@ func main() {
6768
encryptPaths *bool
6869
aessiv *bool
6970
sep0 *bool
71+
fido2 *string
7072
}
7173
args.dumpmasterkey = flag.Bool("dumpmasterkey", false, "Decrypt and dump the master key")
7274
args.decryptPaths = flag.Bool("decrypt-paths", false, "Decrypt file paths using gocryptfs control socket")
7375
args.encryptPaths = flag.Bool("encrypt-paths", false, "Encrypt file paths using gocryptfs control socket")
7476
args.sep0 = flag.Bool("0", false, "Use \\0 instead of \\n as separator")
7577
args.aessiv = flag.Bool("aessiv", false, "Assume AES-SIV mode instead of AES-GCM")
78+
args.fido2 = flag.String("fido2", "", "Protect the masterkey using a FIDO2 token instead of a password")
7679
flag.Usage = usage
7780
flag.Parse()
7881
s := sum(args.dumpmasterkey, args.decryptPaths, args.encryptPaths)
@@ -97,20 +100,30 @@ func main() {
97100
}
98101
defer fd.Close()
99102
if *args.dumpmasterkey {
100-
dumpMasterKey(fn)
103+
dumpMasterKey(fn, *args.fido2)
101104
} else {
102105
inspectCiphertext(fd, *args.aessiv)
103106
}
104107
}
105108

106-
func dumpMasterKey(fn string) {
109+
func dumpMasterKey(fn string, fido2Path string) {
107110
tlog.Info.Enabled = false
108-
pw := readpassword.Once(nil, nil, "")
109-
masterkey, _, err := configfile.LoadAndDecrypt(fn, pw)
111+
cf, err := configfile.Load(fn)
110112
if err != nil {
111113
fmt.Fprintln(os.Stderr, err)
112114
exitcodes.Exit(err)
113115
}
116+
var pw []byte
117+
if cf.IsFeatureFlagSet(configfile.FlagFIDO2) {
118+
if fido2Path == "" {
119+
tlog.Fatal.Printf("Masterkey encrypted using FIDO2 token; need to use the --fido2 option.")
120+
os.Exit(exitcodes.Usage)
121+
}
122+
pw = fido2.Secret(fido2Path, cf.FIDO2.CredentialID, cf.FIDO2.HMACSalt)
123+
} else {
124+
pw = readpassword.Once(nil, nil, "")
125+
}
126+
masterkey, err := cf.DecryptMasterKey(pw)
114127
fmt.Println(hex.EncodeToString(masterkey))
115128
for i := range pw {
116129
pw[i] = 0

init_dir.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
"syscall"
1010

1111
"github.com/rfjakob/gocryptfs/internal/configfile"
12+
"github.com/rfjakob/gocryptfs/internal/cryptocore"
1213
"github.com/rfjakob/gocryptfs/internal/exitcodes"
14+
"github.com/rfjakob/gocryptfs/internal/fido2"
1315
"github.com/rfjakob/gocryptfs/internal/nametransform"
1416
"github.com/rfjakob/gocryptfs/internal/readpassword"
1517
"github.com/rfjakob/gocryptfs/internal/syscallcompat"
@@ -67,14 +69,25 @@ func initDir(args *argContainer) {
6769
}
6870
}
6971
// Choose password for config file
70-
if args.extpass.Empty() {
72+
if args.extpass.Empty() && args.fido2 == "" {
7173
tlog.Info.Printf("Choose a password for protecting your files.")
7274
}
7375
{
74-
password := readpassword.Twice([]string(args.extpass), []string(args.passfile))
76+
var password []byte
77+
var fido2CredentialID, fido2HmacSalt []byte
78+
if args.fido2 != "" {
79+
fido2CredentialID = fido2.Register(args.fido2, filepath.Base(args.cipherdir))
80+
fido2HmacSalt = cryptocore.RandBytes(32)
81+
password = fido2.Secret(args.fido2, fido2CredentialID, fido2HmacSalt)
82+
} else {
83+
// normal password entry
84+
password = readpassword.Twice([]string(args.extpass), []string(args.passfile))
85+
fido2CredentialID = nil
86+
fido2HmacSalt = nil
87+
}
7588
creator := tlog.ProgramName + " " + GitVersion
7689
err = configfile.Create(args.config, password, args.plaintextnames,
77-
args.scryptn, creator, args.aessiv, args.devrandom)
90+
args.scryptn, creator, args.aessiv, args.devrandom, fido2CredentialID, fido2HmacSalt)
7891
if err != nil {
7992
tlog.Fatal.Println(err)
8093
os.Exit(exitcodes.WriteConf)

internal/configfile/config_file.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@ import (
1010
"log"
1111
"syscall"
1212

13+
"os"
14+
1315
"github.com/rfjakob/gocryptfs/internal/contentenc"
1416
"github.com/rfjakob/gocryptfs/internal/cryptocore"
1517
"github.com/rfjakob/gocryptfs/internal/exitcodes"
1618
"github.com/rfjakob/gocryptfs/internal/tlog"
1719
)
18-
import "os"
1920

2021
const (
2122
// ConfDefaultName is the default configuration file name.
@@ -28,6 +29,14 @@ const (
2829
ConfReverseName = ".gocryptfs.reverse.conf"
2930
)
3031

32+
// FIDO2Params is a structure for storing FIDO2 parameters.
33+
type FIDO2Params struct {
34+
// FIDO2 credential
35+
CredentialID []byte
36+
// FIDO2 hmac-secret salt
37+
HMACSalt []byte
38+
}
39+
3140
// ConfFile is the content of a config file.
3241
type ConfFile struct {
3342
// Creator is the gocryptfs version string.
@@ -46,6 +55,8 @@ type ConfFile struct {
4655
// mounting. This mechanism is analogous to the ext4 feature flags that are
4756
// stored in the superblock.
4857
FeatureFlags []string
58+
// FIDO2 parameters
59+
FIDO2 FIDO2Params
4960
// Filename is the name of the config file. Not exported to JSON.
5061
filename string
5162
}
@@ -69,7 +80,7 @@ func randBytesDevRandom(n int) []byte {
6980
// "password" and write it to "filename".
7081
// Uses scrypt with cost parameter logN.
7182
func Create(filename string, password []byte, plaintextNames bool,
72-
logN int, creator string, aessiv bool, devrandom bool) error {
83+
logN int, creator string, aessiv bool, devrandom bool, fido2CredentialID []byte, fido2HmacSalt []byte) error {
7384
var cf ConfFile
7485
cf.filename = filename
7586
cf.Creator = creator
@@ -89,6 +100,11 @@ func Create(filename string, password []byte, plaintextNames bool,
89100
if aessiv {
90101
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagAESSIV])
91102
}
103+
if len(fido2CredentialID) > 0 {
104+
cf.FeatureFlags = append(cf.FeatureFlags, knownFlags[FlagFIDO2])
105+
cf.FIDO2.CredentialID = fido2CredentialID
106+
cf.FIDO2.HMACSalt = fido2HmacSalt
107+
}
92108
{
93109
// Generate new random master key
94110
var key []byte

internal/configfile/config_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func TestLoadV2StrangeFeature(t *testing.T) {
6262
}
6363

6464
func TestCreateConfDefault(t *testing.T) {
65-
err := Create("config_test/tmp.conf", testPw, false, 10, "test", false, false)
65+
err := Create("config_test/tmp.conf", testPw, false, 10, "test", false, false, nil, nil)
6666
if err != nil {
6767
t.Fatal(err)
6868
}
@@ -83,14 +83,14 @@ func TestCreateConfDefault(t *testing.T) {
8383
}
8484

8585
func TestCreateConfDevRandom(t *testing.T) {
86-
err := Create("config_test/tmp.conf", testPw, false, 10, "test", false, true)
86+
err := Create("config_test/tmp.conf", testPw, false, 10, "test", false, true, nil, nil)
8787
if err != nil {
8888
t.Fatal(err)
8989
}
9090
}
9191

9292
func TestCreateConfPlaintextnames(t *testing.T) {
93-
err := Create("config_test/tmp.conf", testPw, true, 10, "test", false, false)
93+
err := Create("config_test/tmp.conf", testPw, true, 10, "test", false, false, nil, nil)
9494
if err != nil {
9595
t.Fatal(err)
9696
}
@@ -111,7 +111,7 @@ func TestCreateConfPlaintextnames(t *testing.T) {
111111

112112
// Reverse mode uses AESSIV
113113
func TestCreateConfFileAESSIV(t *testing.T) {
114-
err := Create("config_test/tmp.conf", testPw, false, 10, "test", true, false)
114+
err := Create("config_test/tmp.conf", testPw, false, 10, "test", true, false, nil, nil)
115115
if err != nil {
116116
t.Fatal(err)
117117
}

internal/configfile/feature_flags.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ const (
2525
// Note that this flag does not change the password hashing algorithm
2626
// which always is scrypt.
2727
FlagHKDF
28+
// FlagFIDO2 means that "-fido2" was used when creating the filesystem.
29+
// The masterkey is protected using a FIDO2 token instead of a password.
30+
FlagFIDO2
2831
)
2932

3033
// knownFlags stores the known feature flags and their string representation
@@ -37,6 +40,7 @@ var knownFlags = map[flagIota]string{
3740
FlagAESSIV: "AESSIV",
3841
FlagRaw64: "Raw64",
3942
FlagHKDF: "HKDF",
43+
FlagFIDO2: "FIDO2",
4044
}
4145

4246
// Filesystems that do not have these feature flags set are deprecated.

internal/exitcodes/exitcodes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ const (
7070
ExcludeError = 29
7171
// DevNull means that /dev/null could not be opened
7272
DevNull = 30
73+
// FIDO2Error - an error was encountered while interacting with a FIDO2 token
74+
FIDO2Error = 31
7375
)
7476

7577
// Err wraps an error with an associated numeric exit code

internal/fido2/fido2.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package fido2
2+
3+
import (
4+
"bytes"
5+
"encoding/base64"
6+
"fmt"
7+
"io"
8+
"os"
9+
"os/exec"
10+
"strings"
11+
12+
"github.com/rfjakob/gocryptfs/internal/cryptocore"
13+
"github.com/rfjakob/gocryptfs/internal/exitcodes"
14+
"github.com/rfjakob/gocryptfs/internal/tlog"
15+
)
16+
17+
type fidoCommand int
18+
19+
const (
20+
cred fidoCommand = iota
21+
assert fidoCommand = iota
22+
assertWithPIN fidoCommand = iota
23+
)
24+
25+
const relyingPartyID = "gocryptfs"
26+
27+
func callFidoCommand(command fidoCommand, device string, stdin []string) ([]string, error) {
28+
var cmd *exec.Cmd
29+
switch command {
30+
case cred:
31+
cmd = exec.Command("fido2-cred", "-M", "-h", "-v", device)
32+
case assert:
33+
cmd = exec.Command("fido2-assert", "-G", "-h", device)
34+
case assertWithPIN:
35+
cmd = exec.Command("fido2-assert", "-G", "-h", "-v", device)
36+
}
37+
tlog.Debug.Printf("callFidoCommand: executing %q with args %v", cmd.Path, cmd.Args)
38+
cmd.Stderr = os.Stderr
39+
in, err := cmd.StdinPipe()
40+
if err != nil {
41+
return nil, err
42+
}
43+
for _, s := range stdin {
44+
// This does not deadlock because the pipe buffer is big enough (64kiB on Linux)
45+
io.WriteString(in, s+"\n")
46+
}
47+
in.Close()
48+
out, err := cmd.Output()
49+
if err != nil {
50+
return nil, fmt.Errorf("%s failed with %v", cmd.Args[0], err)
51+
}
52+
return strings.Split(string(out), "\n"), nil
53+
}
54+
55+
// Register registers a credential using a FIDO2 token
56+
func Register(device string, userName string) (credentialID []byte) {
57+
tlog.Info.Printf("FIDO2 Register: interact with your device ...")
58+
cdh := base64.StdEncoding.EncodeToString(cryptocore.RandBytes(32))
59+
userID := base64.StdEncoding.EncodeToString(cryptocore.RandBytes(32))
60+
stdin := []string{cdh, relyingPartyID, userName, userID}
61+
out, err := callFidoCommand(cred, device, stdin)
62+
if err != nil {
63+
tlog.Fatal.Println(err)
64+
os.Exit(exitcodes.FIDO2Error)
65+
}
66+
credentialID, err = base64.StdEncoding.DecodeString(out[4])
67+
if err != nil {
68+
tlog.Fatal.Println(err)
69+
os.Exit(exitcodes.FIDO2Error)
70+
}
71+
return credentialID
72+
}
73+
74+
// Secret generates a HMAC secret using a FIDO2 token
75+
func Secret(device string, credentialID []byte, salt []byte) (secret []byte) {
76+
tlog.Info.Printf("FIDO2 Secret: interact with your device ...")
77+
cdh := base64.StdEncoding.EncodeToString(cryptocore.RandBytes(32))
78+
crid := base64.StdEncoding.EncodeToString(credentialID)
79+
hmacsalt := base64.StdEncoding.EncodeToString(salt)
80+
stdin := []string{cdh, relyingPartyID, crid, hmacsalt}
81+
// try asserting without PIN first
82+
out, err := callFidoCommand(assert, device, stdin)
83+
if err != nil {
84+
// if that fails, let's assert with PIN
85+
out, err = callFidoCommand(assertWithPIN, device, stdin)
86+
if err != nil {
87+
tlog.Fatal.Println(err)
88+
os.Exit(exitcodes.FIDO2Error)
89+
}
90+
}
91+
secret, err = base64.StdEncoding.DecodeString(out[4])
92+
if err != nil {
93+
tlog.Fatal.Println(err)
94+
os.Exit(exitcodes.FIDO2Error)
95+
}
96+
97+
// sanity checks
98+
secretLen := len(secret)
99+
if secretLen < 32 {
100+
tlog.Fatal.Printf("FIDO2 HMACSecret too short (%d)!\n", secretLen)
101+
os.Exit(exitcodes.FIDO2Error)
102+
}
103+
zero := make([]byte, secretLen)
104+
if bytes.Equal(zero, secret) {
105+
tlog.Fatal.Printf("FIDO2 HMACSecret is all zero!")
106+
os.Exit(exitcodes.FIDO2Error)
107+
}
108+
109+
return secret
110+
}

0 commit comments

Comments
 (0)