diff options
| author | Danny Lin <danny@kdrag0n.dev> | 2021-01-22 20:14:00 -0800 |
|---|---|---|
| committer | Daniel Micay <danielmicay@gmail.com> | 2021-01-23 03:28:02 -0500 |
| commit | 713b4ef564ad41d42b1d1d8a83753a9684f26001 (patch) | |
| tree | a274c0b9125273f6ab511a7ecc0f40fd9509a3f5 /static/js | |
| parent | bd2626b41e5fde8a057a61dd504c11468e2b497a (diff) | |
Implement web install using fastboot.js
This implements the WebUSB-based web installer using fastboot.js to
act as a fastboot client. A bare minimum UI with a plain-text
status/progress caption for each step is included, as well as a plain
button to trigger it and basic error handling.
WebADB has been removed now that we are only using fastboot.js.
Initial features:
- Unlocking and locking the bootloader
- Downloading the latest GrapheneOS release available for the device
- Flashing the factory images zip
- Reusing USB connections
Diffstat (limited to 'static/js')
| -rw-r--r-- | static/js/web-install.js | 129 | ||||
| -rw-r--r-- | static/js/webadb.js | 1055 |
2 files changed, 77 insertions, 1107 deletions
diff --git a/static/js/web-install.js b/static/js/web-install.js index eaebe471..f4ee0c8d 100644 --- a/static/js/web-install.js +++ b/static/js/web-install.js @@ -1,77 +1,102 @@ // @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt MIT -async function unlockBootloader() { - const webusb = await Adb.open("WebUSB"); +import { FastbootDevice } from "./fastboot/fastboot.js"; +import * as Factory from "./fastboot/factory.js"; - if (!webusb.isFastboot()) { - console.log("error: not in fastboot mode"); - } +const RELEASES_URL = "https://releases.grapheneos.org"; - console.log("connecting with fastboot"); +let device = new FastbootDevice(); +let lastReleaseZip = null; - const fastboot = await webusb.connectFastboot(); - await fastboot.send("flashing unlock"); - await fastboot.receive(); +async function ensureConnected(setProgress) { + console.log(device.device); + if (!device.isConnected) { + setProgress("Connecting to device..."); + await device.connect(); + } } -async function downloadRelease() { - const webusb = await Adb.open("WebUSB"); +async function unlockBootloader(setProgress) { + await ensureConnected(setProgress); - if (!webusb.isFastboot()) { - console.log("error: not in fastboot mode"); - } + setProgress("Unlocking bootloader..."); + await device.runCommand("flashing unlock"); + return "Bootloader unlocked."; +} - console.log("connecting with fastboot"); +async function downloadRelease(setProgress) { + await ensureConnected(setProgress); - const fastboot = await webusb.connectFastboot(); - await fastboot.send("getvar:product"); - const response = await fastboot.receive(); - if (fastboot.get_cmd(response) == "FAIL") { - throw new Error("getvar product failed"); - } - const decoder = new TextDecoder(); - const product = decoder.decode(fastboot.get_payload(response)); + setProgress("Getting device model..."); + let product = await device.getVariable("product"); + setProgress("Finding latest release..."); + let metadataResp = await fetch(`${RELEASES_URL}/${product}-stable`); + let metadata = await metadataResp.text(); + let releaseId = metadata.split(" ")[0]; + + // Download and cache the zip as a blob + setProgress(`Downloading ${releaseId} release for ${product}...`); + lastReleaseZip = `${product}-factory-${releaseId}.zip`; + await Factory.downloadZip(`${RELEASES_URL}/${lastReleaseZip}`); + return `Downloaded ${releaseId} release for ${product}.`; +} - const baseUrl = "https://releases.grapheneos.org/"; - const metadata = await (await fetch(baseUrl + product + "-stable")).text(); - const buildNumber = metadata.split(" ")[0]; - const downloadUrl = baseUrl + product + "-factory-" + buildNumber + ".zip"; - console.log(downloadUrl); +async function flashRelease(setProgress) { + await ensureConnected(setProgress); - // Download factory images - // - // Need to do this in a way that works well with huge files, particularly since the zip needs - // to be extracted. Could potentially split it up on the server if this works out badly. + setProgress("Flashing release..."); + await Factory.flashZip(device, lastReleaseZip, (action, partition) => { + let userPartition = partition == "avb_custom_key" ? "verified boot key" : partition; + if (action == "unpack") { + setProgress(`Unpacking image: ${userPartition}`); + } else { + setProgress(`Flashing image: ${userPartition}`); + } + }); + return `Flashed ${lastReleaseZip} to device.`; } -async function lockBootloader() { - const webusb = await Adb.open("WebUSB"); +async function lockBootloader(setProgress) { + await ensureConnected(setProgress); - if (!webusb.isFastboot()) { - console.log("error: not in fastboot mode"); - } + setProgress("Locking bootloader..."); + await device.runCommand("flashing lock"); + return "Bootloader locked."; +} - console.log("connecting with fastboot"); +function addButtonHook(id, callback) { + let statusField = document.getElementById(`${id}-status`); + let statusCallback = (status) => { + statusField.textContent = status; + statusField.className = ""; + }; - const fastboot = await webusb.connectFastboot(); - await fastboot.send("flashing lock"); - await fastboot.receive(); + let button = document.getElementById(`${id}-button`); + button.disabled = false; + button.onclick = async () => { + try { + let finalStatus = await callback(statusCallback); + statusCallback(finalStatus); + } catch (error) { + statusCallback(`Error: ${error.message}`); + statusField.className = "error-text"; + } + }; } +// zip.js is loaded separately. +// eslint-disable-next-line no-undef +zip.configure({ + workerScriptsPath: "/js/fastboot/libs/", +}); + if ("usb" in navigator) { console.log("WebUSB available"); - const unlockBootloaderButton = document.getElementById("unlock-bootloader"); - unlockBootloaderButton.disabled = false; - unlockBootloaderButton.onclick = unlockBootloader; - - const downloadReleaseButton = document.getElementById("download-release"); - downloadReleaseButton.disabled = false; - downloadReleaseButton.onclick = downloadRelease; - - const lockBootloaderButton = document.getElementById("lock-bootloader"); - lockBootloaderButton.disabled = false; - lockBootloaderButton.onclick = lockBootloader; + addButtonHook("unlock-bootloader", unlockBootloader); + addButtonHook("download-release", downloadRelease); + addButtonHook("flash-release", flashRelease); + addButtonHook("lock-bootloader", lockBootloader); } else { console.log("WebUSB unavailable"); } diff --git a/static/js/webadb.js b/static/js/webadb.js deleted file mode 100644 index 2fbd307d..00000000 --- a/static/js/webadb.js +++ /dev/null @@ -1,1055 +0,0 @@ -// 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"); - - this.transport.reset(); - }; - - 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; -})); |
