diff --git a/lib/bledger-browser.js b/lib/bledger-browser.js index ea56062..2c5d2dd 100644 --- a/lib/bledger-browser.js +++ b/lib/bledger-browser.js @@ -9,16 +9,20 @@ 'use strict'; const LedgerError = require('./protocol/error'); +const DeviceError = require('./devices/error'); const LedgerBcoin = require('./bcoin'); const LedgerTXInput = require('./txinput'); const U2F = require('./devices/u2f'); const WebUSB = require('./devices/webusb'); +const WebAuthn = require('./device/webauthn'); exports.bledger = exports; exports.U2F = U2F; exports.WebUSB = WebUSB; +exports.WebAuthn = WebAuthn; exports.LedgerError = LedgerError; +exports.DeviceError = DeviceError; exports.LedgerBcoin = LedgerBcoin; exports.LedgerTXInput = LedgerTXInput; diff --git a/lib/devices/error.js b/lib/devices/error.js new file mode 100644 index 0000000..59e7fda --- /dev/null +++ b/lib/devices/error.js @@ -0,0 +1,34 @@ +/*! + * error.js - Device errors. + * Copyright (c) 2018-2019, The Bcoin Developers (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +/** + * Device error + * @alias module:device.DeviceError + * @extends {Error} + * @property {String} message - error message. + */ + +class DeviceError extends Error { + /** + * Create device error. + * @param {String} reason + * @param {Function} device + */ + + constructor(reason, device) { + super(); + + this.type = 'DeviceError'; + this.message = reason; + + if (Error.captureStackTrace) + Error.captureStackTrace(this, device || DeviceError); + } +} + +module.exports = DeviceError; \ No newline at end of file diff --git a/lib/devices/webauthn.js b/lib/devices/webauthn.js new file mode 100644 index 0000000..3a94f58 --- /dev/null +++ b/lib/devices/webauthn.js @@ -0,0 +1,161 @@ +/*! + * webauthn.js - Ledger WebAuthn communication + * Copyright (c) 2018-2019, The Bcoin Developers (MIT License). + * https://github.com/bcoin-org/bcoin + */ +/* eslint-env browser */ +'use strict'; + +const assert = require('assert'); + +const {Device} = require('./device'); +const DeviceError = require('./error'); +const {Lock} = require('bmutex'); + +/** + * @alias module:device.WebAuthnDevice + * @extends {device.Device} + */ + +class WebAuthnDevice extends Device { + constructor(options) { + super(options); + + this.lock = new Lock(false); + this.opened = false; + + this.type = 'webauthn'; + } + + /** + * Open device. + * @throws {DeviceError} + */ + + async open() { + this.enforce(this.opened === false, 'Device is already open.'); + await WebAuthnDevice.ensureSupport(); + + this.opened = true; + } + + /** + * Close device. + */ + + async close() { + this.enforce(this.opened === true, 'Device is already closed.'); + this.opened = false; + } + + /** + * Exchange APDU commands with device. + * Lock + * @param {Buffer} apdu + * @returns {Promise} + * @throws {LedgerError} + */ + + async exchange(apdu) { + const unlock = await this.lock.lock(); + + try { + return await this._exchange(apdu); + } finally { + unlock(); + } + } + + /** + * Exchange APDU commands with device. + * without lock. + * @param {Buffer} apdu + * @returns {Promise} + * @throws {LedgerError} + */ + + async _exchange(apdu) { + this.enforce(this.opened === true, 'Device is not open.'); + assert(Buffer.isBuffer(apdu), 'apdu is not a buffer.'); + + const requestOptions = { + publicKey: { + timeout: this.timeout, + challenge: Buffer.alloc(32), + allowCredentials: [{ + type: 'public-key', + id: wrapAPDU(apdu, this.scrambleKey) + }] + } + }; + + const credential = await navigator.credentials.get(requestOptions); + + return Buffer.from(credential.response.signature); + } + + /** + * Assert device. + * @param {Boolean} value + * @param {String?} reason + * @throws {DeviceError} + */ + + enforce(value, reason) { + if (!value) + throw new DeviceError(reason, WebAuthnDevice); + } + + /** + * Check if WebAuthn is supported. + * @returns {Boolean} + */ + + static async isSupported() { + return global.navigator && global.navigator.credentials; + } + + /** + * Ensure WebAuthn support. + * @returns {Boolean} + * @throws {DeviceError} + */ + + static async ensureSupport() { + if (!this.isSupported()) + throw new DeviceError('WebAuthn is not supported.', WebAuthnDevice); + } + + /** + * Get WebAuthn devices. + */ + + static async getDevices() { + await this.ensureSupport(); + + return [new WebAuthnDevice()]; + } + + static async requestDevice() { + return new WebAuthnDevice(); + } +} + +/** + * Wrap APDU + * @ignore + * @param {Buffer} apdu + * @param {Buffer} key + * @returns {Buffer} + */ + +function wrapAPDU(apdu, key) { + const result = Buffer.alloc(apdu.length); + + for (let i = 0; i < apdu.length; i++) + result[i] = apdu[i] ^ key[i % key.length]; + + return result; +} + +exports.Device = WebAuthnDevice; \ No newline at end of file