diff --git a/static/grapheneos.css b/static/grapheneos.css index d3c3af01..6272805c 100644 --- a/static/grapheneos.css +++ b/static/grapheneos.css @@ -150,6 +150,11 @@ footer a, footer a:visited { list-style-type: none; } +.error-text { + /* Baseline Material error color */ + color: #b00020; +} + /* latin */ @font-face { font-family: 'Roboto'; 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"; + +let device = new FastbootDevice(); +let lastReleaseZip = null; + +async function ensureConnected(setProgress) { + console.log(device.device); + if (!device.isConnected) { + setProgress("Connecting to device..."); + await device.connect(); } - - console.log("connecting with fastboot"); - - const fastboot = await webusb.connectFastboot(); - await fastboot.send("flashing unlock"); - await fastboot.receive(); } -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"); - } - - console.log("connecting with fastboot"); - - 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)); - - 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); - - // 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("Unlocking bootloader..."); + await device.runCommand("flashing unlock"); + return "Bootloader unlocked."; } -async function lockBootloader() { - const webusb = await Adb.open("WebUSB"); +async function downloadRelease(setProgress) { + await ensureConnected(setProgress); - if (!webusb.isFastboot()) { - console.log("error: not in fastboot mode"); - } + 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]; - console.log("connecting with fastboot"); - - const fastboot = await webusb.connectFastboot(); - await fastboot.send("flashing lock"); - await fastboot.receive(); + // 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}.`; } +async function flashRelease(setProgress) { + await ensureConnected(setProgress); + + 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(setProgress) { + await ensureConnected(setProgress); + + setProgress("Locking bootloader..."); + await device.runCommand("flashing lock"); + return "Bootloader locked."; +} + +function addButtonHook(id, callback) { + let statusField = document.getElementById(`${id}-status`); + let statusCallback = (status) => { + statusField.textContent = status; + statusField.className = ""; + }; + + 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; -})); diff --git a/static/web-install.html b/static/web-install.html index 8834e0fd..3c3023de 100644 --- a/static/web-install.html +++ b/static/web-install.html @@ -26,8 +26,12 @@ - - + + + + + +
@@ -129,11 +133,13 @@

Unlock the bootloader to allow flashing the OS and firmware:

- +

The command needs to be confirmed on the device and will wipe all data. Use one of the volume keys to switch the selection to accepting it and the power button to confirm.

+ +

@@ -144,9 +150,9 @@

Press the button below to start the download:

- + -

Download not implemented yet.

+

@@ -155,14 +161,13 @@

The initial install will be performed by flashing the factory images. This will replace the existing OS installation and wipe all the existing data.

-

Needs to be implemented based on extracting and flashing images from - the downloaded zip (or a split approach to downloading files).

- - +

Wait for the flashing process to complete and proceed to locking the bootloader before using the device as locking wipes the data again.

+ +

@@ -177,11 +182,13 @@

In the bootloader interface, set it to locked:

- +

The command needs to be confirmed on the device and will wipe all data. Use one of the volume buttons to switch the selection to accepting it and the power button to confirm.

+ +