Skip to content

Commit

Permalink
Merge pull request #138 from keepkey/bridge-client
Browse files Browse the repository at this point in the history
Bridge client
  • Loading branch information
markrypto authored Sep 30, 2021
2 parents b697b08 + 749a0c7 commit e0962b1
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 2 deletions.
45 changes: 44 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ How to install (Debian-Ubuntu)
* cd python-keepkey
* python setup.py install (or develop)


Running Tests
-------------

Expand All @@ -118,3 +117,47 @@ Release Process
* sudo python3 setup.py sdist bdist_wheel bdist_egg
* Upload the release
* sudo python3 -m twine upload dist/* -s --sign-with gpg2

KeepKey Bridge
==============
The KeepKey Bridge is a standalone TCP-to-webusb bridge the KeepKey. It runs a python-keepkey client
based process that allows a localhost-based process to communicate with the KeepKey wallet, thus
bypassing the need for a webusb connection from a browser based platform.

The KeepKey Bridge is recommended only for advanced users who have problems connecting the KeepKey
on Windows.

Running the KeepKey Bridge
--------------------------
Download the KeepKey Bridge installer ``kkbsetup.exe`` for Windows in the release package here:

https://github.com/keepkey/python-keepkey/releases

When running the KeepKey Bridge, a blank cmd window with the title "KepKey Bridge" will be visible.
To stop the bridge, simply close the cmd window.

Build for Windows
-----------------
Requirements:

- Windows 10
- python3
- waitress (python package)
- py2exe
- Inno Setup Compiler (optional, for creating Windows install exe)

From a command prompt terminal window, run
``python wbsetup.py py2exe -d windows/dist``

This will create a ``windows\dist`` folder with the Windows stand-alone executable file ``wait-serv.exe``

Inno Setup Compiler
-------------------
This tool builds and packages the executable for install on Windows. Build with the provided installer
script (modify version, etc., as needed)

``windows/KeepKeyBridge.iss``

This will produce an executable install app

``windows/Output/kkbsetup.exe``
9 changes: 9 additions & 0 deletions keepkeylib/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,15 @@ def call_raw(self, msg):
self.transport.write(msg)
return self.transport.read_blocking()

@session
def call_bridge(self, msg):
self.transport.bridgeWrite(msg)
return

@session
def call_bridge_read(self):
return self.transport.bridge_read_blocking()

@session
def call(self, msg):
resp = self.call_raw(msg)
Expand Down
24 changes: 24 additions & 0 deletions keepkeylib/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@ def _close(self):
def _write(self, msg, protobuf_msg):
raise NotImplementedException("Not implemented")

def _bridgeWrite(self, msg, protobuf_msg):
raise NotImplementedException("Not implemented")

def _read(self):
raise NotImplementedException("Not implemented")

def _bridgeRead(self):
raise NotImplementedException("Not implemented")

def _session_begin(self):
pass

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

def bridgeWrite(self, msg):
"""
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.
"""
self._bridgeWrite(msg)

def read(self):
"""
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.
Expand All @@ -93,6 +105,18 @@ def read_blocking(self):

return self._parse_message(data)

def bridge_read_blocking(self):
"""
blocks until data is available to be read.
"""
while True:
data = self._bridgeRead()
if data != None:
break

return data


def _parse_message(self, data):
(msg_type, data) = data
if msg_type == 'protobuf':
Expand Down
23 changes: 22 additions & 1 deletion keepkeylib/transport_webusb.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,17 +108,24 @@ def enumerate(cls):
return devices

def _write(self, msg, protobuf_msg):

msg = bytearray(msg)
while len(msg):
# add reportID and padd with zeroes if necessary
self.handle.interruptWrite(self.endpoint, [63, ] + list(msg[:63]) + [0] * (63 - len(msg[:63])))
msg = msg[63:]

def bridgeWrite(self, msg):
while len(msg):
self.handle.interruptWrite(self.endpoint, list(msg[:64]) + [0] * (64 - len(msg[:64])))
msg = msg[64:]

def _read(self):
(msg_type, datalen) = self._read_headers(FakeRead(self._raw_read))
return (msg_type, self._raw_read(datalen))

def _bridgeRead(self):
return (self._raw_bridgeRead())

def _raw_read(self, length):
start = time.time()
endpoint = 0x80 | self.endpoint
Expand All @@ -139,3 +146,17 @@ def _raw_read(self, length):
self.buffer = self.buffer[length:]
return bytes(ret)

def _raw_bridgeRead(self):
start = time.time()
endpoint = 0x80 | self.endpoint
while True:
data = self.handle.interruptRead(endpoint, 64)
if data:
break
else:
time.sleep(0.001)

if len(data) != 64:
raise TransportException("Unexpected chunk size: %d" % len(chunk))
return data

129 changes: 129 additions & 0 deletions kkbridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env python
from __future__ import print_function

from os import close, error
from flask import Flask, Response, request, jsonify
from flask_cors import CORS, cross_origin

import sys
sys.path = ['../',] + sys.path

from keepkeylib import client
from keepkeylib.client import KeepKeyClient
from keepkeylib.transport_webusb import WebUsbTransport
from keepkeylib import messages_pb2 as messages

import json
import binascii


PACKET_SIZE = 64

kkClient = None

def create_app():
app = Flask(__name__)
CORS(app)
app.config['CORS_HEADERS'] = 'Content-Type'

def initDevice():
global kkClient
if (kkClient != None):
kkClient.close()
kkClient = None

# List all connected KeepKeys on USB
devices = WebUsbTransport.enumerate()

# Check whether we found any
if len(devices) == 0:
return None

# Use first connected device
transport = WebUsbTransport(devices[0])

# Creates object for manipulating KeepKey
client = KeepKeyClient(transport)

return client

@app.route('/init')
def initKK():
global kkClient

kkClient = initDevice()

if (kkClient == None):
data = "No KeepKey found"
return Response(str(data), status=400, mimetype='application/json')
else:
data = kkClient.features
return Response(str(data), status=200, mimetype='application/json')

@app.route('/ping')
def pingKK():
global kkClient

if (kkClient == None):
kkClient = initDevice()
else:
pass

if (kkClient == None):
data = "No KeepKey found"
return Response(str(data), status=404, mimetype='application/json')

try:
ping = kkClient.call(messages.Ping(message='Duck, a bridge!', button_protection = True))
except:
kkClient.close()
kkClient = None
data = "No KeepKey found"
return Response(str(data), status=404, mimetype='application/json')

return Response(str(ping), status=200, mimetype='application/json')

@app.route('/exchange/<string:kind>', methods=['GET', 'POST'])
@cross_origin()
def rest_api(kind):
global kkClient

if (kkClient == None):
kkClient = initDevice()
else:
pass

if (kkClient == None):
data = "No KeepKey found"
return Response(str(data), status=404, mimetype='application/json')

if request.method == 'POST':
content = request.get_json(silent=True)
msg = bytearray.fromhex(content["data"])
try:
kkClient.call_bridge(msg)
except:
kkClient.close()
kkClient = None
kkClient = initDevice()
return Response('{}', status=404, mimetype='application/json')
return Response('{}', status=200, mimetype='application/json')

if request.method == 'GET':
data = kkClient.call_bridge_read()
body = '{"data":"' + binascii.hexlify(data).decode("utf-8") + '"}'
return Response(body, status=200, mimetype='application/json')

return Response('{}', status=404, mimetype='application/json')

return app

if __name__ == '__main__':

app = create_app()
app.run(port='1646')
#app.run()




3 changes: 3 additions & 0 deletions wait-serv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from waitress import serve
import kkbridge
serve(kkbridge.create_app(), host='127.0.0.1', port=1646)
8 changes: 8 additions & 0 deletions wbsetup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from distutils.core import setup
import py2exe

setup(console=['wait-serv.py'])
options = {
"py2exe": {
"dist_dir": "./windows/dist"
}}
58 changes: 58 additions & 0 deletions windows/KeepKeyBridge.iss
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!

#define MyAppName "Keepkey Bridge"
#define MyAppVersion "1.5"
#define MyAppPublisher "Shapeshift"
#define MyAppURL "shapeshift.com"
#define MyAppExeName "wait-serv.exe"
#define MyAppAssocName MyAppName + " File"
#define MyAppAssocExt ".myp"
#define MyAppAssocKey StringChange(MyAppAssocName, " ", "") + MyAppAssocExt

[Setup]
; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
AppId={{6239ED12-BE1C-4AB6-AA1A-12300A3AE957}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
;AppVerName={#MyAppName} {#MyAppVersion}
AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName={autopf}\{#MyAppName}
ChangesAssociations=yes
DisableProgramGroupPage=yes
; Uncomment the following line to run in non administrative install mode (install for current user only.)
;PrivilegesRequired=lowest
OutputBaseFilename=kkbsetup
Compression=lzma
SolidCompression=yes
WizardStyle=modern

[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked

[Files]
Source: "Z:\windows\dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "Z:\windows\dist\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files

[Registry]
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocExt}\OpenWithProgids"; ValueType: string; ValueName: "{#MyAppAssocKey}"; ValueData: ""; Flags: uninsdeletevalue
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}"; ValueType: string; ValueName: ""; ValueData: "{#MyAppAssocName}"; Flags: uninsdeletekey
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"
Root: HKA; Subkey: "Software\Classes\{#MyAppAssocKey}\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".myp"; ValueData: ""

[Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon

[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent

Binary file added windows/dist/libusb-1.0.dll
Binary file not shown.

0 comments on commit e0962b1

Please sign in to comment.