From a8b497d426cc907f2385b35b17c0d77220cc4da0 Mon Sep 17 00:00:00 2001 From: Mariano Uvalle Date: Mon, 6 Nov 2023 07:33:52 +0000 Subject: [PATCH] Prototype working with the register endpoint. --- cmd/cli/main.go | 39 +++++++++ cmd/cli/register.go | 52 +++++++++++ cmd/cli/root.go | 34 ++++++++ cmd/server/main.go | 25 ++++++ go.mod | 9 +- go.sum | 15 ++++ internal/server/crypto.go | 38 ++++++++ internal/server/server.go | 178 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 389 insertions(+), 1 deletion(-) create mode 100644 cmd/cli/main.go create mode 100644 cmd/cli/register.go create mode 100644 cmd/cli/root.go create mode 100644 cmd/server/main.go create mode 100644 internal/server/crypto.go create mode 100644 internal/server/server.go diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..17bdbdb --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "encoding/base64" +) + +func b64(i []byte) string { + return base64.StdEncoding.EncodeToString(i) +} + +var ( + priv1 = "~/dev/ccclip/keys1/private.key" + pub1 = "~/dev/ccclip/keys1/public.key" + + priv2 = "~/dev/ccclip/keys2/private.key" + pub2 = "~/dev/ccclip/keys2/public.key" +) + +func main() { + rootCmd.Execute() + // key1 := crypto.LoadPrivateKey("../keys1/private.key") + // key2 := crypto.LoadPrivateKey("../keys2/private.key") + + // secretMsg := "new-some-secret-messageeee" + + // encrypted := crypto.Encrypt( + // crypto.NewSharedKey(key1, key2.PublicKey(), crypto.SendDirection), + // []byte(secretMsg), + // ) + + // fmt.Printf("Message %q was encrypted to %q\n", secretMsg, b64(encrypted)) + + // decrypted := crypto.Decrypt( + // crypto.NewSharedKey(key2, key1.PublicKey(), crypto.ReceiveDirection), + // encrypted, + // ) + + // fmt.Printf("Message was decrypted as %q\n", string(decrypted)) +} diff --git a/cmd/cli/register.go b/cmd/cli/register.go new file mode 100644 index 0000000..a861565 --- /dev/null +++ b/cmd/cli/register.go @@ -0,0 +1,52 @@ +package main + +import ( + "bytes" + "encoding/json" + "io" + "log" + "net/http" + + "github.com/AYM1607/ccclip/internal/server" + "github.com/spf13/cobra" +) + +var email string +var password string + +func init() { + rootCmd.AddCommand(registerCmd) + + registerCmd.Flags().StringVarP(&email, "email", "e", "", "email will be your login identifier") + registerCmd.Flags().StringVarP(&password, "password", "p", "", "password will secure your account") + + registerCmd.MarkFlagRequired("email") + registerCmd.MarkFlagRequired("password") +} + +var registerCmd = &cobra.Command{ + Use: "register", + Short: "Register a user with a given email and password", + RunE: func(cmd *cobra.Command, args []string) error { + req := server.RegisterRequest{ + Email: email, + Password: password, + } + reqJson, err := json.Marshal(req) + if err != nil { + return err + } + res, err := http.Post("http://localhost:8080/register", "application/json", bytes.NewReader(reqJson)) + if err != nil { + return err + } + resBody, err := io.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + return err + } + + log.Println(string(resBody)) + return nil + }, +} diff --git a/cmd/cli/root.go b/cmd/cli/root.go new file mode 100644 index 0000000..3be0685 --- /dev/null +++ b/cmd/cli/root.go @@ -0,0 +1,34 @@ +package main + +import ( + "log" + + "github.com/spf13/cobra" +) + +var keyset int + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "ccclip", + Short: "copy strings to and from your end to end encrypted cloud clipboard", + Long: `copy strings to and from your end to end encrypted cloud clipboard`, + // Uncomment the following line if your bare application + // has an action associated with it: + // Run: func(cmd *cobra.Command, args []string) { }, +} + +func init() { + rootCmd.PersistentFlags().IntVarP(&keyset, "keyset", "k", 0, "which key set to use, can be 1 or 2") + + rootCmd.MarkPersistentFlagRequired("keyset") +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + log.Fatalln(err.Error()) + } +} diff --git a/cmd/server/main.go b/cmd/server/main.go new file mode 100644 index 0000000..d02702f --- /dev/null +++ b/cmd/server/main.go @@ -0,0 +1,25 @@ +package main + +import ( + "log" + "os" + + "github.com/AYM1607/ccclip/internal/config" + "github.com/AYM1607/ccclip/internal/server" +) + +func main() { + privateKeyPath := os.Getenv("CCCLIP_PRIVATE_KEY") + publicKeyPath := os.Getenv("CCCLIP_PUBLIC_KEY") + if publicKeyPath == "" || privateKeyPath == "" { + log.Fatalf("both public and privae keys must be provided") + } + + config.Default.PrivateKeyPath = privateKeyPath + config.Default.PublicKeyPath = publicKeyPath + + 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 3ca05ce..8957e87 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,15 @@ module github.com/AYM1607/ccclip go 1.21 require ( + github.com/gorilla/mux v1.8.0 + github.com/oklog/ulid/v2 v2.1.0 + github.com/spf13/cobra v1.8.0 golang.org/x/crypto v0.14.0 golang.org/x/term v0.13.0 ) -require golang.org/x/sys v0.13.0 // indirect +require ( + github.com/inconshreveable/mousetrap v1.1.0 // 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 16e3f28..38cffa7 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,21 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +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/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= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/server/crypto.go b/internal/server/crypto.go new file mode 100644 index 0000000..ce8dd45 --- /dev/null +++ b/internal/server/crypto.go @@ -0,0 +1,38 @@ +package server + +import ( + "crypto/ecdh" + "encoding/json" + "time" + + "github.com/AYM1607/ccclip/internal/db" + "github.com/AYM1607/ccclip/pkg/crypto" +) + +type AuthenticatedPayload struct { + DeviceID string `json:"deviceID"` + Payload []byte `json:"payload"` +} + +type FingerPrint struct { + Timestamp time.Time `json:"timestamp"` +} + +func decryptAuthenticatedPayload[T any](p AuthenticatedPayload, d db.DB, pk *ecdh.PrivateKey) (T, error) { + var res T + var zero T + + device, err := d.GetDevice(p.DeviceID) + if err != nil { + return zero, err + } + + key := crypto.NewSharedKey(pk, crypto.PublicKeyFromBytes(device.PublicKey), crypto.ReceiveDirection) + plain := crypto.Decrypt(key, p.Payload) + + err = json.Unmarshal(plain, res) + if err != nil { + return zero, err + } + return res, nil +} diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..c8f1222 --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,178 @@ +package server + +import ( + "crypto/ecdh" + "encoding/json" + "net/http" + + "github.com/gorilla/mux" + + "github.com/AYM1607/ccclip/internal/config" + "github.com/AYM1607/ccclip/internal/db" + "github.com/AYM1607/ccclip/pkg/api" + "github.com/AYM1607/ccclip/pkg/crypto" +) + +func New(addr string) *http.Server { + h := newHttpHandler() + + return &http.Server{ + Addr: addr, + Handler: h, + } +} + +type controller struct { + store db.DB + publicKey *ecdh.PublicKey + // TODO: This should not stay in memory for a long time. + // keeping it as part of the controller for testing purposes only. + privateKey *ecdh.PrivateKey +} + +func newHttpHandler() http.Handler { + r := mux.NewRouter() + + c := &controller{ + store: db.NewLocalDB(), + publicKey: crypto.LoadPublicKey(config.Default.PublicKeyPath), + privateKey: crypto.LoadPrivateKey(config.Default.PrivateKeyPath), + } + + // TODO: These are not restful at all, but it's the simplest for now. FIX IT! + r.HandleFunc("/register", c.handleRegister).Methods("POST") + r.HandleFunc("/registerDevice", c.handleRegisterDevice).Methods("POST") + r.HandleFunc("/userDevices", c.handleGetUserDevices).Methods("POST") + + return r +} + +type RegisterRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + +type RegisterResponse struct { + Message string `json:"message"` +} + +func (c *controller) handleRegister(w http.ResponseWriter, r *http.Request) { + var req RegisterRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if req.Email == "" || req.Password == "" { + http.Error(w, "both email and password are required", http.StatusBadRequest) + return + } + + // TODO: This is obviously just for testing, use Bcrypt or similar for prod. + err = c.store.PutUser(req.Email, req.Password) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + res := RegisterResponse{Message: "user was successfully registered"} + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(res) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +type RegisterDeviceRequest struct { + Email string `json:"email"` + Password string `json:"password"` + PublicKey []byte `json:"publicKey"` +} + +type RegisterDeviceResponse struct { + DeviceID string `json:"deviceID"` + Message string `json:"message"` +} + +// TODO: This should handle devices that are already registered and return the +// existing id. +func (c *controller) handleRegisterDevice(w http.ResponseWriter, r *http.Request) { + var req RegisterDeviceRequest + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + user, err := c.store.GetUser(req.Email) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + // TODO: This is obviously just for testing, use Bcrypt or similar for prod. + if user.PasswordHash != req.Password { + http.Error(w, "password is not correct for the user", http.StatusUnauthorized) + return + } + + deviceId, err := c.store.PutDevice(req.PublicKey, req.Email) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + res := RegisterDeviceResponse{DeviceID: deviceId, Message: "device registered successfully"} + w.WriteHeader(http.StatusCreated) + err = json.NewEncoder(w).Encode(res) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} + +type GetUserDevicesRequest struct { + FingerPrint `json:",inline"` +} + +type GetUserDevicesResponse struct { + Devices []*api.Device `json:"devices"` +} + +func (c *controller) handleGetUserDevices(w http.ResponseWriter, r *http.Request) { + var authReq AuthenticatedPayload + err := json.NewDecoder(r.Body).Decode(&authReq) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + _, err = decryptAuthenticatedPayload[*GetUserDevicesRequest](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. + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + user, err := c.store.GetDeviceUser(authReq.DeviceID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + devices, err := c.store.GetUserDevices(user.ID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + res := GetUserDevicesResponse{Devices: devices} + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(res) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +}