From 26817a27c777530c8e383038711dbfeb0ea6caa7 Mon Sep 17 00:00:00 2001 From: emersion Date: Tue, 19 Sep 2017 14:57:29 +0200 Subject: [PATCH] auth: move auth stuff in this new package --- auth/auth.go | 200 +++++++++++++++++++++++++++++++++ cmd/hydroxide/hydroxide.go | 221 +++++-------------------------------- 2 files changed, 227 insertions(+), 194 deletions(-) create mode 100644 auth/auth.go diff --git a/auth/auth.go b/auth/auth.go new file mode 100644 index 0000000..c3cc5d5 --- /dev/null +++ b/auth/auth.go @@ -0,0 +1,200 @@ +package auth + +import ( + "crypto/rand" + "encoding/base64" + "encoding/json" + "errors" + "io" + "os" + + "github.com/emersion/hydroxide/protonmail" + "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/nacl/secretbox" + "golang.org/x/crypto/openpgp" +) + +const authFile = "auth.json" + +type CachedAuth struct { + protonmail.Auth + LoginPassword string + MailboxPassword string + // TODO: add padding +} + +func readCachedAuths() (map[string]string, error) { + f, err := os.Open(authFile) + if os.IsNotExist(err) { + return nil, nil + } else if err != nil { + return nil, err + } + defer f.Close() + + auths := make(map[string]string) + err = json.NewDecoder(f).Decode(&auths) + return auths, err +} + +func saveAuths(auths map[string]string) error { + f, err := os.Create(authFile) + if err != nil { + return err + } + defer f.Close() + + return json.NewEncoder(f).Encode(auths) +} + +func encrypt(msg []byte, secretKey *[32]byte) (string, error) { + var nonce [24]byte + if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { + return "", err + } + + encrypted := secretbox.Seal(nonce[:], msg, &nonce, secretKey) + return base64.StdEncoding.EncodeToString(encrypted), nil +} + +func decrypt(encryptedString string, secretKey *[32]byte) ([]byte, error) { + encrypted, err := base64.StdEncoding.DecodeString(encryptedString) + if err != nil { + return nil, err + } + + var nonce [24]byte + copy(nonce[:], encrypted[:24]) + decrypted, ok := secretbox.Open(nil, encrypted[24:], &nonce, secretKey) + if !ok { + return nil, errors.New("decryption error") + } + return decrypted, nil +} + +func EncryptAndSave(auth *CachedAuth, username string, secretKey *[32]byte) error { + cleartext, err := json.Marshal(auth) + if err != nil { + return err + } + + encrypted, err := encrypt(cleartext, secretKey) + if err != nil { + return err + } + + auths, err := readCachedAuths() + if err != nil { + return err + } + + if auths == nil { + auths = make(map[string]string) + } + auths[username] = encrypted + + return saveAuths(auths) +} + +func authenticate(c *protonmail.Client, CachedAuth *CachedAuth) (openpgp.EntityList, error) { + auth, err := c.AuthRefresh(&CachedAuth.Auth) + if err != nil { + // TODO: handle expired token, re-authenticate + return nil, err + } + CachedAuth.Auth = *auth + + return c.Unlock(auth, CachedAuth.MailboxPassword) +} + +func GeneratePassword() (secretKey *[32]byte, password string, err error) { + var key [32]byte + if _, err = io.ReadFull(rand.Reader, key[:]); err != nil { + return + } + secretKey = &key + password = base64.StdEncoding.EncodeToString(key[:]) + return +} + +type session struct { + hashedSecretKey []byte + c *protonmail.Client + privateKeys openpgp.EntityList +} + +var ErrUnauthorized = errors.New("Invalid username or password") + +type Manager struct { + newClient func() *protonmail.Client + sessions map[string]*session +} + +func (m *Manager) Auth(username, password string) (*protonmail.Client, openpgp.EntityList, error) { + var secretKey [32]byte + passwordBytes, err := base64.StdEncoding.DecodeString(password) + if err != nil || len(passwordBytes) != len(secretKey) { + return nil, nil, ErrUnauthorized + } + copy(secretKey[:], passwordBytes) + + s, ok := m.sessions[username] + if ok { + err := bcrypt.CompareHashAndPassword(s.hashedSecretKey, secretKey[:]) + if err != nil { + return nil, nil, ErrUnauthorized + } + } else { + auths, err := readCachedAuths() + if err != nil && !os.IsNotExist(err) { + return nil, nil, err + } + + encrypted, ok := auths[username] + if !ok { + return nil, nil, ErrUnauthorized + } + + decrypted, err := decrypt(encrypted, &secretKey) + if err != nil { + return nil, nil, ErrUnauthorized + } + + var cachedAuth CachedAuth + if err := json.Unmarshal(decrypted, &cachedAuth); err != nil { + return nil, nil, err + } + + // authenticate updates cachedAuth with the new refresh token + c := m.newClient() + privateKeys, err := authenticate(c, &cachedAuth) + if err != nil { + return nil, nil, err + } + + if err := EncryptAndSave(&cachedAuth, username, &secretKey); err != nil { + return nil, nil, err + } + + hashed, err := bcrypt.GenerateFromPassword(secretKey[:], bcrypt.DefaultCost) + if err != nil { + return nil, nil, err + } + + s = &session{ + c: c, + privateKeys: privateKeys, + hashedSecretKey: hashed, + } + m.sessions[username] = s + } + + return s.c, s.privateKeys, nil +} + +func NewManager(newClient func() *protonmail.Client) *Manager { + return &Manager{ + newClient: newClient, + sessions: make(map[string]*session), + } +} diff --git a/cmd/hydroxide/hydroxide.go b/cmd/hydroxide/hydroxide.go index d119d8e..435e242 100644 --- a/cmd/hydroxide/hydroxide.go +++ b/cmd/hydroxide/hydroxide.go @@ -2,10 +2,6 @@ package main import ( "bufio" - "crypto/rand" - "encoding/base64" - "encoding/json" - "errors" "flag" "fmt" "io" @@ -14,95 +10,11 @@ import ( "os" "time" + "github.com/emersion/hydroxide/auth" "github.com/emersion/hydroxide/carddav" "github.com/emersion/hydroxide/protonmail" - "golang.org/x/crypto/bcrypt" - "golang.org/x/crypto/nacl/secretbox" - "golang.org/x/crypto/openpgp" ) -const authFile = "auth.json" - -type cachedAuth struct { - protonmail.Auth - LoginPassword string - MailboxPassword string - // TODO: add padding -} - -func readCachedAuths() (map[string]string, error) { - f, err := os.Open(authFile) - if os.IsNotExist(err) { - return nil, nil - } else if err != nil { - return nil, err - } - defer f.Close() - - auths := make(map[string]string) - err = json.NewDecoder(f).Decode(&auths) - return auths, err -} - -func saveAuths(auths map[string]string) error { - f, err := os.Create(authFile) - if err != nil { - return err - } - defer f.Close() - - return json.NewEncoder(f).Encode(auths) -} - -func encrypt(msg []byte, secretKey *[32]byte) (string, error) { - var nonce [24]byte - if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { - return "", err - } - - encrypted := secretbox.Seal(nonce[:], msg, &nonce, secretKey) - return base64.StdEncoding.EncodeToString(encrypted), nil -} - -func decrypt(encryptedString string, secretKey *[32]byte) ([]byte, error) { - encrypted, err := base64.StdEncoding.DecodeString(encryptedString) - if err != nil { - return nil, err - } - - var nonce [24]byte - copy(nonce[:], encrypted[:24]) - decrypted, ok := secretbox.Open(nil, encrypted[24:], &nonce, secretKey) - if !ok { - return nil, errors.New("decryption error") - } - return decrypted, nil -} - -func encryptAndSaveAuth(auth *cachedAuth, username string, secretKey *[32]byte) error { - cleartext, err := json.Marshal(auth) - if err != nil { - return err - } - - encrypted, err := encrypt(cleartext, secretKey) - if err != nil { - return err - } - - auths, err := readCachedAuths() - if err != nil { - return err - } - - if auths == nil { - auths = make(map[string]string) - } - auths[username] = encrypted - - return saveAuths(auths) -} - func newClient() *protonmail.Client { return &protonmail.Client{ RootURL: "https://dev.protonmail.com/api", @@ -112,17 +24,6 @@ func newClient() *protonmail.Client { } } -func authenticate(c *protonmail.Client, cachedAuth *cachedAuth) (openpgp.EntityList, error) { - auth, err := c.AuthRefresh(&cachedAuth.Auth) - if err != nil { - // TODO: handle expired token, re-authenticate - return nil, err - } - cachedAuth.Auth = *auth - - return c.Unlock(auth, cachedAuth.MailboxPassword) -} - func receiveEvents(c *protonmail.Client, last string, ch chan<- *protonmail.Event) { t := time.NewTicker(time.Minute) defer t.Stop() @@ -143,12 +44,6 @@ func receiveEvents(c *protonmail.Client, last string, ch chan<- *protonmail.Even } } -type session struct { - h http.Handler - hashedSecretKey []byte - privateKeys openpgp.EntityList -} - func main() { flag.Parse() @@ -159,10 +54,10 @@ func main() { c := newClient() - var auth *protonmail.Auth + var a *protonmail.Auth /*if cachedAuth, ok := auths[username]; ok { var err error - auth, err = c.AuthRefresh(auth) + a, err = c.AuthRefresh(a) if err != nil { // TODO: handle expired token error log.Fatal(err) @@ -170,7 +65,7 @@ func main() { }*/ var loginPassword string - if auth == nil { + if a == nil { fmt.Printf("Password: ") scanner.Scan() loginPassword = scanner.Text() @@ -187,18 +82,18 @@ func main() { twoFactorCode = scanner.Text() } - auth, err = c.Auth(username, loginPassword, twoFactorCode, authInfo) + a, err = c.Auth(username, loginPassword, twoFactorCode, authInfo) if err != nil { log.Fatal(err) } } var mailboxPassword string - if auth.PasswordMode == protonmail.PasswordSingle { + if a.PasswordMode == protonmail.PasswordSingle { mailboxPassword = loginPassword } if mailboxPassword == "" { - if auth.PasswordMode == protonmail.PasswordTwo { + if a.PasswordMode == protonmail.PasswordTwo { fmt.Printf("Mailbox password: ") } else { fmt.Printf("Password: ") @@ -207,22 +102,21 @@ func main() { mailboxPassword = scanner.Text() } - _, err := c.Unlock(auth, mailboxPassword) + _, err := c.Unlock(a, mailboxPassword) if err != nil { log.Fatal(err) } - var secretKey [32]byte - if _, err := io.ReadFull(rand.Reader, secretKey[:]); err != nil { + secretKey, bridgePassword, err := auth.GeneratePassword() + if err != nil { log.Fatal(err) } - bridgePassword := base64.StdEncoding.EncodeToString(secretKey[:]) - err = encryptAndSaveAuth(&cachedAuth{ - *auth, + err = auth.EncryptAndSave(&auth.CachedAuth{ + *a, loginPassword, mailboxPassword, - }, username, &secretKey) + }, username, secretKey) if err != nil { log.Fatal(err) } @@ -234,7 +128,8 @@ func main() { port = "8080" } - sessions := make(map[string]*session) + sessions := auth.NewManager(newClient) + handlers := make(map[string]http.Handler) s := &http.Server{ Addr: "127.0.0.1:" + port, @@ -248,86 +143,24 @@ func main() { return } - var secretKey [32]byte - passwordBytes, err := base64.StdEncoding.DecodeString(password) - if err != nil || len(passwordBytes) != len(secretKey) { - resp.WriteHeader(http.StatusUnauthorized) - io.WriteString(resp, "Invalid password format") + c, privateKeys, err := sessions.Auth(username, password) + if err != nil { + if err == auth.ErrUnauthorized { + resp.WriteHeader(http.StatusUnauthorized) + } else { + resp.WriteHeader(http.StatusInternalServerError) + } + io.WriteString(resp, err.Error()) return } - copy(secretKey[:], passwordBytes) - - var h http.Handler - s, ok := sessions[username] - if ok { - err := bcrypt.CompareHashAndPassword(s.hashedSecretKey, secretKey[:]) - if err != nil { - resp.WriteHeader(http.StatusUnauthorized) - io.WriteString(resp, "Invalid username or password") - return - } - - h = s.h - } else { - auths, err := readCachedAuths() - if err != nil && !os.IsNotExist(err) { - resp.WriteHeader(http.StatusInternalServerError) - log.Println("Cannot open cached auths") - return - } - - encrypted, ok := auths[username] - if !ok { - resp.WriteHeader(http.StatusUnauthorized) - io.WriteString(resp, "Invalid username or password") - return - } - - decrypted, err := decrypt(encrypted, &secretKey) - if err != nil { - resp.WriteHeader(http.StatusUnauthorized) - io.WriteString(resp, "Invalid username or password") - return - } - - var cachedAuth cachedAuth - if err := json.Unmarshal(decrypted, &cachedAuth); err != nil { - resp.WriteHeader(http.StatusInternalServerError) - log.Printf("Cannot unmarshal cached auth for %q: %v", username, err) - return - } - - // authenticate updates cachedAuth with the new refresh token - c := newClient() - privateKeys, err := authenticate(c, &cachedAuth) - if err != nil { - resp.WriteHeader(http.StatusInternalServerError) - log.Printf("Cannot authenticate %q: %v", username, err) - return - } - - if err := encryptAndSaveAuth(&cachedAuth, username, &secretKey); err != nil { - resp.WriteHeader(http.StatusInternalServerError) - log.Printf("Cannot save auth for %q: %v", username, err) - return - } - - hashed, err := bcrypt.GenerateFromPassword(secretKey[:], bcrypt.DefaultCost) - if err != nil { - resp.WriteHeader(http.StatusInternalServerError) - log.Printf("Cannot hash password for %q: %v", username, err) - return - } + h, ok := handlers[username] + if !ok { events := make(chan *protonmail.Event) - go receiveEvents(c, cachedAuth.EventID, events) + go receiveEvents(c, "", events) h = carddav.NewHandler(c, privateKeys, events) - sessions[username] = &session{ - h: h, - hashedSecretKey: hashed, - privateKeys: privateKeys, - } + handlers[username] = h } h.ServeHTTP(resp, req)