-
Notifications
You must be signed in to change notification settings - Fork 82
/
channel.go
305 lines (269 loc) · 11.3 KB
/
channel.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
// Copyright 2018 Shift Devices AG
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package relay
import (
"encoding/json"
"time"
"github.com/btcsuite/btcutil/base58"
"github.com/digitalbitbox/bitbox-wallet-app/util/config"
"github.com/digitalbitbox/bitbox-wallet-app/util/errp"
"github.com/digitalbitbox/bitbox-wallet-app/util/locker"
"github.com/digitalbitbox/bitbox-wallet-app/util/logging"
"github.com/digitalbitbox/bitbox-wallet-app/util/random"
"github.com/digitalbitbox/bitbox-wallet-app/util/socksproxy"
"github.com/sirupsen/logrus"
)
const (
// configFileName stores the name of the config file that contains the pairing information.
configFileName = "channel.json"
)
// Channel implements an encrypted communication channel between the desktop and the paired mobile.
type Channel struct {
// ChannelID is the identifier which uniquely identifies the channel between the parties.
ChannelID string `json:"id"`
// EncryptionKey is used to encrypt the communication between the desktop and the mobile.
EncryptionKey []byte `json:"key"`
// AuthenticationKey is used to authenticate messages between the desktop and the mobile.
AuthenticationKey []byte `json:"mac"`
// messageBuffer buffers the messages that were not expected by the caller of waitForValue.
messageBuffer [][]byte
// messageBufferLock guards the message buffer.
messageBufferLock locker.Locker
// socksProxy proxies the http requests
socksProxy socksproxy.SocksProxy
log *logrus.Entry
}
// NewChannel returns a new channel with the given channel ID, encryption and authentication key.
func NewChannel(channelID string, encryptionKey []byte, authenticationKey []byte, socksProxy socksproxy.SocksProxy) *Channel {
return &Channel{
ChannelID: channelID,
EncryptionKey: encryptionKey,
AuthenticationKey: authenticationKey,
log: logging.Get().WithGroup("channel"),
socksProxy: socksProxy,
}
}
// NewChannelWithRandomKey returns a new channel with a random encryption key and identifier.
func NewChannelWithRandomKey(socksProxy socksproxy.SocksProxy) *Channel {
channelID := random.BytesOrPanic(32)
encryptionKey := random.BytesOrPanic(32)
authenticationKey := random.BytesOrPanic(32)
// The channel identifier may not contain '=' and thus it cannot be encoded with base64.
return NewChannel(base58.Encode(channelID), encryptionKey, authenticationKey, socksProxy)
}
// NewChannelFromConfigFile returns a new channel with the channel identifier and encryption key
// from the config file or nil if the config file does not exist.
func NewChannelFromConfigFile(configDir string, socksProxy socksproxy.SocksProxy) *Channel {
configFile := config.NewFile(configDir, configFileName)
if configFile.Exists() {
var configuration configuration
if err := configFile.ReadJSON(&configuration); err != nil {
return nil
}
channel := configuration.channel()
channel.socksProxy = socksProxy
return channel
}
return nil
}
// StoreToConfigFile stores the channel to the config file located in the provided configDir.
// Callers can use config.AppDir to obtain standard user location config dir.
func (channel *Channel) StoreToConfigFile(configDir string) error {
configuration := newConfiguration(channel)
configFile := config.NewFile(configDir, configFileName)
return configFile.WriteJSON(configuration)
}
// RemoveConfigFile removes the config file.
// Callers can use config.AppDir to obtain standard user location config dir.
func (channel *Channel) RemoveConfigFile(configDir string) error {
return config.NewFile(configDir, configFileName).Remove()
}
// relayServer returns the configured relay server.
// The server is hardcoded for now but can be loaded from the settings in the future.
func relayServer() Server {
return DefaultServer
}
// getValueFromMessage returns the value of the field in the message and true, if found, or
// an empty string and false, if not found.
func (channel *Channel) getValueFromMessage(message []byte, name string) (string, bool) {
var object map[string]string
err := json.Unmarshal(message, &object)
var value, present = object[name]
if err != nil || !present {
return "", false
}
return value, true
}
// waitForValue waits for the given duration for the value with the given name from the mobile.
// Returns an error if no value with the given name has been received in the given duration.
func (channel *Channel) waitForValue(duration time.Duration, name string) (string, error) {
deadline := time.Now().Add(duration)
for {
unlock := channel.messageBufferLock.Lock()
var message []byte
var value string
var found bool
indexIntoBuffer := -1
for i, bufferedMsg := range channel.messageBuffer {
value, found = channel.getValueFromMessage(bufferedMsg, name)
indexIntoBuffer = i
if found {
channel.log.Debugf("Processed buffered message %s and found value: %s", string(message), value)
break
}
}
if found {
if indexIntoBuffer < len(channel.messageBuffer) {
channel.messageBuffer = append(channel.messageBuffer[:indexIntoBuffer], channel.messageBuffer[indexIntoBuffer+1:]...)
} else {
channel.messageBuffer = channel.messageBuffer[:indexIntoBuffer]
}
channel.log.Debugf("Removing message %s from buffer: %v", string(message), len(channel.messageBuffer))
unlock()
} else {
unlock()
message, err := PullOldestMessage(relayServer(), channel)
if err != nil {
return "", err
}
if message == nil {
if time.Now().Before(deadline) {
continue
}
return "", errp.New("Did not receive a response from the mobile in the given duration.")
}
value, found = channel.getValueFromMessage(message, name)
if !found {
unlock := channel.messageBufferLock.Lock()
channel.messageBuffer = append(channel.messageBuffer, message)
channel.log.Debugf("Added message %s to buffer: %v", string(message), len(channel.messageBuffer))
unlock()
continue
}
}
return value, nil
}
}
// WaitForScanningSuccess waits for the given duration for the scanning success from the mobile.
// Returns nil if the scanning success was retrieved from the relay server and an error otherwise.
func (channel *Channel) WaitForScanningSuccess(duration time.Duration) error {
value, err := channel.waitForValue(duration, "id")
if err != nil {
return err
} else if value != "success" {
return errp.New("Scanning unsuccessful")
}
return nil
}
// WaitForMobilePublicKeyHash waits for the given duration for the public key hash from the mobile.
// Returns an error if no public key hash has been received from the server in the given duration.
func (channel *Channel) WaitForMobilePublicKeyHash(duration time.Duration) (string, error) {
return channel.waitForValue(duration, "hash_ecdh_pubkey")
}
// WaitForMobilePublicKey waits for the given duration for the ECDH public key from the mobile.
// Returns an error if no ECDH public key has been received from the server in the given duration.
func (channel *Channel) WaitForMobilePublicKey(duration time.Duration) (string, error) {
return channel.waitForValue(duration, "ecdh_pubkey")
}
// WaitForCommand waits for the given duration for an ECDH command from mobile.
// Returns the command or an error if no command has been received in the given duration.
func (channel *Channel) WaitForCommand(duration time.Duration) (string, error) {
channel.log.Debug("WaitForCommand")
return channel.waitForValue(duration, "ecdh")
}
// SendHashPubKey sends the hash of the public key from the BitBox to the mobile to finish pairing.
func (channel *Channel) SendHashPubKey(verifyPass interface{}) error {
return PushMessage(relayServer(), channel, map[string]interface{}{
"ecdh": verifyPass,
})
}
// SendPubKey sends the ECDH public key from the BitBox to the paired mobile to finish pairing.
func (channel *Channel) SendPubKey(verifyPass interface{}) error {
return PushMessage(relayServer(), channel, map[string]interface{}{
"ecdh": verifyPass,
})
}
// SendPairingTest sends the encrypted test string from the BitBox to the paired mobile.
func (channel *Channel) SendPairingTest(tfaTestString string) error {
return PushMessage(relayServer(), channel, map[string]string{
"tfa": tfaTestString,
})
}
// action describes the JSON object for actions like ping, pong and clear.
type action struct {
Action string `json:"action"`
}
// SendPing sends a 'ping' to the paired mobile to which it automatically responds with 'pong'.
func (channel *Channel) SendPing() error {
return PushMessage(relayServer(), channel, &action{"ping"})
}
// WaitForPong waits for the given duration for the 'pong' from the mobile after sending 'ping'.
// Returns nil if the pong was retrieved from the relay server and an error otherwise.
func (channel *Channel) WaitForPong(duration time.Duration) error {
value, err := channel.waitForValue(duration, "action")
if err != nil {
return err
} else if value != "pong" {
return errp.New("Unexpected response for ping")
}
return nil
}
// SendClear clears the screen of the paired mobile.
func (channel *Channel) SendClear() error {
return PushMessage(relayServer(), channel, &action{"clear"})
}
// SendXpubEcho sends the encrypted xpub echo from the BitBox to the paired mobile.
func (channel *Channel) SendXpubEcho(xpubEcho string, typ string) error {
return PushMessage(relayServer(), channel, map[string]string{
"echo": xpubEcho,
"type": typ,
})
}
// SendSigningEcho sends the encrypted signing echo from the BitBox to the paired mobile.
func (channel *Channel) SendSigningEcho(
signingEcho string,
coin string,
scriptType string,
transaction string,
) error {
return PushMessage(relayServer(), channel, map[string]string{
"echo": signingEcho,
"coin": coin,
"inputAndChangeType": scriptType,
"tx": transaction,
})
}
// WaitForSigningPin waits for the given duration for the 2FA signing PIN from the mobile.
// Returns an error if no 2FA signing PIN was available on the relay server in the given duration.
// Otherwise, the returned value is either the PIN (on confirmation) or "abort" (on cancel).
func (channel *Channel) WaitForSigningPin(duration time.Duration) (string, error) {
return channel.waitForValue(duration, "pin")
}
// SendRandomNumberEcho sends the encrypted random number echo from the BitBox to the paired mobile.
func (channel *Channel) SendRandomNumberEcho(randomNumberEcho string) error {
return PushMessage(relayServer(), channel, map[string]string{
"echo": randomNumberEcho,
})
}
// WaitForRandomNumberClear waits for the given duration for a random number clear from the mobile.
// Returns nil if a random number clear was retrieved from the relay server and an error otherwise.
func (channel *Channel) WaitForRandomNumberClear(duration time.Duration) error {
value, err := channel.waitForValue(duration, "random")
if err != nil {
return err
} else if value != "clear" {
return errp.New("Unexpected response for random")
}
return nil
}