| ... | ... | @@ -37,6 +37,7 @@ XPCOMUtils.defineLazyModuleGetters(lazy, { | 
| 37 | 37 |    set_panic_hook: "resource://gre/modules/lox_wasm.jsm",
 | 
| 38 | 38 |    invitation_is_trusted: "resource://gre/modules/lox_wasm.jsm",
 | 
| 39 | 39 |    issue_invite: "resource://gre/modules/lox_wasm.jsm",
 | 
|  | 40 | +  handle_issue_invite: "resource://gre/modules/lox_wasm.jsm",
 | 
| 40 | 41 |    prepare_invite: "resource://gre/modules/lox_wasm.jsm",
 | 
| 41 | 42 |    get_invites_remaining: "resource://gre/modules/lox_wasm.jsm",
 | 
| 42 | 43 |    get_trust_level: "resource://gre/modules/lox_wasm.jsm",
 | 
| ... | ... | @@ -91,6 +92,7 @@ const LoxSettingsPrefs = Object.freeze({ | 
| 91 | 92 |  export class LoxError extends Error {
 | 
| 92 | 93 |    static BadInvite = "BadInvite";
 | 
| 93 | 94 |    static LoxServerUnreachable = "LoxServerUnreachable";
 | 
|  | 95 | +  static ErrorResponse = "ErrorResponse";
 | 
| 94 | 96 |  
 | 
| 95 | 97 |    /**
 | 
| 96 | 98 |     * @param {string} message - The error message.
 | 
| ... | ... | @@ -408,89 +410,47 @@ class LoxImpl { | 
| 408 | 410 |    }
 | 
| 409 | 411 |  
 | 
| 410 | 412 |    /**
 | 
| 411 |  | -   * Update Lox credential after Lox key rotation
 | 
| 412 |  | -   * Do not call directly, use #getPubKeys() instead to start the update only
 | 
| 413 |  | -   * once
 | 
|  | 413 | +   * Update Lox credential after Lox key rotation.
 | 
| 414 | 414 |     *
 | 
| 415 |  | -   * @param {string} prevkeys The public keys we are replacing
 | 
|  | 415 | +   * Do not call directly, use #getPubKeys() instead to start the update only
 | 
|  | 416 | +   * once.
 | 
| 416 | 417 |     */
 | 
| 417 |  | -  async #updatePubkeys(prevkeys) {
 | 
| 418 |  | -    let pubKeys;
 | 
| 419 |  | -    try {
 | 
| 420 |  | -      pubKeys = await this.#makeRequest("pubkeys", []);
 | 
| 421 |  | -    } catch (error) {
 | 
| 422 |  | -      lazy.logger.debug("Failed to get pubkeys", error);
 | 
| 423 |  | -      // Make the next call try again.
 | 
| 424 |  | -      this.#pubKeyPromise = null;
 | 
| 425 |  | -      if (!this.#pubKeys) {
 | 
| 426 |  | -        throw error;
 | 
| 427 |  | -      }
 | 
| 428 |  | -      return;
 | 
| 429 |  | -    }
 | 
|  | 418 | +  async #updatePubkeys() {
 | 
|  | 419 | +    let pubKeys = await this.#makeRequest("pubkeys", null);
 | 
| 430 | 420 |      const prevKeys = this.#pubKeys;
 | 
| 431 | 421 |      if (prevKeys !== null) {
 | 
| 432 | 422 |        // check if the lox pubkeys have changed and update the lox
 | 
| 433 |  | -      // credentials if so
 | 
| 434 |  | -      let lox_cred_req;
 | 
| 435 |  | -      try {
 | 
| 436 |  | -        lox_cred_req = JSON.parse(
 | 
| 437 |  | -          lazy.check_lox_pubkeys_update(
 | 
| 438 |  | -            JSON.stringify(pubKeys),
 | 
| 439 |  | -            prevkeys,
 | 
| 440 |  | -            this.#getCredentials(this.#activeLoxId)
 | 
| 441 |  | -          )
 | 
| 442 |  | -        );
 | 
| 443 |  | -      } catch (error) {
 | 
| 444 |  | -        lazy.logger.debug("Check lox pubkey update failed", error);
 | 
| 445 |  | -        // Make the next call try again.
 | 
| 446 |  | -        this.#pubKeyPromise = null;
 | 
| 447 |  | -        return;
 | 
| 448 |  | -      }
 | 
| 449 |  | -      if (lox_cred_req.updated) {
 | 
|  | 423 | +      // credentials if so.
 | 
|  | 424 | +      //
 | 
|  | 425 | +      // The UpdateCredOption rust struct serializes to "req" rather than
 | 
|  | 426 | +      // "request".
 | 
|  | 427 | +      const { updated, req: request } = JSON.parse(
 | 
|  | 428 | +        lazy.check_lox_pubkeys_update(
 | 
|  | 429 | +          pubKeys,
 | 
|  | 430 | +          prevKeys,
 | 
|  | 431 | +          this.#getCredentials(this.#activeLoxId)
 | 
|  | 432 | +        )
 | 
|  | 433 | +      );
 | 
|  | 434 | +      if (updated) {
 | 
|  | 435 | +        // Try update credentials.
 | 
|  | 436 | +        // NOTE: This should be re-callable if any step fails.
 | 
|  | 437 | +        // TODO: Verify this.
 | 
| 450 | 438 |          lazy.logger.debug(
 | 
| 451 | 439 |            `Lox pubkey updated, update Lox credential "${this.#activeLoxId}"`
 | 
| 452 | 440 |          );
 | 
| 453 |  | -        let response;
 | 
| 454 |  | -        try {
 | 
| 455 |  | -          // TODO: If this call doesn't succeed due to a networking error, the Lox
 | 
| 456 |  | -          // credential may be in an unusable state (spent but not updated)
 | 
| 457 |  | -          // until this request can be completed successfully (and until Lox
 | 
| 458 |  | -          // is refactored to send repeat responses:
 | 
| 459 |  | -          // https://gitlab.torproject.org/tpo/anti-censorship/lox/-/issues/74)
 | 
| 460 |  | -          response = await this.#makeRequest("updatecred", lox_cred_req.req);
 | 
| 461 |  | -        } catch (error) {
 | 
| 462 |  | -          lazy.logger.debug("Lox cred update failed.", error);
 | 
| 463 |  | -          // Make the next call try again.
 | 
| 464 |  | -          this.#pubKeyPromise = null;
 | 
| 465 |  | -          return;
 | 
| 466 |  | -        }
 | 
| 467 |  | -        if (response.hasOwnProperty("error")) {
 | 
| 468 |  | -          lazy.logger.error(response.error);
 | 
| 469 |  | -          this.#pubKeyPromise = null;
 | 
| 470 |  | -          lazy.logger.debug(
 | 
| 471 |  | -            `Error response to Lox pubkey update request: "${response.error}", reverting to old pubkeys`
 | 
| 472 |  | -          );
 | 
| 473 |  | -          return;
 | 
| 474 |  | -        }
 | 
| 475 |  | -        let cred;
 | 
| 476 |  | -        try {
 | 
| 477 |  | -          cred = lazy.handle_update_cred(
 | 
| 478 |  | -            lox_cred_req.req,
 | 
| 479 |  | -            JSON.stringify(response),
 | 
| 480 |  | -            pubKeys
 | 
| 481 |  | -          );
 | 
| 482 |  | -        } catch (error) {
 | 
| 483 |  | -          lazy.logger.debug("Unable to handle updated Lox cred", error);
 | 
| 484 |  | -          // Make the next call try again.
 | 
| 485 |  | -          this.#pubKeyPromise = null;
 | 
| 486 |  | -          return;
 | 
| 487 |  | -        }
 | 
|  | 441 | +        // TODO: If this call doesn't succeed due to a networking error, the Lox
 | 
|  | 442 | +        // credential may be in an unusable state (spent but not updated)
 | 
|  | 443 | +        // until this request can be completed successfully (and until Lox
 | 
|  | 444 | +        // is refactored to send repeat responses:
 | 
|  | 445 | +        // https://gitlab.torproject.org/tpo/anti-censorship/lox/-/issues/74)
 | 
|  | 446 | +        let response = await this.#makeRequest("updatecred", request);
 | 
|  | 447 | +        let cred = lazy.handle_update_cred(request, response, pubKeys);
 | 
| 488 | 448 |          this.#changeCredentials(this.#activeLoxId, cred);
 | 
| 489 | 449 |        }
 | 
| 490 | 450 |      }
 | 
| 491 | 451 |      // If we arrive here we haven't had other errors before, we can actually
 | 
| 492 | 452 |      // store the new public key.
 | 
| 493 |  | -    this.#pubKeys = JSON.stringify(pubKeys);
 | 
|  | 453 | +    this.#pubKeys = pubKeys;
 | 
| 494 | 454 |      this.#store();
 | 
| 495 | 455 |    }
 | 
| 496 | 456 |  
 | 
| ... | ... | @@ -498,16 +458,24 @@ class LoxImpl { | 
| 498 | 458 |      // FIXME: We are always refetching #pubKeys, #encTable and #constants once
 | 
| 499 | 459 |      // per session, but they may change more frequently. tor-browser#42502
 | 
| 500 | 460 |      if (this.#pubKeyPromise === null) {
 | 
| 501 |  | -      this.#pubKeyPromise = this.#updatePubkeys();
 | 
|  | 461 | +      this.#pubKeyPromise = this.#updatePubkeys().catch(error => {
 | 
|  | 462 | +        lazy.logger.debug("Failed to update pubKeys", error);
 | 
|  | 463 | +        // Try again with the next call.
 | 
|  | 464 | +        this.#pubKeyPromise = null;
 | 
|  | 465 | +        if (!this.#pubKeys) {
 | 
|  | 466 | +          // Re-throw if we have no pubKeys value for the caller.
 | 
|  | 467 | +          throw error;
 | 
|  | 468 | +        }
 | 
|  | 469 | +      });
 | 
| 502 | 470 |      }
 | 
| 503 | 471 |      await this.#pubKeyPromise;
 | 
| 504 | 472 |    }
 | 
| 505 | 473 |  
 | 
| 506 | 474 |    async #getEncTable() {
 | 
| 507 | 475 |      if (this.#encTablePromise === null) {
 | 
| 508 |  | -      this.#encTablePromise = this.#makeRequest("reachability", [])
 | 
|  | 476 | +      this.#encTablePromise = this.#makeRequest("reachability", null)
 | 
| 509 | 477 |          .then(encTable => {
 | 
| 510 |  | -          this.#encTable = JSON.stringify(encTable);
 | 
|  | 478 | +          this.#encTable = encTable;
 | 
| 511 | 479 |            this.#store();
 | 
| 512 | 480 |          })
 | 
| 513 | 481 |          .catch(error => {
 | 
| ... | ... | @@ -526,10 +494,10 @@ class LoxImpl { | 
| 526 | 494 |    async #getConstants() {
 | 
| 527 | 495 |      if (this.#constantsPromise === null) {
 | 
| 528 | 496 |        // Try to update first, but if that doesn't work fall back to stored data
 | 
| 529 |  | -      this.#constantsPromise = this.#makeRequest("constants", [])
 | 
|  | 497 | +      this.#constantsPromise = this.#makeRequest("constants", null)
 | 
| 530 | 498 |          .then(constants => {
 | 
| 531 | 499 |            const prevValue = this.#constants;
 | 
| 532 |  | -          this.#constants = JSON.stringify(constants);
 | 
|  | 500 | +          this.#constants = constants;
 | 
| 533 | 501 |            this.#store();
 | 
| 534 | 502 |            if (prevValue !== this.#constants) {
 | 
| 535 | 503 |              Services.obs.notifyObservers(null, LoxTopics.UpdateNextUnlock);
 | 
| ... | ... | @@ -579,7 +547,6 @@ class LoxImpl { | 
| 579 | 547 |     */
 | 
| 580 | 548 |    async #backgroundTasks() {
 | 
| 581 | 549 |      this.#assertInitialized();
 | 
| 582 |  | -    let addedEvent = false;
 | 
| 583 | 550 |      // Only run background tasks for the active lox ID.
 | 
| 584 | 551 |      const loxId = this.#activeLoxId;
 | 
| 585 | 552 |      if (!loxId) {
 | 
| ... | ... | @@ -591,37 +558,39 @@ class LoxImpl { | 
| 591 | 558 |      // this should catch key rotations (ideally some days) prior to the next
 | 
| 592 | 559 |      // credential update
 | 
| 593 | 560 |      await this.#getPubKeys();
 | 
|  | 561 | +    let levelup = false;
 | 
| 594 | 562 |      try {
 | 
| 595 |  | -      const levelup = await this.#attemptUpgrade(loxId);
 | 
| 596 |  | -      if (levelup) {
 | 
| 597 |  | -        const level = this.#getLevel(loxId);
 | 
| 598 |  | -        const newEvent = {
 | 
| 599 |  | -          type: "levelup",
 | 
| 600 |  | -          newlevel: level,
 | 
| 601 |  | -        };
 | 
| 602 |  | -        this.#events.push(newEvent);
 | 
| 603 |  | -        this.#store();
 | 
| 604 |  | -        addedEvent = true;
 | 
| 605 |  | -      }
 | 
| 606 |  | -    } catch (err) {
 | 
| 607 |  | -      lazy.logger.error(err);
 | 
|  | 563 | +      levelup = await this.#attemptUpgrade(loxId);
 | 
|  | 564 | +    } catch (error) {
 | 
|  | 565 | +      lazy.logger.error(error);
 | 
| 608 | 566 |      }
 | 
|  | 567 | +    if (levelup) {
 | 
|  | 568 | +      const level = this.#getLevel(loxId);
 | 
|  | 569 | +      const newEvent = {
 | 
|  | 570 | +        type: "levelup",
 | 
|  | 571 | +        newlevel: level,
 | 
|  | 572 | +      };
 | 
|  | 573 | +      this.#events.push(newEvent);
 | 
|  | 574 | +      this.#store();
 | 
|  | 575 | +    }
 | 
|  | 576 | +
 | 
|  | 577 | +    let leveldown = false;
 | 
| 609 | 578 |      try {
 | 
| 610 |  | -      const leveldown = await this.#blockageMigration(loxId);
 | 
| 611 |  | -      if (leveldown) {
 | 
| 612 |  | -        let level = this.#getLevel(loxId);
 | 
| 613 |  | -        const newEvent = {
 | 
| 614 |  | -          type: "blockage",
 | 
| 615 |  | -          newlevel: level,
 | 
| 616 |  | -        };
 | 
| 617 |  | -        this.#events.push(newEvent);
 | 
| 618 |  | -        this.#store();
 | 
| 619 |  | -        addedEvent = true;
 | 
| 620 |  | -      }
 | 
| 621 |  | -    } catch (err) {
 | 
| 622 |  | -      lazy.logger.error(err);
 | 
|  | 579 | +      leveldown = await this.#blockageMigration(loxId);
 | 
|  | 580 | +    } catch (error) {
 | 
|  | 581 | +      lazy.logger.error(error);
 | 
|  | 582 | +    }
 | 
|  | 583 | +    if (leveldown) {
 | 
|  | 584 | +      let level = this.#getLevel(loxId);
 | 
|  | 585 | +      const newEvent = {
 | 
|  | 586 | +        type: "blockage",
 | 
|  | 587 | +        newlevel: level,
 | 
|  | 588 | +      };
 | 
|  | 589 | +      this.#events.push(newEvent);
 | 
|  | 590 | +      this.#store();
 | 
| 623 | 591 |      }
 | 
| 624 |  | -    if (addedEvent) {
 | 
|  | 592 | +
 | 
|  | 593 | +    if (levelup || leveldown) {
 | 
| 625 | 594 |        Services.obs.notifyObservers(null, LoxTopics.UpdateEvents);
 | 
| 626 | 595 |      }
 | 
| 627 | 596 |    }
 | 
| ... | ... | @@ -708,7 +677,7 @@ class LoxImpl { | 
| 708 | 677 |    // to issue open invitations for Lox bridges.
 | 
| 709 | 678 |    async requestOpenInvite() {
 | 
| 710 | 679 |      this.#assertInitialized();
 | 
| 711 |  | -    let invite = await this.#makeRequest("invite", []);
 | 
|  | 680 | +    let invite = JSON.parse(await this.#makeRequest("invite", null));
 | 
| 712 | 681 |      lazy.logger.debug(invite);
 | 
| 713 | 682 |      return invite;
 | 
| 714 | 683 |    }
 | 
| ... | ... | @@ -725,23 +694,20 @@ class LoxImpl { | 
| 725 | 694 |      // It's fine to get pubkey here without a delay since the user will not have a Lox
 | 
| 726 | 695 |      // credential yet
 | 
| 727 | 696 |      await this.#getPubKeys();
 | 
|  | 697 | +    // NOTE: We currently only handle "open invites".
 | 
|  | 698 | +    // "trusted invites" are not yet supported. tor-browser#42974.
 | 
| 728 | 699 |      let request = await lazy.open_invite(JSON.parse(invite).invite);
 | 
| 729 |  | -    let response = await this.#makeRequest(
 | 
| 730 |  | -      "openreq",
 | 
| 731 |  | -      JSON.parse(request).request
 | 
| 732 |  | -    );
 | 
| 733 |  | -    lazy.logger.debug("openreq response: ", response);
 | 
| 734 |  | -    if (response.hasOwnProperty("error")) {
 | 
| 735 |  | -      throw new LoxError(
 | 
| 736 |  | -        `Error response to "openreq": ${response.error}`,
 | 
| 737 |  | -        LoxError.BadInvite
 | 
| 738 |  | -      );
 | 
|  | 700 | +    let response;
 | 
|  | 701 | +    try {
 | 
|  | 702 | +      response = await this.#makeRequest("openreq", request);
 | 
|  | 703 | +    } catch (error) {
 | 
|  | 704 | +      if (error instanceof LoxError && error.code === LoxError.ErrorResponse) {
 | 
|  | 705 | +        throw new LoxError("Error response to openreq", LoxError.BadInvite);
 | 
|  | 706 | +      } else {
 | 
|  | 707 | +        throw error;
 | 
|  | 708 | +      }
 | 
| 739 | 709 |      }
 | 
| 740 |  | -    let cred = lazy.handle_new_lox_credential(
 | 
| 741 |  | -      request,
 | 
| 742 |  | -      JSON.stringify(response),
 | 
| 743 |  | -      this.#pubKeys
 | 
| 744 |  | -    );
 | 
|  | 710 | +    let cred = lazy.handle_new_lox_credential(request, response, this.#pubKeys);
 | 
| 745 | 711 |      // Generate an id that is not already in the #credentials map.
 | 
| 746 | 712 |      let loxId;
 | 
| 747 | 713 |      do {
 | 
| ... | ... | @@ -795,31 +761,32 @@ class LoxImpl { | 
| 795 | 761 |        throw new LoxError(`Cannot generate invites at level ${level}`);
 | 
| 796 | 762 |      }
 | 
| 797 | 763 |      let request = lazy.issue_invite(
 | 
| 798 |  | -      JSON.stringify(this.#getCredentials(loxId)),
 | 
|  | 764 | +      this.#getCredentials(loxId),
 | 
| 799 | 765 |        this.#encTable,
 | 
| 800 | 766 |        this.#pubKeys
 | 
| 801 | 767 |      );
 | 
| 802 |  | -    let response = await this.#makeRequest(
 | 
| 803 |  | -      "issueinvite",
 | 
| 804 |  | -      JSON.parse(request).request
 | 
| 805 |  | -    );
 | 
| 806 |  | -    if (response.hasOwnProperty("error")) {
 | 
| 807 |  | -      lazy.logger.error(response.error);
 | 
| 808 |  | -      throw new LoxError(`Error response to "issueinvite": ${response.error}`);
 | 
| 809 |  | -    } else {
 | 
| 810 |  | -      const invite = lazy.prepare_invite(response);
 | 
| 811 |  | -      this.#invites.push(invite);
 | 
| 812 |  | -      // cap length of stored invites
 | 
| 813 |  | -      if (this.#invites.len > 50) {
 | 
| 814 |  | -        this.#invites.shift();
 | 
| 815 |  | -      }
 | 
| 816 |  | -      this.#store();
 | 
| 817 |  | -      this.#changeCredentials(loxId, response);
 | 
| 818 |  | -      Services.obs.notifyObservers(null, LoxTopics.NewInvite);
 | 
| 819 |  | -      // Return a copy.
 | 
| 820 |  | -      // Right now invite is just a string, but that might change in the future.
 | 
| 821 |  | -      return structuredClone(invite);
 | 
|  | 768 | +    let response = await this.#makeRequest("issueinvite", request);
 | 
|  | 769 | +    // TODO: Do we ever expect handle_issue_invite to fail (beyond
 | 
|  | 770 | +    // implementation bugs)?
 | 
|  | 771 | +    // TODO: What happens if #pubkeys for `issue_invite` differs from the value
 | 
|  | 772 | +    // when calling `handle_issue_invite`? Should we cache the value at the
 | 
|  | 773 | +    // start of this method?
 | 
|  | 774 | +    let cred = lazy.handle_issue_invite(request, response, this.#pubKeys);
 | 
|  | 775 | +
 | 
|  | 776 | +    // Store the new credentials as a priority.
 | 
|  | 777 | +    this.#changeCredentials(loxId, cred);
 | 
|  | 778 | +
 | 
|  | 779 | +    const invite = lazy.prepare_invite(cred);
 | 
|  | 780 | +    this.#invites.push(invite);
 | 
|  | 781 | +    // cap length of stored invites
 | 
|  | 782 | +    if (this.#invites.len > 50) {
 | 
|  | 783 | +      this.#invites.shift();
 | 
| 822 | 784 |      }
 | 
|  | 785 | +    this.#store();
 | 
|  | 786 | +    Services.obs.notifyObservers(null, LoxTopics.NewInvite);
 | 
|  | 787 | +    // Return a copy.
 | 
|  | 788 | +    // Right now invite is just a string, but that might change in the future.
 | 
|  | 789 | +    return structuredClone(invite);
 | 
| 823 | 790 |    }
 | 
| 824 | 791 |  
 | 
| 825 | 792 |    /**
 | 
| ... | ... | @@ -845,15 +812,13 @@ class LoxImpl { | 
| 845 | 812 |        return false;
 | 
| 846 | 813 |      }
 | 
| 847 | 814 |      let response = await this.#makeRequest("checkblockage", request);
 | 
| 848 |  | -    if (response.hasOwnProperty("error")) {
 | 
| 849 |  | -      lazy.logger.error(response.error);
 | 
| 850 |  | -      throw new LoxError(
 | 
| 851 |  | -        `Error response to "checkblockage": ${response.error}`
 | 
| 852 |  | -      );
 | 
| 853 |  | -    }
 | 
|  | 815 | +    // NOTE: If a later method fails, we should be ok to re-call "checkblockage"
 | 
|  | 816 | +    // from the Lox authority. So there shouldn't be any adverse side effects to
 | 
|  | 817 | +    // loosing migrationCred.
 | 
|  | 818 | +    // TODO: Confirm this is safe to lose.
 | 
| 854 | 819 |      const migrationCred = lazy.handle_check_blockage(
 | 
| 855 | 820 |        this.#getCredentials(loxId),
 | 
| 856 |  | -      JSON.stringify(response)
 | 
|  | 821 | +      response
 | 
| 857 | 822 |      );
 | 
| 858 | 823 |      request = lazy.blockage_migration(
 | 
| 859 | 824 |        this.#getCredentials(loxId),
 | 
| ... | ... | @@ -861,26 +826,21 @@ class LoxImpl { | 
| 861 | 826 |        this.#pubKeys
 | 
| 862 | 827 |      );
 | 
| 863 | 828 |      response = await this.#makeRequest("blockagemigration", request);
 | 
| 864 |  | -    if (response.hasOwnProperty("error")) {
 | 
| 865 |  | -      lazy.logger.error(response.error);
 | 
| 866 |  | -      throw new LoxError(
 | 
| 867 |  | -        `Error response to "blockagemigration": ${response.error}`
 | 
| 868 |  | -      );
 | 
| 869 |  | -    }
 | 
| 870 | 829 |      const cred = lazy.handle_blockage_migration(
 | 
| 871 | 830 |        this.#getCredentials(loxId),
 | 
| 872 |  | -      JSON.stringify(response),
 | 
|  | 831 | +      response,
 | 
| 873 | 832 |        this.#pubKeys
 | 
| 874 | 833 |      );
 | 
| 875 | 834 |      this.#changeCredentials(loxId, cred);
 | 
| 876 | 835 |      return true;
 | 
| 877 | 836 |    }
 | 
| 878 | 837 |  
 | 
| 879 |  | -  /** Attempts to upgrade the currently saved Lox credential.
 | 
| 880 |  | -   *  If an upgrade is available, save an event in the event list.
 | 
|  | 838 | +  /**
 | 
|  | 839 | +   * Attempts to upgrade the currently saved Lox credential.
 | 
|  | 840 | +   * If an upgrade is available, save an event in the event list.
 | 
| 881 | 841 |     *
 | 
| 882 |  | -   *  @param {string} loxId Lox ID
 | 
| 883 |  | -   *  @returns {boolean} Whether a levelup event occurred.
 | 
|  | 842 | +   * @param {string} loxId Lox ID
 | 
|  | 843 | +   * @returns {boolean} Whether the credential was successfully migrated.
 | 
| 884 | 844 |     */
 | 
| 885 | 845 |    async #attemptUpgrade(loxId) {
 | 
| 886 | 846 |      await this.#getEncTable();
 | 
| ... | ... | @@ -895,16 +855,18 @@ class LoxImpl { | 
| 895 | 855 |        this.#encTable,
 | 
| 896 | 856 |        this.#pubKeys
 | 
| 897 | 857 |      );
 | 
| 898 |  | -    const response = await this.#makeRequest("levelup", request);
 | 
| 899 |  | -    if (response.hasOwnProperty("error")) {
 | 
| 900 |  | -      lazy.logger.error(response.error);
 | 
| 901 |  | -      throw new LoxError(`Error response to "levelup": ${response.error}`);
 | 
|  | 858 | +    let response;
 | 
|  | 859 | +    try {
 | 
|  | 860 | +      response = await this.#makeRequest("levelup", request);
 | 
|  | 861 | +    } catch (error) {
 | 
|  | 862 | +      if (error instanceof LoxError && error.code === LoxError.ErrorResponse) {
 | 
|  | 863 | +        // Not an error.
 | 
|  | 864 | +        lazy.logger.debug("Not ready for level up", error);
 | 
|  | 865 | +        return false;
 | 
|  | 866 | +      }
 | 
|  | 867 | +      throw error;
 | 
| 902 | 868 |      }
 | 
| 903 |  | -    const cred = lazy.handle_level_up(
 | 
| 904 |  | -      request,
 | 
| 905 |  | -      JSON.stringify(response),
 | 
| 906 |  | -      this.#pubKeys
 | 
| 907 |  | -    );
 | 
|  | 869 | +    const cred = lazy.handle_level_up(request, response, this.#pubKeys);
 | 
| 908 | 870 |      this.#changeCredentials(loxId, cred);
 | 
| 909 | 871 |      return true;
 | 
| 910 | 872 |    }
 | 
| ... | ... | @@ -922,76 +884,40 @@ class LoxImpl { | 
| 922 | 884 |        this.#getPubKeys();
 | 
| 923 | 885 |        return false;
 | 
| 924 | 886 |      }
 | 
| 925 |  | -    let request, response;
 | 
|  | 887 | +    let request;
 | 
| 926 | 888 |      try {
 | 
| 927 | 889 |        request = lazy.trust_promotion(
 | 
| 928 | 890 |          this.#getCredentials(loxId),
 | 
| 929 | 891 |          this.#pubKeys
 | 
| 930 | 892 |        );
 | 
| 931 | 893 |      } catch (err) {
 | 
|  | 894 | +      // This function is called routinely during the background tasks without
 | 
|  | 895 | +      // previous checks on whether an upgrade is possible, so it is expected to
 | 
|  | 896 | +      // fail with a certain frequency. Therefore, do not relay the error to the
 | 
|  | 897 | +      // caller and just log the message for debugging.
 | 
| 932 | 898 |        lazy.logger.debug("Not ready to upgrade", err);
 | 
| 933 | 899 |        return false;
 | 
| 934 | 900 |      }
 | 
| 935 |  | -    try {
 | 
| 936 |  | -      response = await this.#makeRequest(
 | 
| 937 |  | -        "trustpromo",
 | 
| 938 |  | -        JSON.parse(request).request
 | 
| 939 |  | -      );
 | 
| 940 |  | -    } catch (err) {
 | 
| 941 |  | -      lazy.logger.error("Failed trust promotion", err);
 | 
| 942 |  | -      return false;
 | 
| 943 |  | -    }
 | 
| 944 |  | -    if (response.hasOwnProperty("error")) {
 | 
| 945 |  | -      lazy.logger.error("Error response from trustpromo", response.error);
 | 
| 946 |  | -      return false;
 | 
| 947 |  | -    }
 | 
| 948 |  | -    lazy.logger.debug("Got promotion cred", response, request);
 | 
| 949 |  | -    let promoCred;
 | 
| 950 |  | -    try {
 | 
| 951 |  | -      promoCred = lazy.handle_trust_promotion(
 | 
| 952 |  | -        request,
 | 
| 953 |  | -        JSON.stringify(response)
 | 
| 954 |  | -      );
 | 
| 955 |  | -      lazy.logger.debug("Formatted promotion cred");
 | 
| 956 |  | -    } catch (err) {
 | 
| 957 |  | -      lazy.logger.error(
 | 
| 958 |  | -        "Unable to handle trustpromo response properly",
 | 
| 959 |  | -        response.error
 | 
| 960 |  | -      );
 | 
| 961 |  | -      return false;
 | 
| 962 |  | -    }
 | 
| 963 |  | -    try {
 | 
| 964 |  | -      request = lazy.trust_migration(
 | 
| 965 |  | -        this.#getCredentials(loxId),
 | 
| 966 |  | -        promoCred,
 | 
| 967 |  | -        this.#pubKeys
 | 
| 968 |  | -      );
 | 
| 969 |  | -      lazy.logger.debug("Formatted migration request");
 | 
| 970 |  | -    } catch (err) {
 | 
| 971 |  | -      lazy.logger.error("Failed to generate trust migration request", err);
 | 
| 972 |  | -      return false;
 | 
| 973 |  | -    }
 | 
| 974 |  | -    try {
 | 
| 975 |  | -      response = await this.#makeRequest(
 | 
| 976 |  | -        "trustmig",
 | 
| 977 |  | -        JSON.parse(request).request
 | 
| 978 |  | -      );
 | 
| 979 |  | -    } catch (err) {
 | 
| 980 |  | -      lazy.logger.error("Failed trust migration", err);
 | 
| 981 |  | -      return false;
 | 
| 982 |  | -    }
 | 
| 983 |  | -    if (response.hasOwnProperty("error")) {
 | 
| 984 |  | -      lazy.logger.error("Error response from trustmig", response.error);
 | 
| 985 |  | -      return false;
 | 
| 986 |  | -    }
 | 
| 987 |  | -    lazy.logger.debug("Got new credential");
 | 
| 988 |  | -    let cred;
 | 
| 989 |  | -    try {
 | 
| 990 |  | -      cred = lazy.handle_trust_migration(request, response);
 | 
| 991 |  | -    } catch (err) {
 | 
| 992 |  | -      lazy.logger.error("Failed to handle response from trustmig", err);
 | 
| 993 |  | -      return false;
 | 
| 994 |  | -    }
 | 
|  | 901 | +
 | 
|  | 902 | +    let response = await this.#makeRequest("trustpromo", request);
 | 
|  | 903 | +    // FIXME: Store response to "trustpromo" in case handle_trust_promotion
 | 
|  | 904 | +    // or "trustmig" fails. The Lox authority will not accept a re-request
 | 
|  | 905 | +    // to "trustpromo" with the same credentials.
 | 
|  | 906 | +    let promoCred = lazy.handle_trust_promotion(request, response);
 | 
|  | 907 | +    lazy.logger.debug("Formatted promotion cred: ", promoCred);
 | 
|  | 908 | +
 | 
|  | 909 | +    request = lazy.trust_migration(
 | 
|  | 910 | +      this.#getCredentials(loxId),
 | 
|  | 911 | +      promoCred,
 | 
|  | 912 | +      this.#pubKeys
 | 
|  | 913 | +    );
 | 
|  | 914 | +    response = await this.#makeRequest("trustmig", request);
 | 
|  | 915 | +    lazy.logger.debug("Got new credential: ", response);
 | 
|  | 916 | +
 | 
|  | 917 | +    // FIXME: Store response to "trustmig" in case handle_trust_migration
 | 
|  | 918 | +    // fails. The Lox authority will not accept a re-request to "trustmig" with
 | 
|  | 919 | +    // the same credentials.
 | 
|  | 920 | +    let cred = lazy.handle_trust_migration(request, response);
 | 
| 995 | 921 |      this.#changeCredentials(loxId, cred);
 | 
| 996 | 922 |      return true;
 | 
| 997 | 923 |    }
 | 
| ... | ... | @@ -1079,38 +1005,47 @@ class LoxImpl { | 
| 1079 | 1005 |      };
 | 
| 1080 | 1006 |    }
 | 
| 1081 | 1007 |  
 | 
| 1082 |  | -  async #makeRequest(procedure, args) {
 | 
|  | 1008 | +  /**
 | 
|  | 1009 | +   * Fetch from the Lox authority.
 | 
|  | 1010 | +   *
 | 
|  | 1011 | +   * @param {string} procedure - The request endpoint.
 | 
|  | 1012 | +   * @param {string} body - The arguments to send in the body, if any.
 | 
|  | 1013 | +   *
 | 
|  | 1014 | +   * @returns {string} - The response body.
 | 
|  | 1015 | +   */
 | 
|  | 1016 | +  async #fetch(procedure, body) {
 | 
| 1083 | 1017 |      // TODO: Customize to for Lox
 | 
| 1084 |  | -    const serviceUrl = "https://lox.torproject.org";
 | 
| 1085 |  | -    const url = `${serviceUrl}/${procedure}`;
 | 
|  | 1018 | +    const url = `https://lox.torproject.org/${procedure}`;
 | 
|  | 1019 | +    const method = "POST";
 | 
|  | 1020 | +    const contentType = "application/vnd.api+json";
 | 
| 1086 | 1021 |  
 | 
| 1087 | 1022 |      if (lazy.TorConnect.state === lazy.TorConnectState.Bootstrapped) {
 | 
| 1088 | 1023 |        let request;
 | 
| 1089 | 1024 |        try {
 | 
| 1090 | 1025 |          request = await fetch(url, {
 | 
| 1091 |  | -          method: "POST",
 | 
| 1092 |  | -          headers: {
 | 
| 1093 |  | -            "Content-Type": "application/vnd.api+json",
 | 
| 1094 |  | -          },
 | 
| 1095 |  | -          body: JSON.stringify(args),
 | 
|  | 1026 | +          method,
 | 
|  | 1027 | +          headers: { "Content-Type": contentType },
 | 
|  | 1028 | +          body,
 | 
| 1096 | 1029 |          });
 | 
| 1097 | 1030 |        } catch (error) {
 | 
| 1098 |  | -        lazy.logger.debug("fetch fail", url, args, error);
 | 
|  | 1031 | +        lazy.logger.debug("fetch fail", url, body, error);
 | 
| 1099 | 1032 |          throw new LoxError(
 | 
| 1100 | 1033 |            `fetch "${procedure}" from Lox authority failed: ${error?.message}`,
 | 
| 1101 | 1034 |            LoxError.LoxServerUnreachable
 | 
| 1102 | 1035 |          );
 | 
| 1103 | 1036 |        }
 | 
| 1104 | 1037 |        if (!request.ok) {
 | 
| 1105 |  | -        lazy.logger.debug("fetch response", url, args, request);
 | 
|  | 1038 | +        lazy.logger.debug("fetch response", url, body, request);
 | 
| 1106 | 1039 |          // Do not treat as a LoxServerUnreachable type.
 | 
| 1107 | 1040 |          throw new LoxError(
 | 
| 1108 | 1041 |            `Lox authority responded to "${procedure}" with ${request.status}: ${request.statusText}`
 | 
| 1109 | 1042 |          );
 | 
| 1110 | 1043 |        }
 | 
| 1111 |  | -      return request.json();
 | 
|  | 1044 | +      return request.text();
 | 
| 1112 | 1045 |      }
 | 
| 1113 | 1046 |  
 | 
|  | 1047 | +    // TODO: Only make domain fronted requests with user permission.
 | 
|  | 1048 | +    // tor-browser#42606.
 | 
| 1114 | 1049 |      if (this.#domainFrontedRequests === null) {
 | 
| 1115 | 1050 |        this.#domainFrontedRequests = new Promise((resolve, reject) => {
 | 
| 1116 | 1051 |          // TODO: Customize to the values for Lox
 | 
| ... | ... | @@ -1129,9 +1064,9 @@ class LoxImpl { | 
| 1129 | 1064 |      }
 | 
| 1130 | 1065 |      const builder = await this.#domainFrontedRequests;
 | 
| 1131 | 1066 |      try {
 | 
| 1132 |  | -      return await builder.buildPostRequest(url, args);
 | 
|  | 1067 | +      return await builder.buildRequest(url, { method, contentType, body });
 | 
| 1133 | 1068 |      } catch (error) {
 | 
| 1134 |  | -      lazy.logger.debug("Domain front request fail", url, args, error);
 | 
|  | 1069 | +      lazy.logger.debug("Domain front request fail", url, body, error);
 | 
| 1135 | 1070 |        if (error instanceof lazy.DomainFrontRequestNetworkError) {
 | 
| 1136 | 1071 |          throw new LoxError(
 | 
| 1137 | 1072 |            `Domain front fetch "${procedure}" from Lox authority failed: ${error?.message}`,
 | 
| ... | ... | @@ -1149,6 +1084,37 @@ class LoxImpl { | 
| 1149 | 1084 |        );
 | 
| 1150 | 1085 |      }
 | 
| 1151 | 1086 |    }
 | 
|  | 1087 | +
 | 
|  | 1088 | +  /**
 | 
|  | 1089 | +   * Make a request to the lox authority, check for an error response, and
 | 
|  | 1090 | +   * convert it to a string.
 | 
|  | 1091 | +   *
 | 
|  | 1092 | +   * @param {string} procedure - The request endpoint.
 | 
|  | 1093 | +   * @param {?string} request - The request data, as a JSON string containing a
 | 
|  | 1094 | +   *   "request" field. Or `null` to send no data.
 | 
|  | 1095 | +   *
 | 
|  | 1096 | +   * @returns {string} - The stringified JSON response.
 | 
|  | 1097 | +   */
 | 
|  | 1098 | +  async #makeRequest(procedure, request) {
 | 
|  | 1099 | +    // Verify that the response is valid json, by parsing.
 | 
|  | 1100 | +    const jsonResponse = JSON.parse(
 | 
|  | 1101 | +      await this.#fetch(
 | 
|  | 1102 | +        procedure,
 | 
|  | 1103 | +        request ? JSON.stringify(JSON.parse(request).request) : ""
 | 
|  | 1104 | +      )
 | 
|  | 1105 | +    );
 | 
|  | 1106 | +    lazy.logger.debug(`${procedure} response:`, jsonResponse);
 | 
|  | 1107 | +    if (Object.hasOwn(jsonResponse, "error")) {
 | 
|  | 1108 | +      // TODO: Figure out if any of the "error" responses should be treated as
 | 
|  | 1109 | +      // an error. I.e. which of the procedures have soft failures and hard
 | 
|  | 1110 | +      // failures.
 | 
|  | 1111 | +      throw LoxError(
 | 
|  | 1112 | +        `Error response to ${procedure}: ${jsonResponse.error}`,
 | 
|  | 1113 | +        LoxError.ErrorResponse
 | 
|  | 1114 | +      );
 | 
|  | 1115 | +    }
 | 
|  | 1116 | +    return JSON.stringify(jsonResponse);
 | 
|  | 1117 | +  }
 | 
| 1152 | 1118 |  }
 | 
| 1153 | 1119 |  
 | 
| 1154 | 1120 |  export const Lox = new LoxImpl(); |