summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Documentation/MANPAGE.md4
-rw-r--r--cli_args.go7
-rw-r--r--gocryptfs-xray/xray_main.go21
-rw-r--r--init_dir.go19
-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
-rw-r--r--main.go16
10 files changed, 196 insertions, 15 deletions
diff --git a/Documentation/MANPAGE.md b/Documentation/MANPAGE.md
index 43876d3..40c2f26 100644
--- a/Documentation/MANPAGE.md
+++ b/Documentation/MANPAGE.md
@@ -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
diff --git a/cli_args.go b/cli_args.go
index e4073fa..11bb96e 100644
--- a/cli_args.go
+++ b/cli_args.go
@@ -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)
diff --git a/gocryptfs-xray/xray_main.go b/gocryptfs-xray/xray_main.go
index 7e928e7..dec803b 100644
--- a/gocryptfs-xray/xray_main.go
+++ b/gocryptfs-xray/xray_main.go
@@ -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
diff --git a/init_dir.go b/init_dir.go
index 19fabcf..68268a0 100644
--- a/init_dir.go
+++ b/init_dir.go
@@ -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)
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
+}
diff --git a/main.go b/main.go
index 11e15b2..49e213b 100644
--- a/main.go
+++ b/main.go
@@ -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()