Prototype working with the register endpoint.
This commit is contained in:
parent
c3eb1d72b4
commit
a8b497d426
8 changed files with 389 additions and 1 deletions
39
cmd/cli/main.go
Normal file
39
cmd/cli/main.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
)
|
||||
|
||||
func b64(i []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(i)
|
||||
}
|
||||
|
||||
var (
|
||||
priv1 = "~/dev/ccclip/keys1/private.key"
|
||||
pub1 = "~/dev/ccclip/keys1/public.key"
|
||||
|
||||
priv2 = "~/dev/ccclip/keys2/private.key"
|
||||
pub2 = "~/dev/ccclip/keys2/public.key"
|
||||
)
|
||||
|
||||
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))
|
||||
}
|
||||
52
cmd/cli/register.go
Normal file
52
cmd/cli/register.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/AYM1607/ccclip/internal/server"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var email string
|
||||
var password string
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(registerCmd)
|
||||
|
||||
registerCmd.Flags().StringVarP(&email, "email", "e", "", "email will be your login identifier")
|
||||
registerCmd.Flags().StringVarP(&password, "password", "p", "", "password will secure your account")
|
||||
|
||||
registerCmd.MarkFlagRequired("email")
|
||||
registerCmd.MarkFlagRequired("password")
|
||||
}
|
||||
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
log.Println(string(resBody))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
34
cmd/cli/root.go
Normal file
34
cmd/cli/root.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var keyset int
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "ccclip",
|
||||
Short: "copy strings to and from your end to end encrypted cloud clipboard",
|
||||
Long: `copy strings to and from your end to end encrypted cloud clipboard`,
|
||||
// Uncomment the following line if your bare application
|
||||
// has an action associated with it:
|
||||
// Run: func(cmd *cobra.Command, args []string) { },
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().IntVarP(&keyset, "keyset", "k", 0, "which key set to use, can be 1 or 2")
|
||||
|
||||
rootCmd.MarkPersistentFlagRequired("keyset")
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
log.Fatalln(err.Error())
|
||||
}
|
||||
}
|
||||
25
cmd/server/main.go
Normal file
25
cmd/server/main.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/AYM1607/ccclip/internal/config"
|
||||
"github.com/AYM1607/ccclip/internal/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
privateKeyPath := os.Getenv("CCCLIP_PRIVATE_KEY")
|
||||
publicKeyPath := os.Getenv("CCCLIP_PUBLIC_KEY")
|
||||
if publicKeyPath == "" || privateKeyPath == "" {
|
||||
log.Fatalf("both public and privae keys must be provided")
|
||||
}
|
||||
|
||||
config.Default.PrivateKeyPath = privateKeyPath
|
||||
config.Default.PublicKeyPath = publicKeyPath
|
||||
|
||||
port := "8080"
|
||||
log.Printf("Serving on port %s", port)
|
||||
s := server.New(":" + port)
|
||||
log.Fatal(s.ListenAndServe())
|
||||
}
|
||||
9
go.mod
9
go.mod
|
|
@ -3,8 +3,15 @@ module github.com/AYM1607/ccclip
|
|||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/term v0.13.0
|
||||
)
|
||||
|
||||
require golang.org/x/sys v0.13.0 // indirect
|
||||
require (
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
golang.org/x/sys v0.13.0 // indirect
|
||||
)
|
||||
|
|
|
|||
15
go.sum
15
go.sum
|
|
@ -1,6 +1,21 @@
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
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/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
|
|
|||
38
internal/server/crypto.go
Normal file
38
internal/server/crypto.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/AYM1607/ccclip/internal/db"
|
||||
"github.com/AYM1607/ccclip/pkg/crypto"
|
||||
)
|
||||
|
||||
type AuthenticatedPayload struct {
|
||||
DeviceID string `json:"deviceID"`
|
||||
Payload []byte `json:"payload"`
|
||||
}
|
||||
|
||||
type FingerPrint struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
func decryptAuthenticatedPayload[T any](p AuthenticatedPayload, d db.DB, pk *ecdh.PrivateKey) (T, error) {
|
||||
var res T
|
||||
var zero T
|
||||
|
||||
device, err := d.GetDevice(p.DeviceID)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
|
||||
key := crypto.NewSharedKey(pk, crypto.PublicKeyFromBytes(device.PublicKey), crypto.ReceiveDirection)
|
||||
plain := crypto.Decrypt(key, p.Payload)
|
||||
|
||||
err = json.Unmarshal(plain, res)
|
||||
if err != nil {
|
||||
return zero, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
178
internal/server/server.go
Normal file
178
internal/server/server.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
"github.com/AYM1607/ccclip/internal/config"
|
||||
"github.com/AYM1607/ccclip/internal/db"
|
||||
"github.com/AYM1607/ccclip/pkg/api"
|
||||
"github.com/AYM1607/ccclip/pkg/crypto"
|
||||
)
|
||||
|
||||
func New(addr string) *http.Server {
|
||||
h := newHttpHandler()
|
||||
|
||||
return &http.Server{
|
||||
Addr: addr,
|
||||
Handler: h,
|
||||
}
|
||||
}
|
||||
|
||||
type controller struct {
|
||||
store db.DB
|
||||
publicKey *ecdh.PublicKey
|
||||
// TODO: This should not stay in memory for a long time.
|
||||
// keeping it as part of the controller for testing purposes only.
|
||||
privateKey *ecdh.PrivateKey
|
||||
}
|
||||
|
||||
func newHttpHandler() http.Handler {
|
||||
r := mux.NewRouter()
|
||||
|
||||
c := &controller{
|
||||
store: db.NewLocalDB(),
|
||||
publicKey: crypto.LoadPublicKey(config.Default.PublicKeyPath),
|
||||
privateKey: crypto.LoadPrivateKey(config.Default.PrivateKeyPath),
|
||||
}
|
||||
|
||||
// TODO: These are not restful at all, but it's the simplest for now. FIX IT!
|
||||
r.HandleFunc("/register", c.handleRegister).Methods("POST")
|
||||
r.HandleFunc("/registerDevice", c.handleRegisterDevice).Methods("POST")
|
||||
r.HandleFunc("/userDevices", c.handleGetUserDevices).Methods("POST")
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
type RegisterRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type RegisterResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (c *controller) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
var req RegisterRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if req.Email == "" || req.Password == "" {
|
||||
http.Error(w, "both email and password are required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: This is obviously just for testing, use Bcrypt or similar for prod.
|
||||
err = c.store.PutUser(req.Email, req.Password)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res := RegisterResponse{Message: "user was successfully registered"}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
err = json.NewEncoder(w).Encode(res)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type RegisterDeviceRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
PublicKey []byte `json:"publicKey"`
|
||||
}
|
||||
|
||||
type RegisterDeviceResponse struct {
|
||||
DeviceID string `json:"deviceID"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// TODO: This should handle devices that are already registered and return the
|
||||
// existing id.
|
||||
func (c *controller) handleRegisterDevice(w http.ResponseWriter, r *http.Request) {
|
||||
var req RegisterDeviceRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := c.store.GetUser(req.Email)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: This is obviously just for testing, use Bcrypt or similar for prod.
|
||||
if user.PasswordHash != req.Password {
|
||||
http.Error(w, "password is not correct for the user", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
deviceId, err := c.store.PutDevice(req.PublicKey, req.Email)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res := RegisterDeviceResponse{DeviceID: deviceId, Message: "device registered successfully"}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
err = json.NewEncoder(w).Encode(res)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type GetUserDevicesRequest struct {
|
||||
FingerPrint `json:",inline"`
|
||||
}
|
||||
|
||||
type GetUserDevicesResponse struct {
|
||||
Devices []*api.Device `json:"devices"`
|
||||
}
|
||||
|
||||
func (c *controller) handleGetUserDevices(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[*GetUserDevicesRequest](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
|
||||
}
|
||||
|
||||
devices, err := c.store.GetUserDevices(user.ID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
res := GetUserDevicesResponse{Devices: devices}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
err = json.NewEncoder(w).Encode(res)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue