diff --git a/static/js/webadb.js b/static/js/webadb.js new file mode 100644 index 00000000..745cb5d3 --- /dev/null +++ b/static/js/webadb.js @@ -0,0 +1,1053 @@ +// SPDX-License-Identifier: MIT + +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + define([], factory); + } else if (typeof exports === 'object') { + module.exports = factory(); + } else { + root.Adb = factory(); + } +}(this, function() { + 'use strict'; + + let Adb = {}; + + Adb.Opt = {}; + Adb.Opt.debug = false; + Adb.Opt.dump = false; + + Adb.Opt.key_size = 2048; + Adb.Opt.reuse_key = -1; + + // Set this to false for new devices (post Dec 2017) if + // autodetection doesn't handle it automatically. + Adb.Opt.use_checksum = true; + + let db = init_db(); + let keys = db.then(load_keys); + + Adb.open = function(transport) { + if (transport == "WebUSB") + return Adb.WebUSB.Transport.open(); + + throw new Error("Unsupported transport: " + transport); + }; + + Adb.WebUSB = {}; + + Adb.WebUSB.Transport = function(device) { + this.device = device; + + if (Adb.Opt.debug) + console.log(this); + }; + + Adb.WebUSB.Transport.open = function() { + let filters = [ + { classCode: 255, subclassCode: 66, protocolCode: 1 }, + { classCode: 255, subclassCode: 66, protocolCode: 3 } + ]; + + return navigator.usb.requestDevice({ filters: filters }) + .then(device => device.open() + .then(() => new Adb.WebUSB.Transport(device))); + }; + + Adb.WebUSB.Transport.prototype.close = function() { + this.device.close(); + }; + + Adb.WebUSB.Transport.prototype.reset = function() { + this.device.reset(); + }; + + Adb.WebUSB.Transport.prototype.send = function(ep, data) { + if (Adb.Opt.dump) + hexdump(new DataView(data), "" + ep + "==> "); + + return this.device.transferOut(ep, data); + }; + + Adb.WebUSB.Transport.prototype.receive = function(ep, len) { + return this.device.transferIn(ep, len) + .then(response => { + if (Adb.Opt.dump) + hexdump(response.data, "<==" + ep + " "); + + return response.data; + }); + }; + + Adb.WebUSB.Transport.prototype.find = function(filter) { + for (let i in this.device.configurations) { + let conf = this.device.configurations[i]; + for (let j in conf.interfaces) { + let intf = conf.interfaces[j]; + for (let k in intf.alternates) { + let alt = intf.alternates[k]; + if (filter.classCode == alt.interfaceClass && + filter.subclassCode == alt.interfaceSubclass && + filter.protocolCode == alt.interfaceProtocol) { + return { conf: conf, intf: intf, alt: alt }; + } + } + } + } + + return null; + } + + Adb.WebUSB.Transport.prototype.isAdb = function() { + let match = this.find({ classCode: 255, subclassCode: 66, protocolCode: 1 }); + return match != null; + }; + + Adb.WebUSB.Transport.prototype.isFastboot = function() { + let match = this.find({ classCode: 255, subclassCode: 66, protocolCode: 3 }); + return match != null; + }; + + Adb.WebUSB.Transport.prototype.getDevice = function(filter) { + let match = this.find(filter); + return this.device.selectConfiguration(match.conf.configurationValue) + .then(() => this.device.claimInterface(match.intf.interfaceNumber)) + .then(() => this.device.selectAlternateInterface(match.intf.interfaceNumber, match.alt.alternateSetting)) + .then(() => match); + }; + + Adb.WebUSB.Transport.prototype.connectAdb = function(banner, auth_user_notify = null) { + let VERSION = 0x01000000; + let VERSION_NO_CHECKSUM = 0x01000001; + let MAX_PAYLOAD = 256 * 1024; + + let key_idx = 0; + let AUTH_TOKEN = 1; + + let version_used = Adb.Opt.use_checksum ? VERSION : VERSION_NO_CHECKSUM; + let m = new Adb.Message("CNXN", version_used, MAX_PAYLOAD, "" + banner + "\0"); + return this.getDevice({ classCode: 255, subclassCode: 66, protocolCode: 1 }) + .then(match => new Adb.WebUSB.Device(this, match)) + .then(adb => m.send_receive(adb) + .then((function do_auth_response(response) { + if (response.cmd != "AUTH" || response.arg0 != AUTH_TOKEN) + return response; + + return keys.then(keys => + do_auth(adb, keys, key_idx++, response.data.buffer, do_auth_response, auth_user_notify)); + })) + .then(response => { + if (response.cmd != "CNXN") + throw new Error("Failed to connect with '" + banner + "'"); + console.log('version', response.arg0); + if (response.arg0 != VERSION && response.arg0 != VERSION_NO_CHECKSUM) + throw new Error("Version mismatch: " + response.arg0 + " (expected: " + VERSION + " or " + VERSION_NO_CHECKSUM + ")"); + if (Adb.Opt.debug) + console.log("Connected with '" + banner + "', max_payload: " + response.arg1); + adb.max_payload = response.arg1; + if (response.arg0 == VERSION_NO_CHECKSUM) + Adb.Opt.use_checksum = false; + adb.banner = new TextDecoder("utf-8").decode(response.data); + let pieces = adb.banner.split(':'); + adb.mode = pieces[0]; + return adb; + }) + ); + }; + + Adb.WebUSB.Transport.prototype.connectFastboot = function() { + return this.getDevice({ classCode: 255, subclassCode: 66, protocolCode: 3 }) + .then(match => new Fastboot.WebUSB.Device(this, match)) + .then(fastboot => fastboot.send("getvar:max-download-size") + .then(() => fastboot.receive() + .then(response => { + let cmd = decode_cmd(response.getUint32(0, true)); + if (cmd == "FAIL") + throw new Error("Unable to open Fastboot"); + + fastboot.get_cmd = r => decode_cmd(r.getUint32(0, true)); + fastboot.get_payload = r => r.buffer.slice(4); + return fastboot; + }) + ) + ); + }; + + Adb.WebUSB.Device = function(transport, match) { + this.transport = transport; + this.max_payload = 4096; + + this.ep_in = get_ep_num(match.alt.endpoints, "in"); + this.ep_out = get_ep_num(match.alt.endpoints, "out"); + + this.transport.reset(); + } + + Adb.WebUSB.Device.prototype.open = function(service) { + return Adb.Stream.open(this, service); + }; + + Adb.WebUSB.Device.prototype.shell = function(command) { + return Adb.Stream.open(this, "shell:" + command); + }; + + Adb.WebUSB.Device.prototype.tcpip = function(port) { + return Adb.Stream.open(this, "tcpip:" + port); + }; + + Adb.WebUSB.Device.prototype.sync = function() { + return Adb.Stream.open(this, "sync:"); + }; + + Adb.WebUSB.Device.prototype.reboot = function(command="") { + return Adb.Stream.open(this, "reboot:" + command); + }; + + Adb.WebUSB.Device.prototype.send = function(data) { + if (typeof data === "string") { + let encoder = new TextEncoder(); + let string_data = data; + data = encoder.encode(string_data).buffer; + } + + if (data != null && data.length > this.max_payload) + throw new Error("data is too big: " + data.length + " bytes (max: " + this.max_payload + " bytes)"); + + return this.transport.send(this.ep_out, data); + }; + + Adb.WebUSB.Device.prototype.receive = function(len) { + return this.transport.receive(this.ep_in, len); + }; + + let Fastboot = {}; + Fastboot.WebUSB = {}; + + Fastboot.WebUSB.Device = function(transport, match) { + this.transport = transport; + this.max_datasize = 64; + + this.ep_in = get_ep_num(match.alt.endpoints, "in"); + this.ep_out = get_ep_num(match.alt.endpoints, "out"); + }; + + Fastboot.WebUSB.Device.prototype.send = function(data) { + if (typeof data === "string") { + let encoder = new TextEncoder(); + let string_data = data; + data = encoder.encode(string_data).buffer; + } + + if (data != null && data.length > this.max_datasize) + throw new Error("data is too big: " + data.length + " bytes (max: " + this.max_datasize + " bytes)"); + + return this.transport.send(this.ep_out, data); + }; + + Fastboot.WebUSB.Device.prototype.receive = function() { + return this.transport.receive(this.ep_in, 64); + }; + + Adb.Message = function(cmd, arg0, arg1, data = null) { + if (cmd.length != 4) + throw new Error("Invalid command: '" + cmd + "'"); + + this.cmd = cmd; + this.arg0 = arg0; + this.arg1 = arg1; + this.length = (data === null) ? 0 : (typeof data === "string") ? data.length : data.byteLength; + this.data = data; + }; + + Adb.Message.checksum = function(data_view) { + let sum = 0; + + for (let i = 0; i < data_view.byteLength; i++) + sum += data_view.getUint8(i); + + return sum & 0xffffffff; + }; + + Adb.Message.send = function(device, message) { + let header = new ArrayBuffer(24); + let cmd = encode_cmd(message.cmd); + let magic = cmd ^ 0xffffffff; + let data = null; + let len = 0; + let checksum = 0; + + if (Adb.Opt.debug) + console.log(message); + + if (message.data != null) { + if (typeof message.data === "string") { + let encoder = new TextEncoder(); + data = encoder.encode(message.data).buffer; + } else if (ArrayBuffer.isView(message.data)) { + data = message.data.buffer; + } else { + data = message.data; + } + + len = data.byteLength; + if (Adb.Opt.use_checksum) + checksum = Adb.Message.checksum(new DataView(data)); + + if (len > device.max_payload) + throw new Error("data is too big: " + len + " bytes (max: " + device.max_payload + " bytes)"); + } + + let view = new DataView(header); + view.setUint32(0, cmd, true); + view.setUint32(4, message.arg0, true); + view.setUint32(8, message.arg1, true); + view.setUint32(12, len, true); + view.setUint32(16, checksum, true); + view.setUint32(20, magic, true); + + let seq = device.send(header); + if (len > 0) + seq.then(() => device.send(data)); + return seq; + }; + + Adb.Message.receive = function(device) { + return device.receive(24) //Adb.Opt.use_checksum ? 24 : 20) + .then(response => { + let cmd = response.getUint32(0, true); + let arg0 = response.getUint32(4, true); + let arg1 = response.getUint32(8, true); + let len = response.getUint32(12, true); + let check = response.getUint32(16, true); + // Android seems to have stopped providing checksums + if (Adb.use_checksum && response.byteLength > 20) { + let magic = response.getUint32(20, true); + + if ((cmd ^ magic) != -1) + throw new Error("magic mismatch"); + } + + cmd = decode_cmd(cmd); + + if (len == 0) { + let message = new Adb.Message(cmd, arg0, arg1); + if (Adb.Opt.debug) + console.log(message); + return message; + } + + return device.receive(len) + .then(data => { + if (Adb.Opt.use_checksum && Adb.Message.checksum(data) != check) + throw new Error("checksum mismatch"); + + let message = new Adb.Message(cmd, arg0, arg1, data); + if (Adb.Opt.debug) + console.log(message); + return message; + }); + }); + }; + + Adb.Message.prototype.send = function(device) { + return Adb.Message.send(device, this); + }; + + Adb.Message.prototype.send_receive = function(device) { + return this.send(device) + .then(() => Adb.Message.receive(device)); + }; + + Adb.SyncFrame = function(cmd, length = 0, data = null) { + if (cmd.length != 4) + throw new Error("Invalid command: '" + cmd + "'"); + + this.cmd = cmd; + this.length = length; + this.data = data; + }; + + Adb.SyncFrame.send = function(stream, frame) { + let data = new ArrayBuffer(8); + let cmd = encode_cmd(frame.cmd); + + if (Adb.Opt.debug) + console.log(frame); + + let view = new DataView(data); + view.setUint32(0, cmd, true); + view.setUint32(4, frame.length, true); + + return stream.send("WRTE", data); + }; + + Adb.SyncFrame.receive = function(stream) { + return stream.receive() + .then(response => { + if (response.cmd == "WRTE") { + let cmd = decode_cmd(response.data.getUint32(0, true)); + + if (cmd == "OKAY" || cmd == "DATA" || cmd == "DONE" || cmd == "FAIL") { + let len = response.data.getUint32(4, true); + let data = new DataView(response.data.buffer.slice(8)); + + if (len == 0 || data.byteLength >= len) { + let frame = new Adb.SyncFrame(cmd, len, data); + if (Adb.Opt.debug) + console.log(frame); + return frame; + } + + return stream.send("OKAY") + .then(() => stream.receive()) + .then(response => { + if (response.data == null) { + let frame = new Adb.SyncFrame(cmd); + if (Adb.Opt.debug) + console.log(frame); + return frame; + } + + let cmd2 = decode_cmd(response.data.getUint32(0, true)); + + if (cmd2 == "OKAY" || cmd2 == "DATA" || cmd2 == "DONE" || cmd2 == "FAIL") { + let len = response.data.getUint32(4, true); + let data = new DataView(response.data.buffer.slice(8)); + + if (len == 0 || data.byteLength >= len) { + let frame = new Adb.SyncFrame(cmd2, len, data); + if (Adb.Opt.debug) + console.log(frame); + return frame; + } + } + + if (response.data.byteLength < len) + throw new Error("expected at least " + len + ", got " + response.data.byteLength); + + let frame = new Adb.SyncFrame(cmd, len, response.data); + if (Adb.Opt.debug) + console.log(frame); + return frame; + }); + } + + if (Adb.Opt.debug) + console.log(response); + if (Adb.Opt.dump) + hexdump(response.data, "WRTE: "); + + throw new Error("invalid WRTE frame"); + } + + if (response.cmd == "OKAY") { + let frame = new Adb.SyncFrame("OKAY"); + if (Adb.Opt.debug) + console.log(frame); + return frame; + } + + if (Adb.Opt.debug) + console.log(response); + + throw new Error("invalid SYNC frame"); + }); + }; + + Adb.SyncFrame.prototype.send = function(stream) { + return Adb.SyncFrame.send(stream, this); + }; + + Adb.SyncFrame.prototype.send_receive = function(stream) { + return Adb.SyncFrame.send(stream, this) + .then(() => Adb.SyncFrame.receive(stream)); + }; + + Adb.Stream = function(device, service, local_id, remote_id) { + this.device = device; + this.service = service; + this.local_id = local_id; + this.remote_id = remote_id; + this.cancel = null; + }; + + let next_id = 1; + + Adb.Stream.open = function(device, service) { + let local_id = next_id++; + let remote_id = 0; + + let m = new Adb.Message("OPEN", local_id, remote_id, "" + service + "\0"); + return m.send_receive(device) + .then(function do_response(response) { + if (response.arg1 != local_id) + return Adb.Message.receive(device).then(do_response); + + if (response.cmd != "OKAY") + throw new Error("Open failed"); + + remote_id = response.arg0; + + if (Adb.Opt.debug) { + console.log("Opened stream '" + service + "'"); + console.log(" local_id: 0x" + toHex32(local_id)); + console.log(" remote_id: 0x" + toHex32(remote_id)); + } + + return new Adb.Stream(device, service, local_id, remote_id); + }); + }; + + Adb.Stream.prototype.close = function() { + if (this.local_id != 0) { + this.local_id = 0; + return this.send("CLSE"); + } + + if (Adb.Opt.debug) { + console.log("Closed stream '" + this.service + "'"); + console.log(" local_id: 0x" + toHex32(this.local_id)); + console.log(" remote_id: 0x" + toHex32(this.remote_id)); + } + + this.service = ""; + this.remote_id = 0; + }; + + Adb.Stream.prototype.send = function(cmd, data=null) { + let m = new Adb.Message(cmd, this.local_id, this.remote_id, data); + return m.send(this.device); + }; + + Adb.Stream.prototype.receive = function() { + return Adb.Message.receive(this.device) + .then(response => { + // remote's prospective of local_id/remote_id is reversed + if (response.arg0 != 0 && response.arg0 != this.remote_id) + throw new Error("Incorrect arg0: 0x" + toHex32(response.arg0) + " (expected 0x" + toHex32(this.remote_id) + ")"); + if (this.local_id != 0 && response.arg1 != this.local_id) + throw new Error("Incorrect arg1: 0x" + toHex32(response.arg1) + " (expected 0x" + toHex32(this.local_id) + ")"); + return response; + }); + }; + + Adb.Stream.prototype.send_receive = function(cmd, data=null) { + return this.send(cmd, data) + .then(() => this.receive()); + }; + + Adb.Stream.prototype.abort = function() { + if (Adb.Opt.debug) + console.log("aborting..."); + + let self = this; + return new Promise(function(resolve, reject) { + self.cancel = function() { + if (Adb.Opt.debug) + console.log("aborted"); + self.cancel = null; + resolve(); + }; + }); + }; + + Adb.Stream.prototype.stat = function(filename) { + let frame = new Adb.SyncFrame("STAT", filename.length); + return frame.send_receive(this) + .then(check_ok("STAT failed on " + filename)) + .then(response => { + let encoder = new TextEncoder(); + return this.send_receive("WRTE", encoder.encode(filename)) + }) + .then(check_ok("STAT failed on " + filename)) + .then(response => { + return this.receive().then(response => + this.send("OKAY").then(() => + response.data)); + }) + .then(response => { + let id = decode_cmd(response.getUint32(0, true)); + let mode = response.getUint32(4, true); + let size = response.getUint32(8, true); + let time = response.getUint32(12, true); + + if (Adb.Opt.debug) { + console.log("STAT: " + filename); + console.log("id: " + id); + console.log("mode: " + mode); + console.log("size: " + size); + console.log("time: " + time); + } + + if (id != "STAT") + throw new Error("STAT failed on " + filename); + + return { mode: mode, size: size, time: time }; + }); + }; + + Adb.Stream.prototype.pull = function(filename) { + let frame = new Adb.SyncFrame("RECV", filename.length); + return frame.send_receive(this) + .then(check_ok("PULL RECV failed on " + filename)) + .then(response => { + let encoder = new TextEncoder(); + return this.send_receive("WRTE", encoder.encode(filename)) + }) + .then(check_ok("PULL WRTE failed on " + filename)) + .then(() => Adb.SyncFrame.receive(this)) + .then(check_cmd("DATA", "PULL DATA failed on " + filename)) + .catch(err => { + return this.send("OKAY") + .then(() => { throw err; }); + }) + .then(response => { + return this.send("OKAY") + .then(() => response); + }) + .then(response => { + let len = response.length; + if (response.data.byteLength == len + 8) { + let cmd = response.data.getUint32(len, true); + let zero = response.data.getUint32(len + 4, true); + if (decode_cmd(cmd) != "DONE" || zero != 0) + throw new Error("PULL DONE failed on " + filename); + + return new DataView(response.data.buffer, 0, len); + } + + if (response.data.byteLength > 64 * 1024) { + let cmd = response.data.getUint32(response.data.byteLength - 8, true); + let zero = response.data.getUint32(response.data.byteLength - 4, true); + if (decode_cmd(cmd) != "DONE" || zero != 0) + throw new Error("PULL DONE failed on " + filename); + + return new DataView(response.data.buffer, 0, response.data.byteLength - 8); + } + + if (response.data.byteLength != len) + throw new Error("PULL DATA failed on " + filename + ": " + response.data.byteLength + "!=" + len); + + return this.receive() + .then(response => { + let cmd = response.data.getUint32(0, true); + let zero = response.data.getUint32(4, true); + if (decode_cmd(cmd) != "DONE" || zero != 0) + throw new Error("PULL DONE failed on " + filename); + }) + .then(() => this.send("OKAY")) + .then(() => response.data); + }); + }; + + Adb.Stream.prototype.push_start = function(filename, mode) { + let mode_str = mode.toString(10); + let encoder = new TextEncoder(); + + let frame = new Adb.SyncFrame("SEND", filename.length + 1 + mode_str.length); + return frame.send_receive(this) + .then(check_ok("PUSH failed on " + filename)) + .then(response => { + return this.send("WRTE", encoder.encode(filename)) + }) + .then(() => Adb.SyncFrame.receive(this)) + .then(check_ok("PUSH failed on " + filename)) + .then(response => { + return this.send("WRTE", encoder.encode("," + mode_str)) + }) + .then(() => Adb.SyncFrame.receive(this)) + .then(check_ok("PUSH failed on " + filename)); + }; + + Adb.Stream.prototype.push_data = function(data) { + if (typeof data === "string") { + let encoder = new TextEncoder(); + let string_data = data; + data = encoder.encode(string_data).buffer; + } else if (ArrayBuffer.isView(data)) { + data = data.buffer; + } + + let frame = new Adb.SyncFrame("DATA", data.byteLength); + return frame.send_receive(this) + .then(check_ok("PUSH failed")) + .then(response => { + return this.send("WRTE", data); + }) + .then(() => Adb.SyncFrame.receive(this)) + .then(check_ok("PUSH failed")); + }; + + Adb.Stream.prototype.push_done = function() { + let frame = new Adb.SyncFrame("DONE", Math.round(Date.now() / 1000)); + return frame.send_receive(this) + .then(check_ok("PUSH failed")) + .then(response => { + return Adb.SyncFrame.receive(this); + }) + .then(check_ok("PUSH failed")) + .then(response => { + return this.send("OKAY"); + }); + }; + + Adb.Stream.prototype.push = function(file, filename, mode, on_progress = null) { + // we need reduced logging during the data transfer otherwise the console may explode + let old_debug = Adb.Opt.debug; + let old_dump = Adb.Opt.dump; + Adb.Opt.debug = false; + Adb.Opt.dump = false; + + // read the whole file + return read_blob(file).then(data => + this.push_start(filename, mode).then(() => { + let seq = Promise.resolve(); + let rem = file.size; + let max = Math.min(0x10000, this.device.max_payload); + while (rem > 0) { + // these two are needed here for the closure + let len = Math.min(rem, max); + let count = file.size - rem; + seq = seq.then(() => { + if (this.cancel) { + Adb.Opt.debug = old_debug; + Adb.Opt.dump = old_dump; + this.cancel(); + throw new Error("cancelled"); + } + if (on_progress != null) + on_progress(count, file.size); + return this.push_data(data.slice(count, count + len)); + }); + rem -= len; + } + return seq.then(() => { + Adb.Opt.debug = old_debug; + Adb.Opt.dump = old_dump; + return this.push_done(); + }); + })); + }; + + Adb.Stream.prototype.quit = function() { + let frame = new Adb.SyncFrame("QUIT"); + return frame.send_receive(this) + .then(check_ok("QUIT failed")) + .then(response => { + return this.receive(); + }) + .then(check_cmd("CLSE", "QUIT failed")) + .then(response => { + return this.close(); + }); + }; + + function check_cmd(cmd, err_msg) + { + return function(response) { + if (response.cmd == "FAIL") { + let decoder = new TextDecoder(); + throw new Error(decoder.decode(response.data)); + } + if (response.cmd != cmd) + throw new Error(err_msg); + return response; + }; + } + + function check_ok(err_msg) + { + return check_cmd("OKAY", err_msg); + } + + function paddit(text, width, padding) + { + let padlen = width - text.length; + let padded = ""; + + for (let i = 0; i < padlen; i++) + padded += padding; + + return padded + text; + } + + function toHex8(num) + { + return paddit(num.toString(16), 2, "0"); + } + + function toHex16(num) + { + return paddit(num.toString(16), 4, "0"); + } + + function toHex32(num) + { + return paddit(num.toString(16), 8, "0"); + } + + function toB64(buffer) + { + return btoa(new Uint8Array(buffer).reduce((s, b) => s + String.fromCharCode(b), "")); + } + + function hexdump(view, prefix="") + { + let decoder = new TextDecoder(); + + for (let i = 0; i < view.byteLength; i += 16) { + let max = (view.byteLength - i) > 16 ? 16 : (view.byteLength - i); + let row = prefix + toHex16(i) + " "; + let j; + + for (j = 0; j < max; j++) + row += " " + toHex8(view.getUint8(i + j)); + for (; j < 16; j++) + row += " "; + + row += " | " + decoder.decode(new DataView(view.buffer, i, max)); + console.log(row); + } + } + + function get_ep_num(endpoints, dir, type = "bulk") + { + let e, ep; + for (e in endpoints) + if (ep = endpoints[e], ep.direction == dir && ep.type == type) + return ep.endpointNumber; + if (Adb.Opt.debug) + console.log(endpoints); + throw new Error("Cannot find " + dir + " endpoint"); + } + + function encode_cmd(cmd) + { + let encoder = new TextEncoder(); + let buffer = encoder.encode(cmd).buffer; + let view = new DataView(buffer); + return view.getUint32(0, true); + } + + function decode_cmd(cmd) + { + let decoder = new TextDecoder(); + let buffer = new ArrayBuffer(4); + let view = new DataView(buffer); + view.setUint32(0, cmd, true); + return decoder.decode(buffer); + } + + function generate_key() + { + let extractable = Adb.Opt.dump; + + return crypto.subtle.generateKey({ + name: "RSASSA-PKCS1-v1_5", + modulusLength: Adb.Opt.key_size, + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: { name: "SHA-1" } + }, extractable, [ "sign", "verify" ]) + .then(key => { + if (!Adb.Opt.dump) + return key; + + return privkey_dump(key) + .then(() => pubkey_dump(key)) + .then(() => key); + }); + } + + function do_auth(adb, keys, key_idx, token, do_auth_response, auth_user_notify) + { + let AUTH_SIGNATURE = 2; + let AUTH_RSAPUBLICKEY = 3; + + if (key_idx < keys.length) { + let slot = keys.length - key_idx - 1; + let key = keys[slot]; + let seq = Promise.resolve(); + + if (Adb.Opt.debug) + console.log("signing with key " + slot + "..."); + if (Adb.Opt.dump) { + seq = seq.then(() => privkey_dump(key)) + .then(() => pubkey_dump(key)) + .then(() => hexdump(new DataView(token))) + .then(() => console.log("-----BEGIN TOKEN-----\n" + toB64(token) + "\n-----END TOKEN-----")); + } + + return seq.then(() => crypto.subtle.sign({ name: "RSASSA-PKCS1-v1_5" }, key.privateKey, token)) + .then(signed => { + if (Adb.Opt.dump) + console.log("-----BEGIN SIGNATURE-----\n" + toB64(signed) + "\n-----END SIGNATURE-----"); + + let m = new Adb.Message("AUTH", AUTH_SIGNATURE, 0, signed); + return m.send_receive(adb).then(do_auth_response); + }); + } + + let seq = null; + let dirty = false; + + if (Adb.Opt.reuse_key !== false) { + key_idx = Adb.Opt.reuse_key === true ? -1 : Adb.Opt.reuse_key; + + if (key_idx < 0) + key_idx += keys.length; + + if (key_idx >= 0 && key_idx < keys.length) { + if (Adb.Opt.debug) + console.log("reusing key " + key_idx + "..."); + seq = Promise.resolve(keys[key_idx]); + } + } + + if (seq === null) { + if (Adb.Opt.debug) + console.log("generating key " + key_idx + " (" + Adb.Opt.key_size + " bits)..."); + + seq = generate_key(); + dirty = true; + } + + return seq.then(key => { + return crypto.subtle.exportKey("spki", key.publicKey) + .then(pubkey => { + let m = new Adb.Message("AUTH", AUTH_RSAPUBLICKEY, 0, toB64(pubkey) + "\0"); + return m.send(adb); + }) + .then(() => { + if (Adb.Opt.debug) + console.log("waiting for user confirmation..."); + if (auth_user_notify != null) + auth_user_notify(key.publicKey); + return Adb.Message.receive(adb); + }) + .then(response => { + // return response; + if (response.cmd != "CNXN") + return response; + if (!dirty) + return response; + + keys.push(key); + return db.then(db => store_key(db, key)) + .then(() => response); + }); + }); + } + + function privkey_dump(key) + { + if (!key.privateKey.extractable) { + console.log("cannot dump the private key, it's not extractable"); + return; + } + + return crypto.subtle.exportKey("pkcs8", key.privateKey) + .then(privkey => console.log("-----BEGIN PRIVATE KEY-----\n" + toB64(privkey) + "\n-----END PRIVATE KEY-----")); + } + + function pubkey_dump(key) + { + if (!key.publicKey.extractable) { + console.log("cannot dump the public key, it's not extractable"); + return; + } + + return crypto.subtle.exportKey("spki", key.publicKey) + .then(pubkey => console.log("-----BEGIN PUBLIC KEY-----\n" + toB64(pubkey) + "\n-----END PUBLIC KEY-----")); + } + + function read_blob(blob) + { + return new Promise(function(resolve, reject) { + let reader = new FileReader(); + reader.onload = e => resolve(e.target.result); + reader.onerror = e => reject(e.target.error); + reader.readAsArrayBuffer(blob); + }); + } + + function promisify(request, onsuccess = "onsuccess", onerror = "onerror") + { + return new Promise(function (resolve, reject) { + request[onsuccess] = event => resolve(event.target.result); + request[onerror] = event => reject(event.target.errorCode); + }); + } + + function init_db() + { + let req = window.indexedDB.open("WebADB", 1); + + req.onupgradeneeded = function (event) { + let db = event.target.result; + + if (Adb.Opt.debug) + console.log("DB: migrating from version " + event.oldVersion + " to " + event.newVersion + "..."); + + if (db.objectStoreNames.contains('keys')) { + if (Adb.Opt.debug) + console.log("DB: deleting old keys..."); + + db.deleteObjectStore('keys'); + } + + db.createObjectStore("keys", { autoIncrement: true }); + }; + + return promisify(req); + } + + function load_keys(db) + { + let transaction = db.transaction("keys"); + let store = transaction.objectStore("keys"); + let cursor = store.openCursor(); + let keys = []; + + cursor.onsuccess = function (event) { + let result = event.target.result; + if (result != null) { + keys.push(result.value); + result.continue(); + } + }; + + return promisify(transaction, "oncomplete").then(function (result) { + if (Adb.Opt.debug) + console.log("DB: loaded " + keys.length + " keys"); + return keys; + }); + } + + function store_key(db, key) + { + let transaction = db.transaction("keys", "readwrite"); + let store = transaction.objectStore('keys'); + let request = store.put(key); + + return promisify(request).then(function (result) { + if (Adb.Opt.debug) + console.log("DB: stored key " + (result - 1)); + return result; + }); + } + + function clear_keys(db) + { + let transaction = db.transaction("keys", "readwrite"); + let store = transaction.objectStore("keys"); + let request = store.clear(); + + return promisify(request).then(function (result) { + if (Adb.Opt.debug) + console.log("DB: removed all the keys"); + return result; + }); + } + + return Adb; +})); diff --git a/validate_static b/validate_static index d488fd97..bb46fc00 100755 --- a/validate_static +++ b/validate_static @@ -10,6 +10,6 @@ for file in static/**/*.@(json|webmanifest); do done xmllint --noout static/**/*.@(html|svg|xml) -eslint static/**/*.js +eslint static/**/!(webadb).js stylelint static/**/*.css validatornu --Werror --also-check-css --also-check-svg static/**/*.@(css|html|svg)