From 7bcdd2dd8075f2668a5fa0c7b43ff1f7b7444bd4 Mon Sep 17 00:00:00 2001 From: Mariano Uvalle Date: Fri, 10 Nov 2023 07:48:11 +0000 Subject: [PATCH] Functionally done! --- cmd/server/main.go | 10 +- go.mod | 1 + go.sum | 2 + internal/config/config.go | 5 +- internal/db/db.go | 2 +- internal/db/local.go | 2 +- internal/db/sqlite.go | 182 +++++++++++++++++++++++++++++++ internal/server/client/client.go | 17 --- internal/server/server.go | 29 +++-- pkg/api/api.go | 2 +- 10 files changed, 219 insertions(+), 33 deletions(-) create mode 100644 internal/db/sqlite.go diff --git a/cmd/server/main.go b/cmd/server/main.go index d02702f..6728948 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -11,14 +11,20 @@ import ( func main() { privateKeyPath := os.Getenv("CCCLIP_PRIVATE_KEY") publicKeyPath := os.Getenv("CCCLIP_PUBLIC_KEY") + databaseLocation := os.Getenv("CCCLIP_DATABASE_LOCATION") + port := os.Getenv("CCCLIP_PORT") + if publicKeyPath == "" || privateKeyPath == "" { - log.Fatalf("both public and privae keys must be provided") + log.Fatalf("database location and public and privae keys must be provided") } config.Default.PrivateKeyPath = privateKeyPath config.Default.PublicKeyPath = publicKeyPath + config.Default.DatabaseLocation = databaseLocation - port := "8080" + if port == "" { + port = "8080" + } log.Printf("Serving on port %s", port) s := server.New(":" + port) log.Fatal(s.ListenAndServe()) diff --git a/go.mod b/go.mod index 75f5c99..beea1bd 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-sqlite3 v1.14.18 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sys v0.13.0 // indirect ) diff --git a/go.sum b/go.sum index f37f106..68fecef 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/mattn/go-sqlite3 v1.14.18 h1:JL0eqdCOq6DJVNPSvArO/bIV9/P7fbGrV00LZHc+5aI= +github.com/mattn/go-sqlite3 v1.14.18/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= diff --git a/internal/config/config.go b/internal/config/config.go index 1c5ac82..95ba1e2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,8 +1,9 @@ package config type Config struct { - PublicKeyPath string - PrivateKeyPath string + PublicKeyPath string + PrivateKeyPath string + DatabaseLocation string } var Default = Config{} diff --git a/internal/db/db.go b/internal/db/db.go index ef63108..7929dee 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -4,7 +4,7 @@ import "github.com/AYM1607/ccclip/pkg/api" type DB interface { // Users. - PutUser(id string, passwordHash string) error + PutUser(id string, passwordHash []byte) error GetUser(id string) (*api.User, error) // Devices. diff --git a/internal/db/local.go b/internal/db/local.go index 2ee60b5..4ba5ddd 100644 --- a/internal/db/local.go +++ b/internal/db/local.go @@ -27,7 +27,7 @@ func NewLocalDB() DB { } } -func (d *localDB) PutUser(id, passwordHash string) error { +func (d *localDB) PutUser(id string, passwordHash []byte) error { if _, ok := d.users[id]; ok { return errors.New("user exists") } diff --git a/internal/db/sqlite.go b/internal/db/sqlite.go new file mode 100644 index 0000000..ece60a6 --- /dev/null +++ b/internal/db/sqlite.go @@ -0,0 +1,182 @@ +package db + +import ( + "database/sql" + "fmt" + + "github.com/AYM1607/ccclip/pkg/api" + _ "github.com/mattn/go-sqlite3" + "github.com/oklog/ulid/v2" +) + +type sqliteDB struct { + internalDB *sql.DB +} + +func NewSQLiteDB(location string) DB { + internalDb, err := sql.Open("sqlite3", fmt.Sprintf("%s?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=on", location)) + if err != nil { + panic(fmt.Sprintf("could not connect to sqlite: %s", err.Error())) + } + db := &sqliteDB{ + internalDB: internalDb, + } + if err := db.setup(); err != nil { + panic(fmt.Sprintf("unable to initialize sqlite: %s", err.Error())) + } + return db +} + +func (d *sqliteDB) PutUser(id string, passwordHash []byte) error { + _, err := d.internalDB.Exec("INSERT INTO users(id, password_hash) values(?, ?)", id, passwordHash) + return err +} + +func (d *sqliteDB) GetUser(id string) (*api.User, error) { + res := &api.User{} + err := d.internalDB.QueryRow("SELECT id, password_hash FROM users WHERE id = ?", id).Scan(&res.ID, &res.PasswordHash) + if err != nil { + return nil, err + } + return res, nil +} + +func (d *sqliteDB) PutDevice(pubKey []byte, userId string) (string, error) { + id := ulid.Make().String() + _, err := d.internalDB.Exec("INSERT INTO devices(id, public_key, user_id) values(?, ? ,?)", id, pubKey, userId) + if err != nil { + return "", nil + } + return id, nil +} + +func (d *sqliteDB) GetDevice(id string) (*api.Device, error) { + res := &api.Device{} + err := d.internalDB.QueryRow("SELECT id, public_key FROM devices WHERE id = ?", id).Scan(&res.ID, &res.PublicKey) + if err != nil { + return nil, err + } + return res, nil +} + +func (d *sqliteDB) GetUserDevices(userId string) ([]*api.Device, error) { + res := []*api.Device{} + rows, err := d.internalDB.Query("SELECT id, public_key FROM devices WHERE user_id = ?", userId) + if err != nil { + return nil, err + } + defer rows.Close() + for rows.Next() { + device := &api.Device{} + err := rows.Scan(&device.ID, &device.PublicKey) + if err != nil { + return nil, err + } + res = append(res, device) + } + err = rows.Err() + if err != nil { + return nil, err + } + return res, nil +} + +func (d *sqliteDB) GetDeviceUser(deviceId string) (*api.User, error) { + var userId string + err := d.internalDB.QueryRow("SELECT user_id FROM devices WHERE id = ?", deviceId).Scan(&userId) + if err != nil { + return nil, err + } + + res := &api.User{} + err = d.internalDB.QueryRow("SELECT id, password_hash FROM users WHERE id = ?", userId).Scan(&res.ID, &res.PasswordHash) + if err != nil { + return nil, err + } + return res, nil +} + +func (d *sqliteDB) PutClipboard(userId string, clipboard *api.Clipboard) error { + tx, err := d.internalDB.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + clipRes, err := tx.Exec("INSERT INTO clipboards(user_id, sender_public_key) values(?, ?)", userId, clipboard.SenderPublicKey) + clipId, err := clipRes.LastInsertId() + if err != nil { + return nil + } + + for deviceId, ciphertext := range clipboard.Payloads { + _, err := tx.Exec("INSERT INTO clipboard_items(ciphertext, clipboard_id, device_id) values(?, ?, ?)", ciphertext, clipId, deviceId) + if err != nil { + return nil + } + } + + return tx.Commit() +} + +func (d *sqliteDB) GetClipboard(userId string) (*api.Clipboard, error) { + var latestClipId int + latestClip := &api.Clipboard{} + err := d.internalDB.QueryRow("SELECT id, sender_public_key FROM clipboards WHERE user_id = ? ORDER BY id DESC LIMIT 1", userId).Scan(&latestClipId, &latestClip.SenderPublicKey) + if err != nil { + return nil, err + } + + rows, err := d.internalDB.Query("SELECT device_id, ciphertext FROM clipboard_items WHERE clipboard_id = ?", latestClipId) + if err != nil { + return nil, err + } + defer rows.Close() + + latestClip.Payloads = map[string][]byte{} + for rows.Next() { + var deviceId string + var cipherText []byte + err := rows.Scan(&deviceId, &cipherText) + if err != nil { + return nil, err + } + latestClip.Payloads[deviceId] = cipherText + } + + if err := rows.Err(); err != nil { + return nil, err + } + return latestClip, nil +} + +func (d *sqliteDB) setup() error { + setupStm := ` + CREATE TABLE IF NOT EXISTS users( + id TEXT PRIMARY KEY, + password_hash BLOB + ) STRICT; + CREATE TABLE IF NOT EXISTS devices( + id TEXT PRIMARY KEY, + public_key BLOB, + user_id TEXT, + FOREIGN KEY(user_id) REFERENCES users(id) + ) STRICT; + CREATE TABLE IF NOT EXISTS clipboards( + id INTEGER PRIMARY KEY, + user_id TEXT, + sender_public_key BLOB, + FOREIGN KEY(user_id) REFERENCES users(id) + ) STRICT; + CREATE TABLE IF NOT EXISTS clipboard_items( + id INTEGER PRIMARY KEY, + ciphertext BLOB, + clipboard_id INTEGER, + device_id TEXT, + FOREIGN KEY(clipboard_id) REFERENCES clipboards(id), + FOREIGN KEY(device_id) REFERENCES devices(id) + ) STRICT; + ` + _, err := d.internalDB.Exec(setupStm) + return err +} diff --git a/internal/server/client/client.go b/internal/server/client/client.go index 7ab4b5e..aacdd95 100644 --- a/internal/server/client/client.go +++ b/internal/server/client/client.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "io" - "log" "net/http" "time" @@ -43,13 +42,6 @@ func (c *Client) Register(email, password string) error { return errors.New("got unexpected response code from server") } - resBody, err := io.ReadAll(res.Body) - defer res.Body.Close() - if err != nil { - return err - } - log.Println(string(resBody)) - return nil } @@ -77,7 +69,6 @@ func (c *Client) RegisterDevice(email, password string, devicePublicKey []byte) if err != nil { return nil, err } - log.Println(string(hresBody)) var res server.RegisterDeviceResponse err = json.Unmarshal(hresBody, &res) @@ -122,12 +113,6 @@ func (c *Client) SetClipboard(plaintext string, deviceId string, pvk *ecdh.Priva if err != nil { return err } - hresBody, err := io.ReadAll(hres.Body) - defer hres.Body.Close() - if err != nil { - return err - } - log.Println(string(hresBody)) if hres.StatusCode != http.StatusOK { return errors.New("got unexpected response code from server") @@ -165,7 +150,6 @@ func (c *Client) GetClipboard(deviceId string, pvk *ecdh.PrivateKey) (string, er if err != nil { return "", err } - log.Println(string(hresBody)) if hres.StatusCode != http.StatusOK { return "", errors.New("got unexpected response code from server") @@ -213,7 +197,6 @@ func (c *Client) getDevices(deviceId string, pvk *ecdh.PrivateKey) ([]*api.Devic if err != nil { return nil, err } - log.Println(string(hresBody)) if hres.StatusCode != http.StatusOK { return nil, errors.New("got unexpected response code from server") diff --git a/internal/server/server.go b/internal/server/server.go index 04af2a0..79ea2a2 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -7,6 +7,7 @@ import ( "net/http" "github.com/gorilla/mux" + "golang.org/x/crypto/bcrypt" "github.com/AYM1607/ccclip/internal/config" "github.com/AYM1607/ccclip/internal/db" @@ -23,6 +24,8 @@ func New(addr string) *http.Server { } } +const minPasswordWork = 12 + type controller struct { store db.DB publicKey *ecdh.PublicKey @@ -42,8 +45,16 @@ func newHttpHandler() http.Handler { if err != nil { panic("could not load server's private key") } + + var store db.DB + if config.Default.DatabaseLocation == "" { + store = db.NewLocalDB() + } else { + store = db.NewSQLiteDB(config.Default.DatabaseLocation) + } + c := &controller{ - store: db.NewLocalDB(), + store: store, publicKey: pbk, privateKey: pvk, } @@ -79,8 +90,13 @@ func (c *controller) handleRegister(w http.ResponseWriter, r *http.Request) { return } - // TODO: This is obviously just for testing, use Bcrypt or similar for prod. - err = c.store.PutUser(req.Email, req.Password) + passwordHash, err := bcrypt.GenerateFromPassword([]byte(req.Password), minPasswordWork) + if err != nil { + log.Printf("could not hash password: %s", err.Error()) + http.Error(w, "password invalid", http.StatusInternalServerError) + } + + err = c.store.PutUser(req.Email, passwordHash) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -122,8 +138,7 @@ func (c *controller) handleRegisterDevice(w http.ResponseWriter, r *http.Request return } - // TODO: This is obviously just for testing, use Bcrypt or similar for prod. - if user.PasswordHash != req.Password { + if err := bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(req.Password)); err != nil { http.Error(w, "password is not correct for the user", http.StatusUnauthorized) return } @@ -201,8 +216,6 @@ func (c *controller) handleSetClipboard(w http.ResponseWriter, r *http.Request) return } - log.Println("authReq: ", authReq) - req, err := decryptAuthenticatedPayload[*SetClipboardRequest](authReq, c.store, c.privateKey) // TODO: verify the request fingerprint. Right now we're just trusting that // if it decrypts successfully then we can trust it. @@ -211,8 +224,6 @@ func (c *controller) handleSetClipboard(w http.ResponseWriter, r *http.Request) return } - log.Println("req: ", req) - user, err := c.store.GetDeviceUser(authReq.DeviceID) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/pkg/api/api.go b/pkg/api/api.go index 8a21dfe..783f3cf 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -2,7 +2,7 @@ package api type User struct { ID string - PasswordHash string + PasswordHash []byte } type Device struct {