Working end to end with in-memory data store.

This commit is contained in:
Mariano Uvalle 2023-11-09 07:34:00 +00:00
parent 320dc46010
commit 426500df45
7 changed files with 313 additions and 27 deletions

40
cmd/cli/getClipboard.go Normal file
View file

@ -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
},
}

View file

@ -1,13 +1,7 @@
package main package main
import ( import (
"encoding/json"
"errors"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/AYM1607/ccclip/internal/configfile"
) )
func init() { func init() {
@ -18,22 +12,23 @@ var getDevicesCmd = &cobra.Command{
Use: "get-devices", Use: "get-devices",
Short: "Register a user with a given email and password", Short: "Register a user with a given email and password",
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
cc, err := configfile.EnsureAndGet() // cc, err := configfile.EnsureAndGet()
if err != nil { // if err != nil {
return err // return err
} // }
if cc.DeviceId == "" { // if cc.DeviceId == "" {
return errors.New("your device is not registered") // return errors.New("your device is not registered")
} // }
pvk, err := configfile.LoadPrivateKey() // pvk, err := configfile.LoadPrivateKey()
if err != nil { // if err != nil {
return err // return err
} // }
devices, err := apiclient.GetDevices(cc.DeviceId, pvk) // devices, err := apiclient.GetDevices(cc.DeviceId, pvk)
if err != nil { // if err != nil {
return err // return err
} // }
return json.NewEncoder(os.Stdout).Encode(devices) // return json.NewEncoder(os.Stdout).Encode(devices)
return nil
}, },
} }

43
cmd/cli/setClipboard.go Normal file
View file

@ -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
},
}

View file

@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/AYM1607/ccclip/internal/server" "github.com/AYM1607/ccclip/internal/server"
"github.com/AYM1607/ccclip/pkg/api"
"github.com/AYM1607/ccclip/pkg/crypto" "github.com/AYM1607/ccclip/pkg/crypto"
) )
@ -62,6 +63,7 @@ func (c *Client) RegisterDevice(email, password string, devicePublicKey []byte)
if err != nil { if err != nil {
return nil, err return nil, err
} }
hres, err := http.Post(c.url+"/registerDevice", "application/json", bytes.NewReader(reqJson)) hres, err := http.Post(c.url+"/registerDevice", "application/json", bytes.NewReader(reqJson))
if err != nil { if err != nil {
return nil, err return nil, err
@ -85,7 +87,102 @@ func (c *Client) RegisterDevice(email, password string, devicePublicKey []byte)
return &res, nil 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{ req := server.GetUserDevicesRequest{
FingerPrint: server.FingerPrint{Timestamp: time.Now().UTC()}, 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 { if err != nil {
return nil, err return nil, err
} }
return &res, nil return res.Devices, nil
} }

View file

@ -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
}

View file

@ -3,6 +3,7 @@ package server
import ( import (
"crypto/ecdh" "crypto/ecdh"
"encoding/json" "encoding/json"
"log"
"net/http" "net/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -51,6 +52,8 @@ func newHttpHandler() http.Handler {
r.HandleFunc("/register", c.handleRegister).Methods("POST") r.HandleFunc("/register", c.handleRegister).Methods("POST")
r.HandleFunc("/registerDevice", c.handleRegisterDevice).Methods("POST") r.HandleFunc("/registerDevice", c.handleRegisterDevice).Methods("POST")
r.HandleFunc("/userDevices", c.handleGetUserDevices).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 return r
} }
@ -184,3 +187,94 @@ func (c *controller) handleGetUserDevices(w http.ResponseWriter, r *http.Request
return 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
}
}

View file

@ -11,7 +11,7 @@ type Device struct {
} }
type Clipboard struct { type Clipboard struct {
SenderDeviceID string SenderPublicKey []byte
// Payloads maps DeviceIDs to base64 encoded data. // Payloads maps DeviceIDs to encrypted data.
Payloads map[string]string Payloads map[string][]byte
} }