diff --git a/cmd/cli/getDevices.go b/cmd/cli/getDevices.go new file mode 100644 index 0000000..7affb45 --- /dev/null +++ b/cmd/cli/getDevices.go @@ -0,0 +1,39 @@ +package main + +import ( + "encoding/json" + "errors" + "os" + + "github.com/spf13/cobra" + + "github.com/AYM1607/ccclip/internal/configfile" +) + +func init() { + rootCmd.AddCommand(getDevicesCmd) +} + +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 + } + + return json.NewEncoder(os.Stdout).Encode(devices) + }, +} diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 17bdbdb..207cfa8 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -2,38 +2,20 @@ package main import ( "encoding/base64" + + "github.com/AYM1607/ccclip/internal/server/client" ) func b64(i []byte) string { return base64.StdEncoding.EncodeToString(i) } -var ( - priv1 = "~/dev/ccclip/keys1/private.key" - pub1 = "~/dev/ccclip/keys1/public.key" +var apiclient *client.Client - priv2 = "~/dev/ccclip/keys2/private.key" - pub2 = "~/dev/ccclip/keys2/public.key" -) +func init() { + apiclient = client.New("http://localhost:8080") +} 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 index a861565..ee35635 100644 --- a/cmd/cli/register.go +++ b/cmd/cli/register.go @@ -1,14 +1,11 @@ package main import ( - "bytes" - "encoding/json" - "io" - "log" - "net/http" + "fmt" - "github.com/AYM1607/ccclip/internal/server" "github.com/spf13/cobra" + + "github.com/AYM1607/ccclip/internal/configfile" ) var email string @@ -28,25 +25,16 @@ 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) + err := apiclient.Register(email, password) 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 + return fmt.Errorf("could not register user: %w", err) } - log.Println(string(resBody)) - return nil + cc, err := configfile.EnsureAndGet() + if err != nil { + return err + } + cc.Email = email + return configfile.Write(cc) }, } diff --git a/cmd/cli/registerDevice.go b/cmd/cli/registerDevice.go new file mode 100644 index 0000000..cb5c2ae --- /dev/null +++ b/cmd/cli/registerDevice.go @@ -0,0 +1,58 @@ +package main + +import ( + "errors" + + "github.com/spf13/cobra" + + "github.com/AYM1607/ccclip/internal/configfile" + "github.com/AYM1607/ccclip/pkg/crypto" +) + +func init() { + rootCmd.AddCommand(registerDeviceCommand) + + registerDeviceCommand.Flags().StringVarP(&password, "password", "p", "", "password for your account") + + registerDeviceCommand.MarkFlagRequired("password") +} + +var registerDeviceCommand = &cobra.Command{ + Use: "register-device", + Short: "Register a device for the given user", + RunE: func(cmd *cobra.Command, args []string) error { + cc, err := configfile.EnsureAndGet() + if err != nil { + return err + } + + if cc.Email == "" { + return errors.New("you don't have an account configured for thist device") + } + + if cc.DeviceId != "" { + return errors.New("this device is already registered") + } + + pvk := crypto.NewPrivateKey() + pbk := pvk.PublicKey() + + res, err := apiclient.RegisterDevice(cc.Email, password, pbk.Bytes()) + if err != nil { + return err + } + + // Write the key files first, if those fail to write then we should not + // save the device Id. + cc.DeviceId = res.DeviceID + err = configfile.SavePrivateKey(pvk) + if err != nil { + return err + } + err = configfile.SavePublicKey(pbk) + if err != nil { + return err + } + return configfile.Write(cc) + }, +} diff --git a/cmd/cli/root.go b/cmd/cli/root.go index 3be0685..08d4d64 100644 --- a/cmd/cli/root.go +++ b/cmd/cli/root.go @@ -3,11 +3,10 @@ package main import ( "log" + "github.com/AYM1607/ccclip/internal/configfile" "github.com/spf13/cobra" ) -var keyset int - // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "ccclip", @@ -19,9 +18,9 @@ var rootCmd = &cobra.Command{ } func init() { - rootCmd.PersistentFlags().IntVarP(&keyset, "keyset", "k", 0, "which key set to use, can be 1 or 2") + rootCmd.PersistentFlags().StringVarP(&configfile.Path, "config-path", "c", "", "directory where to store the config file") - rootCmd.MarkPersistentFlagRequired("keyset") + rootCmd.MarkPersistentFlagRequired("config-path") } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/cmd/cli/util.go b/cmd/cli/util.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/cmd/cli/util.go @@ -0,0 +1 @@ +package main diff --git a/go.mod b/go.mod index 8957e87..75f5c99 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/spf13/cobra v1.8.0 golang.org/x/crypto v0.14.0 golang.org/x/term v0.13.0 + gopkg.in/yaml.v2 v2.4.0 ) require ( diff --git a/go.sum b/go.sum index 38cffa7..f37f106 100644 --- a/go.sum +++ b/go.sum @@ -17,5 +17,8 @@ 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/configfile/config.go b/internal/configfile/config.go new file mode 100644 index 0000000..72d4e11 --- /dev/null +++ b/internal/configfile/config.go @@ -0,0 +1,74 @@ +package configfile + +import ( + "crypto/ecdh" + "encoding/json" + "fmt" + "os" + "path" + + "github.com/AYM1607/ccclip/pkg/crypto" +) + +const FileName = "ccclip.yaml" +const PrivateKeyFileName = "private.key" +const PublicKeyFileName = "public.key" + +type ConfigFile struct { + Email string `yaml:"email"` + DeviceId string `yaml:"deviceId"` +} + +var Path string + +func EnsureAndGet() (ConfigFile, error) { + err := os.MkdirAll(Path, os.FileMode(int(0660))) + if err != nil { + return ConfigFile{}, fmt.Errorf("could not create config directory: %w", err) + } + rawC, err := os.ReadFile(path.Join(Path, FileName)) + if err != nil { + if os.IsNotExist(err) { + return ConfigFile{}, nil + } + return ConfigFile{}, fmt.Errorf("could not read current config file: %w", err) + } + var c ConfigFile + return c, json.Unmarshal(rawC, &c) +} + +func Write(c ConfigFile) error { + err := os.MkdirAll(Path, os.FileMode(int(0660))) + if err != nil { + return err + } + rawC, err := json.Marshal(c) + if err != nil { + return fmt.Errorf("could not convert config to json") + } + err = os.WriteFile(path.Join(Path, FileName), rawC, os.FileMode(int(0660))) + if err != nil { + return fmt.Errorf("could not write file to config directory: %w", err) + } + return nil +} + +func LoadPrivateKey() (*ecdh.PrivateKey, error) { + fp := path.Join(Path, PrivateKeyFileName) + return crypto.LoadPrivateKey(fp) +} + +func LoadPublicKey() (*ecdh.PublicKey, error) { + fp := path.Join(Path, PublicKeyFileName) + return crypto.LoadPublicKey(fp) +} + +func SavePrivateKey(k *ecdh.PrivateKey) error { + fp := path.Join(Path, PrivateKeyFileName) + return crypto.SavePrivateKey(fp, k) +} + +func SavePublicKey(k *ecdh.PublicKey) error { + fp := path.Join(Path, PublicKeyFileName) + return crypto.SavePublicKey(fp, k) +} diff --git a/internal/server/client/client.go b/internal/server/client/client.go new file mode 100644 index 0000000..d25dd0d --- /dev/null +++ b/internal/server/client/client.go @@ -0,0 +1,131 @@ +package client + +import ( + "bytes" + "crypto/ecdh" + "encoding/json" + "errors" + "io" + "log" + "net/http" + "time" + + "github.com/AYM1607/ccclip/internal/server" + "github.com/AYM1607/ccclip/pkg/crypto" +) + +type Client struct { + url string +} + +func New(url string) *Client { + return &Client{ + url: url, + } +} + +func (c *Client) Register(email, password string) error { + req := server.RegisterRequest{ + Email: email, + Password: password, + } + + reqJson, err := json.Marshal(req) + if err != nil { + return err + } + res, err := http.Post(c.url+"/register", "application/json", bytes.NewReader(reqJson)) + if err != nil { + return err + } + if res.StatusCode != http.StatusCreated { + return errors.New("got unexpected response code from server") + } + + resBody, err := io.ReadAll(res.Body) + defer res.Body.Close() + if err != nil { + return err + } + log.Println(string(resBody)) + + return nil +} + +func (c *Client) RegisterDevice(email, password string, devicePublicKey []byte) (*server.RegisterDeviceResponse, error) { + req := server.RegisterDeviceRequest{ + Email: email, + Password: password, + PublicKey: devicePublicKey, + } + reqJson, err := json.Marshal(req) + if err != nil { + return nil, err + } + hres, err := http.Post(c.url+"/registerDevice", "application/json", bytes.NewReader(reqJson)) + if err != nil { + return nil, err + } + if hres.StatusCode != http.StatusCreated { + return nil, errors.New("got unexpected response code from server") + } + + hresBody, err := io.ReadAll(hres.Body) + defer hres.Body.Close() + if err != nil { + return nil, err + } + log.Println(string(hresBody)) + + var res server.RegisterDeviceResponse + err = json.Unmarshal(hresBody, &res) + if err != nil { + return nil, err + } + return &res, nil +} + +func (c *Client) GetDevices(deviceId string, pvk *ecdh.PrivateKey) (*server.GetUserDevicesResponse, error) { + req := server.GetUserDevicesRequest{ + FingerPrint: server.FingerPrint{Timestamp: time.Now().UTC()}, + } + reqBytes, err := json.Marshal(req) + if err != nil { + return nil, 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 nil, err + } + + hres, err := http.Post(c.url+"/userDevices", "application/json", bytes.NewReader(authReqJson)) + if err != nil { + return nil, err + } + + hresBody, err := io.ReadAll(hres.Body) + defer hres.Body.Close() + if err != nil { + return nil, err + } + log.Println(string(hresBody)) + + if hres.StatusCode != http.StatusOK { + return nil, errors.New("got unexpected response code from server") + } + + var res server.GetUserDevicesResponse + err = json.Unmarshal(hresBody, &res) + if err != nil { + return nil, err + } + return &res, nil +} diff --git a/internal/server/client/key.go b/internal/server/client/key.go new file mode 100644 index 0000000..db8ceda --- /dev/null +++ b/internal/server/client/key.go @@ -0,0 +1,22 @@ +package client + +import ( + "crypto/ecdh" + "encoding/base64" + "fmt" + + "github.com/AYM1607/ccclip/pkg/crypto" +) + +const serverPublicKeyB64 = "JTyaIVDHe1Nwqmd4NFlkvqj+MZOVp5s3JZP+T3QuoT8=" + +var serverPublicKey *ecdh.PublicKey + +func init() { + pkeyBytes := make([]byte, crypto.KeySize) + _, err := base64.StdEncoding.Decode(pkeyBytes, []byte(serverPublicKeyB64)) + if err != nil { + panic(fmt.Sprintf("cannot decode server public key: %s", err.Error())) + } + serverPublicKey = crypto.PublicKeyFromBytes(pkeyBytes) +} diff --git a/internal/server/crypto.go b/internal/server/crypto.go index ce8dd45..e1d9011 100644 --- a/internal/server/crypto.go +++ b/internal/server/crypto.go @@ -3,6 +3,7 @@ package server import ( "crypto/ecdh" "encoding/json" + "reflect" "time" "github.com/AYM1607/ccclip/internal/db" @@ -22,6 +23,11 @@ func decryptAuthenticatedPayload[T any](p AuthenticatedPayload, d db.DB, pk *ecd var res T var zero T + _T := reflect.TypeOf(res) + if _T.Kind() == reflect.Pointer { + res = reflect.New(_T.Elem()).Interface().(T) + } + device, err := d.GetDevice(p.DeviceID) if err != nil { return zero, err diff --git a/internal/server/server.go b/internal/server/server.go index c8f1222..e283b20 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -33,10 +33,18 @@ type controller struct { func newHttpHandler() http.Handler { r := mux.NewRouter() + pbk, err := crypto.LoadPublicKey(config.Default.PublicKeyPath) + if err != nil { + panic("could not load server's public key") + } + pvk, err := crypto.LoadPrivateKey(config.Default.PrivateKeyPath) + if err != nil { + panic("could not load server's private key") + } c := &controller{ store: db.NewLocalDB(), - publicKey: crypto.LoadPublicKey(config.Default.PublicKeyPath), - privateKey: crypto.LoadPrivateKey(config.Default.PrivateKeyPath), + publicKey: pbk, + privateKey: pvk, } // TODO: These are not restful at all, but it's the simplest for now. FIX IT! diff --git a/pkg/crypto/keys.go b/pkg/crypto/keys.go index 50d244a..1b31990 100644 --- a/pkg/crypto/keys.go +++ b/pkg/crypto/keys.go @@ -19,6 +19,11 @@ const ( ReceiveDirection ) +var ( + privateKeyFileMode = os.FileMode(int(0600)) + publicKeyFileMode = os.FileMode(int(0644)) +) + func NewPrivateKey() *ecdh.PrivateKey { c := ecdh.X25519() k, err := c.GenerateKey(rand.Reader) @@ -75,21 +80,44 @@ func PublicKeyFromBytes(keyBytes []byte) *ecdh.PublicKey { return key } -func LoadPrivateKey(fp string) *ecdh.PrivateKey { - return PrivateKeyFromBytes(loadKey(fp)) -} - -func LoadPublicKey(fp string) *ecdh.PublicKey { - return PublicKeyFromBytes(loadKey(fp)) -} - -func loadKey(fn string) []byte { - b64Key, err := os.ReadFile(fn) +func LoadPrivateKey(fp string) (*ecdh.PrivateKey, error) { + kb, err := loadKey(fp) if err != nil { - panic(err) + return nil, err + } + return PrivateKeyFromBytes(kb), nil +} + +func LoadPublicKey(fp string) (*ecdh.PublicKey, error) { + kb, err := loadKey(fp) + if err != nil { + return nil, err + } + return PublicKeyFromBytes(kb), nil +} + +func SavePrivateKey(fp string, k *ecdh.PrivateKey) error { + return saveKey(fp, k.Bytes(), privateKeyFileMode) +} + +func SavePublicKey(fp string, k *ecdh.PublicKey) error { + return saveKey(fp, k.Bytes(), publicKeyFileMode) +} + +func loadKey(fp string) ([]byte, error) { + b64Key, err := os.ReadFile(fp) + if err != nil { + return nil, err } keyBytes := make([]byte, KeySize) base64.StdEncoding.Decode(keyBytes, b64Key) - return keyBytes + return keyBytes, nil +} + +func saveKey(fp string, key []byte, fm os.FileMode) error { + b64Key := make([]byte, base64.StdEncoding.EncodedLen(len(key))) + base64.StdEncoding.Encode(b64Key, key) + + return os.WriteFile(fp, b64Key, fm) }