Working end to end with in-memory data store.
This commit is contained in:
parent
320dc46010
commit
426500df45
7 changed files with 313 additions and 27 deletions
40
cmd/cli/getClipboard.go
Normal file
40
cmd/cli/getClipboard.go
Normal 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
|
||||
},
|
||||
}
|
||||
|
|
@ -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
|
||||
},
|
||||
}
|
||||
|
|
|
|||
43
cmd/cli/setClipboard.go
Normal file
43
cmd/cli/setClipboard.go
Normal 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
|
||||
},
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
17
internal/server/client/encryption.go
Normal file
17
internal/server/client/encryption.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue