From 8ed0676e514ef086b9c1e5035c46fd09e65f3af3 Mon Sep 17 00:00:00 2001 From: Jason Streifling Date: Sun, 27 Oct 2024 13:29:46 +0100 Subject: [PATCH] Encrypt sensitive user data with aes256 --- .air.toml | 3 +- cmd/backend/atom.go | 2 +- cmd/backend/config.go | 23 +++- cmd/backend/rss.go | 2 +- cmd/backend/users.go | 232 ++++++++++++++++++++++++++++++---- cmd/frontend/sessions.go | 2 +- cmd/frontend/users.go | 14 +- cmd/main.go | 4 +- create_db.sql | 8 +- web/templates/first-user.html | 3 +- 10 files changed, 246 insertions(+), 47 deletions(-) diff --git a/.air.toml b/.air.toml index 566c4f9..f024cc5 100644 --- a/.air.toml +++ b/.air.toml @@ -4,13 +4,14 @@ tmp_dir = "tmp" [build] args_bin = [ + "-aes tmp/cpolis.aes", "-articles tmp/articles", "-config tmp/config.toml", "-desc 'Freiheit, Gleichheit, Brüderlichkeit, Toleranz und Humanität'", "-domain localhost", "-feed tmp/cpolis.atom", "-firebase tmp/firebase.json", - "-key tmp/key.gob", + "-gob tmp/cpolis.gob", "-link https://distrikt-ni-st.de", "-log tmp/cpolis.log", "-pdfs tmp/pdfs", diff --git a/cmd/backend/atom.go b/cmd/backend/atom.go index 21d0db2..832f567 100644 --- a/cmd/backend/atom.go +++ b/cmd/backend/atom.go @@ -39,7 +39,7 @@ func GenerateAtomFeed(c *Config, db *DB) (*string, error) { entry.Links[linkID].Rel = "enclosure" entry.Links[linkID].Type = "image/webp" - user, err := db.GetUser(article.AuthorID) + user, err := db.GetUser(c, article.AuthorID) if err != nil { return nil, fmt.Errorf("error getting user user info for RSS feed: %v", err) } diff --git a/cmd/backend/config.go b/cmd/backend/config.go index 0c67eaa..d21a780 100644 --- a/cmd/backend/config.go +++ b/cmd/backend/config.go @@ -12,6 +12,7 @@ import ( ) type Config struct { + AESKeyFile string ArticleDir string ConfigFile string DBName string @@ -19,7 +20,7 @@ type Config struct { Domain string AtomFeed string FirebaseKey string - KeyFile string + GOBKeyFile string Link string LogFile string PDFDir string @@ -34,12 +35,13 @@ type Config struct { func newConfig() *Config { return &Config{ + AESKeyFile: "/var/www/cpolis/aes.key", ArticleDir: "/var/www/cpolis/articles", ConfigFile: "/etc/cpolis/config.toml", DBName: "cpolis", AtomFeed: "/var/www/cpolis/cpolis.atom", FirebaseKey: "/var/www/cpolis/serviceAccountKey.json", - KeyFile: "/var/www/cpolis/cpolis.key", + GOBKeyFile: "/var/www/cpolis/gob.key", LogFile: "/var/log/cpolis.log", MaxImgHeight: 1080, MaxImgWidth: 1920, @@ -102,6 +104,7 @@ func (c *Config) handleCliArgs() error { var port int var err error + flag.StringVar(&c.AESKeyFile, "aes", c.AESKeyFile, "aes key file") flag.StringVar(&c.ArticleDir, "articles", c.ArticleDir, "articles directory") flag.StringVar(&c.AtomFeed, "feed", c.AtomFeed, "atom feed file") flag.StringVar(&c.ConfigFile, "config", c.ConfigFile, "config file") @@ -109,7 +112,7 @@ func (c *Config) handleCliArgs() error { flag.StringVar(&c.Description, "desc", c.Description, "channel description") flag.StringVar(&c.Domain, "domain", c.Domain, "domain name") flag.StringVar(&c.FirebaseKey, "firebase", c.FirebaseKey, "Firebase service account key file") - flag.StringVar(&c.KeyFile, "key", c.KeyFile, "key file") + flag.StringVar(&c.GOBKeyFile, "gob", c.GOBKeyFile, "gob key file") flag.StringVar(&c.Link, "link", c.Link, "channel Link") flag.StringVar(&c.LogFile, "log", c.LogFile, "log file") flag.StringVar(&c.PDFDir, "pdfs", c.PDFDir, "pdf directory") @@ -147,6 +150,14 @@ func (c *Config) setupConfig(cliConfig *Config) error { var err error defaultConfig := newConfig() + if cliConfig.AESKeyFile != defaultConfig.AESKeyFile { + c.AESKeyFile = cliConfig.AESKeyFile + } + c.AESKeyFile, err = mkFile(c.AESKeyFile, 0600, 0700) + if err != nil { + return fmt.Errorf("error setting up file: %v", err) + } + if cliConfig.ArticleDir != defaultConfig.ArticleDir { c.ArticleDir = cliConfig.ArticleDir } @@ -187,10 +198,10 @@ func (c *Config) setupConfig(cliConfig *Config) error { return fmt.Errorf("error setting up file: %v", err) } - if cliConfig.KeyFile != defaultConfig.KeyFile { - c.KeyFile = cliConfig.KeyFile + if cliConfig.GOBKeyFile != defaultConfig.GOBKeyFile { + c.GOBKeyFile = cliConfig.GOBKeyFile } - c.KeyFile, err = mkFile(c.KeyFile, 0600, 0700) + c.GOBKeyFile, err = mkFile(c.GOBKeyFile, 0600, 0700) if err != nil { return fmt.Errorf("error setting up file: %v", err) } diff --git a/cmd/backend/rss.go b/cmd/backend/rss.go index cc1106f..3a5982d 100644 --- a/cmd/backend/rss.go +++ b/cmd/backend/rss.go @@ -39,7 +39,7 @@ func GenerateRSS(c *Config, db *DB) (*string, error) { tagNames = append(tagNames, "autogenerated") } - user, err := db.GetUser(article.AuthorID) + user, err := db.GetUser(c, article.AuthorID) if err != nil { return nil, fmt.Errorf("error getting user user info for RSS feed: %v", err) } diff --git a/cmd/backend/users.go b/cmd/backend/users.go index fc7bba2..741ee99 100644 --- a/cmd/backend/users.go +++ b/cmd/backend/users.go @@ -2,9 +2,16 @@ package backend import ( "context" + "crypto/aes" + "crypto/cipher" + "crypto/rand" "database/sql" + "encoding/base64" + "errors" "fmt" + "io" "log" + "os" "golang.org/x/crypto/bcrypt" ) @@ -18,24 +25,130 @@ const ( ) type User struct { - UserName string - FirstName string - LastName string - ID int64 - Role int + UserName string + FirstName string + LastName string + Email string + ProfilePicLink string + ID int64 + Role int } -func (db *DB) AddUser(u *User, pass string) (int64, error) { +func readKey(filename string) ([]byte, error) { + key, err := os.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("error reading from aes key file: %v", err) + } + + if len(key) != 44 { + return nil, errors.New("key is not 32 bytes long") + } + + key, err = base64.StdEncoding.DecodeString(string(key)) + if err != nil { + return nil, fmt.Errorf("error base64 decoding key: %v", err) + } + + return key, nil +} + +func key(c *Config) ([]byte, error) { + key, err := readKey(c.AESKeyFile) + if err != nil { + key = make([]byte, 32) + if _, err := rand.Read(key); err != nil { + return nil, fmt.Errorf("error generating random key: %v", err) + } + + fileKey := make([]byte, 44) + base64.StdEncoding.Encode(fileKey, key) + if err = os.WriteFile(c.AESKeyFile, fileKey, 0600); err != nil { + return nil, fmt.Errorf("error writing key to file: %v", err) + } + } + + return key, nil +} + +func aesEncrypt(c *Config, plaintext string) (string, error) { + key, err := key(c) + if err != nil { + return "", fmt.Errorf("error retrieving key: %v", err) + } + + block, err := aes.NewCipher(key) + if err != nil { + return "", fmt.Errorf("error creating cipher block: %v", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("error creating new gcm: %v", err) + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", fmt.Errorf("error creating nonce: %v", err) + } + + cipherText := gcm.Seal(nonce, nonce, []byte(plaintext), nil) + return base64.StdEncoding.EncodeToString(cipherText), nil +} + +func aesDecrypt(c *Config, ciphertext string) (string, error) { + key, err := key(c) + if err != nil { + return "", fmt.Errorf("error retrieving key: %v", err) + } + + block, err := aes.NewCipher(key) + if err != nil { + return "", fmt.Errorf("error creating cipher block: %v", err) + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", fmt.Errorf("error creating new gcm: %v", err) + } + + data, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", fmt.Errorf("error base64 decoding ciphertext: %v", err) + } + + nonceSize := gcm.NonceSize() + nonce, cipherText := data[:nonceSize], data[nonceSize:] + + plaintext, err := gcm.Open(nil, nonce, cipherText, nil) + if err != nil { + return "", fmt.Errorf("error aes decoding ciphertext: %v", err) + } + + return string(plaintext), nil +} + +func (db *DB) AddUser(c *Config, u *User, pass string) (int64, error) { hashedPass, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost) if err != nil { return 0, fmt.Errorf("error creating password hash: %v", err) } + aesFirstName, err := aesEncrypt(c, u.FirstName) + if err != nil { + return 0, fmt.Errorf("error encrypting first name: %v", err) + } + + aesLastName, err := aesEncrypt(c, u.LastName) + if err != nil { + return 0, fmt.Errorf("error encrypting last name: %v", err) + } + query := ` INSERT INTO users (username, password, first_name, last_name, role) VALUES (?, ?, ?, ?, ?) ` - result, err := db.Exec(query, u.UserName, string(hashedPass), u.FirstName, u.LastName, u.Role) + + result, err := db.Exec(query, u.UserName, string(hashedPass), aesFirstName, aesLastName, u.Role) if err != nil { return 0, fmt.Errorf("error inserting new user %v into DB: %v", u.UserName, err) } @@ -129,7 +242,10 @@ func (tx *Tx) ChangePassword(id int64, oldPass, newPass string) error { } // TODO: No need for ID field in general -func (db *DB) GetUser(id int64) (*User, error) { +func (db *DB) GetUser(c *Config, id int64) (*User, error) { + var aesFirstName, aesLastName string + var err error + user := new(User) query := ` SELECT id, username, first_name, last_name, role @@ -138,15 +254,24 @@ func (db *DB) GetUser(id int64) (*User, error) { ` row := db.QueryRow(query, id) - if err := row.Scan(&user.ID, &user.UserName, &user.FirstName, - &user.LastName, &user.Role); err != nil { + if err := row.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &user.Role); err != nil { return nil, fmt.Errorf("error reading user information: %v", err) } + user.FirstName, err = aesDecrypt(c, aesFirstName) + if err != nil { + return nil, fmt.Errorf("error decrypting first name: %v", err) + } + + user.LastName, err = aesDecrypt(c, aesLastName) + if err != nil { + return nil, fmt.Errorf("error decrypting last name: %v", err) + } + return user, nil } -func (db *DB) UpdateOwnUserAttributes(id int64, user, first, last, oldPass, newPass, newPass2 string) error { +func (db *DB) UpdateOwnUserAttributes(c *Config, id int64, userName, firstName, lastName, oldPass, newPass, newPass2 string) error { passwordEmpty := true if len(newPass) > 0 || len(newPass2) > 0 { if newPass != newPass2 { @@ -174,10 +299,26 @@ func (db *DB) UpdateOwnUserAttributes(id int64, user, first, last, oldPass, newP } } + aesFirstName, err := aesEncrypt(c, firstName) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error encrypting first name: %v", err) + } + + aesLastName, err := aesEncrypt(c, lastName) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error encrypting last name: %v", err) + } + if err = tx.UpdateAttributes( - &Attribute{Table: "users", ID: id, AttName: "username", Value: user}, - &Attribute{Table: "users", ID: id, AttName: "first_name", Value: first}, - &Attribute{Table: "users", ID: id, AttName: "last_name", Value: last}, + &Attribute{Table: "users", ID: id, AttName: "username", Value: userName}, + &Attribute{Table: "users", ID: id, AttName: "first_name", Value: aesFirstName}, + &Attribute{Table: "users", ID: id, AttName: "last_name", Value: aesLastName}, ); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) @@ -202,7 +343,7 @@ func (db *DB) UpdateOwnUserAttributes(id int64, user, first, last, oldPass, newP return fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) } -func (db *DB) AddFirstUser(u *User, pass string) (int64, error) { +func (db *DB) AddFirstUser(c *Config, u *User, pass string) (int64, error) { var numUsers int64 txOptions := &sql.TxOptions{Isolation: sql.LevelSerializable} selectQuery := "SELECT COUNT(*) FROM users" @@ -239,7 +380,23 @@ func (db *DB) AddFirstUser(u *User, pass string) (int64, error) { return 0, fmt.Errorf("error creating password hash: %v", err) } - result, err := tx.Exec(insertQuery, u.UserName, string(hashedPass), u.FirstName, u.LastName, u.Role) + aesFirstName, err := aesEncrypt(c, u.FirstName) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return 0, fmt.Errorf("error encrypting first name: %v", err) + } + + aesLastName, err := aesEncrypt(c, u.LastName) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return 0, fmt.Errorf("error encrypting last name: %v", err) + } + + result, err := tx.Exec(insertQuery, u.UserName, string(hashedPass), aesFirstName, aesLastName, u.Role) if err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) @@ -270,7 +427,10 @@ func (db *DB) AddFirstUser(u *User, pass string) (int64, error) { return 0, fmt.Errorf("error: %v unsuccessful retries for DB operation, aborting", TxMaxRetries) } -func (db *DB) GetAllUsers() (map[int64]*User, error) { +func (db *DB) GetAllUsers(c *Config) (map[int64]*User, error) { + var aesFirstName, aesLastName string + var err error + query := "SELECT id, username, first_name, last_name, role FROM users" rows, err := db.Query(query) @@ -281,10 +441,20 @@ func (db *DB) GetAllUsers() (map[int64]*User, error) { users := make(map[int64]*User, 0) for rows.Next() { user := new(User) - if err = rows.Scan(&user.ID, &user.UserName, &user.FirstName, - &user.LastName, &user.Role); err != nil { + if err = rows.Scan(&user.ID, &user.UserName, &aesFirstName, &aesLastName, &user.Role); err != nil { return nil, fmt.Errorf("error getting user info: %v", err) } + + user.FirstName, err = aesDecrypt(c, aesFirstName) + if err != nil { + return nil, fmt.Errorf("error decrypting first name: %v", err) + } + + user.LastName, err = aesDecrypt(c, aesLastName) + if err != nil { + return nil, fmt.Errorf("error decrypting last name: %v", err) + } + users[user.ID] = user } @@ -311,7 +481,7 @@ func (tx *Tx) SetPassword(id int64, newPass string) error { return nil } -func (db *DB) UpdateUserAttributes(id int64, user, first, last, newPass, newPass2 string, role int) error { +func (db *DB) UpdateUserAttributes(c *Config, id int64, userName, firstName, lastName, newPass, newPass2 string, role int) error { passwordEmpty := true if len(newPass) > 0 || len(newPass2) > 0 { if newPass != newPass2 { @@ -339,10 +509,26 @@ func (db *DB) UpdateUserAttributes(id int64, user, first, last, newPass, newPass } } + aesFirstName, err := aesEncrypt(c, firstName) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error encrypting first name: %v", err) + } + + aesLastName, err := aesEncrypt(c, lastName) + if err != nil { + if rollbackErr := tx.Rollback(); rollbackErr != nil { + log.Fatalf("transaction error: %v, rollback error: %v", err, rollbackErr) + } + return fmt.Errorf("error encrypting last name: %v", err) + } + if err = tx.UpdateAttributes( - &Attribute{Table: "users", ID: id, AttName: "username", Value: user}, - &Attribute{Table: "users", ID: id, AttName: "first_name", Value: first}, - &Attribute{Table: "users", ID: id, AttName: "last_name", Value: last}, + &Attribute{Table: "users", ID: id, AttName: "username", Value: userName}, + &Attribute{Table: "users", ID: id, AttName: "first_name", Value: aesFirstName}, + &Attribute{Table: "users", ID: id, AttName: "last_name", Value: aesLastName}, &Attribute{Table: "users", ID: id, AttName: "role", Value: role}, ); err != nil { if rollbackErr := tx.Rollback(); rollbackErr != nil { diff --git a/cmd/frontend/sessions.go b/cmd/frontend/sessions.go index 14169ce..8320dcc 100644 --- a/cmd/frontend/sessions.go +++ b/cmd/frontend/sessions.go @@ -111,7 +111,7 @@ func Login(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - user, err := db.GetUser(id) + user, err := db.GetUser(c, id) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/cmd/frontend/users.go b/cmd/frontend/users.go index 06cf1cc..8d12a70 100644 --- a/cmd/frontend/users.go +++ b/cmd/frontend/users.go @@ -94,7 +94,7 @@ func AddUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - _, err = db.AddUser(user, pass) + _, err = db.AddUser(c, user, pass) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -120,7 +120,7 @@ func EditSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - user, err := db.GetUser(session.Values["id"].(int64)) + user, err := db.GetUser(c, session.Values["id"].(int64)) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -177,7 +177,7 @@ func UpdateSelf(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - if err = db.UpdateOwnUserAttributes(user.ID, user.UserName, user.FirstName, user.LastName, oldPass, newPass, newPass2); err != nil { + if err = db.UpdateOwnUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, oldPass, newPass, newPass2); err != nil { log.Println("error: user:", user.ID, err) http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError) return @@ -222,7 +222,7 @@ func AddFirstUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - user.ID, err = db.AddFirstUser(user, pass) + user.ID, err = db.AddFirstUser(c, user, pass) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -269,7 +269,7 @@ func ShowAllUsers(c *b.Config, db *b.DB, s *b.CookieStore, action string) http.H }) data.Action = action - data.Users, err = db.GetAllUsers() + data.Users, err = db.GetAllUsers(c) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -301,7 +301,7 @@ func EditUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - user, err := db.GetUser(id) + user, err := db.GetUser(c, id) if err != nil { log.Println(err) http.Error(w, err.Error(), http.StatusInternalServerError) @@ -368,7 +368,7 @@ func UpdateUser(c *b.Config, db *b.DB, s *b.CookieStore) http.HandlerFunc { return } - if err = db.UpdateUserAttributes(user.ID, user.UserName, user.FirstName, user.LastName, newPass, newPass2, user.Role); err != nil { + if err = db.UpdateUserAttributes(c, user.ID, user.UserName, user.FirstName, user.LastName, newPass, newPass2, user.Role); err != nil { log.Println("error: user:", user.ID, err) http.Error(w, "Benutzerdaten konnten nicht aktualisiert werden.", http.StatusInternalServerError) return diff --git a/cmd/main.go b/cmd/main.go index 8c4220f..4e96b1e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -34,13 +34,13 @@ func main() { } defer db.Close() - key, err := b.LoadKey(config.KeyFile) + key, err := b.LoadKey(config.GOBKeyFile) if err != nil { key, err = b.NewKey() if err != nil { log.Fatalln(err) } - if err = b.SaveKey(key, config.KeyFile); err != nil { + if err = b.SaveKey(key, config.GOBKeyFile); err != nil { log.Fatalln(err) } } diff --git a/create_db.sql b/create_db.sql index 162236f..0fd917e 100644 --- a/create_db.sql +++ b/create_db.sql @@ -8,10 +8,10 @@ CREATE TABLE users ( id INT AUTO_INCREMENT, username VARCHAR(15) NOT NULL UNIQUE, password VARCHAR(60) NOT NULL, - first_name VARCHAR(60) NOT NULL, - last_name VARCHAR(60) NOT NULL, - email VARCHAR(60) NOT NULL, - profile_pic_link VARCHAR(255) NOT NULL, + first_name VARCHAR(255) NOT NULL, + last_name VARCHAR(255) NOT NULL, + -- email VARCHAR(255) NOT NULL, + -- profile_pic_link VARCHAR(255) NOT NULL, role INT NOT NULL, PRIMARY KEY (id) ); diff --git a/web/templates/first-user.html b/web/templates/first-user.html index 4fb84f2..4831341 100644 --- a/web/templates/first-user.html +++ b/web/templates/first-user.html @@ -26,7 +26,8 @@
- +
{{end}}