diff --git a/account.go b/account.go index f05e94c..d8ea0df 100644 --- a/account.go +++ b/account.go @@ -154,7 +154,7 @@ func signupWithRegistration(app *App, signup userRegistration, w http.ResponseWr Created: time.Now().Truncate(time.Second).UTC(), } if signup.Email != "" { - encEmail, err := data.Encrypt(app.keys.emailKey, signup.Email) + encEmail, err := data.Encrypt(app.keys.EmailKey, signup.Email) if err != nil { log.Error("Unable to encrypt email: %s\n", err) } else { diff --git a/app.go b/app.go index 67a2fec..74a8598 100644 --- a/app.go +++ b/app.go @@ -37,6 +37,7 @@ import ( "github.com/writeas/web-core/log" "github.com/writeas/writefreely/author" "github.com/writeas/writefreely/config" + "github.com/writeas/writefreely/key" "github.com/writeas/writefreely/migrations" "github.com/writeas/writefreely/page" ) @@ -69,13 +70,17 @@ type App struct { db *datastore cfg *config.Config cfgFile string - keys *Keychain + keys *key.Keychain sessionStore *sessions.CookieStore formDecoder *schema.Decoder timeline *localTimeline } +func (app *App) SetKeys(k *key.Keychain) { + app.keys = k +} + // handleViewHome shows page at root path. Will be the Pad if logged in and the // catch-all landing page otherwise. func handleViewHome(app *App, w http.ResponseWriter, r *http.Request) error { diff --git a/database.go b/database.go index 8ec93f7..72d2780 100644 --- a/database.go +++ b/database.go @@ -29,6 +29,7 @@ import ( "github.com/writeas/web-core/log" "github.com/writeas/web-core/query" "github.com/writeas/writefreely/author" + "github.com/writeas/writefreely/key" ) const ( @@ -44,7 +45,7 @@ var ( type writestore interface { CreateUser(*User, string) error - UpdateUserEmail(keys *Keychain, userID int64, email string) error + UpdateUserEmail(keys *key.Keychain, userID int64, email string) error UpdateEncryptedUserEmail(int64, []byte) error GetUserByID(int64) (*User, error) GetUserForAuth(string) (*User, error) @@ -219,8 +220,8 @@ func (db *datastore) CreateUser(u *User, collectionTitle string) error { // FIXME: We're returning errors inconsistently in this file. Do we use Errorf // for returned value, or impart? -func (db *datastore) UpdateUserEmail(keys *Keychain, userID int64, email string) error { - encEmail, err := data.Encrypt(keys.emailKey, email) +func (db *datastore) UpdateUserEmail(keys *key.Keychain, userID int64, email string) error { + encEmail, err := data.Encrypt(keys.EmailKey, email) if err != nil { return fmt.Errorf("Couldn't encrypt email %s: %s\n", email, err) } @@ -1780,7 +1781,7 @@ func (db *datastore) ChangeSettings(app *App, u *User, s *userSettings) error { // Update email if given if s.Email != "" { - encEmail, err := data.Encrypt(app.keys.emailKey, s.Email) + encEmail, err := data.Encrypt(app.keys.EmailKey, s.Email) if err != nil { log.Error("Couldn't encrypt email %s: %s\n", s.Email, err) return impart.HTTPError{http.StatusInternalServerError, "Unable to encrypt email address."} diff --git a/key/key.go b/key/key.go new file mode 100644 index 0000000..14b0784 --- /dev/null +++ b/key/key.go @@ -0,0 +1,62 @@ +/* + * Copyright © 2019 A Bunch Tell LLC. + * + * This file is part of WriteFreely. + * + * WriteFreely is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, included + * in the LICENSE file in this source code package. + */ + +// Package key holds application keys and utilities around generating them. +package key + +import ( + "crypto/rand" +) + +const ( + EncKeysBytes = 32 +) + +type Keychain struct { + EmailKey, CookieAuthKey, CookieKey []byte +} + +// GenerateKeys generates necessary keys for the app on the given Keychain, +// skipping any that already exist. +func (keys *Keychain) GenerateKeys() error { + // Generate keys only if they don't already exist + var err, keyErrs error + if len(keys.EmailKey) == 0 { + keys.EmailKey, err = GenerateBytes(EncKeysBytes) + if err != nil { + keyErrs = err + } + } + if len(keys.CookieAuthKey) == 0 { + keys.CookieAuthKey, err = GenerateBytes(EncKeysBytes) + if err != nil { + keyErrs = err + } + } + if len(keys.CookieKey) == 0 { + keys.CookieKey, err = GenerateBytes(EncKeysBytes) + if err != nil { + keyErrs = err + } + } + + return keyErrs +} + +// GenerateBytes returns securely generated random bytes. +func GenerateBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/keys.go b/keys.go index 219e9c7..067908e 100644 --- a/keys.go +++ b/keys.go @@ -11,8 +11,8 @@ package writefreely import ( - "crypto/rand" "github.com/writeas/web-core/log" + "github.com/writeas/writefreely/key" "io/ioutil" "os" "path/filepath" @@ -20,8 +20,6 @@ import ( const ( keysDir = "keys" - - encKeysBytes = 32 ) var ( @@ -30,9 +28,6 @@ var ( cookieKeyPath = filepath.Join(keysDir, "cookies_enc.aes256") ) -type Keychain struct { - emailKey, cookieAuthKey, cookieKey []byte -} func initKeyPaths(app *App) { emailKeyPath = filepath.Join(app.cfg.Server.KeysParentDir, emailKeyPath) @@ -42,12 +37,12 @@ func initKeyPaths(app *App) { func initKeys(app *App) error { var err error - app.keys = &Keychain{} + app.keys = &key.Keychain{} if debugging { log.Info(" %s", emailKeyPath) } - app.keys.emailKey, err = ioutil.ReadFile(emailKeyPath) + app.keys.EmailKey, err = ioutil.ReadFile(emailKeyPath) if err != nil { return err } @@ -55,7 +50,7 @@ func initKeys(app *App) error { if debugging { log.Info(" %s", cookieAuthKeyPath) } - app.keys.cookieAuthKey, err = ioutil.ReadFile(cookieAuthKeyPath) + app.keys.CookieAuthKey, err = ioutil.ReadFile(cookieAuthKeyPath) if err != nil { return err } @@ -63,7 +58,7 @@ func initKeys(app *App) error { if debugging { log.Info(" %s", cookieKeyPath) } - app.keys.cookieKey, err = ioutil.ReadFile(cookieKeyPath) + app.keys.CookieKey, err = ioutil.ReadFile(cookieKeyPath) if err != nil { return err } @@ -85,7 +80,7 @@ func generateKey(path string) error { } log.Info("Generating %s.", path) - b, err := generateBytes(encKeysBytes) + b, err := key.GenerateBytes(key.EncKeysBytes) if err != nil { log.Error("FAILED. %s. Run writefreely --gen-keys again.", err) return err @@ -98,14 +93,3 @@ func generateKey(path string) error { log.Info("Success.") return nil } - -// generateBytes returns securely generated random bytes. -func generateBytes(n int) ([]byte, error) { - b := make([]byte, n) - _, err := rand.Read(b) - if err != nil { - return nil, err - } - - return b, nil -} diff --git a/session.go b/session.go index a3a7aaa..95bfb18 100644 --- a/session.go +++ b/session.go @@ -34,7 +34,7 @@ func initSession(app *App) *sessions.CookieStore { gob.Register(&User{}) // Create the cookie store - store := sessions.NewCookieStore(app.keys.cookieAuthKey, app.keys.cookieKey) + store := sessions.NewCookieStore(app.keys.CookieAuthKey, app.keys.CookieKey) store.Options = &sessions.Options{ Path: "/", MaxAge: sessionLength, diff --git a/users.go b/users.go index 6b245d5..d5e9a91 100644 --- a/users.go +++ b/users.go @@ -16,6 +16,7 @@ import ( "github.com/guregu/null/zero" "github.com/writeas/web-core/data" "github.com/writeas/web-core/log" + "github.com/writeas/writefreely/key" ) type ( @@ -79,13 +80,13 @@ type ( // EmailClear decrypts and returns the user's email, caching it in the user // object. -func (u *User) EmailClear(keys *Keychain) string { +func (u *User) EmailClear(keys *key.Keychain) string { if u.clearEmail != "" { return u.clearEmail } if u.Email.Valid && u.Email.String != "" { - email, err := data.Decrypt(keys.emailKey, []byte(u.Email.String)) + email, err := data.Decrypt(keys.EmailKey, []byte(u.Email.String)) if err != nil { log.Error("Error decrypting user email: %v", err) } else {