vpn client + api: add feature to approve one device from another
diff --git a/core/client/cmd/pcloud/main.go b/core/client/cmd/pcloud/main.go
index 287407b..38c55ff 100644
--- a/core/client/cmd/pcloud/main.go
+++ b/core/client/cmd/pcloud/main.go
@@ -2,7 +2,11 @@
import (
"bytes"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
"encoding/json"
+ "errors"
"flag"
"fmt"
"image"
@@ -24,14 +28,23 @@
var p *processor
+type ScanQRFor int
+
+const (
+ ScanQRForJoin ScanQRFor = 0
+ ScanQRForApprove = 1
+)
+
type processor struct {
vc VPNClient
app App
st Storage
ui *UI
- inviteQrCh chan image.Image
- inviteQrScannedCh chan []byte
+ scanQRFor ScanQRFor
+ inviteQrCh chan image.Image
+ qrScannedCh chan []byte
+ joinQrCh chan image.Image
onConnectCh chan interface{}
onDisconnectCh chan interface{}
@@ -43,20 +56,21 @@
th := material.NewTheme(gofont.Collection())
app := createApp()
return &processor{
- vc: NewDirectVPNClient(*vpnApiAddr),
- app: app,
- st: app.CreateStorage(),
- ui: NewUI(th),
- inviteQrCh: make(chan image.Image, 1),
- inviteQrScannedCh: make(chan []byte, 1),
- onConnectCh: make(chan interface{}, 1),
- onDisconnectCh: make(chan interface{}, 1),
- onConfigCh: make(chan struct{}, 1),
+ vc: NewDirectVPNClient(*vpnApiAddr),
+ app: app,
+ st: app.CreateStorage(),
+ ui: NewUI(th, app.Capabilities()),
+ inviteQrCh: make(chan image.Image, 1),
+ qrScannedCh: make(chan []byte, 1),
+ joinQrCh: make(chan image.Image, 1),
+ onConnectCh: make(chan interface{}, 1),
+ onDisconnectCh: make(chan interface{}, 1),
+ onConfigCh: make(chan struct{}, 1),
}
}
-func (p *processor) InviteQRCodeScanned(code []byte) {
- p.inviteQrScannedCh <- code
+func (p *processor) QRCodeScanned(code []byte) {
+ p.qrScannedCh <- code
}
func (p *processor) ConnectRequested(service interface{}) {
@@ -70,7 +84,31 @@
p.onDisconnectCh <- service
}
+func (p *processor) generatePublicPrivateKey() error {
+ config, err := p.st.Get()
+ if err != nil {
+ return err
+ }
+ if config.Enc.PrivateKey != nil {
+ return nil
+ }
+ privKey, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ panic(err)
+ }
+ config.Enc.PrivateKey = x509.MarshalPKCS1PrivateKey(privKey)
+ config.Enc.PublicKey = x509.MarshalPKCS1PublicKey(&privKey.PublicKey)
+ config.Network.PublicKey, config.Network.PrivateKey, err = x25519Keypair()
+ if err != nil {
+ return err
+ }
+ return p.st.Store(config)
+}
+
func (p *processor) run() error {
+ if err := p.generatePublicPrivateKey(); err != nil {
+ return err
+ }
w := app.NewWindow(
app.Size(unit.Px(1500), unit.Px(1500)),
app.Title("PCloud"),
@@ -89,7 +127,7 @@
if config, err := p.st.Get(); err != nil {
return err
} else {
- if config.Network != nil {
+ if config.Network.Config != nil {
p.onConfigCh <- struct{}{}
}
}
@@ -111,10 +149,57 @@
case img := <-p.inviteQrCh:
p.ui.InviteQRGenerated(img)
w.Invalidate()
- case code := <-p.inviteQrScannedCh:
- if err := p.JoinAndGetNetworkConfig(code); err != nil {
- return err
+ case code := <-p.qrScannedCh:
+ switch p.scanQRFor {
+ case ScanQRForJoin:
+ if err := p.JoinAndGetNetworkConfig(code); err != nil {
+ return err
+ }
+ case ScanQRForApprove:
+ if err := p.ApproveOther(code); err != nil {
+ return err
+ }
+ default:
+ return errors.New("Must not reach!")
}
+ case img := <-p.joinQrCh:
+ p.ui.JoinQRGenerated(img)
+ w.Invalidate()
+ go func() {
+ cnt := 0
+ for {
+ fmt.Println(cnt)
+ cnt++
+ if cnt > 5 {
+ break
+ }
+ time.Sleep(time.Second)
+ config, err := p.st.Get()
+ if err != nil {
+ continue
+ }
+ privKey, err := x509.ParsePKCS1PrivateKey(config.Enc.PrivateKey)
+ if err != nil {
+ continue
+ }
+ hostname, err := p.app.GetHostname()
+ if err != nil {
+ continue
+ }
+ hostname = sanitizeHostname(hostname)
+ network, err := p.vc.Get("https://vpn.lekva.me", hostname, privKey, config.Network.PrivateKey)
+ if err != nil {
+ continue
+ }
+ config.ApiAddr = "https://vpn.lekva.me"
+ config.Network.Config = network
+ if err := p.st.Store(config); err != nil {
+ continue
+ }
+ p.onConfigCh <- struct{}{}
+ break
+ }
+ }()
case <-p.onConfigCh:
if err := p.app.TriggerService(); err != nil {
return err
@@ -147,8 +232,25 @@
panic(err)
}
}()
+ case EventGetJoinQRCode:
+ go func() {
+ if img, err := p.generateJoinQRCode(); err == nil {
+ p.joinQrCh <- img
+ } else {
+ // TODO(giolekva): do not panic
+ panic(err)
+ }
+ }()
+ case EventApproveOther:
+ p.scanQRFor = ScanQRForApprove
+ if err := p.app.LaunchBarcodeScanner(); err != nil {
+ return err
+ }
case EventScanBarcode:
- return p.app.LaunchBarcodeScanner()
+ p.scanQRFor = ScanQRForJoin
+ if err := p.app.LaunchBarcodeScanner(); err != nil {
+ return err
+ }
default:
return fmt.Errorf("Unhandled event: %#v", e)
}
@@ -156,12 +258,19 @@
return nil
}
-type qrCodeData struct {
+type inviteQrCodeData struct {
VPNApiAddr string `json:"vpn_api_addr"`
Message []byte `json:"message"`
Signature []byte `json:"signature"`
}
+type joinQrCodeData struct {
+ EncPublicKey []byte `json:"enc_public_key"`
+ Name string `json:"name"`
+ NetPublicKey []byte `json:"net_public_key"`
+ IPCidr string `json:"ip_cidr"`
+}
+
func (p *processor) generateInviteQRCode() (image.Image, error) {
config, err := p.st.Get()
if err != nil {
@@ -172,7 +281,7 @@
if err != nil {
return nil, err
}
- c := qrCodeData{
+ c := inviteQrCodeData{
config.ApiAddr,
message,
signature,
@@ -192,8 +301,43 @@
return img, nil
}
+func (p *processor) generateJoinQRCode() (image.Image, error) {
+ config, err := p.st.Get()
+ if err != nil {
+ return nil, err
+ }
+ hostname, err := p.app.GetHostname()
+ if err != nil {
+ return nil, err
+ }
+ hostname = sanitizeHostname(hostname)
+ c := joinQrCodeData{
+ EncPublicKey: config.Enc.PublicKey,
+ Name: hostname,
+ NetPublicKey: config.Network.PublicKey,
+ IPCidr: "111.0.0.14/24",
+ }
+ var data bytes.Buffer
+ if err := json.NewEncoder(&data).Encode(c); err != nil {
+ return nil, err
+ }
+ qr, err := qrcode.Encode(data.String(), qrcode.Medium, 1024)
+ if err != nil {
+ return nil, err
+ }
+ img, err := png.Decode(bytes.NewReader(qr))
+ if err != nil {
+ return nil, err
+ }
+ return img, nil
+}
+
func (p *processor) JoinAndGetNetworkConfig(code []byte) error {
- var invite qrCodeData
+ config, err := p.st.Get()
+ if err != nil {
+ return err
+ }
+ var invite inviteQrCodeData
if err := json.NewDecoder(bytes.NewReader(code)).Decode(&invite); err != nil {
return err
}
@@ -201,19 +345,39 @@
if err != nil {
return err
}
- hostname = strings.ToLower(strings.ReplaceAll(hostname, " ", "-"))
- fmt.Printf("------ %s\n", hostname)
- network, err := p.vc.Join(invite.VPNApiAddr, hostname, invite.Message, invite.Signature)
+ hostname = sanitizeHostname(hostname)
+ network, err := p.vc.Join(invite.VPNApiAddr, hostname, config.Network.PublicKey, config.Network.PrivateKey, invite.Message, invite.Signature)
if err != nil {
return err
}
- if err := p.st.Store(Config{invite.VPNApiAddr, network}); err != nil {
+ config.ApiAddr = invite.VPNApiAddr
+ config.Network.Config = network
+ if err := p.st.Store(config); err != nil {
return err
}
p.onConfigCh <- struct{}{}
return nil
}
+func (p *processor) ApproveOther(code []byte) error {
+ config, err := p.st.Get()
+ if err != nil {
+ return err
+ }
+ var approve joinQrCodeData
+ if err := json.NewDecoder(bytes.NewReader(code)).Decode(&approve); err != nil {
+ return err
+ }
+ return p.vc.Approve(config.ApiAddr, approve.Name, approve.IPCidr, approve.EncPublicKey, approve.NetPublicKey)
+}
+
+func sanitizeHostname(hostname string) string {
+ return strings.ToLower(
+ strings.ReplaceAll(
+ strings.ReplaceAll(hostname, " ", "-"),
+ ".", "-"))
+}
+
func main() {
flag.Parse()
p = newProcessor()