mirror of https://github.com/status-im/go-waku.git
498 lines
12 KiB
Go
498 lines
12 KiB
Go
package noise
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"sync"
|
|
|
|
n "github.com/waku-org/go-noise"
|
|
"github.com/waku-org/go-waku/waku/v2/protocol/pb"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var ErrPairingTimeout = errors.New("pairing has timed out")
|
|
|
|
type Sender interface {
|
|
Publish(ctx context.Context, contentTopic string, payload *n.PayloadV2) error
|
|
}
|
|
|
|
type Receiver interface {
|
|
// Subscribe will return a channel to obtain next message received in a content topic
|
|
Subscribe(ctx context.Context, contentTopic string) <-chan *pb.WakuMessage
|
|
}
|
|
|
|
type Pairing struct {
|
|
sync.RWMutex
|
|
ContentTopic string
|
|
msgCh <-chan *pb.WakuMessage
|
|
randomFixLenVal []byte
|
|
myCommittedStaticKey []byte
|
|
params PairingParameters
|
|
handshake *n.Handshake
|
|
authCode string
|
|
authCodeEmitted chan string
|
|
authCodeConfirmed chan bool
|
|
messenger NoiseMessenger
|
|
logger *zap.Logger
|
|
|
|
started bool
|
|
completed bool
|
|
hsResult *n.HandshakeResult
|
|
}
|
|
|
|
type PairingParameterOption func(*PairingParameters) error
|
|
|
|
func WithInitiatorParameters(qrString string, qrMessageNametag n.MessageNametag) PairingParameterOption {
|
|
return func(params *PairingParameters) error {
|
|
params.initiator = true
|
|
qr, err := StringToQR(qrString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
params.qr = qr
|
|
params.qrMessageNametag = qrMessageNametag
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithResponderParameters(applicationName, applicationVersion, shardId string, qrMessageNameTag *n.MessageNametag) PairingParameterOption {
|
|
return func(params *PairingParameters) error {
|
|
params.initiator = false
|
|
if qrMessageNameTag == nil {
|
|
b := make([]byte, n.MessageNametagLength)
|
|
_, err := rand.Read(b)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
params.qrMessageNametag = n.BytesToMessageNametag(b)
|
|
} else {
|
|
params.qrMessageNametag = *qrMessageNameTag
|
|
}
|
|
params.qr = NewQR(applicationName, applicationVersion, shardId, params.ephemeralPublicKey, params.myCommitedStaticKey)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
const DefaultApplicationName = "waku-noise-sessions"
|
|
const DefaultApplicationVersion = "0.1"
|
|
const DefaultShardId = "10"
|
|
|
|
func WithDefaultResponderParameters() PairingParameterOption {
|
|
return WithResponderParameters(DefaultApplicationName, DefaultApplicationVersion, DefaultShardId, nil)
|
|
}
|
|
|
|
type PairingParameters struct {
|
|
myCommitedStaticKey []byte
|
|
ephemeralPublicKey ed25519.PublicKey
|
|
initiator bool
|
|
qr QR
|
|
qrMessageNametag n.MessageNametag
|
|
}
|
|
|
|
func NewPairing(myStaticKey n.Keypair, myEphemeralKey n.Keypair, opts PairingParameterOption, messenger NoiseMessenger, logger *zap.Logger) (*Pairing, error) {
|
|
b := make([]byte, 32)
|
|
_, err := rand.Read(b)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
myCommitedStaticKey := n.CommitPublicKey(sha256.New, myStaticKey.Public, b)
|
|
|
|
var params PairingParameters
|
|
params.myCommitedStaticKey = myCommitedStaticKey
|
|
params.ephemeralPublicKey = myEphemeralKey.Public
|
|
err = opts(¶ms)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hs, err := n.NewHandshake_WakuPairing_25519_ChaChaPoly_SHA256(myStaticKey, myEphemeralKey, params.initiator, params.qr.Bytes(), params.qr.ephemeralPublicKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
contentTopic := "/" + params.qr.applicationName + "/" + params.qr.applicationVersion + "/wakunoise/1/sessions_shard-" + params.qr.shardId + "/proto"
|
|
|
|
// TODO: check if subscription is removed on stop
|
|
msgCh := messenger.Subscribe(context.Background(), contentTopic)
|
|
|
|
return &Pairing{
|
|
ContentTopic: contentTopic,
|
|
msgCh: msgCh,
|
|
randomFixLenVal: b, // this represents r or s depending if you're responder or initiator
|
|
myCommittedStaticKey: myCommitedStaticKey,
|
|
authCodeEmitted: make(chan string, 1),
|
|
authCodeConfirmed: make(chan bool, 1),
|
|
params: params,
|
|
handshake: hs,
|
|
messenger: messenger,
|
|
logger: logger.Named("waku-pairing1"),
|
|
}, nil
|
|
}
|
|
|
|
func (p *Pairing) PairingInfo() (qrString string, qrMessageNametag n.MessageNametag) {
|
|
p.RLock()
|
|
defer p.RUnlock()
|
|
return p.params.qr.String(), p.params.qrMessageNametag
|
|
}
|
|
|
|
func (p *Pairing) Execute(ctx context.Context) error {
|
|
p.RLock()
|
|
if p.started {
|
|
p.RUnlock()
|
|
return errors.New("pairing already executed. Create new pairing object")
|
|
}
|
|
|
|
defer p.messenger.Stop()
|
|
|
|
p.RUnlock()
|
|
p.Lock()
|
|
p.started = true
|
|
p.Unlock()
|
|
|
|
var doneCh <-chan error
|
|
if p.params.initiator {
|
|
doneCh = p.initiatorHandshake(ctx, p.msgCh)
|
|
} else {
|
|
doneCh = p.responderHandshake(ctx, p.msgCh)
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
p.Lock()
|
|
defer p.Unlock()
|
|
return ErrPairingTimeout
|
|
case err := <-doneCh:
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (p *Pairing) isAuthCodeConfirmed(ctx context.Context) (bool, error) {
|
|
// wait for user to confirm or not, or for the whole pairing process to time out
|
|
select {
|
|
case <-ctx.Done():
|
|
return false, ErrPairingTimeout
|
|
case confirmed := <-p.authCodeConfirmed:
|
|
return confirmed, nil
|
|
}
|
|
}
|
|
|
|
func (p *Pairing) executeReadStepWithNextMessage(ctx context.Context, nextMsgChan <-chan *pb.WakuMessage, messageNametag n.MessageNametag) (*n.HandshakeStepResult, error) {
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, ErrPairingTimeout
|
|
case msg := <-nextMsgChan:
|
|
payload, err := DecodePayloadV2(msg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
step, err := p.handshake.Step(payload, nil, messageNametag)
|
|
if err != nil {
|
|
if errors.Is(err, n.ErrNametagNotExpected) || errors.Is(err, n.ErrUnexpectedMessageNametag) {
|
|
p.logger.Debug(err.Error())
|
|
continue
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
return step, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Pairing) initiatorHandshake(ctx context.Context, msgCh <-chan *pb.WakuMessage) (doneCh chan error) {
|
|
doneCh = make(chan error, 1)
|
|
|
|
go func() {
|
|
defer close(doneCh)
|
|
// The handshake initiator writes a Waku2 payload v2 containing the handshake message
|
|
// and the (encrypted) transport message
|
|
// The message is sent with a messageNametag equal to the one received through the QR code
|
|
hsStep, err := p.handshake.Step(nil, p.myCommittedStaticKey, p.params.qrMessageNametag)
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
// We prepare a message from initiator's payload2
|
|
// At this point wakuMsg is sent over the Waku network to receiver content topic
|
|
err = p.messenger.Publish(ctx, p.ContentTopic, hsStep.PayloadV2)
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
// We generate an authorization code using the handshake state
|
|
// this check has to be confirmed with a user interaction, comparing auth codes in both ends
|
|
authCode, err := p.handshake.Authcode()
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
p.Lock()
|
|
p.authCode = authCode
|
|
p.Unlock()
|
|
|
|
p.authCodeEmitted <- authCode
|
|
|
|
p.logger.Info("waiting for authcode confirmation....")
|
|
|
|
confirmed, err := p.isAuthCodeConfirmed(ctx)
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
if !confirmed {
|
|
p.logger.Info("authcode not confirmed")
|
|
doneCh <- errors.New("authcode not confirmed")
|
|
return
|
|
}
|
|
|
|
// 2nd step
|
|
// <- sB, eAsB {r}
|
|
hsMessageNametag, err := p.handshake.ToMessageNametag()
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
hsStep, err = p.executeReadStepWithNextMessage(ctx, msgCh, hsMessageNametag)
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
// Initiator further checks if receiver's commitment opens to receiver's static key received
|
|
expectedReceiverCommittedStaticKey := n.CommitPublicKey(sha256.New, p.handshake.RemoteStaticPublicKey(), hsStep.TransportMessage)
|
|
if !bytes.Equal(expectedReceiverCommittedStaticKey, p.params.qr.committedStaticKey) {
|
|
doneCh <- errors.New("expected committed static key does not match the receiver actual committed static key")
|
|
return
|
|
}
|
|
|
|
// 3rd step
|
|
// -> sA, sAeB, sAsB {s}
|
|
// Similarly as in first step, the initiator writes a Waku2 payload containing the handshake message and the (encrypted) transport message
|
|
hsMessageNametag, err = p.handshake.ToMessageNametag()
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
hsStep, err = p.handshake.Step(nil, p.randomFixLenVal, hsMessageNametag)
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
err = p.messenger.Publish(ctx, p.ContentTopic, hsStep.PayloadV2)
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
hsResult, err := p.handshake.FinalizeHandshake()
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
// Secure Transfer Phase
|
|
if !p.handshake.IsComplete() {
|
|
doneCh <- errors.New("handshake is in undefined state")
|
|
return
|
|
}
|
|
|
|
p.Lock()
|
|
p.hsResult = hsResult
|
|
p.completed = true
|
|
p.Unlock()
|
|
}()
|
|
|
|
return doneCh
|
|
}
|
|
|
|
func (p *Pairing) responderHandshake(ctx context.Context, msgCh <-chan *pb.WakuMessage) (doneCh chan error) {
|
|
doneCh = make(chan error, 1)
|
|
|
|
func() {
|
|
defer close(doneCh)
|
|
|
|
// the received reads the initiator's payloads, and returns the (decrypted) transport message the initiator sent
|
|
// Note that the received verifies if the received payloadV2 has the expected messageNametag set
|
|
hsStep, err := p.executeReadStepWithNextMessage(ctx, msgCh, p.params.qrMessageNametag)
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
initiatorCommittedStaticKey := hsStep.TransportMessage
|
|
|
|
// We generate an authorization code using the handshake state
|
|
// this check has to be confirmed with a user interaction, comparing auth codes in both ends
|
|
authCode, err := p.handshake.Authcode()
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
p.Lock()
|
|
p.authCode = authCode
|
|
p.Unlock()
|
|
|
|
p.authCodeEmitted <- authCode
|
|
|
|
p.logger.Info("waiting for authcode confirmation....")
|
|
|
|
confirmed, err := p.isAuthCodeConfirmed(ctx)
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
if !confirmed {
|
|
p.logger.Info("authcode not confirmed")
|
|
doneCh <- errors.New("authcode not confirmed")
|
|
return
|
|
}
|
|
|
|
// 2nd step
|
|
// <- sB, eAsB {r}
|
|
// Receiver writes and returns a payload
|
|
hsMessageNametag, err := p.handshake.ToMessageNametag()
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
hsStep, err = p.handshake.Step(nil, p.randomFixLenVal, hsMessageNametag)
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
// We prepare a Waku message from receiver's payload2
|
|
err = p.messenger.Publish(ctx, p.ContentTopic, hsStep.PayloadV2)
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
// 3rd step
|
|
// -> sA, sAeB, sAsB {s}
|
|
hsMessageNametag, err = p.handshake.ToMessageNametag()
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
// The receiver reads the initiator's payload sent by the initiator
|
|
hsStep, err = p.executeReadStepWithNextMessage(ctx, msgCh, hsMessageNametag)
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
// The receiver further checks if the initiator's commitment opens to the initiator's static key received
|
|
expectedInitiatorCommittedStaticKey := n.CommitPublicKey(sha256.New, p.handshake.RemoteStaticPublicKey(), hsStep.TransportMessage)
|
|
if !bytes.Equal(expectedInitiatorCommittedStaticKey, initiatorCommittedStaticKey) {
|
|
doneCh <- errors.New("expected committed static key does not match the initiator actual committed static key")
|
|
return
|
|
}
|
|
|
|
hsResult, err := p.handshake.FinalizeHandshake()
|
|
if err != nil {
|
|
doneCh <- err
|
|
return
|
|
}
|
|
|
|
// Secure Transfer Phase
|
|
if !p.handshake.IsComplete() {
|
|
doneCh <- errors.New("handshake is in undefined state")
|
|
return
|
|
}
|
|
|
|
p.Lock()
|
|
p.completed = true
|
|
p.hsResult = hsResult
|
|
p.Unlock()
|
|
}()
|
|
return doneCh
|
|
}
|
|
|
|
func (p *Pairing) HandshakeComplete() bool {
|
|
p.RLock()
|
|
defer p.RUnlock()
|
|
return p.completed
|
|
}
|
|
|
|
// Returns a WakuMessage with version 2 and encrypted payload
|
|
func (p *Pairing) Encrypt(plaintext []byte) (*pb.WakuMessage, error) {
|
|
p.RLock()
|
|
defer p.RUnlock()
|
|
if !p.completed {
|
|
return nil, errors.New("pairing is not complete")
|
|
}
|
|
|
|
payload, err := p.hsResult.WriteMessage(plaintext, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
encodedPayload, err := EncodePayloadV2(payload)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
encodedPayload.ContentTopic = p.ContentTopic
|
|
|
|
return encodedPayload, nil
|
|
}
|
|
|
|
func (p *Pairing) Decrypt(msg *pb.WakuMessage) ([]byte, error) {
|
|
p.RLock()
|
|
defer p.RUnlock()
|
|
if !p.completed {
|
|
return nil, errors.New("pairing is not complete")
|
|
}
|
|
|
|
payload, err := DecodePayloadV2(msg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return p.hsResult.ReadMessage(payload, nil)
|
|
}
|
|
|
|
func (p *Pairing) ConfirmAuthCode(confirmed bool) error {
|
|
p.RLock()
|
|
authcode := p.authCode
|
|
p.RUnlock()
|
|
|
|
if authcode != "" {
|
|
p.authCodeConfirmed <- confirmed
|
|
return nil
|
|
}
|
|
|
|
return errors.New("authcode has not been generated yet")
|
|
}
|
|
|
|
func (p *Pairing) AuthCode() <-chan string {
|
|
ch := make(chan string, 1)
|
|
|
|
p.Lock()
|
|
authcode := p.authCode
|
|
p.Unlock()
|
|
|
|
if authcode != "" {
|
|
ch <- authcode
|
|
} else {
|
|
ch <- <-p.authCodeEmitted
|
|
}
|
|
close(ch)
|
|
return ch
|
|
}
|