diff --git a/packages/transport/Makefile b/packages/transport/Makefile index ba63527b0a..a5110b3cc1 100644 --- a/packages/transport/Makefile +++ b/packages/transport/Makefile @@ -7,5 +7,7 @@ node_modules: build: rm -rf dist - mkdir -p dist + cp -r src/ dist + find dist/ -type f ! -name '*.js' | xargs -I {} rm {} + find dist/ -name '*.js' | xargs -I {} mv {} {}.flow `npm bin`/browserify src/index.js > dist/index.js diff --git a/packages/transport/dist/defered.js.flow b/packages/transport/dist/defered.js.flow new file mode 100644 index 0000000000..7bd35cbd06 --- /dev/null +++ b/packages/transport/dist/defered.js.flow @@ -0,0 +1,23 @@ +/* @flow */ + +"use strict"; + +export type Defered = { + promise: Promise; + resolve: () => void; + reject: (e: Error) => void; +}; + +export function create(): Defered { + let _resolve: () => void = () => {}; + let _reject: (e: Error) => void = (e) => {}; + const promise = new Promise((resolve, reject) => { + _resolve = resolve; + _reject = reject; + }); + return { + promise, + resolve: _resolve, + reject: _reject, + }; +} diff --git a/packages/transport/dist/handler/index.js.flow b/packages/transport/dist/handler/index.js.flow new file mode 100644 index 0000000000..95502e614b --- /dev/null +++ b/packages/transport/dist/handler/index.js.flow @@ -0,0 +1,217 @@ +/* @flow */ + +"use strict"; + +import type {TrezorDeviceInfo, Transport} from '../transport'; +import {create as createDefered} from '../defered'; +import {parseConfigure} from '../protobuf/parse_protocol'; +import {verifyHexBin} from './verify'; +import {buildAndSend} from './send'; +import {receiveAndParse} from './receive'; + +import type {Defered} from '../defered'; +import type {Messages} from '../protobuf/messages'; + +export type MessageFromTrezor = {type: string, message: Object}; + +// eslint-disable-next-line quotes +const stringify = require('json-stable-stringify'); + +type TrezorDeviceInfoWithSession = TrezorDeviceInfo & { + session: ?string; +} + +type InternalAcquireInput = { + path: string; + previous: ?string; + checkPrevious: boolean; +} + +type AcquireInput = { + path: string; + previous: ?string; +} | string; + +function parseAcquireInput(input: AcquireInput): InternalAcquireInput { + // eslint-disable-next-line quotes + if (typeof input !== 'string') { + const path = input.path.toString(); + const previous = input.previous == null ? null : input.previous.toString(); + return { + path, + previous, + checkPrevious: true, + }; + } else { + const path = input.toString(); + return { + path, + previous: null, + checkPrevious: false, + }; + } +} + +function compare(a: TrezorDeviceInfoWithSession, b: TrezorDeviceInfoWithSession): number { + if (!isNaN(a.path)) { + return parseInt(a.path) - parseInt(b.path); + } else { + return a.path < a.path ? -1 : (a.path > a.path ? 1 : 0); + } +} + +function timeoutPromise(delay: number): Promise { + return new Promise((resolve) => { + window.setTimeout(() => resolve(), delay); + }); +} + +const ITER_MAX = 60; +const ITER_DELAY = 500; + +export class Handler { + transport: Transport; + _lock: Promise = Promise.resolve(); + + // path => promise rejecting on release + deferedOnRelease: {[path: string]: Defered} = {}; + + // path => session + connections: {[path: string]: string} = {}; + + // session => path + reverse: {[session: string]: string} = {}; + + _messages: ?Messages; + + constructor(transport: Transport) { + this.transport = transport; + } + + lock(fn: () => (X|Promise)): Promise { + const res = this._lock.then(() => fn()); + this._lock = res.catch(() => {}); + return res; + } + + enumerate(): Promise> { + return this.lock((): Promise> => { + return this.transport.enumerate().then((devices) => devices.map(device => { + return { + ...device, + session: this.connections[device.path], + }; + })).then(devices => { + this._releaseDisconnected(devices); + return devices; + }).then(devices => { + return devices.sort(compare); + }); + }); + } + + _releaseDisconnected(devices: Array) { + } + + _lastStringified: string = ``; + + listen(old: ?Array): Promise> { + const oldStringified = stringify(old); + const last = old == null ? this._lastStringified : oldStringified; + return this._runIter(0, last); + } + + _runIter(iteration: number, oldStringified: string): Promise> { + return this.enumerate().then(devices => { + const stringified = stringify(devices); + if ((stringified !== oldStringified) || (iteration === ITER_MAX)) { + this._lastStringified = stringified; + return devices; + } + return timeoutPromise(ITER_DELAY).then(() => this._runIter(iteration + 1, stringified)); + }); + } + + _checkAndReleaseBeforeAcquire(parsed: InternalAcquireInput): Promise { + const realPrevious = this.connections[parsed.path]; + if (parsed.checkPrevious) { + let error = false; + if (realPrevious == null) { + error = (parsed.previous != null); + } else { + error = (parsed.previous !== realPrevious); + } + if (error) { + throw new Error(`wrong previous session`); + } + } + if (realPrevious != null) { + const releasePromise: Promise = this._realRelease(parsed.path, realPrevious); + return releasePromise; + } else { + return Promise.resolve(); + } + } + + acquire(input: AcquireInput): Promise { + const parsed = parseAcquireInput(input); + return this.lock((): Promise => { + return this._checkAndReleaseBeforeAcquire(parsed).then(() => + this.transport.connect(parsed.path) + ).then((session: string) => { + this.connections[parsed.path] = session; + this.reverse[session] = parsed.path; + this.deferedOnRelease[parsed.path] = createDefered(); + return session; + }); + }); + } + + release(session: string): Promise { + const path = this.reverse[session]; + return this.lock(() => this._realRelease(path, session)); + } + + _realRelease(path:string, session: string): Promise { + return this.transport.disconnect(path, session).then(() => { + this._releaseCleanup(session); + }); + } + + _releaseCleanup(session: string) { + const path: string = this.reverse[session]; + delete this.reverse[session]; + delete this.connections[path]; + this.deferedOnRelease[path].reject(new Error(`Device released or disconnected`)); + return; + } + + configure(signedData: string): Promise { + return verifyHexBin(signedData).then((data: Buffer): Messages => { + return parseConfigure(data); + }).then((messages: Messages) => { + this._messages = messages; + return; + }); + } + + _sendTransport(session: string): (data: ArrayBuffer) => Promise { + const path: string = this.reverse[session]; + return (data) => this.transport.send(path, session, data); + } + + _receiveTransport(session: string): () => Promise { + const path: string = this.reverse[session]; + return () => this.transport.receive(path, session); + } + + call(session: string, name: string, data: Object): Promise { + if (this._messages == null) { + return Promise.reject(new Error(`Handler not configured.`)); + } + const messages = this._messages; + return buildAndSend(messages, this._sendTransport(session), name, data).then(() => { + return receiveAndParse(messages, this._receiveTransport(session)); + }); + } +} diff --git a/packages/transport/dist/handler/receive.js.flow b/packages/transport/dist/handler/receive.js.flow new file mode 100644 index 0000000000..a22aa90eb4 --- /dev/null +++ b/packages/transport/dist/handler/receive.js.flow @@ -0,0 +1,112 @@ +/* @flow */ + +"use strict"; + +// Logic of recieving data from trezor +// Logic of "call" is broken to two parts - sending and recieving + +import {MessageDecoder} from "../protobuf/message_decoder.js"; +import {ByteBuffer} from "protobufjs"; +import type {Messages} from "../protobuf/messages.js"; +import type {MessageFromTrezor} from "./index"; + +const MESSAGE_HEADER_BYTE: number = 0x23; + +// input that might or might not be fully parsed yet +class PartiallyParsedInput { + // Message type number + typeNumber: number; + // Expected length of the raq message, in bytes + expectedLength: number; + // Buffer with the beginning of message; can be non-complete and WILL be modified + // during the object's lifetime + buffer: ByteBuffer; + constructor(typeNumber: number, length: number) { + this.typeNumber = typeNumber; + this.expectedLength = length; + this.buffer = new ByteBuffer(length); + } + isDone(): boolean { + return (this.buffer.offset >= this.expectedLength); + } + append(buffer: ByteBuffer):void { + this.buffer.append(buffer); + } + arrayBuffer(): ArrayBuffer { + const byteBuffer: ByteBuffer = this.buffer; + byteBuffer.reset(); + return byteBuffer.toArrayBuffer(); + } +} + +// Parses first raw input that comes from Trezor and returns some information about the whole message. +function parseFirstInput(bytes: ArrayBuffer): PartiallyParsedInput { + // convert to ByteBuffer so it's easier to read + const byteBuffer: ByteBuffer = ByteBuffer.concat([bytes]); + + // checking first two bytes + const sharp1: number = byteBuffer.readByte(); + const sharp2: number = byteBuffer.readByte(); + if (sharp1 !== MESSAGE_HEADER_BYTE || sharp2 !== MESSAGE_HEADER_BYTE) { + throw new Error(`Didn't receive expected header signature.`); + } + + // reading things from header + const type: number = byteBuffer.readUint16(); + const length: number = byteBuffer.readUint32(); + + // creating a new buffer with the right size + const res: PartiallyParsedInput = new PartiallyParsedInput(type, length); + res.append(byteBuffer); + return res; +} + +// If the whole message wasn't loaded in the first input, loads more inputs until everything is loaded. +// note: the return value is not at all important since it's still the same parsedinput +function receiveRest( + parsedInput: PartiallyParsedInput, + receiver: () => Promise +): Promise { + if (parsedInput.isDone()) { + return Promise.resolve(); + } + + return receiver().then((data) => { + // sanity check + if (data == null) { + throw new Error(`Received no data.`); + } + + parsedInput.append(data); + return receiveRest(parsedInput, receiver); + }); +} + +// Receives the whole message as a raw data buffer (but without headers or type info) +function receiveBuffer( + receiver: () => Promise +): Promise { + return receiver().then((data: ArrayBuffer) => { + const partialInput: PartiallyParsedInput = parseFirstInput(data); + + return receiveRest(partialInput, receiver).then(() => { + return partialInput; + }); + }); +} + +// Reads data from device and returns decoded message, that can be sent back to trezor.js +export function receiveAndParse( + messages: Messages, + receiver: () => Promise +): Promise { + return receiveBuffer(receiver).then((received) => { + const typeId: number = received.typeNumber; + const buffer: ArrayBuffer = received.arrayBuffer(); + const decoder: MessageDecoder = new MessageDecoder(messages, typeId, buffer); + return { + message: decoder.decodedJSON(), + type: decoder.messageName(), + }; + }); +} diff --git a/packages/transport/dist/handler/send.js.flow b/packages/transport/dist/handler/send.js.flow new file mode 100644 index 0000000000..fda63f1110 --- /dev/null +++ b/packages/transport/dist/handler/send.js.flow @@ -0,0 +1,152 @@ +/* @flow */ + +"use strict"; + +// Logic of sending data to trezor +// +// Logic of "call" is broken to two parts - sending and recieving + +import * as ProtoBuf from "protobufjs"; +import {ByteBuffer} from "protobufjs"; +import type {Messages} from "../protobuf/messages.js"; + +const HEADER_SIZE = 1 + 1 + 4 + 2; +const MESSAGE_HEADER_BYTE: number = 0x23; +const BUFFER_SIZE: number = 63; + +// Sends more buffers to device. +function sendBuffers( + sender: (data: ArrayBuffer) => Promise, + buffers: Array +): Promise { + return buffers.reduce((prevPromise: Promise, buffer: ArrayBuffer) => { + return prevPromise.then(() => { + return sender(buffer); + }); + }, Promise.resolve()); +} + +// already built PB message +class BuiltMessage { + message: ProtoBuf.Builder.Message; + type: number; + + constructor(messages: Messages, // Builders, generated by reading config + name: string, // Name of the message + data: Object // data as "pure" object, from trezor.js + ) { + const Builder = messages.messagesByName[name]; + if (Builder == null) { + throw new Error(`The message name ${name} is not found.`); + } + + // cleans up stuff from angular and remove "null" that crashes in builder + cleanupInput(data); + + if (data) { + this.message = new Builder(data); + } else { + this.message = new Builder(); + } + + this.type = messages.messageTypes[`MessageType_${name}`]; + } + + // encodes into "raw" data, but it can be too long and needs to be split into + // smaller buffers + _encodeLong(): Uint8Array { + const headerSize: number = HEADER_SIZE; // should be 8 + const bytes: Uint8Array = new Uint8Array(this.message.encodeAB()); + const fullSize: number = headerSize + bytes.length; + + const encodedByteBuffer = new ByteBuffer(fullSize); + + // first encode header + + // 2*1 byte + encodedByteBuffer.writeByte(MESSAGE_HEADER_BYTE); + encodedByteBuffer.writeByte(MESSAGE_HEADER_BYTE); + + // 2 bytes + encodedByteBuffer.writeUint16(this.type); + + // 4 bytes (so 8 in total) + encodedByteBuffer.writeUint32(bytes.length); + + // then put in the actual message + encodedByteBuffer.append(bytes); + + // and convert to uint8 array + // (it can still be too long to send though) + const encoded: Uint8Array = new Uint8Array(encodedByteBuffer.buffer); + + return encoded; + } + + // encodes itself and splits into "nice" chunks + encode(): Array { + const bytes: Uint8Array = this._encodeLong(); + + const result: Array = []; + const size: number = BUFFER_SIZE; + + // How many pieces will there actually be + const count: number = Math.floor((bytes.length - 1) / size) + 1; + + // slice and dice + for (let i = 0; i < count; i++) { + const slice: Uint8Array = bytes.subarray(i * size, (i + 1) * size); + const newArray: Uint8Array = new Uint8Array(size); + newArray.set(slice); + result.push(newArray.buffer); + } + + return result; + } +} + +// Removes $$hashkey from angular and remove nulls +function cleanupInput(message: Object): void { + delete message.$$hashKey; + + for (const key in message) { + const value = message[key]; + if (value == null) { + delete message[key]; + } else { + if (Array.isArray(value)) { + value.forEach((i) => { + if (typeof i === `object`) { + cleanupInput(i); + } + }); + } + if (typeof value === `object`) { + cleanupInput(value); + } + } + } +} + +// Builds buffers to send. +// messages: Builders, generated by reading config +// name: Name of the message +// data: Data to serialize, exactly as given by trezor.js +// Returning buffers that will be sent to Trezor +function buildBuffers(messages: Messages, name: string, data: Object): Array { + const message: BuiltMessage = new BuiltMessage(messages, name, data); + const encoded: Array = message.encode(); + return encoded; +} + +// Sends message to device. +// Resolves iff everything gets sent +export function buildAndSend( + messages: Messages, + sender: (data: ArrayBuffer) => Promise, + name: string, + data: Object +): Promise { + const buffers: Array = buildBuffers(messages, name, data); + return sendBuffers(sender, buffers); +} diff --git a/packages/transport/dist/handler/verify.js.flow b/packages/transport/dist/handler/verify.js.flow new file mode 100644 index 0000000000..fa43732ecd --- /dev/null +++ b/packages/transport/dist/handler/verify.js.flow @@ -0,0 +1,52 @@ +/* @flow */ + +"use strict"; + +// Module for verifying ECDSA signature of configuration. + +import {ECPair, ECSignature, crypto} from "bitcoinjs-lib"; + +import BigInteger from "bigi"; + +/* eslint-disable quotes */ +const SATOSHI_KEYS: Array = [ + '\x04\xd5\x71\xb7\xf1\x48\xc5\xe4\x23\x2c\x38\x14\xf7\x77\xd8\xfa\xea\xf1\xa8\x42\x16\xc7\x8d\x56\x9b\x71\x04\x1f\xfc\x76\x8a\x5b\x2d\x81\x0f\xc3\xbb\x13\x4d\xd0\x26\xb5\x7e\x65\x00\x52\x75\xae\xde\xf4\x3e\x15\x5f\x48\xfc\x11\xa3\x2e\xc7\x90\xa9\x33\x12\xbd\x58', + '\x04\x63\x27\x9c\x0c\x08\x66\xe5\x0c\x05\xc7\x99\xd3\x2b\xd6\xba\xb0\x18\x8b\x6d\xe0\x65\x36\xd1\x10\x9d\x2e\xd9\xce\x76\xcb\x33\x5c\x49\x0e\x55\xae\xe1\x0c\xc9\x01\x21\x51\x32\xe8\x53\x09\x7d\x54\x32\xed\xa0\x6b\x79\x20\x73\xbd\x77\x40\xc9\x4c\xe4\x51\x6c\xb1', + '\x04\x43\xae\xdb\xb6\xf7\xe7\x1c\x56\x3f\x8e\xd2\xef\x64\xec\x99\x81\x48\x25\x19\xe7\xef\x4f\x4a\xa9\x8b\x27\x85\x4e\x8c\x49\x12\x6d\x49\x56\xd3\x00\xab\x45\xfd\xc3\x4c\xd2\x6b\xc8\x71\x0d\xe0\xa3\x1d\xbd\xf6\xde\x74\x35\xfd\x0b\x49\x2b\xe7\x0a\xc7\x5f\xde\x58', + '\x04\x87\x7c\x39\xfd\x7c\x62\x23\x7e\x03\x82\x35\xe9\xc0\x75\xda\xb2\x61\x63\x0f\x78\xee\xb8\xed\xb9\x24\x87\x15\x9f\xff\xed\xfd\xf6\x04\x6c\x6f\x8b\x88\x1f\xa4\x07\xc4\xa4\xce\x6c\x28\xde\x0b\x19\xc1\xf4\xe2\x9f\x1f\xcb\xc5\xa5\x8f\xfd\x14\x32\xa3\xe0\x93\x8a', + '\x04\x73\x84\xc5\x1a\xe8\x1a\xdd\x0a\x52\x3a\xdb\xb1\x86\xc9\x1b\x90\x6f\xfb\x64\xc2\xc7\x65\x80\x2b\xf2\x6d\xbd\x13\xbd\xf1\x2c\x31\x9e\x80\xc2\x21\x3a\x13\x6c\x8e\xe0\x3d\x78\x74\xfd\x22\xb7\x0d\x68\xe7\xde\xe4\x69\xde\xcf\xbb\xb5\x10\xee\x9a\x46\x0c\xda\x45', +]; +/* eslint-enable */ + +const keys: Array = SATOSHI_KEYS.map(key => new Buffer(key, `binary`)); + +// Verifies ECDSA signature +// pubkeys - Public keys +// signature - ECDSA signature (concatenated R and S, both 32 bytes) +// data - Data that are signed +// returns True, iff the signature is correct with any of the pubkeys +function verify(pubkeys: Array, bsignature: Buffer, data: Buffer): boolean { + const r = BigInteger.fromBuffer(bsignature.slice(0, 32)); + const s = BigInteger.fromBuffer(bsignature.slice(32)); + const signature = new ECSignature(r, s); + + const hash = crypto.sha256(data); + + return pubkeys.some(pubkey => { + const pair = ECPair.fromPublicKeyBuffer(pubkey); + return pair.verify(hash, signature); + }); +} + +// Verifies if a given data is a correctly signed config +// Returns the data, if correctly signed, else reject +export function verifyHexBin(data: string): Promise { + const signature = new Buffer(data.slice(0, 64 * 2), `hex`); + const dataB = new Buffer(data.slice(64 * 2), `hex`); + const verified = verify(keys, signature, dataB); + if (!verified) { + return Promise.reject(`Not correctly signed.`); + } else { + return Promise.resolve(dataB); + } +} diff --git a/packages/transport/dist/index.js.flow b/packages/transport/dist/index.js.flow new file mode 100644 index 0000000000..b8bcf3b314 --- /dev/null +++ b/packages/transport/dist/index.js.flow @@ -0,0 +1,6 @@ +/* @flow */ + +import {Handler} from './handler'; + +// not sure how to do this in ES6 syntax, so I won't +module.exports = Handler; diff --git a/packages/transport/dist/protobuf/config_proto_compiled.js.flow b/packages/transport/dist/protobuf/config_proto_compiled.js.flow new file mode 100644 index 0000000000..eeb883ec69 --- /dev/null +++ b/packages/transport/dist/protobuf/config_proto_compiled.js.flow @@ -0,0 +1,1059 @@ +/* + re-build this by: + +sed 's/\(google\/protobuf\)/\.\/\1/' trezor-common/protob/config.proto > trezor-common/protob/config_fixed.proto +$(npm bin)/proto2js trezor-common/protob/config_fixed.proto -commonjs > config_proto_compiled.js +rm trezor-common/protob/config_fixed.proto + +given trezor-common is from github trezor-common + +the config.proto is not changed much + +*/ +module.exports = require("protobufjs").newBuilder({})["import"]({ + "package": null, + "messages": [ + { + "name": "DeviceDescriptor", + "fields": [ + { + "rule": "optional", + "options": {}, + "type": "uint32", + "name": "vendor_id", + "id": 1 + }, + { + "rule": "optional", + "options": {}, + "type": "uint32", + "name": "product_id", + "id": 2 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "serial_number", + "id": 3 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "path", + "id": 4 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {} + }, + { + "name": "Configuration", + "fields": [ + { + "rule": "repeated", + "options": {}, + "type": "string", + "name": "whitelist_urls", + "id": 1 + }, + { + "rule": "repeated", + "options": {}, + "type": "string", + "name": "blacklist_urls", + "id": 2 + }, + { + "rule": "required", + "options": {}, + "type": "google.protobuf.FileDescriptorSet", + "name": "wire_protocol", + "id": 3 + }, + { + "rule": "repeated", + "options": {}, + "type": "DeviceDescriptor", + "name": "known_devices", + "id": 4 + }, + { + "rule": "optional", + "options": {}, + "type": "uint32", + "name": "valid_until", + "id": 5 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {} + } + ], + "enums": [], + "imports": [ + { + "package": "google.protobuf", + "messages": [ + { + "name": "FileDescriptorSet", + "fields": [ + { + "rule": "repeated", + "options": {}, + "type": "FileDescriptorProto", + "name": "file", + "id": 1 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {} + }, + { + "name": "FileDescriptorProto", + "fields": [ + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "name", + "id": 1 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "package", + "id": 2 + }, + { + "rule": "repeated", + "options": {}, + "type": "string", + "name": "dependency", + "id": 3 + }, + { + "rule": "repeated", + "options": {}, + "type": "int32", + "name": "public_dependency", + "id": 10 + }, + { + "rule": "repeated", + "options": {}, + "type": "int32", + "name": "weak_dependency", + "id": 11 + }, + { + "rule": "repeated", + "options": {}, + "type": "DescriptorProto", + "name": "message_type", + "id": 4 + }, + { + "rule": "repeated", + "options": {}, + "type": "EnumDescriptorProto", + "name": "enum_type", + "id": 5 + }, + { + "rule": "repeated", + "options": {}, + "type": "ServiceDescriptorProto", + "name": "service", + "id": 6 + }, + { + "rule": "repeated", + "options": {}, + "type": "FieldDescriptorProto", + "name": "extension", + "id": 7 + }, + { + "rule": "optional", + "options": {}, + "type": "FileOptions", + "name": "options", + "id": 8 + }, + { + "rule": "optional", + "options": {}, + "type": "SourceCodeInfo", + "name": "source_code_info", + "id": 9 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {} + }, + { + "name": "DescriptorProto", + "fields": [ + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "name", + "id": 1 + }, + { + "rule": "repeated", + "options": {}, + "type": "FieldDescriptorProto", + "name": "field", + "id": 2 + }, + { + "rule": "repeated", + "options": {}, + "type": "FieldDescriptorProto", + "name": "extension", + "id": 6 + }, + { + "rule": "repeated", + "options": {}, + "type": "DescriptorProto", + "name": "nested_type", + "id": 3 + }, + { + "rule": "repeated", + "options": {}, + "type": "EnumDescriptorProto", + "name": "enum_type", + "id": 4 + }, + { + "rule": "repeated", + "options": {}, + "type": "ExtensionRange", + "name": "extension_range", + "id": 5 + }, + { + "rule": "optional", + "options": {}, + "type": "MessageOptions", + "name": "options", + "id": 7 + } + ], + "enums": [], + "messages": [ + { + "name": "ExtensionRange", + "fields": [ + { + "rule": "optional", + "options": {}, + "type": "int32", + "name": "start", + "id": 1 + }, + { + "rule": "optional", + "options": {}, + "type": "int32", + "name": "end", + "id": 2 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {} + } + ], + "options": {}, + "oneofs": {} + }, + { + "name": "FieldDescriptorProto", + "fields": [ + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "name", + "id": 1 + }, + { + "rule": "optional", + "options": {}, + "type": "int32", + "name": "number", + "id": 3 + }, + { + "rule": "optional", + "options": {}, + "type": "Label", + "name": "label", + "id": 4 + }, + { + "rule": "optional", + "options": {}, + "type": "Type", + "name": "type", + "id": 5 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "type_name", + "id": 6 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "extendee", + "id": 2 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "default_value", + "id": 7 + }, + { + "rule": "optional", + "options": {}, + "type": "FieldOptions", + "name": "options", + "id": 8 + } + ], + "enums": [ + { + "name": "Type", + "values": [ + { + "name": "TYPE_DOUBLE", + "id": 1 + }, + { + "name": "TYPE_FLOAT", + "id": 2 + }, + { + "name": "TYPE_INT64", + "id": 3 + }, + { + "name": "TYPE_UINT64", + "id": 4 + }, + { + "name": "TYPE_INT32", + "id": 5 + }, + { + "name": "TYPE_FIXED64", + "id": 6 + }, + { + "name": "TYPE_FIXED32", + "id": 7 + }, + { + "name": "TYPE_BOOL", + "id": 8 + }, + { + "name": "TYPE_STRING", + "id": 9 + }, + { + "name": "TYPE_GROUP", + "id": 10 + }, + { + "name": "TYPE_MESSAGE", + "id": 11 + }, + { + "name": "TYPE_BYTES", + "id": 12 + }, + { + "name": "TYPE_UINT32", + "id": 13 + }, + { + "name": "TYPE_ENUM", + "id": 14 + }, + { + "name": "TYPE_SFIXED32", + "id": 15 + }, + { + "name": "TYPE_SFIXED64", + "id": 16 + }, + { + "name": "TYPE_SINT32", + "id": 17 + }, + { + "name": "TYPE_SINT64", + "id": 18 + } + ], + "options": {} + }, + { + "name": "Label", + "values": [ + { + "name": "LABEL_OPTIONAL", + "id": 1 + }, + { + "name": "LABEL_REQUIRED", + "id": 2 + }, + { + "name": "LABEL_REPEATED", + "id": 3 + } + ], + "options": {} + } + ], + "messages": [], + "options": {}, + "oneofs": {} + }, + { + "name": "EnumDescriptorProto", + "fields": [ + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "name", + "id": 1 + }, + { + "rule": "repeated", + "options": {}, + "type": "EnumValueDescriptorProto", + "name": "value", + "id": 2 + }, + { + "rule": "optional", + "options": {}, + "type": "EnumOptions", + "name": "options", + "id": 3 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {} + }, + { + "name": "EnumValueDescriptorProto", + "fields": [ + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "name", + "id": 1 + }, + { + "rule": "optional", + "options": {}, + "type": "int32", + "name": "number", + "id": 2 + }, + { + "rule": "optional", + "options": {}, + "type": "EnumValueOptions", + "name": "options", + "id": 3 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {} + }, + { + "name": "ServiceDescriptorProto", + "fields": [ + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "name", + "id": 1 + }, + { + "rule": "repeated", + "options": {}, + "type": "MethodDescriptorProto", + "name": "method", + "id": 2 + }, + { + "rule": "optional", + "options": {}, + "type": "ServiceOptions", + "name": "options", + "id": 3 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {} + }, + { + "name": "MethodDescriptorProto", + "fields": [ + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "name", + "id": 1 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "input_type", + "id": 2 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "output_type", + "id": 3 + }, + { + "rule": "optional", + "options": {}, + "type": "MethodOptions", + "name": "options", + "id": 4 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {} + }, + { + "name": "FileOptions", + "fields": [ + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "java_package", + "id": 1 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "java_outer_classname", + "id": 8 + }, + { + "rule": "optional", + "options": { + "default": false + }, + "type": "bool", + "name": "java_multiple_files", + "id": 10 + }, + { + "rule": "optional", + "options": { + "default": false + }, + "type": "bool", + "name": "java_generate_equals_and_hash", + "id": 20 + }, + { + "rule": "optional", + "options": { + "default": "SPEED" + }, + "type": "OptimizeMode", + "name": "optimize_for", + "id": 9 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "go_package", + "id": 11 + }, + { + "rule": "optional", + "options": { + "default": false + }, + "type": "bool", + "name": "cc_generic_services", + "id": 16 + }, + { + "rule": "optional", + "options": { + "default": false + }, + "type": "bool", + "name": "java_generic_services", + "id": 17 + }, + { + "rule": "optional", + "options": { + "default": false + }, + "type": "bool", + "name": "py_generic_services", + "id": 18 + }, + { + "rule": "repeated", + "options": {}, + "type": "UninterpretedOption", + "name": "uninterpreted_option", + "id": 999 + } + ], + "enums": [ + { + "name": "OptimizeMode", + "values": [ + { + "name": "SPEED", + "id": 1 + }, + { + "name": "CODE_SIZE", + "id": 2 + }, + { + "name": "LITE_RUNTIME", + "id": 3 + } + ], + "options": {} + } + ], + "messages": [], + "options": {}, + "oneofs": {}, + "extensions": [ + 1000, + 536870911 + ] + }, + { + "name": "MessageOptions", + "fields": [ + { + "rule": "optional", + "options": { + "default": false + }, + "type": "bool", + "name": "message_set_wire_format", + "id": 1 + }, + { + "rule": "optional", + "options": { + "default": false + }, + "type": "bool", + "name": "no_standard_descriptor_accessor", + "id": 2 + }, + { + "rule": "repeated", + "options": {}, + "type": "UninterpretedOption", + "name": "uninterpreted_option", + "id": 999 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {}, + "extensions": [ + 1000, + 536870911 + ] + }, + { + "name": "FieldOptions", + "fields": [ + { + "rule": "optional", + "options": { + "default": "STRING" + }, + "type": "CType", + "name": "ctype", + "id": 1 + }, + { + "rule": "optional", + "options": {}, + "type": "bool", + "name": "packed", + "id": 2 + }, + { + "rule": "optional", + "options": { + "default": false + }, + "type": "bool", + "name": "lazy", + "id": 5 + }, + { + "rule": "optional", + "options": { + "default": false + }, + "type": "bool", + "name": "deprecated", + "id": 3 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "experimental_map_key", + "id": 9 + }, + { + "rule": "optional", + "options": { + "default": false + }, + "type": "bool", + "name": "weak", + "id": 10 + }, + { + "rule": "repeated", + "options": {}, + "type": "UninterpretedOption", + "name": "uninterpreted_option", + "id": 999 + } + ], + "enums": [ + { + "name": "CType", + "values": [ + { + "name": "STRING", + "id": 0 + }, + { + "name": "CORD", + "id": 1 + }, + { + "name": "STRING_PIECE", + "id": 2 + } + ], + "options": {} + } + ], + "messages": [], + "options": {}, + "oneofs": {}, + "extensions": [ + 1000, + 536870911 + ] + }, + { + "name": "EnumOptions", + "fields": [ + { + "rule": "optional", + "options": { + "default": true + }, + "type": "bool", + "name": "allow_alias", + "id": 2 + }, + { + "rule": "repeated", + "options": {}, + "type": "UninterpretedOption", + "name": "uninterpreted_option", + "id": 999 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {}, + "extensions": [ + 1000, + 536870911 + ] + }, + { + "name": "EnumValueOptions", + "fields": [ + { + "rule": "repeated", + "options": {}, + "type": "UninterpretedOption", + "name": "uninterpreted_option", + "id": 999 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {}, + "extensions": [ + 1000, + 536870911 + ] + }, + { + "name": "ServiceOptions", + "fields": [ + { + "rule": "repeated", + "options": {}, + "type": "UninterpretedOption", + "name": "uninterpreted_option", + "id": 999 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {}, + "extensions": [ + 1000, + 536870911 + ] + }, + { + "name": "MethodOptions", + "fields": [ + { + "rule": "repeated", + "options": {}, + "type": "UninterpretedOption", + "name": "uninterpreted_option", + "id": 999 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {}, + "extensions": [ + 1000, + 536870911 + ] + }, + { + "name": "UninterpretedOption", + "fields": [ + { + "rule": "repeated", + "options": {}, + "type": "NamePart", + "name": "name", + "id": 2 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "identifier_value", + "id": 3 + }, + { + "rule": "optional", + "options": {}, + "type": "uint64", + "name": "positive_int_value", + "id": 4 + }, + { + "rule": "optional", + "options": {}, + "type": "int64", + "name": "negative_int_value", + "id": 5 + }, + { + "rule": "optional", + "options": {}, + "type": "double", + "name": "double_value", + "id": 6 + }, + { + "rule": "optional", + "options": {}, + "type": "bytes", + "name": "string_value", + "id": 7 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "aggregate_value", + "id": 8 + } + ], + "enums": [], + "messages": [ + { + "name": "NamePart", + "fields": [ + { + "rule": "required", + "options": {}, + "type": "string", + "name": "name_part", + "id": 1 + }, + { + "rule": "required", + "options": {}, + "type": "bool", + "name": "is_extension", + "id": 2 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {} + } + ], + "options": {}, + "oneofs": {} + }, + { + "name": "SourceCodeInfo", + "fields": [ + { + "rule": "repeated", + "options": {}, + "type": "Location", + "name": "location", + "id": 1 + } + ], + "enums": [], + "messages": [ + { + "name": "Location", + "fields": [ + { + "rule": "repeated", + "options": { + "packed": true + }, + "type": "int32", + "name": "path", + "id": 1 + }, + { + "rule": "repeated", + "options": { + "packed": true + }, + "type": "int32", + "name": "span", + "id": 2 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "leading_comments", + "id": 3 + }, + { + "rule": "optional", + "options": {}, + "type": "string", + "name": "trailing_comments", + "id": 4 + } + ], + "enums": [], + "messages": [], + "options": {}, + "oneofs": {} + } + ], + "options": {}, + "oneofs": {} + } + ], + "enums": [], + "imports": [], + "options": { + "java_package": "com.google.protobuf", + "java_outer_classname": "DescriptorProtos", + "optimize_for": "SPEED" + }, + "services": [] + } + ], + "options": {}, + "services": [] +}).build(); diff --git a/packages/transport/dist/protobuf/message_decoder.js.flow b/packages/transport/dist/protobuf/message_decoder.js.flow new file mode 100644 index 0000000000..0212dc496e --- /dev/null +++ b/packages/transport/dist/protobuf/message_decoder.js.flow @@ -0,0 +1,102 @@ +/* @flow */ + +"use strict"; + +// Helper module for converting Trezor's raw input to +// ProtoBuf's message and from there to regular JSON to trezor.js + +import * as ProtoBuf from "protobufjs"; +import {ByteBuffer, Long} from "protobufjs"; + +import {Messages} from "./messages.js"; + +export class MessageDecoder { + // Builders, generated by reading config + messages: Messages; + // message type number + type: number; + // raw data to push to Trezor + data: ArrayBuffer; + + constructor(messages: Messages, type: number, data: ArrayBuffer) { + this.type = type; + this.data = data; + this.messages = messages; + } + + // Returns an info about this message, + // which includes the constructor object and a name + _messageInfo() : MessageInfo { + const r = this.messages.messagesByType[this.type]; + if (r == null) { + throw new Error(`Method type not found`, this.type); + } + return new MessageInfo(r.constructor, r.name); + } + + // Returns the name of the message + messageName() : string { + return this._messageInfo().name; + } + + // Returns the actual decoded message, as a ProtoBuf.js object + _decodedMessage() : ProtoBuf.Builder.Message { + const constructor = this._messageInfo().messageConstructor; + return constructor.decode(this.data); + } + + // Returns the message decoded to JSON, that could be handed back + // to trezor.js + decodedJSON() : Object { + const decoded = this._decodedMessage(); + const converted = messageToJSON(decoded); + + return JSON.parse(JSON.stringify(converted)); + } +} + +class MessageInfo { + messageConstructor: ProtoBuf.Builder.Message; + name: string; + constructor(messageConstructor: ProtoBuf.Builder.Message, name: string) { + this.messageConstructor = messageConstructor; + this.name = name; + } +} + +// Converts any ProtoBuf message to JSON in Trezor.js-friendly format +function messageToJSON(message: ProtoBuf.Builder.Message) : Object { + const res = {}; + const meta = message.$type; + + for (const key in message) { + const value = message[key]; + if (typeof value === `function`) { + // ignoring + } else if (value instanceof ByteBuffer) { + const hex = value.toHex(); + res[key] = hex; + } else if (value instanceof Long) { + const num = value.toNumber(); + res[key] = num; + } else if (Array.isArray(value)) { + const decodedArr = value.map((i) => { + if (typeof i === `object`) { + return messageToJSON(i); + } else { + return i; + } + }); + res[key] = decodedArr; + } else if (value instanceof ProtoBuf.Builder.Message) { + res[key] = messageToJSON(value); + } else if (meta._fieldsByName[key].type.name === `enum`) { + const enumValues = meta._fieldsByName[key].resolvedType.getChildren(); + res[key] = enumValues.find(e => e.id === value).name; + } else { + res[key] = value; + } + } + return res; +} + diff --git a/packages/transport/dist/protobuf/messages.js.flow b/packages/transport/dist/protobuf/messages.js.flow new file mode 100644 index 0000000000..9c8da7f631 --- /dev/null +++ b/packages/transport/dist/protobuf/messages.js.flow @@ -0,0 +1,34 @@ +/* @flow */ + +"use strict"; + +// This is a simple class that represents information about messages, +// as they are loaded from the protobuf definition, +// so they are understood by both sending and recieving code. + +import * as ProtoBuf from "protobufjs"; + +type MessageArray = { [key: KeyType]: ProtoBuf.Bulder.Message }; + +export class Messages { + messagesByName: MessageArray; + messagesByType: MessageArray; + messageTypes: { [key: string]: number }; + + constructor(messages: MessageArray) { + this.messagesByName = messages; + + const messagesByType: MessageArray = {}; + Object.keys(messages.MessageType).forEach(longName => { + const typeId = messages.MessageType[longName]; + const shortName = longName.split(`_`)[1]; + messagesByType[typeId] = { + name: shortName, + constructor: messages[shortName], + }; + }); + this.messagesByType = messagesByType; + this.messageTypes = messages.MessageType; + } +} + diff --git a/packages/transport/dist/protobuf/parse_protocol.js.flow b/packages/transport/dist/protobuf/parse_protocol.js.flow new file mode 100644 index 0000000000..36e310d491 --- /dev/null +++ b/packages/transport/dist/protobuf/parse_protocol.js.flow @@ -0,0 +1,30 @@ +/* @flow */ + +"use strict"; + +// Module for loading the protobuf description from serialized description + +import * as ProtoBuf from "protobufjs"; + +import {Messages} from "./messages.js"; +import {protocolToJSON} from "./to_json.js"; +import * as compiledConfigProto from "./config_proto_compiled.js"; + +// Parse configure data (it has to be already verified) +export function parseConfigure(data: Buffer): Messages { + const configBuilder = compiledConfigProto[`Configuration`]; + const loadedConfig = configBuilder.decode(data); + + const validUntil = loadedConfig.valid_until; + const timeNow = Math.floor(Date.now() / 1000); + if (timeNow >= validUntil) { + throw new Error(`Config too old; ` + timeNow + ` >= ` + validUntil); + } + + const wireProtocol = loadedConfig.wire_protocol; + const protocolJSON = protocolToJSON(wireProtocol.toRaw()); + const protobufMessages = ProtoBuf.newBuilder({})[`import`](protocolJSON).build(); + + return new Messages(protobufMessages); +} + diff --git a/packages/transport/dist/protobuf/to_json.js.flow b/packages/transport/dist/protobuf/to_json.js.flow new file mode 100644 index 0000000000..0d53b3d471 --- /dev/null +++ b/packages/transport/dist/protobuf/to_json.js.flow @@ -0,0 +1,121 @@ +/* @flow */ + +"use strict"; + +// Helper module that does conversion from already parsed protobuf's +// FileDescriptorSet to JSON, that can be used to initialize ProtoBuf.js +// +// Theoretically this should not be necessary, since FileDescriptorSet is protobuf "native" description, +// but ProtoBuf.js does NOT know how to make Builder from FileDescriptorSet, but it can build it from JSON. +// See https://github.com/dcodeIO/ProtoBuf.js/issues/250 +// +// This conversion is probably not very stable and does not "scale" that well, since it's +// intended just for our relatively small usecase. +// But it works here. + +import {shim} from 'object.values'; +if (!Object.values) { + shim(); +} + +export function protocolToJSON(p: any): Object { + // TODO: what if there are more files? + const res = fileToJSON(p.file[2]); + res.imports = [fileToJSON(p.file[1])]; + return res; +} + +function fileToJSON(f: any): Object { + const res = {}; + res.package = f.package; + res.options = f.options; + res.services = []; + const messagesSimple = Object.values(f.message_type).map(messageToJSON); + const messagesRef = extensionToJSON(f.extension); + res.messages = messagesRef.concat(messagesSimple); + res.enums = Object.values(f.enum_type).map(enumToJSON); + return res; +} + +function enumToJSON(enumm: any): Object { + const res = {}; + res.name = enumm.name; + res.values = Object.values(enumm.value).map(enum_valueToJSON); + res.options = {}; + return res; +} + +function extensionToJSON(extensions: {[key: string]: any}): Array { + const res = {}; + Object.values(extensions).forEach(function (extension: any) { + const extendee = extension.extendee.slice(1); + if (res[extendee] == null) { + res[extendee] = {}; + res[extendee].ref = extendee; + res[extendee].fields = []; + } + res[extendee].fields.push(fieldToJSON(extension)); + }); + return Object.values(res); +} + +function enum_valueToJSON(val: any): Object { + const res = {}; + res.name = val.name; + res.id = val.number; + return res; +} + +function messageToJSON(message: any): Object { + const res = {}; + res.enums = []; + res.name = message.name; + res.options = message.options || {}; + res.messages = []; + res.fields = Object.values(message.field).map(fieldToJSON); + res.oneofs = {}; + return res; +} + +const type_map = { + "1": `double`, + "2": `float`, + "3": `int64`, + "4": `uint64`, + "5": `int32`, + "6": `fixed64`, + "7": `fixed32`, + "8": `bool`, + "9": `string`, + "10": `group`, + "11": `message`, + "12": `bytes`, + "13": `uint32`, + "14": `enum`, + "15": `sfixed32`, + "16": `sfixed64`, + "17": `sint32`, + "18": `sint64`, +}; + +function fieldToJSON(field: any): Object { + const res = {}; + if (field.label === 1) { + res.rule = `optional`; + } + if (field.label === 2) { + res.rule = `required`; + } + if (field.label === 3) { + res.rule = `repeated`; + } + res.type = type_map[field.type]; + if (field.type_name) { + res.type = field.type_name.slice(1); + } + res.name = field.name; + res.options = field.options || {}; + res.id = field.number; + return res; +} + diff --git a/packages/transport/dist/transport.js.flow b/packages/transport/dist/transport.js.flow new file mode 100644 index 0000000000..a9cd31ef5e --- /dev/null +++ b/packages/transport/dist/transport.js.flow @@ -0,0 +1,17 @@ +/* @flow */ + +"use strict"; + +// does not have session +export type TrezorDeviceInfo = { + path: string; +} + +export type Transport = { + enumerate: () => Promise>; + send: (path: string, session: string, data: ArrayBuffer) => Promise; + receive: (path: string, session: string) => Promise; + connect: (path: string) => Promise; + disconnect: (path: string, session: string) => Promise; +} +