Add support for FIDO2 tokens
This commit is contained in:
parent
6a9c49e9cf
commit
1e624a4cc3
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
19
init_dir.go
19
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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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
110
internal/fido2/fido2.go
Normal 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
16
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()
|
||||
|
Loading…
Reference in New Issue
Block a user