@@ -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 { | |||
@@ -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 { | |||
@@ -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."} | |||
@@ -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 | |||
} |
@@ -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 | |||
} |
@@ -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, | |||
@@ -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 { | |||