vpn client + api: add feature to approve one device from another
diff --git "a/core/client/cmd/pcloud/\043app_android.go\043" "b/core/client/cmd/pcloud/\043app_android.go\043"
new file mode 100644
index 0000000..2b28a11
--- /dev/null
+++ "b/core/client/cmd/pcloud/\043app_android.go\043"
@@ -0,0 +1,241 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "time"
+ "unsafe"
+
+ "gioui.org/app"
+ "github.com/giolekva/pcloud/core/client/jni"
+ "github.com/sirupsen/logrus"
+ "github.com/slackhq/nebula"
+ "github.com/slackhq/nebula/cert"
+ nc "github.com/slackhq/nebula/config"
+)
+
+type androidApp struct {
+ jvm *jni.JVM
+ appCtx jni.Object // PCloudApp
+ activity jni.Object // PCloudActivity
+ service jni.Object // PCloudVPNService
+ nebulaConfig []byte
+ ctrl *nebula.Control
+}
+
+func createApp() App {
+ return &androidApp{
+ jvm: (*jni.JVM)(unsafe.Pointer(app.JavaVM())),
+ appCtx: jni.Object(app.AppContext()),
+ }
+}
+
+func (a *androidApp) LaunchBarcodeScanner() error {
+ return jni.Do(a.jvm, func(env *jni.Env) error {
+ cls := jni.GetObjectClass(env, a.activity)
+ m := jni.GetMethodID(env, cls, "launchBarcodeScanner", "()Ljava/lang/String;")
+ _, err := jni.CallObjectMethod(env, a.activity, m)
+ return err
+ })
+}
+
+func (a *androidApp) OnView(e app.ViewEvent) error {
+ a.deleteActivityRef()
+ view := jni.Object(e.View)
+ if view == 0 {
+ return nil
+ }
+ activity, err := a.contextForView(view)
+ if err != nil {
+ return err
+ }
+ a.activity = activity
+ return nil
+}
+
+func (a *androidApp) deleteActivityRef() {
+ if a.activity == 0 {
+ return
+ }
+ jni.Do(a.jvm, func(env *jni.Env) error {
+ jni.DeleteGlobalRef(env, a.activity)
+ return nil
+ })
+ a.activity = 0
+}
+
+func (a *androidApp) contextForView(view jni.Object) (jni.Object, error) {
+ if view == 0 {
+ return 0, errors.New("Should not reach")
+ }
+ var ctx jni.Object
+ err := jni.Do(a.jvm, func(env *jni.Env) error {
+ cls := jni.GetObjectClass(env, view)
+ m := jni.GetMethodID(env, cls, "getContext", "()Landroid/content/Context;")
+ var err error
+ ctx, err = jni.CallObjectMethod(env, view, m)
+ ctx = jni.NewGlobalRef(env, ctx)
+ return err
+ })
+ if err != nil {
+ return 0, err
+ }
+ return ctx, nil
+}
+
+func (a *androidApp) StartVPN(config []byte) error {
+ fmt.Println("2222222")
+ a.nebulaConfig = config
+ fmt.Println(string(a.nebulaConfig))
+ return jni.Do(a.jvm, func(env *jni.Env) error {
+ fmt.Println(123123)
+ cls := jni.GetObjectClass(env, a.activity)
+ m := jni.GetMethodID(env, cls, "startVpn", "(Ljava/lang/String;)Ljava/lang/String;")
+ jConfig := jni.JavaString(env, string(config))
+ _, err := jni.CallObjectMethod(env, a.activity, m, jni.Value(jConfig))
+ fmt.Println(123123123)
+ return err
+
+ })
+}
+
+func (a *androidApp) Connect(serv interface{}) error {
+ s, ok := serv.(jni.Object)
+ if !ok {
+ return fmt.Errorf("Unexpected service type: %T", serv)
+ }
+ jni.Do(a.jvm, func(env *jni.Env) error {
+ if jni.IsSameObject(env, s, a.service) {
+ // We already have a reference.
+ jni.DeleteGlobalRef(env, s)
+ return nil
+ }
+ if a.service != 0 {
+ jni.DeleteGlobalRef(env, a.service)
+ }
+ // netns.SetAndroidProtectFunc(func(fd int) error {
+ // return jni.Do(a.jvm, func(env *jni.Env) error {
+ // // Call https://developer.android.com/reference/android/net/VpnService#protect(int)
+ // // to mark fd as a socket that should bypass the VPN and use the underlying network.
+ // cls := jni.GetObjectClass(env, s)
+ // m := jni.GetMethodID(env, cls, "protect", "(I)Z")
+ // ok, err := jni.CallBooleanMethod(env, s, m, jni.Value(fd))
+ // // TODO(bradfitz): return an error back up to netns if this fails, once
+ // // we've had some experience with this and analyzed the logs over a wide
+ // // range of Android phones. For now we're being paranoid and conservative
+ // // and do the JNI call to protect best effort, only logging if it fails.
+ // // The risk of returning an error is that it breaks users on some Android
+ // // versions even when they're not using exit nodes. I'd rather the
+ // // relatively few number of exit node users file bug reports if Tailscale
+ // // doesn't work and then we can look for this log print.
+ // if err != nil || !ok {
+ // log.Printf("[unexpected] VpnService.protect(%d) = %v, %v", fd, ok, err)
+ // }
+ // return nil // even on error. see big TODO above.
+ // })
+ // })
+ a.service = s
+ return nil
+ })
+ return a.buildVPNConfigurationAndConnect()
+}
+
+func (a *androidApp) buildVPNConfigurationAndConnect() error {
+ if string(a.nebulaConfig) == "" {
+ return nil
+ }
+ fmt.Println(333333333)
+ fmt.Println(string(a.nebulaConfig))
+ // return nil
+ // if err := a.callVoidMethod(a.appCtx, "prepareVPN", "(Landroid/app/Activity;I)V",
+ // jni.Value(act), jni.Value(requestPrepareVPN)); err != nil {
+ // return nil
+ // }
+
+ config := nc.NewC(logrus.StandardLogger())
+ if err := config.LoadString(string(a.nebulaConfig)); err != nil {
+ return err
+ }
+ pki := config.GetMap("pki", nil)
+ hostCert, _, err := cert.UnmarshalNebulaCertificateFromPEM([]byte(pki["cert"].(string)))
+ if err != nil {
+ panic(err)
+ }
+ t := time.Now()
+ fmt.Println("#########3")
+ fmt.Println(t.String())
+ fmt.Println(hostCert.Details.NotBefore.String())
+ fmt.Println(hostCert.Details.NotAfter.String())
+ fmt.Println(t.Before(hostCert.Details.NotBefore))
+ fmt.Println(t.After(hostCert.Details.NotAfter))
+ fmt.Println("#########3")
+ // return nil
+ return jni.Do(a.jvm, func(env *jni.Env) error {
+ fmt.Println("---------")
+ cls := jni.GetObjectClass(env, a.service)
+ m := jni.GetMethodID(env, cls, "newBuilder", "()Landroid/net/VpnService$Builder;")
+ b, err := jni.CallObjectMethod(env, a.service, m)
+ if err != nil {
+ return fmt.Errorf("PCloudVPNService.newBuilder: %v", err)
+ }
+ fmt.Println("---------")
+ bcls := jni.GetObjectClass(env, b)
+ addAddress := jni.GetMethodID(env, bcls, "addAddress", "(Ljava/lang/String;I)Landroid/net/VpnService$Builder;")
+ addRoute := jni.GetMethodID(env, bcls, "addRoute", "(Ljava/lang/String;I)Landroid/net/VpnService$Builder;")
+ for _, ipNet := range hostCert.Details.Ips {
+ ip := ipNet.IP.String()
+ prefix, _ := ipNet.Mask.Size()
+ _, err := jni.CallObjectMethod(
+ env,
+ b,
+ addAddress,
+ jni.Value(jni.JavaString(env, ip)),
+ jni.Value(jni.Value(prefix)))
+ if err != nil {
+ return err
+ }
+ _, err = jni.CallObjectMethod(
+ env,
+ b,
+ addRoute,
+ jni.Value(jni.JavaString(env, ip)),
+ jni.Value(jni.Value(prefix)))
+ }
+ tun := config.GetMap("tun", nil)
+ setMtu := jni.GetMethodID(env, bcls, "setMtu", "(I)Landroid/net/VpnService$Builder;")
+ if _, err := jni.CallObjectMethod(env, b, setMtu, jni.Value(tun["mtu"].(int))); err != nil {
+ return err
+ }
+ establish := jni.GetMethodID(env, bcls, "establish", "()Landroid/os/ParcelFileDescriptor;")
+ parcelFD, err := jni.CallObjectMethod(env, b, establish)
+ if err != nil {
+ return err
+ }
+ parcelCls := jni.GetObjectClass(env, parcelFD)
+ detachFd := jni.GetMethodID(env, parcelCls, "detachFd", "()I")
+ tunFD, err := jni.CallIntMethod(env, parcelFD, detachFd)
+ if err != nil {
+ return fmt.Errorf("detachFd: %v", err)
+ }
+ fd := int(tunFD)
+ protect := jni.GetMethodID(env, cls, "protect", "(I)")
+ ok, err := jni.CallBooleanMethod(env, a.service, protect, jni.Value(fd))
+ if err != nil || !ok {
+ return fmt.Errorf("protect: %v %v", err, ok)
+ }
+ // getFd := jni.GetMethodID(env, parcelCls, "getFd", "()I")
+ // tunFD, err := jni.CallIntMethod(env, parcelFD, getFd)
+ // if err != nil {
+ // return err
+ // }
+
+ fmt.Println("===========")
+ ctrl, err := nebula.Main(config, false, "pcloud", logrus.StandardLogger(), &fd)
+ if err != nil {
+ return err
+ }
+ a.ctrl = ctrl
+ return nil
+ })
+
+}
diff --git a/core/client/cmd/pcloud/app.go b/core/client/cmd/pcloud/app.go
index a0cab6c..5020e68 100644
--- a/core/client/cmd/pcloud/app.go
+++ b/core/client/cmd/pcloud/app.go
@@ -3,6 +3,7 @@
import "gioui.org/app"
type App interface {
+ Capabilities() DeviceCapabilities
LaunchBarcodeScanner() error
OnView(app.ViewEvent) error
UpdateService(service interface{}) error
diff --git a/core/client/cmd/pcloud/app_android.go b/core/client/cmd/pcloud/app_android.go
index 3d18663..8feb2e7 100644
--- a/core/client/cmd/pcloud/app_android.go
+++ b/core/client/cmd/pcloud/app_android.go
@@ -28,6 +28,12 @@
}
}
+func (a *androidApp) Capabilities() DeviceCapabilities {
+ return DeviceCapabilities{
+ HasCamera: true,
+ }
+}
+
func (a *androidApp) CreateStorage() Storage {
return CreateStorage(a.jvm, a.appCtx)
}
@@ -134,11 +140,11 @@
}
func (a *androidApp) Connect(config Config) error {
- if config.Network == nil {
+ if config.Network.Config == nil {
return nil
}
nebulaConfig := nc.NewC(logrus.StandardLogger())
- if err := nebulaConfig.LoadString(string(config.Network)); err != nil {
+ if err := nebulaConfig.LoadString(string(config.Network.Config)); err != nil {
return err
}
pki := nebulaConfig.GetMap("pki", nil)
diff --git a/core/client/cmd/pcloud/app_darwin.go b/core/client/cmd/pcloud/app_darwin.go
deleted file mode 100644
index 3e4bd04..0000000
--- a/core/client/cmd/pcloud/app_darwin.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package main
-
-import (
- "errors"
-
- "gioui.org/app"
-)
-
-type darwinApp struct {
-}
-
-func createApp() App {
- return &darwinApp{}
-}
-
-func (a *darwinApp) LaunchBarcodeScanner() error {
- return errors.New("no camera")
-}
-
-func (a *darwinApp) OnView(e app.ViewEvent) error {
- return nil
-}
-
-func (a *darwinApp) Connect(config Config) error {
- return nil
-}
-
-func (a *darwinApp) UpdateService(serv interface{}) error {
- return nil
-}
-
-func (a *darwinApp) TriggerService() error {
- return nil
-}
-
-func (a *darwinApp) CreateStorage() Storage {
- return nil
-}
-
-func (a *darwinApp) GetHostname() (string, error) {
- return "", nil
-}
diff --git a/core/client/cmd/pcloud/app_macos.go b/core/client/cmd/pcloud/app_macos.go
new file mode 100644
index 0000000..3a231d3
--- /dev/null
+++ b/core/client/cmd/pcloud/app_macos.go
@@ -0,0 +1,70 @@
+//go:build darwin && !ios
+// +build darwin,!ios
+
+package main
+
+import (
+ "errors"
+ "os"
+
+ "gioui.org/app"
+ "github.com/sirupsen/logrus"
+ "github.com/slackhq/nebula"
+ nc "github.com/slackhq/nebula/config"
+)
+
+type macosApp struct {
+ ctrl *nebula.Control
+}
+
+func createApp() App {
+ return &macosApp{}
+}
+
+func (a *macosApp) Capabilities() DeviceCapabilities {
+ return DeviceCapabilities{
+ HasCamera: false,
+ }
+}
+
+func (a *macosApp) LaunchBarcodeScanner() error {
+ return errors.New("no camera")
+}
+
+func (a *macosApp) OnView(e app.ViewEvent) error {
+ return nil
+}
+
+func (a *macosApp) Connect(config Config) error {
+ if config.Network.Config == nil {
+ return nil
+ }
+ nebulaConfig := nc.NewC(logrus.StandardLogger())
+ if err := nebulaConfig.LoadString(string(config.Network.Config)); err != nil {
+ return err
+ }
+ ctrl, err := nebula.Main(nebulaConfig, false, "pcloud", logrus.StandardLogger(), nil)
+ if err != nil {
+ return err
+ }
+ ctrl.Start()
+ a.ctrl = ctrl
+ return nil
+}
+
+func (a *macosApp) UpdateService(serv interface{}) error {
+ return nil
+}
+
+func (a *macosApp) TriggerService() error {
+ p.ConnectRequested(nil)
+ return nil
+}
+
+func (a *macosApp) CreateStorage() Storage {
+ return CreateStorage()
+}
+
+func (a *macosApp) GetHostname() (string, error) {
+ return os.Hostname()
+}
diff --git a/core/client/cmd/pcloud/callbacks_android.go b/core/client/cmd/pcloud/callbacks_android.go
index b7060aa..4664538 100644
--- a/core/client/cmd/pcloud/callbacks_android.go
+++ b/core/client/cmd/pcloud/callbacks_android.go
@@ -15,7 +15,7 @@
func Java_me_lekva_pcloud_PCloudActivity_qrcodeScanned(env *C.JNIEnv, this C.jobject, contents C.jobject) {
jenv := (*jni.Env)(unsafe.Pointer(env))
code := jni.GoString(jenv, jni.String(contents))
- p.InviteQRCodeScanned([]byte(code))
+ p.QRCodeScanned([]byte(code))
}
//export Java_me_lekva_pcloud_PCloudVPNService_connect
diff --git a/core/client/cmd/pcloud/client.go b/core/client/cmd/pcloud/client.go
index 78a31f1..f755281 100644
--- a/core/client/cmd/pcloud/client.go
+++ b/core/client/cmd/pcloud/client.go
@@ -2,10 +2,15 @@
import (
"bytes"
+ "crypto/aes"
+ "crypto/cipher"
"crypto/rand"
+ "crypto/rsa"
+ "crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/json"
+ "fmt"
"io"
"net/http"
@@ -16,7 +21,9 @@
type VPNClient interface {
Sign(apiAddr string, message []byte) ([]byte, error)
- Join(apiAddr, hostname string, message, signature []byte) ([]byte, error)
+ Join(apiAddr, hostname string, publicKey, privateKey []byte, message, signature []byte) ([]byte, error)
+ Approve(apiAddr, hostname, ipCidr string, encPublicKey, netPublicKey []byte) error
+ Get(apiAddr, hostname string, encPrivateKey *rsa.PrivateKey, netPrivateKey []byte) ([]byte, error)
}
type directVPNClient struct {
@@ -69,16 +76,12 @@
cfgYamlB64 string
}
-func (c *directVPNClient) Join(apiAddr, hostname string, message, signature []byte) ([]byte, error) {
- pubKey, privKey, err := x25519Keypair()
- if err != nil {
- return nil, err
- }
+func (c *directVPNClient) Join(apiAddr, hostname string, publicKey, privateKey []byte, message, signature []byte) ([]byte, error) {
req := joinReq{
message,
signature,
hostname,
- cert.MarshalX25519PublicKey(pubKey),
+ cert.MarshalX25519PublicKey(publicKey),
"111.0.0.13/24",
}
var data bytes.Buffer
@@ -111,7 +114,101 @@
if pki, ok = cfgMap["pki"].(map[string]interface{}); !ok {
panic("Must not reach")
}
- pki["key"] = string(cert.MarshalX25519PrivateKey(privKey))
+ pki["key"] = string(cert.MarshalX25519PrivateKey(privateKey))
+ return yaml.Marshal(cfgMap)
+}
+
+type approveReq struct {
+ EncPublicKey []byte `json:"enc_public_key"`
+ Name string `json:"name"`
+ NetPublicKey []byte `json:"net_public_key"`
+ IPCidr string `json:"ip_cidr"`
+}
+
+func (c *directVPNClient) Approve(apiAddr, hostname, ipCidr string, encPublicKey, netPublicKey []byte) error {
+ req := approveReq{
+ encPublicKey,
+ hostname,
+ cert.MarshalX25519PublicKey(netPublicKey),
+ ipCidr,
+ }
+ var data bytes.Buffer
+ if err := json.NewEncoder(&data).Encode(req); err != nil {
+ return err
+ }
+ client := &http.Client{
+ // TODO(giolekva): remove, for some reason valid certificates are not accepted on gioui android.
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ },
+ }
+ _, err := client.Post(apiAddr+"/api/approve", "application/json", &data)
+ return err
+}
+
+type getResp struct {
+ Key []byte `json:"key"`
+ Nonce []byte `json:"nonce"`
+ Data []byte `json:"data"`
+}
+
+func (c *directVPNClient) Get(apiAddr, hostname string, encPrivateKey *rsa.PrivateKey, netPrivateKey []byte) ([]byte, error) {
+ client := &http.Client{
+ // TODO(giolekva): remove, for some reason valid certificates are not accepted on gioui android.
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ },
+ }
+ r, err := client.Get(apiAddr + "/api/get/" + hostname)
+ if err != nil {
+ fmt.Println(err.Error())
+ return nil, err
+ }
+ var resp getResp
+ if err := json.NewDecoder(r.Body).Decode(&resp); err != nil {
+ fmt.Println(err.Error())
+ return nil, err
+ }
+ // TODO(giolekva): encrypt key and nonce together
+ key, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, encPrivateKey, resp.Key, []byte(""))
+ if err != nil {
+ fmt.Println(err.Error())
+ return nil, err
+ }
+ nonce, err := rsa.DecryptOAEP(sha256.New(), rand.Reader, encPrivateKey, resp.Nonce, []byte(""))
+ if err != nil {
+ fmt.Println(1123123)
+ fmt.Println(err.Error())
+ return nil, err
+ }
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ fmt.Println(err.Error())
+ return nil, err
+ }
+ aesgcm, err := cipher.NewGCM(block)
+ if err != nil {
+ fmt.Println(err.Error())
+ return nil, err
+ }
+
+ cfgYaml, err := aesgcm.Open(nil, nonce, resp.Data, nil)
+ if err != nil {
+ fmt.Println(22222)
+ fmt.Println(err.Error())
+ return nil, err
+ }
+ var cfgMap map[string]interface{}
+ if err := yaml.Unmarshal(cfgYaml, &cfgMap); err != nil {
+ fmt.Println(err.Error())
+ return nil, err
+ }
+ var pki map[string]interface{}
+ var ok bool
+ if pki, ok = cfgMap["pki"].(map[string]interface{}); !ok {
+ panic("Must not reach")
+ }
+ pki["key"] = string(cert.MarshalX25519PrivateKey(netPrivateKey))
return yaml.Marshal(cfgMap)
}
diff --git a/core/client/cmd/pcloud/config.go b/core/client/cmd/pcloud/config.go
index 0cf551d..6a12ada 100644
--- a/core/client/cmd/pcloud/config.go
+++ b/core/client/cmd/pcloud/config.go
@@ -1,6 +1,14 @@
package main
type Config struct {
- ApiAddr string `json:"api_addr"`
- Network []byte `json:"network"`
+ ApiAddr string `json:"api_addr,omitempty"`
+ Enc struct {
+ PublicKey []byte `json:"public_key,omitempty"`
+ PrivateKey []byte `json:"private_key,omitempty"`
+ } `json:"encyption,omitempty"`
+ Network struct {
+ PublicKey []byte `json:"public_key,omitempty"`
+ PrivateKey []byte `json:"private_key,omitempty"`
+ Config []byte `json:"network,omitempty"`
+ } `json:"network,omitempty"`
}
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()
diff --git a/core/client/cmd/pcloud/storage_darwin.go b/core/client/cmd/pcloud/storage_darwin.go
index 8fabc98..d45d34a 100644
--- a/core/client/cmd/pcloud/storage_darwin.go
+++ b/core/client/cmd/pcloud/storage_darwin.go
@@ -1,6 +1,11 @@
package main
-import "errors"
+import (
+ "bytes"
+ "encoding/json"
+
+ "github.com/keybase/go-keychain"
+)
type darwinStorage struct {
}
@@ -10,9 +15,43 @@
}
func (s *darwinStorage) Get() (Config, error) {
- return nil, errors.New("Not implemented")
+ q := configItem()
+ q.SetMatchLimit(keychain.MatchLimitOne)
+ q.SetReturnData(true)
+ results, err := keychain.QueryItem(q)
+ if err != nil {
+ return Config{}, err
+ } else if len(results) != 1 {
+ return Config{}, nil
+ }
+ var config Config
+ err = json.NewDecoder(bytes.NewReader(results[0].Data)).Decode(&config)
+ return config, err
}
func (s *darwinStorage) Store(config Config) error {
- return errors.New("Not implemented")
+ var data bytes.Buffer
+ if err := json.NewEncoder(&data).Encode(config); err != nil {
+ return err
+ }
+ q := configItem()
+ item := configItem()
+ item.SetData(data.Bytes())
+ if err := keychain.UpdateItem(q, item); err == keychain.ErrorItemNotFound {
+ return keychain.AddItem(item)
+ } else {
+ return err
+ }
+}
+
+func configItem() keychain.Item {
+ item := keychain.NewItem()
+ item.SetSecClass(keychain.SecClassGenericPassword)
+ item.SetService("pcloud")
+ item.SetAccount("pcloud")
+ item.SetLabel("pcloud-config")
+ item.SetAccessGroup("me.lekva.pcloud")
+ item.SetSynchronizable(keychain.SynchronizableNo)
+ item.SetAccessible(keychain.AccessibleWhenUnlocked)
+ return item
}
diff --git a/core/client/cmd/pcloud/ui.go b/core/client/cmd/pcloud/ui.go
index 78afa76..8f35a3b 100644
--- a/core/client/cmd/pcloud/ui.go
+++ b/core/client/cmd/pcloud/ui.go
@@ -17,33 +17,51 @@
D = layout.Dimensions
)
+type DeviceCapabilities struct {
+ HasCamera bool
+}
+
type UI struct {
- th *material.Theme
+ th *material.Theme
+ cap DeviceCapabilities
+
invite struct {
open widget.Clickable
show bool
qr image.Image
}
+ approve struct {
+ open widget.Clickable
+ show bool
+ }
+
join struct {
- open widget.Clickable
- show bool
- qrcode string
+ open widget.Clickable
+ show bool
+ qr image.Image
}
}
-func NewUI(th *material.Theme) *UI {
- return &UI{th: th}
+func NewUI(th *material.Theme, cap DeviceCapabilities) *UI {
+ return &UI{th: th, cap: cap}
}
func (ui *UI) InviteQRGenerated(img image.Image) {
ui.invite.qr = img
}
+func (ui *UI) JoinQRGenerated(img image.Image) {
+ ui.join.qr = img
+}
+
func (ui *UI) OnBack() bool {
if ui.invite.show {
ui.invite.show = false
return true
+ } else if ui.approve.show {
+ ui.approve.show = false
+ return true
} else if ui.join.show {
ui.join.show = false
return true
@@ -54,14 +72,22 @@
func (ui *UI) Layout(gtx C) []UIEvent {
var events []UIEvent
if ui.invite.open.Clicked() {
- ui.join.show = false
ui.invite.show = true
+ ui.approve.show = false
+ ui.join.show = false
ui.invite.qr = nil
events = append(events, EventGetInviteQRCode{})
+ } else if ui.approve.open.Clicked() {
+ events = append(events, EventApproveOther{})
} else if ui.join.open.Clicked() {
- // ui.invite.show = false
- // ui.join.show = true
- events = append(events, EventScanBarcode{})
+ if ui.cap.HasCamera {
+ events = append(events, EventScanBarcode{})
+ } else {
+ ui.invite.show = false
+ ui.approve.show = false
+ ui.join.show = true
+ events = append(events, EventGetJoinQRCode{})
+ }
}
if ui.invite.show {
ui.layout(gtx, ui.layoutInvite)
@@ -96,13 +122,18 @@
}
func (ui *UI) layoutActions(gtx C) D {
- return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceAround, WeightSum: 2.2}.Layout(gtx,
+ return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceAround, WeightSum: 3.2}.Layout(gtx,
layout.Flexed(1, func(gtx C) D {
b := material.Button(ui.th, &ui.invite.open, "Invite")
b.CornerRadius = unit.Px(20)
return b.Layout(gtx)
}),
layout.Flexed(1, func(gtx C) D {
+ b := material.Button(ui.th, &ui.approve.open, "Approve")
+ b.CornerRadius = unit.Px(20)
+ return b.Layout(gtx)
+ }),
+ layout.Flexed(1, func(gtx C) D {
b := material.Button(ui.th, &ui.join.open, "Join")
b.CornerRadius = unit.Px(20)
return b.Layout(gtx)
@@ -110,24 +141,26 @@
)
}
-func (ui *UI) layoutInvite(gtx C) D {
- if ui.invite.qr == nil {
+func layoutQR(gtx C, qr image.Image) D {
+ if qr == nil {
return ColorBox(gtx, gtx.Constraints.Max, color.NRGBA{})
}
- d := ui.invite.qr.Bounds().Max.Sub(ui.invite.qr.Bounds().Min)
+ d := qr.Bounds().Max.Sub(qr.Bounds().Min)
return layout.Inset{
Left: unit.Px(0.5 * float32(gtx.Constraints.Max.X-d.X)),
Top: unit.Px(0.5 * float32(gtx.Constraints.Max.Y-d.Y)),
}.Layout(gtx, func(gtx C) D {
- paint.NewImageOp(ui.invite.qr).Add(gtx.Ops)
+ paint.NewImageOp(qr).Add(gtx.Ops)
paint.PaintOp{}.Add(gtx.Ops)
return D{Size: gtx.Constraints.Max}
})
+
+}
+
+func (ui *UI) layoutInvite(gtx C) D {
+ return layoutQR(gtx, ui.invite.qr)
}
func (ui *UI) layoutJoin(gtx C) D {
- if ui.join.qrcode == "" {
- return ColorBox(gtx, gtx.Constraints.Min, color.NRGBA{R: 255, A: 255})
- }
- return ColorBox(gtx, gtx.Constraints.Min, color.NRGBA{R: 255, G: 255, B: 255, A: 255})
+ return layoutQR(gtx, ui.join.qr)
}
diff --git a/core/client/cmd/pcloud/ui_events.go b/core/client/cmd/pcloud/ui_events.go
index 581c5d4..e59be5b 100644
--- a/core/client/cmd/pcloud/ui_events.go
+++ b/core/client/cmd/pcloud/ui_events.go
@@ -5,3 +5,7 @@
type EventScanBarcode struct{}
type EventGetInviteQRCode struct{}
+
+type EventGetJoinQRCode struct{}
+
+type EventApproveOther struct{}
diff --git a/core/client/go.mod b/core/client/go.mod
index c14150b..f419e35 100644
--- a/core/client/go.mod
+++ b/core/client/go.mod
@@ -5,11 +5,11 @@
require (
gioui.org v0.0.0-20211201162354-9a5298914282
gioui.org/cmd v0.0.0-20211214104907-f5c9d2725c7b
+ github.com/keybase/go-keychain v0.0.0-20211119201326-e02f34051621
github.com/sirupsen/logrus v1.8.1
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/slackhq/nebula v1.5.1
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
- golang.org/x/exp v0.0.0-20210722180016-6781d3edade3
golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098 // indirect
sigs.k8s.io/yaml v1.1.0
)
diff --git a/core/client/go.sum b/core/client/go.sum
index f062a62..2a36dad 100644
--- a/core/client/go.sum
+++ b/core/client/go.sum
@@ -253,6 +253,9 @@
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kardianos/service v1.2.0/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM=
+github.com/keybase/go-keychain v0.0.0-20211119201326-e02f34051621 h1:aMQ7pA4f06yOVXSulygyGvy4xA94fyzjUGs0iqQdMOI=
+github.com/keybase/go-keychain v0.0.0-20211119201326-e02f34051621/go.mod h1:enrU/ug069Om7vWxuFE6nikLI2BZNwevMiGSo43Kt5w=
+github.com/keybase/go.dbus v0.0.0-20200324223359-a94be52c0b03/go.mod h1:a8clEhrrGV/d76/f9r2I41BwANMihfZYV9C223vaxqE=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -399,6 +402,7 @@
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
@@ -448,6 +452,7 @@
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=