diff --git a/mobile/status.go b/mobile/status.go index d4602be1e..b62ffcd8f 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -1023,6 +1023,21 @@ func GenerateImages(filepath string, aX, aY, bX, bY int) string { return string(data) } +// StartSearchForLocalPairingPeers starts a UDP multicast beacon that both listens for and broadcasts to LAN peers +// on discovery the beacon will emit a signal with the details of the discovered peer. +// +// Currently, beacons are configured to search for 2 minutes pinging the network every 500 ms; +// - If no peer discovery is made before this time elapses the operation will terminate. +// - If a peer is discovered the pairing.PeerNotifier will terminate operation after 5 seconds, giving the peer +// reasonable time to discover this device. +// +// Peer details are represented by a json.Marshal peers.LocalPairingPeerHello +func StartSearchForLocalPairingPeers() string { + pn := pairing.NewPeerNotifier() + err := pn.Search() + return makeJSONResponse(err) +} + // GetConnectionStringForBeingBootstrapped starts a pairing.ReceiverServer // then generates a pairing.ConnectionParams. Used when the device is Logged out or has no Account keys // and the device has no camera to read a QR code with diff --git a/server/pairing/events.go b/server/pairing/events.go index ee5276e13..6d8a5197e 100644 --- a/server/pairing/events.go +++ b/server/pairing/events.go @@ -6,6 +6,7 @@ type EventType string const ( // Both Sender and Receiver + EventPeerDiscovered EventType = "peer-discovered" EventConnectionError EventType = "connection-error" EventConnectionSuccess EventType = "connection-success" EventTransferError EventType = "transfer-error" @@ -33,4 +34,5 @@ const ( ActionPairingAccount ActionSyncDevice ActionPairingInstallation + ActionPeerDiscovery ) diff --git a/server/pairing/peers.go b/server/pairing/peers.go new file mode 100644 index 000000000..062f01d0c --- /dev/null +++ b/server/pairing/peers.go @@ -0,0 +1,43 @@ +package pairing + +import ( + "runtime" + + "go.uber.org/zap" + + "github.com/status-im/status-go/logutils" + "github.com/status-im/status-go/server" + "github.com/status-im/status-go/server/pairing/peers" + "github.com/status-im/status-go/signal" +) + +type PeerNotifier struct { + logger *zap.Logger + stop chan struct{} +} + +func NewPeerNotifier() *PeerNotifier { + logger := logutils.ZapLogger().Named("PeerNotifier") + stop := make(chan struct{}) + + return &PeerNotifier{ + logger: logger, + stop: stop, + } +} + +func (p *PeerNotifier) handler(hello *peers.LocalPairingPeerHello) { + signal.SendLocalPairingEvent(Event{Type: EventPeerDiscovered, Action: ActionPeerDiscovery, Data: hello}) + p.logger.Debug("received peers.LocalPairingPeerHello message", zap.Any("hello message", hello)) + // TODO p.stop <- struct{}{} Don't do this immediately start a countdown to kill after 5 seconds to allow the + // peer to discover us. +} + +func (p *PeerNotifier) Search() error { + dn, err := server.GetDeviceName() + if err != nil { + return err + } + + return peers.Search(dn, runtime.GOOS, p.handler, p.stop, p.logger) +} diff --git a/server/pairing/peers/peers.go b/server/pairing/peers/peers.go deleted file mode 100644 index b324cc002..000000000 --- a/server/pairing/peers/peers.go +++ /dev/null @@ -1,101 +0,0 @@ -package peers - -import ( - "crypto/ecdsa" - "crypto/rand" - "fmt" - "runtime" - - "github.com/golang/protobuf/proto" - udpp2p "github.com/schollz/peerdiscovery" - "go.uber.org/zap" - - "github.com/status-im/status-go/server" -) - -var ( - // pk Yes this is an actual ECDSA **private** key committed to a public repository visible to anyone. - // DO NOT use this key for anything other than signing udp "hellos". The key's value is in giving other Status - // installations CONFIDENCE, NOT proof, that the sender of the UDP pings is another Status device. - // We do not rely on UDP message information to orchestrate connections or swap secrets. The use case is purely - // to make preflight checks which ADVISE the application and the user. - // - // A signature is more robust and flexible than an application identifier, and serves the same role as an ID, while - // securing the payload against tampering. - pk = []byte{0xbf, 0x3b, 0x37, 0x04, 0x30, 0x04, 0x32, 0x15, 0x72, 0xb0, 0x7f, 0x56, 0x72, 0x30, 0xae, 0x5b, 0x41, 0xf4, 0x4b, 0x42, 0x4a, 0xa2, 0x33, 0x53, 0x76, 0xed, 0x7a, 0xb9, 0x2d, 0x40, 0x37, 0x73} - k = &ecdsa.PrivateKey{} -) - -func init() { - k = server.ToECDSA(pk) -} - -type UDPNotifier struct { - logger *zap.Logger - id []byte - notifyOutput func(*LocalPairingPeerHello) -} - -func NewUDPNotifier(logger *zap.Logger, outputFunc func(*LocalPairingPeerHello)) (*UDPNotifier, error) { - randId := make([]byte, 32) - _, err := rand.Read(randId) - if err != nil { - return nil, err - } - - n := new(UDPNotifier) - n.logger = logger - n.id = randId - n.notifyOutput = outputFunc - return n, nil -} - -func (u *UDPNotifier) MakePayload(deviceName string) (*LocalPairingPeerHello, error) { - return NewLocalPairingPeerHello(u.id, deviceName, runtime.GOOS, k) -} - -func (u *UDPNotifier) notify(d udpp2p.Discovered) { - h := new(LocalPairingPeerHello) - err := proto.Unmarshal(d.Payload, &h.LocalPairingPeerHello) - if err != nil { - u.logger.Error("notify unmarshalling of payload failed", zap.Error(err)) - return - } - - ok := h.verify(&k.PublicKey) - if !ok { - u.logger.Error("verification of unmarshalled payload failed", zap.Any("LocalPairingPeerHello", h)) - return - } - - h.Discovered = d - u.notifyOutput(h) -} - -func (u *UDPNotifier) MakeUDPP2PSettings(deviceName string) (*udpp2p.Settings, error) { - if u.notifyOutput == nil { - return nil, fmt.Errorf("UDPNotifier has no notiftOutput function defined") - } - - h, err := u.MakePayload(deviceName) - if err != nil { - return nil, err - } - - mh, err := proto.Marshal(&h.LocalPairingPeerHello) - if err != nil { - return nil, err - } - - return &udpp2p.Settings{ - Notify: u.notify, - Payload: mh, - }, nil -} - -func Search() { - discoveries, _ := udpp2p.Discover(udpp2p.Settings{Limit: 1, AllowSelf: true}) - for _, d := range discoveries { - fmt.Printf("discovered '%s'\n", d.Address) - } -} diff --git a/server/pairing/peers/private_key.go b/server/pairing/peers/private_key.go new file mode 100644 index 000000000..cdc655bde --- /dev/null +++ b/server/pairing/peers/private_key.go @@ -0,0 +1,24 @@ +package peers + +import ( + "crypto/ecdsa" + + "github.com/status-im/status-go/server" +) + +var ( + // pk Yes this is an actual ECDSA **private** key committed to a public repository visible to anyone. + // DO NOT use this key for anything other than signing udp "hellos". The key's value is in giving other Status + // installations CONFIDENCE, NOT proof, that the sender of the UDP pings is another Status device. + // We do not rely on UDP message information to orchestrate connections or swap secrets. The use case is purely + // to make preflight checks which ADVISE the application and the user. + // + // A signature is more robust and flexible than an application identifier, and serves the same role as an ID, while + // securing the payload against tampering. + pk = []byte{0xbf, 0x3b, 0x37, 0x04, 0x30, 0x04, 0x32, 0x15, 0x72, 0xb0, 0x7f, 0x56, 0x72, 0x30, 0xae, 0x5b, 0x41, 0xf4, 0x4b, 0x42, 0x4a, 0xa2, 0x33, 0x53, 0x76, 0xed, 0x7a, 0xb9, 0x2d, 0x40, 0x37, 0x73} + k = &ecdsa.PrivateKey{} +) + +func init() { + k = server.ToECDSA(pk) +} diff --git a/server/pairing/peers/udp_notifier.go b/server/pairing/peers/udp_notifier.go new file mode 100644 index 000000000..fb56e4db4 --- /dev/null +++ b/server/pairing/peers/udp_notifier.go @@ -0,0 +1,98 @@ +package peers + +import ( + "crypto/rand" + "fmt" + "time" + + "github.com/golang/protobuf/proto" + udpp2p "github.com/schollz/peerdiscovery" + "go.uber.org/zap" +) + +type NotifyHandler func(*LocalPairingPeerHello) + +type UDPNotifier struct { + logger *zap.Logger + id []byte + notifyOutput NotifyHandler +} + +func NewUDPNotifier(logger *zap.Logger, outputFunc NotifyHandler) (*UDPNotifier, error) { + randId := make([]byte, 32) + _, err := rand.Read(randId) + if err != nil { + return nil, err + } + + n := new(UDPNotifier) + n.logger = logger + n.id = randId + n.notifyOutput = outputFunc + return n, nil +} + +func (u *UDPNotifier) makePayload(deviceName, deviceType string) (*LocalPairingPeerHello, error) { + return NewLocalPairingPeerHello(u.id, deviceName, deviceType, k) +} + +func (u *UDPNotifier) notify(d udpp2p.Discovered) { + h := new(LocalPairingPeerHello) + err := proto.Unmarshal(d.Payload, &h.LocalPairingPeerHello) + if err != nil { + u.logger.Error("notify unmarshalling of payload failed", zap.Error(err)) + return + } + + ok := h.verify(&k.PublicKey) + if !ok { + u.logger.Error("verification of unmarshalled payload failed", zap.Any("LocalPairingPeerHello", h)) + return + } + + h.Discovered = d + u.notifyOutput(h) +} + +func (u *UDPNotifier) MakeUDPP2PSettings(deviceName, deviceType string) (*udpp2p.Settings, error) { + if u.notifyOutput == nil { + return nil, fmt.Errorf("UDPNotifier has no notiftOutput function defined") + } + + h, err := u.makePayload(deviceName, deviceType) + if err != nil { + return nil, err + } + + mh, err := proto.Marshal(&h.LocalPairingPeerHello) + if err != nil { + return nil, err + } + + return &udpp2p.Settings{ + Notify: u.notify, + Payload: mh, + }, nil +} + +func Search(deviceName, deviceType string, notify NotifyHandler, stop chan struct{}, logger *zap.Logger) error { + un, err := NewUDPNotifier(logger, notify) + if err != nil { + return err + } + + settings, err := un.MakeUDPP2PSettings(deviceName, deviceType) + if err != nil { + return err + } + + settings.Delay = 500 * time.Millisecond + settings.TimeLimit = 2 * time.Minute + settings.StopChan = stop + + go func() { + _, err = udpp2p.Discover(*settings) + logger.Error("error while discovering udp peers", zap.Error(err)) + }() + return nil +} diff --git a/server/pairing/peers/peers_test.go b/server/pairing/peers/udp_notifier_test.go similarity index 83% rename from server/pairing/peers/peers_test.go rename to server/pairing/peers/udp_notifier_test.go index 68c5a5805..1bfeeea07 100644 --- a/server/pairing/peers/peers_test.go +++ b/server/pairing/peers/udp_notifier_test.go @@ -1,6 +1,7 @@ package peers import ( + "runtime" "sync" "testing" "time" @@ -8,7 +9,6 @@ import ( udpp2p "github.com/schollz/peerdiscovery" "github.com/stretchr/testify/suite" - "github.com/status-im/status-go/server" "github.com/status-im/status-go/server/servertest" ) @@ -30,7 +30,7 @@ type testSignalLogger struct { lock sync.Mutex } -func NewTestSignalLogger() *testSignalLogger { +func newTestSignalLogger() *testSignalLogger { tsl := new(testSignalLogger) tsl.log = make(map[string]map[string]bool) return tsl @@ -39,17 +39,15 @@ func NewTestSignalLogger() *testSignalLogger { func (t *testSignalLogger) testSignal(h *LocalPairingPeerHello) { t.lock.Lock() defer t.lock.Unlock() + if _, ok := t.log[h.Discovered.Address]; !ok { t.log[h.Discovered.Address] = make(map[string]bool) } t.log[h.Discovered.Address][h.DeviceName] = true } -func (s *UDPPeerDiscoverySuite) Test() { - n, err := server.GetDeviceName() - s.Require().NoError(err) - - tsl := NewTestSignalLogger() +func (s *UDPPeerDiscoverySuite) TestUDPNotifier() { + tsl := newTestSignalLogger() u1, err := NewUDPNotifier(s.Logger, tsl.testSignal) s.Require().NoError(err) @@ -57,13 +55,13 @@ func (s *UDPPeerDiscoverySuite) Test() { u2, err := NewUDPNotifier(s.Logger, tsl.testSignal) s.Require().NoError(err) - n1 := n + " - device 1" - n2 := n + " - device 2" + n1 := "device 1" + n2 := "device 2" wg := sync.WaitGroup{} wg.Add(1) go func() { - settings, err := u1.MakeUDPP2PSettings(n1) + settings, err := u1.MakeUDPP2PSettings(n1, runtime.GOOS) s.Require().NoError(err) settings.TimeLimit = 2 * time.Second @@ -77,7 +75,7 @@ func (s *UDPPeerDiscoverySuite) Test() { wg.Add(1) go func() { - settings, err := u2.MakeUDPP2PSettings(n2) + settings, err := u2.MakeUDPP2PSettings(n2, runtime.GOOS) s.Require().NoError(err) settings.TimeLimit = 2 * time.Second