From 7363a4dd4e850e83dfb3fbdf4ccf257edf144bdd Mon Sep 17 00:00:00 2001 From: Christopher Date: Tue, 12 Feb 2019 13:53:56 -0700 Subject: [PATCH 1/2] Add Ping and Cancel buttons to example page --- example/index.html | 8 +++++++- example/js/main.js | 26 ++++++++++++++++++++++++++ src/webUSBDevice.ts | 24 ++++++++++++++++++++---- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/example/index.html b/example/index.html index a6d3a71..737fa23 100644 --- a/example/index.html +++ b/example/index.html @@ -29,8 +29,14 @@

WebUSB

To seed the window.keepkeyManager with WebUSB devices, first pair a device, then click start.

+
+ + + + +
- + diff --git a/example/js/main.js b/example/js/main.js index 80629b8..94d5849 100644 --- a/example/js/main.js +++ b/example/js/main.js @@ -147,3 +147,29 @@ window.connectWebUSB = function () { console.error(String(e)) }) } + +window.allPings = [] + +const log = (name, p) => { + console.log('sending', name) + return window.allPings.push(p.then(res => console.log(`${name} response:`, res)) + .catch(e => console.error(`${name} error:`, e))) +} + +let pingCount = 0 + +window.pingWithButton = function () { + log(`ping ${++pingCount} with button`, manager.exec('ping', { message: 'ping' + pingCount, buttonProtection: true })) +} + +window.pingWithPIN = function () { + log(`ping ${++pingCount} with PIN`, manager.exec('ping', { message: 'ping' + pingCount, pinProtection: true })) +} + +window.ping = function () { + log(`ping ${++pingCount}`, manager.exec('ping', { message: 'ping' + pingCount })) +} + +window.cancelPending = function () { + log('cancelPending', window.keepkey.device.cancelPending()) +} diff --git a/src/webUSBDevice.ts b/src/webUSBDevice.ts index f80be1f..a251003 100644 --- a/src/webUSBDevice.ts +++ b/src/webUSBDevice.ts @@ -13,6 +13,13 @@ export interface WebUSBDeviceConfig { const SEGMENT_SIZE = 63 +const failureMessageFactory = (e: Error) => { + const msg = new Messages.Failure() + msg.setCode(Types.FailureType.FAILURE_UNEXPECTEDMESSAGE) + msg.setMessage(String(e)) + return ByteBuffer.wrap(msg.serializeBinary()) +} + export default class WebUSBDevice extends Device { private queue: PQueue public usbDevice: USBDevice @@ -43,6 +50,18 @@ export default class WebUSBDevice extends Device { } } + public async cancelPending () { + console.log('pending', this.queue.pending) + try { + // Pending promise will get a read, then a second read will be from the cancel command + const cancelMsg = new Messages.Cancel() + const buffer = this.toMessageBuffer(Messages.MessageType.MESSAGETYPE_CANCEL, cancelMsg) + await this.write(buffer) + } catch (e) { + console.error(`Unexpected: ${String(e)}`) + } + } + public async disconnect (): Promise { if (!this.usbDevice.opened) return try { @@ -66,10 +85,7 @@ export default class WebUSBDevice extends Device { await this.write(buffer) return await this.read() } catch (e) { - const msg = new Messages.Failure() - msg.setCode(Types.FailureType.FAILURE_UNEXPECTEDMESSAGE) - msg.setMessage(String(e)) - return ByteBuffer.wrap(msg.serializeBinary()) + return failureMessageFactory(e) } }) } From c8bc85a91e1ff3a7ef73cc94e700b7365b1044c6 Mon Sep 17 00:00:00 2001 From: Christopher Date: Tue, 12 Feb 2019 16:17:03 -0700 Subject: [PATCH 2/2] Emit events for all writes and reads. Some cancel cleanup. --- example/js/main.js | 20 +++++++++++++++++--- src/device.ts | 13 +++++++++++++ src/keepkey.ts | 6 +----- src/webUSBDevice.test.ts | 2 +- src/webUSBDevice.ts | 29 ++++++++++++++++------------- 5 files changed, 48 insertions(+), 22 deletions(-) diff --git a/example/js/main.js b/example/js/main.js index 94d5849..7da70ba 100644 --- a/example/js/main.js +++ b/example/js/main.js @@ -126,6 +126,9 @@ window.connectWebUSB = function () { const k = manager.get() console.log('Putting first keepkey on window as window.keepkey: ', k) window.keepkey = k + k.device.events.on('write', data => console.log('Event: write', data)) + k.device.events.on('read', data => console.log('Event: read', data)) + k.device.events.on('reading', () => console.log('Event: reading')) } return manager.exec('getFeatures') }) @@ -148,12 +151,23 @@ window.connectWebUSB = function () { }) } +// So we can see which commands aren't resolving window.allPings = [] const log = (name, p) => { console.log('sending', name) - return window.allPings.push(p.then(res => console.log(`${name} response:`, res)) - .catch(e => console.error(`${name} error:`, e))) + const details = { name, state: 'pending' } + details.p = p + .then(res => { + console.log(`${name} response:`, res) + details.state = 'resolved' + }) + .catch(e => { + console.error(`${name} error:`, e) + details.state = 'rejected' + }) + allPings.push(details) + return p } let pingCount = 0 @@ -171,5 +185,5 @@ window.ping = function () { } window.cancelPending = function () { - log('cancelPending', window.keepkey.device.cancelPending()) + log('cancelPending', window.keepkey.cancel()) } diff --git a/src/device.ts b/src/device.ts index 6384ad4..1220a14 100644 --- a/src/device.ts +++ b/src/device.ts @@ -84,6 +84,8 @@ export default abstract class Device { public abstract sendRaw (buffer: ByteBuffer): Promise + public abstract cancelPending (): Promise + protected toMessageBuffer (msgTypeEnum: number, msg: jspb.Message): ByteBuffer { const messageBuffer = msg.serializeBinary() @@ -112,4 +114,15 @@ export default abstract class Device { const reader = new jspb.BinaryReader(dataView.slice(9), 0, buff.limit - (9 + 2)) return [typeID, MessageType.deserializeBinaryFromReader(msg, reader)] } + + protected static failureMessageFactory (e?: Error | string) { + const msg = new Messages.Failure() + msg.setCode(Types.FailureType.FAILURE_UNEXPECTEDMESSAGE) + if (typeof e === 'string') { + msg.setMessage(e) + } else { + msg.setMessage(String(e)) + } + return ByteBuffer.wrap(msg.serializeBinary()) + } } diff --git a/src/keepkey.ts b/src/keepkey.ts index 040e74c..9868b51 100644 --- a/src/keepkey.ts +++ b/src/keepkey.ts @@ -138,11 +138,7 @@ export default class KeepKey { // Cancel aborts the last device action that required user interaction // It can follow a button request, passphrase request, or pin request public async cancel (): Promise { - const cancel = new Messages.Cancel() - // send - await this.device.exchange(Messages.MessageType.MESSAGETYPE_CANCEL, cancel) - - // Emit event to notify clients that an action has been cancelled + await this.device.cancelPending() this.device.events.emit('CANCEL_ACTION') } diff --git a/src/webUSBDevice.test.ts b/src/webUSBDevice.test.ts index 24358d6..29e2f19 100644 --- a/src/webUSBDevice.test.ts +++ b/src/webUSBDevice.test.ts @@ -7,7 +7,7 @@ describe('WebUSBDevice', () => { test('should return a Failure message if `read` throws an error', async () => { const config = { usbDevice: { transferIn: jest.fn().mockRejectedValueOnce(new Error('TEST')) - }, events: {} } + }, events: { emit: jest.fn() } } // @ts-ignore const device = new WebUSBDevice(config) const msg = new Messages.Cancel() diff --git a/src/webUSBDevice.ts b/src/webUSBDevice.ts index a251003..bad4165 100644 --- a/src/webUSBDevice.ts +++ b/src/webUSBDevice.ts @@ -4,7 +4,6 @@ import { default as PQueue } from 'p-queue' import Device from './device' import Messages from './kkProto/messages_pb' -import Types from './kkProto/types_pb' export interface WebUSBDeviceConfig { usbDevice: USBDevice, @@ -13,13 +12,6 @@ export interface WebUSBDeviceConfig { const SEGMENT_SIZE = 63 -const failureMessageFactory = (e: Error) => { - const msg = new Messages.Failure() - msg.setCode(Types.FailureType.FAILURE_UNEXPECTEDMESSAGE) - msg.setMessage(String(e)) - return ByteBuffer.wrap(msg.serializeBinary()) -} - export default class WebUSBDevice extends Device { private queue: PQueue public usbDevice: USBDevice @@ -53,12 +45,19 @@ export default class WebUSBDevice extends Device { public async cancelPending () { console.log('pending', this.queue.pending) try { - // Pending promise will get a read, then a second read will be from the cancel command + // If there are no pending commands, we should wait for a read back from the cancel command + // Otherwise the pending promise will read the error + if (this.queue.pending === 0) { + this.queue.add(() => this.read(), { priority: 1000 }) + .then(() => console.log('cancenPending read done')) + .catch(e => console.log('cancenPending read failed', e)) + } + const cancelMsg = new Messages.Cancel() const buffer = this.toMessageBuffer(Messages.MessageType.MESSAGETYPE_CANCEL, cancelMsg) await this.write(buffer) } catch (e) { - console.error(`Unexpected: ${String(e)}`) + console.error('Cancel Pending Error', e) } } @@ -68,7 +67,7 @@ export default class WebUSBDevice extends Device { // If the device is disconnected, this will fail and throw, which is fine. await this.usbDevice.releaseInterface(0) } catch (e) { - console.log(e) + console.log('Disconnect Error (Ignored):', e) } } @@ -85,13 +84,14 @@ export default class WebUSBDevice extends Device { await this.write(buffer) return await this.read() } catch (e) { - return failureMessageFactory(e) + return Device.failureMessageFactory(e) } }) } protected async write (buff: ByteBuffer): Promise { // break frame into segments + this.events.emit('write', buff) for (let i = 0; i < buff.limit; i += SEGMENT_SIZE) { let segment = buff.toArrayBuffer().slice(i, i + SEGMENT_SIZE) let padding = new Array(SEGMENT_SIZE - segment.byteLength + 1).join('\0') @@ -105,6 +105,7 @@ export default class WebUSBDevice extends Device { } protected async read (): Promise { + this.events.emit('reading') let first = await this.readChunk() // Check that buffer starts with: "?##" [ 0x3f, 0x23, 0x23 ] // "?" = USB marker, "##" = KeepKey magic bytes @@ -128,7 +129,9 @@ export default class WebUSBDevice extends Device { } } - return ByteBuffer.wrap(buffer) + const res = ByteBuffer.wrap(buffer) + this.events.emit('read', res) + return res } else { console.error('Invalid message', { msgLength, valid, first }) throw new Error('Invalid message')