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 }