diff --git a/cmd/cli/getClipboard.go b/cmd/cli/getClipboard.go new file mode 100644 index 0000000..480601c --- /dev/null +++ b/cmd/cli/getClipboard.go @@ -0,0 +1,40 @@ +package main + +import ( + "errors" + "fmt" + + "github.com/AYM1607/ccclip/internal/configfile" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(getClipboardCmd) +} + +var getClipboardCmd = &cobra.Command{ + Use: "get-clipboard", + Short: "get the currently stored clipboard", + RunE: func(cmd *cobra.Command, args []string) error { + cc, err := configfile.EnsureAndGet() + if err != nil { + return err + } + + if cc.DeviceId == "" { + return errors.New("you must log in and register your device") + } + pvk, err := configfile.LoadPrivateKey() + if err != nil { + return fmt.Errorf("could not load this device's private key: %w", err) + } + + plain, err := apiclient.GetClipboard(cc.DeviceId, pvk) + if err != nil { + return fmt.Errorf("could not set clipboard: %w", err) + } + + fmt.Printf("Your current clipbard is %q\n", plain) + return nil + }, +} diff --git a/cmd/cli/getDevices.go b/cmd/cli/getDevices.go index 7affb45..e122093 100644 --- a/cmd/cli/getDevices.go +++ b/cmd/cli/getDevices.go @@ -1,13 +1,7 @@ package main import ( - "encoding/json" - "errors" - "os" - "github.com/spf13/cobra" - - "github.com/AYM1607/ccclip/internal/configfile" ) func init() { @@ -18,22 +12,23 @@ var getDevicesCmd = &cobra.Command{ Use: "get-devices", Short: "Register a user with a given email and password", RunE: func(cmd *cobra.Command, args []string) error { - cc, err := configfile.EnsureAndGet() - if err != nil { - return err - } - if cc.DeviceId == "" { - return errors.New("your device is not registered") - } - pvk, err := configfile.LoadPrivateKey() - if err != nil { - return err - } - devices, err := apiclient.GetDevices(cc.DeviceId, pvk) - if err != nil { - return err - } + // cc, err := configfile.EnsureAndGet() + // if err != nil { + // return err + // } + // if cc.DeviceId == "" { + // return errors.New("your device is not registered") + // } + // pvk, err := configfile.LoadPrivateKey() + // if err != nil { + // return err + // } + // devices, err := apiclient.GetDevices(cc.DeviceId, pvk) + // if err != nil { + // return err + // } - return json.NewEncoder(os.Stdout).Encode(devices) + // return json.NewEncoder(os.Stdout).Encode(devices) + return nil }, } diff --git a/cmd/cli/setClipboard.go b/cmd/cli/setClipboard.go new file mode 100644 index 0000000..b329fd4 --- /dev/null +++ b/cmd/cli/setClipboard.go @@ -0,0 +1,43 @@ +package main + +import ( + "errors" + "fmt" + + "github.com/AYM1607/ccclip/internal/configfile" + "github.com/spf13/cobra" +) + +var clipboard string + +func init() { + rootCmd.AddCommand(setClipboardCmd) + + setClipboardCmd.Flags().StringVar(&clipboard, "clip", "", "the string to send") + setClipboardCmd.MarkFlagRequired("clip") +} + +var setClipboardCmd = &cobra.Command{ + Use: "set-clipboard", + Short: "set the given string as the cloud clipboard", + RunE: func(cmd *cobra.Command, args []string) error { + cc, err := configfile.EnsureAndGet() + if err != nil { + return err + } + + if cc.DeviceId == "" { + return errors.New("you must log in and register your device") + } + pvk, err := configfile.LoadPrivateKey() + if err != nil { + return fmt.Errorf("could not load this device's private key: %w", err) + } + + err = apiclient.SetClipboard(clipboard, cc.DeviceId, pvk) + if err != nil { + return fmt.Errorf("could not set clipboard: %w", err) + } + return nil + }, +} diff --git a/internal/server/client/client.go b/internal/server/client/client.go index d25dd0d..7ab4b5e 100644 --- a/internal/server/client/client.go +++ b/internal/server/client/client.go @@ -11,6 +11,7 @@ import ( "time" "github.com/AYM1607/ccclip/internal/server" + "github.com/AYM1607/ccclip/pkg/api" "github.com/AYM1607/ccclip/pkg/crypto" ) @@ -62,6 +63,7 @@ func (c *Client) RegisterDevice(email, password string, devicePublicKey []byte) if err != nil { return nil, err } + hres, err := http.Post(c.url+"/registerDevice", "application/json", bytes.NewReader(reqJson)) if err != nil { return nil, err @@ -85,7 +87,102 @@ func (c *Client) RegisterDevice(email, password string, devicePublicKey []byte) return &res, nil } -func (c *Client) GetDevices(deviceId string, pvk *ecdh.PrivateKey) (*server.GetUserDevicesResponse, error) { +func (c *Client) SetClipboard(plaintext string, deviceId string, pvk *ecdh.PrivateKey) error { + devices, err := c.getDevices(deviceId, pvk) + if err != nil { + return err + } + payloads := encryptForAll(plaintext, pvk, devices) + + req := server.SetClipboardRequest{ + FingerPrint: server.FingerPrint{Timestamp: time.Now().UTC()}, + Clipboard: &api.Clipboard{ + SenderPublicKey: pvk.PublicKey().Bytes(), + Payloads: payloads, + }, + } + reqBytes, err := json.Marshal(req) + if err != nil { + return err + } + + key := crypto.NewSharedKey(pvk, serverPublicKey, crypto.SendDirection) + encryptedReq := crypto.Encrypt(key, reqBytes) + + authReq := &server.AuthenticatedPayload{ + DeviceID: deviceId, + Payload: encryptedReq, + } + authReqJson, err := json.Marshal(authReq) + if err != nil { + return err + } + + hres, err := http.Post(c.url+"/setClipboard", "application/json", bytes.NewReader(authReqJson)) + 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") + } + return nil +} + +func (c *Client) GetClipboard(deviceId string, pvk *ecdh.PrivateKey) (string, error) { + req := server.GetClipboardRequest{ + FingerPrint: server.FingerPrint{Timestamp: time.Now().UTC()}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + return "", err + } + + key := crypto.NewSharedKey(pvk, serverPublicKey, crypto.SendDirection) + encryptedReq := crypto.Encrypt(key, reqBytes) + + authReq := server.AuthenticatedPayload{ + DeviceID: deviceId, + Payload: encryptedReq, + } + authReqJson, err := json.Marshal(authReq) + if err != nil { + return "", err + } + + hres, err := http.Post(c.url+"/clipboard", "application/json", bytes.NewReader(authReqJson)) + 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") + } + + var res server.GetClipboardResponse + err = json.Unmarshal(hresBody, &res) + if err != nil { + return "", err + } + + key = crypto.NewSharedKey(pvk, crypto.PublicKeyFromBytes(res.SenderPublicKey), crypto.ReceiveDirection) + plain := crypto.Decrypt(key, res.Ciphertext) + return string(plain), nil +} + +func (c *Client) getDevices(deviceId string, pvk *ecdh.PrivateKey) ([]*api.Device, error) { req := server.GetUserDevicesRequest{ FingerPrint: server.FingerPrint{Timestamp: time.Now().UTC()}, } @@ -127,5 +224,5 @@ func (c *Client) GetDevices(deviceId string, pvk *ecdh.PrivateKey) (*server.GetU if err != nil { return nil, err } - return &res, nil + return res.Devices, nil } diff --git a/internal/server/client/encryption.go b/internal/server/client/encryption.go new file mode 100644 index 0000000..4418c22 --- /dev/null +++ b/internal/server/client/encryption.go @@ -0,0 +1,17 @@ +package client + +import ( + "crypto/ecdh" + + "github.com/AYM1607/ccclip/pkg/api" + "github.com/AYM1607/ccclip/pkg/crypto" +) + +func encryptForAll(plaintext string, pvk *ecdh.PrivateKey, devices []*api.Device) map[string][]byte { + res := map[string][]byte{} + for _, d := range devices { + key := crypto.NewSharedKey(pvk, crypto.PublicKeyFromBytes(d.PublicKey), crypto.SendDirection) + res[d.ID] = crypto.Encrypt(key, []byte(plaintext)) + } + return res +} diff --git a/internal/server/server.go b/internal/server/server.go index e283b20..04af2a0 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -3,6 +3,7 @@ package server import ( "crypto/ecdh" "encoding/json" + "log" "net/http" "github.com/gorilla/mux" @@ -51,6 +52,8 @@ func newHttpHandler() http.Handler { r.HandleFunc("/register", c.handleRegister).Methods("POST") r.HandleFunc("/registerDevice", c.handleRegisterDevice).Methods("POST") r.HandleFunc("/userDevices", c.handleGetUserDevices).Methods("POST") + r.HandleFunc("/setClipboard", c.handleSetClipboard).Methods("POST") + r.HandleFunc("/clipboard", c.handleGetClipboard).Methods("POST") return r } @@ -184,3 +187,94 @@ func (c *controller) handleGetUserDevices(w http.ResponseWriter, r *http.Request return } } + +type SetClipboardRequest struct { + FingerPrint `json:",inline"` + Clipboard *api.Clipboard `json:"clipboard"` +} + +func (c *controller) handleSetClipboard(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 + } + + 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. + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + log.Println("req: ", req) + + user, err := c.store.GetDeviceUser(authReq.DeviceID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + err = c.store.PutClipboard(user.ID, req.Clipboard) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +type GetClipboardRequest struct { + FingerPrint `json:",inline"` +} + +type GetClipboardResponse struct { + Ciphertext []byte `json:"ciphertext"` + SenderPublicKey []byte `json:"senderPublicKey"` +} + +func (c *controller) handleGetClipboard(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[*GetClipboardRequest](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 + } + + clip, err := c.store.GetClipboard(user.ID) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if _, ok := clip.Payloads[authReq.DeviceID]; !ok { + http.Error(w, "current clipboard was not produced for this device", http.StatusNotFound) + return + } + + res := GetClipboardResponse{SenderPublicKey: clip.SenderPublicKey, Ciphertext: clip.Payloads[authReq.DeviceID]} + w.WriteHeader(http.StatusOK) + err = json.NewEncoder(w).Encode(res) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } +} diff --git a/pkg/api/api.go b/pkg/api/api.go index 3772e07..8a21dfe 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -11,7 +11,7 @@ type Device struct { } type Clipboard struct { - SenderDeviceID string - // Payloads maps DeviceIDs to base64 encoded data. - Payloads map[string]string + SenderPublicKey []byte + // Payloads maps DeviceIDs to encrypted data. + Payloads map[string][]byte }