next-eeze.git

commit e980dcad52e4cc0e6d4d3e895806b1a610672930

Author: Adam <git@apiote.xyz>

use fido2 device to derive master key

 config/init.go | 34 ++++++++++--
 crypto/crypto.go | 74 ++++++++++++++++++++++++++
 eeze.go | 47 ++++++++++++++--
 fido/fido.go | 82 +++++++++++++++++++++++++++++
 fs/fs.go | 139 +++++++++++++++++++++++++------------------------


diff --git a/config/init.go b/config/init.go
index 09a531b3e9e55f70d56b01fc836aad2e7aa96aa5..000159fe9507d34994b328f7b0fdf13e0385ddad 100644
--- a/config/init.go
+++ b/config/init.go
@@ -1,8 +1,11 @@
 package config
 
 import (
+	"notabug.org/apiote/next-eeze/crypto"
+	"notabug.org/apiote/next-eeze/fido"
 	"notabug.org/apiote/next-eeze/fs"
 
+	"encoding/hex"
 	"fmt"
 	"os"
 
@@ -26,12 +29,31 @@
 	fs.SaveCredentials(credentials, masterPassword)
 }
 
-func Reëncrypt(masterPassword string) (string, error) {
-	fmt.Print("New master password: ")
-	// todo memguard
-	p_b, _ := terminal.ReadPassword(int(os.Stdin.Fd()))
-	newMasterPassword := string(p_b)
-	fmt.Print("\n")
+func Reëncrypt(masterPassword string, useFido bool) (string, error) {
+	newMasterPassword := ""
+	err := fs.RemoveFidoCredential()
+	if err != nil {
+		fmt.Println(err)
+		return "", err
+	}
+	if useFido {
+		cdh := crypto.MakeSalt()
+		salt := crypto.MakeSalt()
+		credID := fido.Setup("next-eeze", "", cdh) // todo pin
+		secret := fido.GetHmacSecret("next-eeze", "", cdh, salt, credID)
+		newMasterPassword = hex.EncodeToString(secret)
+		fs.SaveFidoCredential(fs.FidoCredential{
+			Salt:   salt,
+			Cdh:    cdh,
+			CredID: credID,
+		})
+	} else {
+		fmt.Print("New master password: ")
+		// todo memguard
+		p_b, _ := terminal.ReadPassword(int(os.Stdin.Fd()))
+		newMasterPassword = string(p_b)
+		fmt.Print("\n")
+	}
 	// todo memguard
 	credentials, err := fs.ReadCredentials(masterPassword)
 	if err != nil {




diff --git a/crypto/crypto.go b/crypto/crypto.go
new file mode 100644
index 0000000000000000000000000000000000000000..e36f8dd5590f595dab11cfc8f20cc4983dd80d8e
--- /dev/null
+++ b/crypto/crypto.go
@@ -0,0 +1,74 @@
+package crypto
+
+import (
+	"io"
+	"crypto/rand"
+	"crypto/aes"
+	"crypto/cipher"
+	"errors"
+
+	"golang.org/x/crypto/argon2"
+)
+
+func MakeSalt() []byte {
+	key := [32]byte{}
+	_, err := io.ReadFull(rand.Reader, key[:])
+	if err != nil {
+		panic(err)
+	}
+	return key[:]
+}
+
+// todo memguard password
+func DeriveKey(password string, salt []byte) [32]byte {
+	// todo memguard
+	key := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
+	// todo memguard
+	var keyArr [32]byte
+	copy(keyArr[:], key)
+	return keyArr
+}
+
+// todo memguard plaintext, key
+func Encrypt(plaintext []byte, key [32]byte) (ciphertext []byte, err error) {
+	block, err := aes.NewCipher(key[:])
+	if err != nil {
+		return nil, err
+	}
+
+	gcm, err := cipher.NewGCM(block)
+	if err != nil {
+		return nil, err
+	}
+
+	nonce := make([]byte, gcm.NonceSize())
+	_, err = io.ReadFull(rand.Reader, nonce)
+	if err != nil {
+		return nil, err
+	}
+
+	return gcm.Seal(nonce, nonce, plaintext, nil), nil
+}
+
+// todo memguard key
+func Decrypt(ciphertext []byte, key [32]byte) (plaintext []byte, err error) {
+	block, err := aes.NewCipher(key[:])
+	if err != nil {
+		return nil, err
+	}
+
+	gcm, err := cipher.NewGCM(block)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(ciphertext) < gcm.NonceSize() {
+		return nil, errors.New("malformed ciphertext")
+	}
+
+	return gcm.Open(nil,
+		ciphertext[:gcm.NonceSize()],
+		ciphertext[gcm.NonceSize():],
+		nil,
+	)
+}




diff --git a/eeze.go b/eeze.go
index 78e278753bed0d1874414fbaa65e83945241aad0..d293bd5515c0885187a025ddfc3df008ee611aaa 100644
--- a/eeze.go
+++ b/eeze.go
@@ -3,9 +3,12 @@
 import (
 	"notabug.org/apiote/next-eeze/agent"
 	"notabug.org/apiote/next-eeze/config"
+	"notabug.org/apiote/next-eeze/fido"
+	"notabug.org/apiote/next-eeze/fs"
 	"notabug.org/apiote/next-eeze/operation"
 	"notabug.org/apiote/next-eeze/server"
 
+	"encoding/hex"
 	"fmt"
 	"log"
 	"os"
@@ -15,15 +18,41 @@
 	"git.sr.ht/~sircmpwn/getopt"
 )
 
-func rememberMasterPassword() string {
+func readMasterPassword() (string, error) {
+	present, err := fs.IsFidoCredentialPresent()
+	if err != nil {
+		fmt.Println(err)
+		return "", err
+	}
+	if present {
+		return readMasterPasswordFido()
+	} else {
+		return readMasterPasswordStdin()
+	}
+}
+
+func readMasterPasswordFido() (string, error) {
+	c, err := fs.ReadFidoCredential()
+	if err != nil {
+		return "", err
+	}
+	// todo memguard
+	secret := fido.GetHmacSecret("next-eeze", "", c.Cdh, c.Salt, c.CredID) // todo pin
+
+	return hex.EncodeToString(secret), nil
+}
+
+func readMasterPasswordStdin() (string, error) {
 	fmt.Print("Master password: ")
 	// todo memguard
-	masterPass_b, _ := terminal.ReadPassword(int(os.Stdin.Fd()))
+	masterPass_b, err := terminal.ReadPassword(int(os.Stdin.Fd()))
+	if err != nil {
+		return "", err
+	}
 	// todo memguard
 	masterPassword := string(masterPass_b)
 	fmt.Print("\n")
-	agent.GiveMasterPassword(masterPassword)
-	return masterPassword
+	return masterPassword, nil
 }
 
 func main() {
@@ -43,6 +72,7 @@ 	f := getopt.Bool("f", false, "show full entry in Get, instead of just username/password")
 	p := getopt.Bool("p", false, "show just password in Get")
 	i := getopt.Bool("i", false, "in Config: set server, username, password (initialise)")
 	r := getopt.Bool("r", false, "in Config: reëncrypt (change master password)")
+	fido2 := getopt.Bool("2", false, "in Config, reëncrypt: use fido2 device")
 	n := getopt.Bool("n", false, "do not ask for anything, fail if password cannot be obtained from agent")
 	b := getopt.Bool("b", false, "block until password can be received from agent")
 
@@ -53,7 +83,8 @@ 		return
 	}
 
 	if *P {
-		_ = rememberMasterPassword()
+		masterPassword, _ := readMasterPassword()
+		agent.GiveMasterPassword(masterPassword)
 		return
 	}
 
@@ -71,7 +102,8 @@ 		log.Fatalln("Password needed in non-interactive mode")
 	}
 
 	if masterPassword == "" || (*C && (*i || *r)) {
-		masterPassword = rememberMasterPassword()
+		masterPassword, _ = readMasterPassword()
+		agent.GiveMasterPassword(masterPassword)
 	}
 
 	if *C {
@@ -79,7 +111,7 @@ 		if *i {
 			config.Init(masterPassword)
 		} else if *r {
 			// todo memguard
-			newMasterPassword, err := config.Reëncrypt(masterPassword)
+			newMasterPassword, err := config.Reëncrypt(masterPassword, *fido2)
 			if err != nil {
 				log.Println("Error reëncrypting. ", err)
 				return
@@ -92,6 +124,7 @@ 		err = server.Sync(masterPassword)
 	} else if *G {
 		var r string
 		r, err = operation.Get(u, l, s, *f, *p, masterPassword)
+		// todo if error:wrongPass -> kill agent
 		fmt.Println(r)
 	} else if *L {
 		var r string




diff --git a/fido/fido.go b/fido/fido.go
new file mode 100644
index 0000000000000000000000000000000000000000..0559cbbc8ccf7e6f2b9cae72b0bddc11af2cebbf
--- /dev/null
+++ b/fido/fido.go
@@ -0,0 +1,82 @@
+package fido
+
+import (
+	"bytes"
+	"log"
+
+	"github.com/keys-pub/go-libfido2"
+)
+
+// todo errors
+
+func Setup(rpID, pin string, cdh []byte) []byte {
+	locs, err := libfido2.DeviceLocations()
+	if err != nil {
+		log.Fatal(err)
+	}
+	if len(locs) == 0 {
+		log.Fatal("No devices")
+		return []byte{}
+	}
+
+	path := locs[0].Path
+	device, err := libfido2.NewDevice(path)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	attest, err := device.MakeCredential(
+		cdh,
+		libfido2.RelyingParty{
+			ID:   rpID,
+			Name: "hmac-secret",
+		},
+		libfido2.User{
+			ID:   bytes.Repeat([]byte{0x01}, 16),
+			Name: "hmac-secret",
+		},
+		libfido2.ES256,
+		pin,
+		&libfido2.MakeCredentialOpts{
+			Extensions: []libfido2.Extension{libfido2.HMACSecretExtension},
+			RK:         libfido2.True,
+		},
+	)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return attest.CredentialID
+}
+
+func GetHmacSecret(rpID, pin string, cdh, salt, credID []byte) []byte {
+	locs, err := libfido2.DeviceLocations()
+	if err != nil {
+		log.Fatal(err)
+	}
+	if len(locs) == 0 {
+		log.Fatal("No devices")
+		return []byte{}
+	}
+
+	path := locs[0].Path
+	device, err := libfido2.NewDevice(path)
+	if err != nil {
+		log.Fatal(err)
+	}
+	assertion, err := device.Assertion(
+		rpID,
+		cdh,
+		[][]byte{credID},
+		pin,
+		&libfido2.AssertionOpts{
+			Extensions: []libfido2.Extension{libfido2.HMACSecretExtension},
+			HMACSalt:   salt,
+		},
+	)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return assertion.HMACSecret
+}




diff --git a/fs/fs.go b/fs/fs.go
index 407cad670812a473d7e22ba1f3fcace01f80ff0d..c4d84bf0d28ebf2b17ba1d6a8e83fd7dfc33192a 100644
--- a/fs/fs.go
+++ b/fs/fs.go
@@ -1,20 +1,14 @@
 package fs
 
 import (
+	"notabug.org/apiote/next-eeze/crypto"
 	"notabug.org/apiote/next-eeze/password"
 
-	"crypto/aes"
-	"crypto/cipher"
-	"crypto/rand"
-	"errors"
-	"io"
 	"io/ioutil"
 	"log"
 	"os"
 	"os/user"
 	"path/filepath"
-
-	"golang.org/x/crypto/argon2"
 
 	"git.sr.ht/~sircmpwn/go-bare"
 )
@@ -30,6 +24,12 @@ 	Username string
 	Password string
 }
 
+type FidoCredential struct {
+	Cdh    []byte
+	Salt   []byte
+	CredID []byte
+}
+
 func getDataLocation() string {
 	usr, _ := user.Current()
 	dir := usr.HomeDir
@@ -39,9 +39,9 @@ }
 
 // todo memguard passwords, masterPassword
 func SaveBare(passwords []password.BarePassword, masterPassword string) error {
-	salt := makeSalt()
+	salt := crypto.MakeSalt()
 	// todo memguard
-	key := deriveKey(masterPassword, salt)
+	key := crypto.DeriveKey(masterPassword, salt)
 	result, err := os.Create(getDataLocation() + "/passwords.bare")
 	if err != nil {
 		log.Fatal("Error creating passwords file. ", err)
@@ -54,7 +54,7 @@ 	if err != nil {
 		log.Fatal("Error marshalling passwords. ", err)
 		return err
 	}
-	cipherText, err := encrypt(bytes, key)
+	cipherText, err := crypto.Encrypt(bytes, key)
 	if err != nil {
 		log.Fatal("Error encrypting credentials. ", err)
 		return err
@@ -96,23 +96,29 @@ }
 
 // todo memguard masterPassword
 func Read(masterPassword string) ([]password.BarePassword, error) {
-	enc := EncryptedContent{}
 	// todo memguard
 	passwords := []password.BarePassword{}
-	content, err := ioutil.ReadFile(getDataLocation() + "/passwords.bare")
+	f, err := os.Open(getDataLocation() + "/passwords.bare")
 	if err != nil {
-		log.Fatal("Error reading to file. ", err)
-		return nil, err
+		log.Fatal("Error opening. ", err)
+		return passwords, err
 	}
-	err = bare.Unmarshal(content, &enc)
+	defer f.Close()
+	r := bare.NewReader(f)
+	salt, err :=  r.ReadData()
 	if err != nil {
-		log.Fatal("Error unmarshalling encrypted. ", err)
+		log.Fatal("Error reading salt. ", err)
+		return passwords, err
+	}
+	cipher, err :=  r.ReadData()
+	if err != nil {
+		log.Fatal("Error reading cipher. ", err)
 		return passwords, err
 	}
 	// todo memguard
-	key := deriveKey(masterPassword, enc.Salt)
+	key := crypto.DeriveKey(masterPassword, salt)
 	// todo memguard
-	plaintext, err := decrypt(enc.CipherText, key)
+	plaintext, err := crypto.Decrypt(cipher, key)
 	if err != nil {
 		log.Fatal("Error decrypting passwords. ", err)
 		return passwords, err
@@ -127,9 +133,9 @@ }
 
 // todo memguard credentials, masterPassword
 func SaveCredentials(credentials Credentials, masterPassword string) error {
-	salt := makeSalt()
+	salt := crypto.MakeSalt()
 	// todo memguard
-	key := deriveKey(masterPassword, salt)
+	key := crypto.DeriveKey(masterPassword, salt)
 
 	result, err := os.Create(getDataLocation() + "/credentials.bare")
 	if err != nil {
@@ -143,7 +149,7 @@ 	if err != nil {
 		log.Fatal("Error marshalling credentials. ", err)
 		return err
 	}
-	cipherText, err := encrypt(bytes, key)
+	cipherText, err := crypto.Encrypt(bytes, key)
 	if err != nil {
 		log.Fatal("Error encrypting credentials. ", err)
 		return err
@@ -182,9 +188,9 @@ 		log.Fatal("Error unmarshalling encrypted. ", err)
 		return credentials, err
 	}
 	// todo memguard
-	key := deriveKey(masterPassword, enc.Salt)
+	key := crypto.DeriveKey(masterPassword, enc.Salt)
 	// todo memguard
-	plaintext, err := decrypt(enc.CipherText, key)
+	plaintext, err := crypto.Decrypt(enc.CipherText, key)
 	if err != nil {
 		log.Fatal("Error decrypting credentials. ", err)
 		return credentials, err
@@ -193,65 +199,60 @@ 	err = bare.Unmarshal(plaintext, &credentials)
 	return credentials, nil
 }
 
-func makeSalt() []byte {
-	key := [32]byte{}
-	_, err := io.ReadFull(rand.Reader, key[:])
+// todo memguards
+func SaveFidoCredential(c FidoCredential) error {
+	bytes, err := bare.Marshal(&c)
+	if err != nil {
+		log.Fatal("Error creating credentials file. ", err)
+		return err
+	}
+	result, err := os.Create(getDataLocation() + "/fido.bare")
 	if err != nil {
-		panic(err)
+		log.Fatal("Error creating credentials file. ", err)
+		return err
 	}
-	return key[:]
-}
-
-// todo memguard password
-func deriveKey(password string, salt []byte) [32]byte {
-	// todo memguard
-	key := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
-	// todo memguard
-	var keyArr [32]byte
-	copy(keyArr[:], key)
-	return keyArr
-}
-
-// todo memguard plaintext, key
-func encrypt(plaintext []byte, key [32]byte) (ciphertext []byte, err error) {
-	block, err := aes.NewCipher(key[:])
+	defer result.Close()
+	_, err = result.Write(bytes)
 	if err != nil {
-		return nil, err
+		log.Fatal("Error writing to file. ", err)
+		return err
 	}
+	return nil
+}
 
-	gcm, err := cipher.NewGCM(block)
+// todo memguards
+func ReadFidoCredential() (FidoCredential, error) {
+	c := FidoCredential{}
+	content, err := ioutil.ReadFile(getDataLocation() + "/fido.bare")
 	if err != nil {
-		return nil, err
+		log.Fatal("Error reading to file. ", err)
+		return c, err
 	}
-
-	nonce := make([]byte, gcm.NonceSize())
-	_, err = io.ReadFull(rand.Reader, nonce)
+	err = bare.Unmarshal(content, &c)
 	if err != nil {
-		return nil, err
+		log.Fatal("Error unmarshalling encrypted. ", err)
+		return c, err
 	}
-
-	return gcm.Seal(nonce, nonce, plaintext, nil), nil
+	return c, nil
 }
 
-// todo memguard key
-func decrypt(ciphertext []byte, key [32]byte) (plaintext []byte, err error) {
-	block, err := aes.NewCipher(key[:])
-	if err != nil {
-		return nil, err
+func RemoveFidoCredential() error {
+	err := os.Remove(getDataLocation() + "/fido.bare")
+	if err != nil && os.IsNotExist(err) {
+		return nil
+	} else {
+		return err
 	}
+}
 
-	gcm, err := cipher.NewGCM(block)
+func IsFidoCredentialPresent() (bool, error) {
+	_, err := os.Stat(getDataLocation() + "/fido.bare")
 	if err != nil {
-		return nil, err
+		if os.IsNotExist(err) {
+			return false, nil
+		} else {
+			return false, err
+		}
 	}
-
-	if len(ciphertext) < gcm.NonceSize() {
-		return nil, errors.New("malformed ciphertext")
-	}
-
-	return gcm.Open(nil,
-		ciphertext[:gcm.NonceSize()],
-		ciphertext[gcm.NonceSize():],
-		nil,
-	)
+	return true, nil
 }