Skip to content

Commit e0962b1

Browse files
authored
Merge pull request #138 from keepkey/bridge-client
Bridge client
2 parents b697b08 + 749a0c7 commit e0962b1

9 files changed

Lines changed: 297 additions & 2 deletions

File tree

README.rst

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,6 @@ How to install (Debian-Ubuntu)
9898
* cd python-keepkey
9999
* python setup.py install (or develop)
100100

101-
102101
Running Tests
103102
-------------
104103

@@ -118,3 +117,47 @@ Release Process
118117
* sudo python3 setup.py sdist bdist_wheel bdist_egg
119118
* Upload the release
120119
* sudo python3 -m twine upload dist/* -s --sign-with gpg2
120+
121+
KeepKey Bridge
122+
==============
123+
The KeepKey Bridge is a standalone TCP-to-webusb bridge the KeepKey. It runs a python-keepkey client
124+
based process that allows a localhost-based process to communicate with the KeepKey wallet, thus
125+
bypassing the need for a webusb connection from a browser based platform.
126+
127+
The KeepKey Bridge is recommended only for advanced users who have problems connecting the KeepKey
128+
on Windows.
129+
130+
Running the KeepKey Bridge
131+
--------------------------
132+
Download the KeepKey Bridge installer ``kkbsetup.exe`` for Windows in the release package here:
133+
134+
https://github.com/keepkey/python-keepkey/releases
135+
136+
When running the KeepKey Bridge, a blank cmd window with the title "KepKey Bridge" will be visible.
137+
To stop the bridge, simply close the cmd window.
138+
139+
Build for Windows
140+
-----------------
141+
Requirements:
142+
143+
- Windows 10
144+
- python3
145+
- waitress (python package)
146+
- py2exe
147+
- Inno Setup Compiler (optional, for creating Windows install exe)
148+
149+
From a command prompt terminal window, run
150+
``python wbsetup.py py2exe -d windows/dist``
151+
152+
This will create a ``windows\dist`` folder with the Windows stand-alone executable file ``wait-serv.exe``
153+
154+
Inno Setup Compiler
155+
-------------------
156+
This tool builds and packages the executable for install on Windows. Build with the provided installer
157+
script (modify version, etc., as needed)
158+
159+
``windows/KeepKeyBridge.iss``
160+
161+
This will produce an executable install app
162+
163+
``windows/Output/kkbsetup.exe``

keepkeylib/client.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,15 @@ def call_raw(self, msg):
190190
self.transport.write(msg)
191191
return self.transport.read_blocking()
192192

193+
@session
194+
def call_bridge(self, msg):
195+
self.transport.bridgeWrite(msg)
196+
return
197+
198+
@session
199+
def call_bridge_read(self):
200+
return self.transport.bridge_read_blocking()
201+
193202
@session
194203
def call(self, msg):
195204
resp = self.call_raw(msg)

keepkeylib/transport.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,15 @@ def _close(self):
2222
def _write(self, msg, protobuf_msg):
2323
raise NotImplementedException("Not implemented")
2424

25+
def _bridgeWrite(self, msg, protobuf_msg):
26+
raise NotImplementedException("Not implemented")
27+
2528
def _read(self):
2629
raise NotImplementedException("Not implemented")
2730

31+
def _bridgeRead(self):
32+
raise NotImplementedException("Not implemented")
33+
2834
def _session_begin(self):
2935
pass
3036

@@ -68,6 +74,12 @@ def write(self, msg):
6874
header = struct.pack(">HL", mapping.get_type(msg), len(ser))
6975
self._write(b"##" + header + ser, msg)
7076

77+
def bridgeWrite(self, msg):
78+
"""
79+
Write message to transport. msg should be a member of a valid `protobuf class <https://developers.google.com/protocol-buffers/docs/pythontutorial>`_ with a SerializeToString() method.
80+
"""
81+
self._bridgeWrite(msg)
82+
7183
def read(self):
7284
"""
7385
If there is data available to be read from the transport, reads the data and tries to parse it as a protobuf message. If the parsing succeeds, return a protobuf object.
@@ -93,6 +105,18 @@ def read_blocking(self):
93105

94106
return self._parse_message(data)
95107

108+
def bridge_read_blocking(self):
109+
"""
110+
blocks until data is available to be read.
111+
"""
112+
while True:
113+
data = self._bridgeRead()
114+
if data != None:
115+
break
116+
117+
return data
118+
119+
96120
def _parse_message(self, data):
97121
(msg_type, data) = data
98122
if msg_type == 'protobuf':

keepkeylib/transport_webusb.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,24 @@ def enumerate(cls):
108108
return devices
109109

110110
def _write(self, msg, protobuf_msg):
111-
112111
msg = bytearray(msg)
113112
while len(msg):
114113
# add reportID and padd with zeroes if necessary
115114
self.handle.interruptWrite(self.endpoint, [63, ] + list(msg[:63]) + [0] * (63 - len(msg[:63])))
116115
msg = msg[63:]
117116

117+
def bridgeWrite(self, msg):
118+
while len(msg):
119+
self.handle.interruptWrite(self.endpoint, list(msg[:64]) + [0] * (64 - len(msg[:64])))
120+
msg = msg[64:]
121+
118122
def _read(self):
119123
(msg_type, datalen) = self._read_headers(FakeRead(self._raw_read))
120124
return (msg_type, self._raw_read(datalen))
121125

126+
def _bridgeRead(self):
127+
return (self._raw_bridgeRead())
128+
122129
def _raw_read(self, length):
123130
start = time.time()
124131
endpoint = 0x80 | self.endpoint
@@ -139,3 +146,17 @@ def _raw_read(self, length):
139146
self.buffer = self.buffer[length:]
140147
return bytes(ret)
141148

149+
def _raw_bridgeRead(self):
150+
start = time.time()
151+
endpoint = 0x80 | self.endpoint
152+
while True:
153+
data = self.handle.interruptRead(endpoint, 64)
154+
if data:
155+
break
156+
else:
157+
time.sleep(0.001)
158+
159+
if len(data) != 64:
160+
raise TransportException("Unexpected chunk size: %d" % len(chunk))
161+
return data
162+

kkbridge.py

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#!/usr/bin/env python
2+
from __future__ import print_function
3+
4+
from os import close, error
5+
from flask import Flask, Response, request, jsonify
6+
from flask_cors import CORS, cross_origin
7+
8+
import sys
9+
sys.path = ['../',] + sys.path
10+
11+
from keepkeylib import client
12+
from keepkeylib.client import KeepKeyClient
13+
from keepkeylib.transport_webusb import WebUsbTransport
14+
from keepkeylib import messages_pb2 as messages
15+
16+
import json
17+
import binascii
18+
19+
20+
PACKET_SIZE = 64
21+
22+
kkClient = None
23+
24+
def create_app():
25+
app = Flask(__name__)
26+
CORS(app)
27+
app.config['CORS_HEADERS'] = 'Content-Type'
28+
29+
def initDevice():
30+
global kkClient
31+
if (kkClient != None):
32+
kkClient.close()
33+
kkClient = None
34+
35+
# List all connected KeepKeys on USB
36+
devices = WebUsbTransport.enumerate()
37+
38+
# Check whether we found any
39+
if len(devices) == 0:
40+
return None
41+
42+
# Use first connected device
43+
transport = WebUsbTransport(devices[0])
44+
45+
# Creates object for manipulating KeepKey
46+
client = KeepKeyClient(transport)
47+
48+
return client
49+
50+
@app.route('/init')
51+
def initKK():
52+
global kkClient
53+
54+
kkClient = initDevice()
55+
56+
if (kkClient == None):
57+
data = "No KeepKey found"
58+
return Response(str(data), status=400, mimetype='application/json')
59+
else:
60+
data = kkClient.features
61+
return Response(str(data), status=200, mimetype='application/json')
62+
63+
@app.route('/ping')
64+
def pingKK():
65+
global kkClient
66+
67+
if (kkClient == None):
68+
kkClient = initDevice()
69+
else:
70+
pass
71+
72+
if (kkClient == None):
73+
data = "No KeepKey found"
74+
return Response(str(data), status=404, mimetype='application/json')
75+
76+
try:
77+
ping = kkClient.call(messages.Ping(message='Duck, a bridge!', button_protection = True))
78+
except:
79+
kkClient.close()
80+
kkClient = None
81+
data = "No KeepKey found"
82+
return Response(str(data), status=404, mimetype='application/json')
83+
84+
return Response(str(ping), status=200, mimetype='application/json')
85+
86+
@app.route('/exchange/<string:kind>', methods=['GET', 'POST'])
87+
@cross_origin()
88+
def rest_api(kind):
89+
global kkClient
90+
91+
if (kkClient == None):
92+
kkClient = initDevice()
93+
else:
94+
pass
95+
96+
if (kkClient == None):
97+
data = "No KeepKey found"
98+
return Response(str(data), status=404, mimetype='application/json')
99+
100+
if request.method == 'POST':
101+
content = request.get_json(silent=True)
102+
msg = bytearray.fromhex(content["data"])
103+
try:
104+
kkClient.call_bridge(msg)
105+
except:
106+
kkClient.close()
107+
kkClient = None
108+
kkClient = initDevice()
109+
return Response('{}', status=404, mimetype='application/json')
110+
return Response('{}', status=200, mimetype='application/json')
111+
112+
if request.method == 'GET':
113+
data = kkClient.call_bridge_read()
114+
body = '{"data":"' + binascii.hexlify(data).decode("utf-8") + '"}'
115+
return Response(body, status=200, mimetype='application/json')
116+
117+
return Response('{}', status=404, mimetype='application/json')
118+
119+
return app
120+
121+
if __name__ == '__main__':
122+
123+
app = create_app()
124+
app.run(port='1646')
125+
#app.run()
126+
127+
128+
129+

wait-serv.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from waitress import serve
2+
import kkbridge
3+
serve(kkbridge.create_app(), host='127.0.0.1', port=1646)

wbsetup.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from distutils.core import setup
2+
import py2exe
3+
4+
setup(console=['wait-serv.py'])
5+
options = {
6+
"py2exe": {
7+
"dist_dir": "./windows/dist"
8+
}}

windows/KeepKeyBridge.iss

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
; Script generated by the Inno Setup Script Wizard.
2+
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
3+
4+
#define MyAppName "Keepkey Bridge"
5+
#define MyAppVersion "1.5"
6+
#define MyAppPublisher "Shapeshift"
7+
#define MyAppURL "shapeshift.com"
8+
#define MyAppExeName "wait-serv.exe"
9+
#define MyAppAssocName MyAppName + " File"
10+
#define MyAppAssocExt ".myp"
11+
#define MyAppAssocKey StringChange(MyAppAssocName, " ", "") + MyAppAssocExt
12+
13+
[Setup]
14+
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
15+
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
16+
AppId={{6239ED12-BE1C-4AB6-AA1A-12300A3AE957}
17+
AppName={#MyAppName}
18+
AppVersion={#MyAppVersion}
19+
;AppVerName={#MyAppName} {#MyAppVersion}
20+
AppPublisher={#MyAppPublisher}
21+
AppPublisherURL={#MyAppURL}
22+
AppSupportURL={#MyAppURL}
23+
AppUpdatesURL={#MyAppURL}
24+
DefaultDirName={autopf}\{#MyAppName}
25+
ChangesAssociations=yes
26+
DisableProgramGroupPage=yes
27+
; Uncomment the following line to run in non administrative install mode (install for current user only.)
28+
;PrivilegesRequired=lowest
29+
OutputBaseFilename=kkbsetup
30+
Compression=lzma
31+
SolidCompression=yes
32+
WizardStyle=modern
33+
34+
[Languages]
35+
Name: "english"; MessagesFile: "compiler:Default.isl"
36+
37+
[Tasks]
38+
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
39+
40+
[Files]
41+
Source: "Z:\windows\dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
42+
Source: "Z:\windows\dist\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
43+
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
44+
45+
[Registry]
46+
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue
47+
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppAssocName}"; Flags: uninsdeletekey
48+
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"
49+
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""
50+
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".myp"; ValueData: ""
51+
52+
[Icons]
53+
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
54+
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
55+
56+
[Run]
57+
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
58+

windows/dist/libusb-1.0.dll

278 KB
Binary file not shown.

0 commit comments

Comments
 (0)