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..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') }) @@ -147,3 +150,40 @@ window.connectWebUSB = function () { console.error(String(e)) }) } + +// So we can see which commands aren't resolving +window.allPings = [] + +const log = (name, p) => { + console.log('sending', name) + 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 + +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.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 f80be1f..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, @@ -43,13 +42,32 @@ export default class WebUSBDevice extends Device { } } + public async cancelPending () { + console.log('pending', this.queue.pending) + try { + // 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('Cancel Pending Error', e) + } + } + public async disconnect (): Promise { if (!this.usbDevice.opened) return try { // 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) } } @@ -66,16 +84,14 @@ 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 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') @@ -89,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 @@ -112,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')