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
|
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
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"
|
"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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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 (
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue