1// Copyright 2015 Keybase, Inc. All rights reserved. Use of
2// this source code is governed by the included BSD license.
3
4package engine
5
6import (
7	"golang.org/x/net/context"
8
9	"github.com/keybase/client/go/kex2"
10	"github.com/keybase/client/go/libkb"
11	keybase1 "github.com/keybase/client/go/protocol/keybase1"
12)
13
14// DeviceAdd is an engine.
15type DeviceAdd struct {
16	libkb.Contextified
17}
18
19// NewDeviceAdd creates a DeviceAdd engine.
20func NewDeviceAdd(g *libkb.GlobalContext) *DeviceAdd {
21	return &DeviceAdd{
22		Contextified: libkb.NewContextified(g),
23	}
24}
25
26// Name is the unique engine name.
27func (e *DeviceAdd) Name() string {
28	return "DeviceAdd"
29}
30
31// GetPrereqs returns the engine prereqs.
32func (e *DeviceAdd) Prereqs() Prereqs {
33	return Prereqs{Device: true}
34}
35
36// RequiredUIs returns the required UIs.
37func (e *DeviceAdd) RequiredUIs() []libkb.UIKind {
38	return []libkb.UIKind{libkb.ProvisionUIKind}
39}
40
41// SubConsumers returns the other UI consumers for this engine.
42func (e *DeviceAdd) SubConsumers() []libkb.UIConsumer {
43	return []libkb.UIConsumer{
44		&Kex2Provisioner{},
45	}
46}
47
48func (e *DeviceAdd) promptLoop(m libkb.MetaContext, provisioner *Kex2Provisioner, secret *libkb.Kex2Secret, provisioneeType keybase1.DeviceType) (err error) {
49	sb := secret.Secret()
50	arg := keybase1.DisplayAndPromptSecretArg{
51		Secret:          sb[:],
52		Phrase:          secret.Phrase(),
53		OtherDeviceType: provisioneeType,
54	}
55	for i := 0; i < 10; i++ {
56		receivedSecret, err := m.UIs().ProvisionUI.DisplayAndPromptSecret(m.Ctx(), arg)
57		if err != nil {
58			m.Warning("DisplayAndPromptSecret error: %s", err)
59			return err
60		}
61
62		if receivedSecret.Secret != nil && len(receivedSecret.Secret) > 0 {
63			m.Debug("received secret, adding to provisioner")
64			var ks kex2.Secret
65			copy(ks[:], receivedSecret.Secret)
66			provisioner.AddSecret(ks)
67			return nil
68		}
69
70		if len(receivedSecret.Phrase) > 0 {
71			m.Debug("received secret phrase, checking validity")
72			checker := libkb.MakeCheckKex2SecretPhrase(m.G())
73			if !checker.F(receivedSecret.Phrase) {
74				m.Debug("secret phrase failed validity check (attempt %d)", i+1)
75				arg.PreviousErr = checker.Hint
76				continue
77			}
78			uid := m.CurrentUID()
79			m.Debug("received secret phrase, adding to provisioner with uid=%s", uid)
80			ks, err := libkb.NewKex2SecretFromUIDAndPhrase(uid, receivedSecret.Phrase)
81			if err != nil {
82				m.Warning("NewKex2SecretFromPhrase error: %s", err)
83				return err
84			}
85			provisioner.AddSecret(ks.Secret())
86			return nil
87		}
88
89		if provisioneeType == keybase1.DeviceType_MOBILE {
90			// for mobile provisionee, only displaying the secret so it's
91			// ok/expected that nothing came back
92			m.Debug("device add DisplayAndPromptSecret returned empty secret, stopping retry loop")
93			return nil
94		}
95	}
96
97	return libkb.RetryExhaustedError{}
98}
99
100// Run starts the engine.
101func (e *DeviceAdd) Run(m libkb.MetaContext) (err error) {
102	defer m.Trace("DeviceAdd#Run", &err)()
103
104	m.G().LocalSigchainGuard().Set(m.Ctx(), "DeviceAdd")
105	defer m.G().LocalSigchainGuard().Clear(m.Ctx(), "DeviceAdd")
106
107	arg := keybase1.ChooseDeviceTypeArg{Kind: keybase1.ChooseType_NEW_DEVICE}
108	provisioneeType, err := m.UIs().ProvisionUI.ChooseDeviceType(context.TODO(), arg)
109	if err != nil {
110		return err
111	}
112	uid := m.CurrentUID()
113
114	// make a new secret; continue to generate legacy Kex2 secrets for now.
115	kex2SecretTyp := libkb.Kex2SecretTypeV1Desktop
116	if provisioneeType == keybase1.DeviceType_MOBILE || m.G().IsMobileAppType() {
117		kex2SecretTyp = libkb.Kex2SecretTypeV1Mobile
118	}
119	m.Debug("provisionee device type: %v; uid: %s; secret type: %d", provisioneeType, uid, kex2SecretTyp)
120	secret, err := libkb.NewKex2SecretFromTypeAndUID(kex2SecretTyp, uid)
121	if err != nil {
122		return err
123	}
124	m.Debug("secret phrase received")
125
126	// provisioner needs ppstream, and UI is confusing when it asks for
127	// it at the same time as asking for the secret, so get it first
128	// before prompting for the kex2 secret:
129	pps, err := libkb.GetPassphraseStreamStored(m)
130	if err != nil {
131		return err
132	}
133
134	// create provisioner engine
135	provisioner := NewKex2Provisioner(m.G(), secret.Secret(), pps)
136
137	var canceler func()
138	m, canceler = m.WithContextCancel()
139
140	// display secret and prompt for secret from X in a goroutine:
141	go func() {
142		err := e.promptLoop(m, provisioner, secret, provisioneeType)
143		if err != nil {
144			m.Debug("DeviceAdd prompt loop error: %s", err)
145			canceler()
146		}
147	}()
148
149	defer func() {
150		canceler()
151	}()
152
153	if err := RunEngine2(m, provisioner); err != nil {
154		if err == kex2.ErrHelloTimeout {
155			err = libkb.CanceledError{M: "Failed to provision device: are you sure you typed the secret properly?"}
156		}
157		return err
158	}
159
160	m.G().KeyfamilyChanged(m.Ctx(), m.G().Env.GetUID())
161
162	return nil
163}
164