penguin/homekey-go

homekey-go

drivers/sesame/registration_test.go

raw ยท 6003 bytes

package sesame

import (
	"bytes"
	"context"
	"crypto/ecdh"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"sync/atomic"
	"testing"
	"time"
)

func waitRegistration(t *testing.T, p *RegistrationConnection, phase string) {
	t.Helper()
	deadline := time.Now().Add(time.Second)
	for time.Now().Before(deadline) {
		if p.State().Phase == phase {
			return
		}
		time.Sleep(time.Millisecond)
	}
	t.Fatalf("registration phase = %+v, want %s", p.State(), phase)
}
func linkClosed(f *fakeLink) bool { f.mu.Lock(); defer f.mu.Unlock(); return f.closed }

func TestPreparedRegistrationWaitsAndHandsOffOriginalSession(t *testing.T) {
	f := newLink()
	device, err := ecdh.P256().GenerateKey(rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	var peer *sessionCipher
	var registrations, logins, dials atomic.Int32
	var expectedSecret string
	f.write = func(kind byte, payload []byte) error {
		if kind == 1 {
			if payload[0] == 2 {
				logins.Add(1)
				return errors.New("registration must not login again")
			}
			if len(payload) != 69 || payload[0] != 1 {
				return errors.New("bad registration")
			}
			registrations.Add(1)
			pub, err := ecdh.P256().NewPublicKey(append([]byte{4}, payload[1:65]...))
			if err != nil {
				return err
			}
			shared, err := device.ECDH(pub)
			if err != nil {
				return err
			}
			expectedSecret = hex.EncodeToString(shared[:16])
			peer, _, err = newCipher(shared[:16], unhex("11223344"))
			if err != nil {
				return err
			}
			reply := make([]byte, 77)
			copy(reply, []byte{0, 0, 0, 128, 45, 0, 18})
			copy(reply[7:], []byte{90, 0, 0, 0, 0, 0})
			copy(reply[13:], device.PublicKey().Bytes()[1:])
			f.emit(1, append([]byte{7, 1, 0}, reply...))
			return nil
		}
		plain, err := peer.decrypt(payload)
		if err != nil {
			return err
		}
		if !bytes.Equal(plain, []byte{83, 0, 14}) {
			return errors.New("wrong live-session operation")
		}
		ack, err := peer.encrypt([]byte{7, 83, 0})
		if err != nil {
			return err
		}
		f.emit(2, ack)
		status, err := peer.encrypt([]byte{8, 81, 0, 0, 0, 128, 46, 0, 16})
		if err != nil {
			return err
		}
		f.emit(2, status)
		return nil
	}
	p := PrepareRegistration(context.Background(), testTarget, func(context.Context, Target) (Link, Target, error) { dials.Add(1); return f, testTarget, nil })
	defer p.Close()
	waitRegistration(t, p, "ready")
	if registrations.Load() != 0 || linkClosed(f) {
		t.Fatal("acted before Register")
	}
	saved := ""
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()
	c, err := p.Register(ctx, func(key Credential) error { saved = key.Secret; return nil })
	if err != nil {
		t.Fatal(err)
	}
	defer c.Close()
	if saved == "" || saved != expectedSecret {
		t.Fatal("credential not persisted")
	}
	p.Close()
	if linkClosed(f) {
		t.Fatal("handoff closed the live connection")
	}
	if state := c.State(); !state.Online || state.Position == nil || *state.Position != 45 || state.LockPosition == nil || *state.LockPosition != 90 {
		t.Fatalf("registration state lost: %+v", state)
	}
	life, stop := context.WithCancel(context.Background())
	done := make(chan struct{})
	go func() { c.Run(life); close(done) }()
	defer func() { stop(); <-done }()
	if err = c.Action(ctx, "unlock"); err != nil {
		t.Fatal(err)
	}
	deadline := time.Now().Add(time.Second)
	for c.State().Position != nil && *c.State().Position != 46 && time.Now().Before(deadline) {
		time.Sleep(time.Millisecond)
	}
	if c.State().Position == nil || *c.State().Position != 46 {
		t.Fatal("live notifications not handed off")
	}
	if dials.Load() != 1 || registrations.Load() != 1 || logins.Load() != 0 {
		t.Fatal("unexpected reconnect, replay or login")
	}
}

func TestPreparedRegistrationCannotRegisterBeforeInitialAndClosesOnCancel(t *testing.T) {
	f := newLink()
	<-f.packets
	var writes atomic.Int32
	f.write = func(byte, []byte) error { writes.Add(1); return nil }
	p := PrepareRegistration(context.Background(), testTarget, func(context.Context, Target) (Link, Target, error) { return f, testTarget, nil })
	waitRegistration(t, p, "waiting-token")
	if _, err := p.Register(context.Background(), func(Credential) error { return nil }); err == nil {
		t.Fatal("registered before initial token")
	}
	p.Close()
	if !linkClosed(f) || writes.Load() != 0 {
		t.Fatal("cancel did not clean up idle link")
	}
}

func TestPreparedRegistrationDisconnectAndTimeoutNeverReplay(t *testing.T) {
	for _, lost := range []bool{true, false} {
		t.Run(map[bool]string{true: "disconnect", false: "exchange-timeout"}[lost], func(t *testing.T) {
			f := newLink()
			var writes atomic.Int32
			f.write = func(byte, []byte) error { writes.Add(1); return nil }
			p := PrepareRegistration(context.Background(), testTarget, func(context.Context, Target) (Link, Target, error) { return f, testTarget, nil })
			defer p.Close()
			waitRegistration(t, p, "ready")
			if lost {
				f.done <- errors.New("lost link")
				waitRegistration(t, p, "disconnected")
			}
			ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)
			defer cancel()
			_, err := p.Register(ctx, func(Credential) error { t.Fatal("persisted without response"); return nil })
			if err == nil {
				t.Fatal("accepted incomplete registration")
			}
			<-p.done
			want := int32(1)
			if lost {
				want = 0
			}
			if writes.Load() != want || !linkClosed(f) {
				t.Fatal("replayed command or leaked link")
			}
			if _, err = p.Register(context.Background(), func(Credential) error { return nil }); err == nil {
				t.Fatal("reused failed registration")
			}
			if writes.Load() != want {
				t.Fatal("replayed failed registration")
			}
		})
	}
}

func TestPreparedRegistrationCancelsInFlightSetup(t *testing.T) {
	started := make(chan struct{})
	p := PrepareRegistration(context.Background(), testTarget, func(ctx context.Context, _ Target) (Link, Target, error) {
		close(started)
		<-ctx.Done()
		return nil, Target{}, ctx.Err()
	})
	<-started
	p.Close()
	if p.State().Connected {
		t.Fatal("connected after setup cancellation")
	}
}