Source code

Revision control

Copy as Markdown

Other Tools

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
import {
COMMAND_SENDTAB,
COMMAND_SENDTAB_TAIL,
COMMAND_CLOSETAB,
COMMAND_CLOSETAB_TAIL,
SCOPE_OLD_SYNC,
log,
import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
import { Observers } from "resource://services-common/observers.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"INVALID_SHAREABLE_SCHEMES",
"services.sync.engine.tabs.filteredSchemes",
"",
null,
val => {
return new Set(val.split("|"));
}
);
export class FxAccountsCommands {
constructor(fxAccountsInternal) {
this._fxai = fxAccountsInternal;
this.sendTab = new SendTab(this, fxAccountsInternal);
this.closeTab = new CloseRemoteTab(this, fxAccountsInternal);
this._invokeRateLimitExpiry = 0;
}
async availableCommands() {
// Invalid keys usually means the account is not verified yet.
const encryptedSendTabKeys = await this.sendTab.getEncryptedSendTabKeys();
let commands = {};
if (encryptedSendTabKeys) {
commands[COMMAND_SENDTAB] = encryptedSendTabKeys;
}
// Close Tab is still a worked-on feature, so we should not broadcast it widely yet
let closeTabEnabled = Services.prefs.getBoolPref(
"identity.fxaccounts.commands.remoteTabManagement.enabled",
false
);
if (closeTabEnabled) {
const encryptedCloseTabKeys =
await this.closeTab.getEncryptedCloseTabKeys();
if (encryptedCloseTabKeys) {
commands[COMMAND_CLOSETAB] = encryptedCloseTabKeys;
}
}
return commands;
}
async invoke(command, device, payload) {
const { sessionToken } = await this._fxai.getUserAccountData([
"sessionToken",
]);
const client = this._fxai.fxAccountsClient;
const now = Date.now();
if (now < this._invokeRateLimitExpiry) {
const remaining = (this._invokeRateLimitExpiry - now) / 1000;
throw new Error(
`Invoke for ${command} is rate-limited for ${remaining} seconds.`
);
}
try {
let info = await client.invokeCommand(
sessionToken,
command,
device.id,
payload
);
if (!info.enqueued || !info.notified) {
// We want an error log here to help diagnose users who report failure.
log.error("Sending was only partially successful", info);
} else {
log.info("Successfully sent", info);
}
} catch (err) {
if (err.code && err.code === 429 && err.retryAfter) {
this._invokeRateLimitExpiry = Date.now() + err.retryAfter * 1000;
}
throw err;
}
log.info(`Payload sent to device ${device.id}.`);
}
/**
* Poll and handle device commands for the current device.
* This method can be called either in response to a Push message,
* or by itself as a "commands recovery" mechanism.
*
* @param {Number} notifiedIndex "Command received" push messages include
* the index of the command that triggered the message. We use it as a
* hint when we have no "last command index" stored.
*/
async pollDeviceCommands(notifiedIndex = 0) {
// Whether the call to `pollDeviceCommands` was initiated by a Push message from the FxA
// servers in response to a message being received or simply scheduled in order
// to fetch missed messages.
log.info(`Polling device commands.`);
await this._fxai.withCurrentAccountState(async state => {
const { device } = await state.getUserAccountData(["device"]);
if (!device) {
throw new Error("No device registration.");
}
// We increment lastCommandIndex by 1 because the server response includes the current index.
// If we don't have a `lastCommandIndex` stored, we fall back on the index from the push message we just got.
const lastCommandIndex = device.lastCommandIndex + 1 || notifiedIndex;
// We have already received this message before.
if (notifiedIndex > 0 && notifiedIndex < lastCommandIndex) {
return;
}
const { index, messages } = await this._fetchDeviceCommands(
lastCommandIndex
);
if (messages.length) {
await state.updateUserAccountData({
device: { ...device, lastCommandIndex: index },
});
log.info(`Handling ${messages.length} messages`);
await this._handleCommands(messages, notifiedIndex);
}
});
return true;
}
async _fetchDeviceCommands(index, limit = null) {
const userData = await this._fxai.getUserAccountData();
if (!userData) {
throw new Error("No user.");
}
const { sessionToken } = userData;
if (!sessionToken) {
throw new Error("No session token.");
}
const client = this._fxai.fxAccountsClient;
const opts = { index };
if (limit != null) {
opts.limit = limit;
}
return client.getCommands(sessionToken, opts);
}
_getReason(notifiedIndex, messageIndex) {
// The returned reason value represents an explanation for why the command associated with the
// message of the given `messageIndex` is being handled. If `notifiedIndex` is zero the command
// is a part of a fallback polling process initiated by "Sync Now" ["poll"]. If `notifiedIndex` is
// greater than `messageIndex` this is a push command that was previously missed ["push-missed"],
// otherwise we assume this is a push command with no missed messages ["push"].
if (notifiedIndex == 0) {
return "poll";
} else if (notifiedIndex > messageIndex) {
return "push-missed";
}
// Note: The returned reason might be "push" in the case where a user sends multiple tabs
// in quick succession. We are not attempting to distinguish this from other push cases at
// present.
return "push";
}
async _handleCommands(messages, notifiedIndex) {
try {
await this._fxai.device.refreshDeviceList();
} catch (e) {
log.warn("Error refreshing device list", e);
}
// We debounce multiple incoming tabs so we show a single notification.
const tabsReceived = [];
const tabsToClose = [];
for (const { index, data } of messages) {
const { command, payload, sender: senderId } = data;
const reason = this._getReason(notifiedIndex, index);
const sender =
senderId && this._fxai.device.recentDeviceList
? this._fxai.device.recentDeviceList.find(d => d.id == senderId)
: null;
if (!sender) {
log.warn(
"Incoming command is from an unknown device (maybe disconnected?)"
);
}
switch (command) {
case COMMAND_CLOSETAB:
try {
const { urls } = await this.closeTab.handleTabClose(
senderId,
payload,
reason
);
log.info(
`Close Tab received with FxA commands: "${urls.length} tabs"
from ${sender ? sender.name : "Unknown device"}.`
);
// URLs are PII, so only logged at trace.
log.trace(`Close Remote Tabs received URLs: ${urls}`);
tabsToClose.push({ urls, sender });
} catch (e) {
log.error(`Error while handling incoming Close Tab payload.`, e);
}
break;
case COMMAND_SENDTAB:
try {
const { title, uri } = await this.sendTab.handle(
senderId,
payload,
reason
);
log.info(
`Tab received with FxA commands: "${
title || "<no title>"
}" from ${sender ? sender.name : "Unknown device"}.`
);
// URLs are PII, so only logged at trace.
log.trace(`Tab received URL: ${uri}`);
// This should eventually be rare to hit as all platforms will be using the same
// scheme filter list, but we have this here in the case other platforms
// haven't caught up and/or trying to send invalid uris using older versions
const scheme = Services.io.newURI(uri).scheme;
if (lazy.INVALID_SHAREABLE_SCHEMES.has(scheme)) {
throw new Error("Invalid scheme found for received URI.");
}
tabsReceived.push({ title, uri, sender });
} catch (e) {
log.error(`Error while handling incoming Send Tab payload.`, e);
}
break;
default:
log.info(`Unknown command: ${command}.`);
}
}
if (tabsReceived.length) {
this._notifyFxATabsReceived(tabsReceived);
}
if (tabsToClose.length) {
this._notifyFxATabsClosed(tabsToClose);
}
}
_notifyFxATabsReceived(tabsReceived) {
Observers.notify("fxaccounts:commands:open-uri", tabsReceived);
}
_notifyFxATabsClosed(tabsToClose) {
Observers.notify("fxaccounts:commands:close-uri", tabsToClose);
}
}
/**
* Send Tab is built on top of FxA commands.
*
* Devices exchange keys wrapped in the oldsync key between themselves (getEncryptedSendTabKeys)
* during the device registration flow. The FxA server can theoretically never
* retrieve the send tab keys since it doesn't know the oldsync key.
*
* Note about the keys:
* The server has the `pushPublicKey`. The FxA server encrypt the send-tab payload again using the
* push keys - after the client has encrypted the payload using the send-tab keys.
* The push keys are different from the send-tab keys. The FxA server uses
* the push keys to deliver the tabs using same mechanism we use for web-push.
* However, clients use the send-tab keys for end-to-end encryption.
*/
export class SendTab {
constructor(commands, fxAccountsInternal) {
this._commands = commands;
this._fxai = fxAccountsInternal;
}
/**
* @param {Device[]} to - Device objects (typically returned by fxAccounts.getDevicesList()).
* @param {Object} tab
* @param {string} tab.url
* @param {string} tab.title
* @returns A report object, in the shape of
* {succeded: [Device], error: [{device: Device, error: Exception}]}
*/
async send(to, tab) {
log.info(`Sending a tab to ${to.length} devices.`);
const flowID = this._fxai.telemetry.generateFlowID();
const encoder = new TextEncoder();
const data = { entries: [{ title: tab.title, url: tab.url }] };
const report = {
succeeded: [],
failed: [],
};
for (let device of to) {
try {
const streamID = this._fxai.telemetry.generateFlowID();
const targetData = Object.assign({ flowID, streamID }, data);
const bytes = encoder.encode(JSON.stringify(targetData));
const encrypted = await this._encrypt(bytes, device);
// FxA expects an object as the payload, but we only have a single encrypted string; wrap it.
// If you add any plaintext items to this payload, please carefully consider the privacy implications
// of revealing that data to the FxA server.
const payload = { encrypted };
await this._commands.invoke(COMMAND_SENDTAB, device, payload);
this._fxai.telemetry.recordEvent(
"command-sent",
COMMAND_SENDTAB_TAIL,
this._fxai.telemetry.sanitizeDeviceId(device.id),
{ flowID, streamID }
);
report.succeeded.push(device);
} catch (error) {
log.error("Error while invoking a send tab command.", error);
report.failed.push({ device, error });
}
}
return report;
}
// Returns true if the target device is compatible with FxA Commands Send tab.
isDeviceCompatible(device) {
return (
device.availableCommands && device.availableCommands[COMMAND_SENDTAB]
);
}
// Handle incoming send tab payload, called by FxAccountsCommands.
async handle(senderID, { encrypted }, reason) {
const bytes = await this._decrypt(encrypted);
const decoder = new TextDecoder("utf8");
const data = JSON.parse(decoder.decode(bytes));
const { flowID, streamID, entries } = data;
const current = data.hasOwnProperty("current")
? data.current
: entries.length - 1;
const { title, url: uri } = entries[current];
// `flowID` and `streamID` are in the top-level of the JSON, `entries` is
// an array of "tabs" with `current` being what index is the one we care
// about, or the last one if not specified.
this._fxai.telemetry.recordEvent(
"command-received",
COMMAND_SENDTAB_TAIL,
this._fxai.telemetry.sanitizeDeviceId(senderID),
{ flowID, streamID, reason }
);
return {
title,
uri,
};
}
async _encrypt(bytes, device) {
let bundle = device.availableCommands[COMMAND_SENDTAB];
if (!bundle) {
throw new Error(`Device ${device.id} does not have send tab keys.`);
}
const oldsyncKey = await this._fxai.keys.getKeyForScope(SCOPE_OLD_SYNC);
// Older clients expect this to be hex, due to pre-JWK sync key ids :-(
const ourKid = this._fxai.keys.kidAsHex(oldsyncKey);
const { kid: theirKid } = JSON.parse(
device.availableCommands[COMMAND_SENDTAB]
);
if (theirKid != ourKid) {
throw new Error("Target Send Tab key ID is different from ours");
}
const json = JSON.parse(bundle);
const wrapper = new lazy.CryptoWrapper();
wrapper.deserialize({ payload: json });
const syncKeyBundle = lazy.BulkKeyBundle.fromJWK(oldsyncKey);
let { publicKey, authSecret } = await wrapper.decrypt(syncKeyBundle);
authSecret = urlsafeBase64Decode(authSecret);
publicKey = urlsafeBase64Decode(publicKey);
const { ciphertext: encrypted } = await lazy.PushCrypto.encrypt(
bytes,
publicKey,
authSecret
);
return urlsafeBase64Encode(encrypted);
}
async _getPersistedSendTabKeys() {
const { device } = await this._fxai.getUserAccountData(["device"]);
return device && device.sendTabKeys;
}
async _decrypt(ciphertext) {
let { privateKey, publicKey, authSecret } =
await this._getPersistedSendTabKeys();
publicKey = urlsafeBase64Decode(publicKey);
authSecret = urlsafeBase64Decode(authSecret);
ciphertext = new Uint8Array(urlsafeBase64Decode(ciphertext));
return lazy.PushCrypto.decrypt(
privateKey,
publicKey,
authSecret,
// The only Push encoding we support.
{ encoding: "aes128gcm" },
ciphertext
);
}
async _generateAndPersistSendTabKeys() {
let [publicKey, privateKey] = await lazy.PushCrypto.generateKeys();
publicKey = urlsafeBase64Encode(publicKey);
let authSecret = lazy.PushCrypto.generateAuthenticationSecret();
authSecret = urlsafeBase64Encode(authSecret);
const sendTabKeys = {
publicKey,
privateKey,
authSecret,
};
await this._fxai.withCurrentAccountState(async state => {
const { device } = await state.getUserAccountData(["device"]);
await state.updateUserAccountData({
device: {
...device,
sendTabKeys,
},
});
});
return sendTabKeys;
}
async _getPersistedEncryptedSendTabKey() {
const { encryptedSendTabKeys } = await this._fxai.getUserAccountData([
"encryptedSendTabKeys",
]);
return encryptedSendTabKeys;
}
async _generateAndPersistEncryptedSendTabKey() {
let sendTabKeys = await this._getPersistedSendTabKeys();
if (!sendTabKeys) {
log.info("Could not find sendtab keys, generating them");
sendTabKeys = await this._generateAndPersistSendTabKeys();
}
// Strip the private key from the bundle to encrypt.
const keyToEncrypt = {
publicKey: sendTabKeys.publicKey,
authSecret: sendTabKeys.authSecret,
};
if (!(await this._fxai.keys.canGetKeyForScope(SCOPE_OLD_SYNC))) {
log.info("Can't fetch keys, so unable to determine sendtab keys");
return null;
}
let oldsyncKey;
try {
oldsyncKey = await this._fxai.keys.getKeyForScope(SCOPE_OLD_SYNC);
} catch (ex) {
log.warn("Failed to fetch keys, so unable to determine sendtab keys", ex);
return null;
}
const wrapper = new lazy.CryptoWrapper();
wrapper.cleartext = keyToEncrypt;
const keyBundle = lazy.BulkKeyBundle.fromJWK(oldsyncKey);
await wrapper.encrypt(keyBundle);
const encryptedSendTabKeys = JSON.stringify({
// This is expected in hex, due to pre-JWK sync key ids :-(
kid: this._fxai.keys.kidAsHex(oldsyncKey),
IV: wrapper.IV,
hmac: wrapper.hmac,
ciphertext: wrapper.ciphertext,
});
await this._fxai.withCurrentAccountState(async state => {
await state.updateUserAccountData({
encryptedSendTabKeys,
});
});
return encryptedSendTabKeys;
}
async getEncryptedSendTabKeys() {
let encryptedSendTabKeys = await this._getPersistedEncryptedSendTabKey();
const sendTabKeys = await this._getPersistedSendTabKeys();
if (!encryptedSendTabKeys || !sendTabKeys) {
log.info("Generating and persisting encrypted sendtab keys");
// `_generateAndPersistEncryptedKeys` requires the sync key
// which cannot be accessed if the login manager is locked
// (i.e when the primary password is locked) or if the sync keys
// aren't accessible (account isn't verified)
// so this function could fail to retrieve the keys
// however, device registration will trigger when the account
// is verified, so it's OK
// Note that it's okay to persist those keys, because they are
// already persisted in plaintext and the encrypted bundle
// does not include the sync-key (the sync key is used to encrypt
// it though)
encryptedSendTabKeys =
await this._generateAndPersistEncryptedSendTabKey();
}
return encryptedSendTabKeys;
}
}
/**
* Close Tabs is built on-top of device commands and handles
* actions a client wants to perform on tabs found on other devices
* This class is very similar to the Send Tab component in FxAccountsCommands
*
* Devices exchange keys wrapped in the oldsync key between themselves (getEncryptedCloseTabKeys)
* during the device registration flow. The FxA server can theoretically never
* retrieve the close tab keys since it doesn't know the oldsync key.
*
* Note: Close Tabs does things slightly different from SendTab
* The sender encrypts the close-tab command using the receiver's public key,
* and the FxA server stores it (without re-encrypting).
* A web-push notifies the receiver that a new command is available.
* The receiver decrypts the payload using its private key.
*/
export class CloseRemoteTab {
constructor(commands, fxAccountsInternal) {
this._commands = commands;
this._fxai = fxAccountsInternal;
this.pendingClosedTabs = new Map();
// pushes happen per device, making a timer per device makes sending
// the pushes a little more sane
this.pushTimers = new Map();
}
/**
* Sending a push everytime the user wants to close a tab on a remote device
* could lead to excessive notifications to the users device, push throttling, etc
* so we add the tabs to a queue and have a timer that sends the push after a certain
* amount of "inactivity"
*/
/**
* @param {Device} targetDevice - Device object (typically returned by fxAccounts.getDevicesList()).
* @param {String} tab - url for the tab to close
* @param {Integer} how far to delay, in miliseconds, the push for this timer
*/
enqueueTabToClose(targetDevice, tab, pushDelay = 6000) {
if (this.pendingClosedTabs.has(targetDevice.id)) {
this.pendingClosedTabs.get(targetDevice.id).tabs.push(tab);
} else {
this.pendingClosedTabs.set(targetDevice.id, {
device: targetDevice,
tabs: [tab],
});
}
// extend the timer
this._refreshPushTimer(targetDevice.id, pushDelay);
}
async _refreshPushTimer(deviceId, pushDelay) {
// If the user is still performing "actions" for this device
// reset the timer to send the push
if (this.pushTimers.has(deviceId)) {
clearTimeout(this.pushTimers.get(deviceId));
}
// There is a possibility that the browser closes before this actually executes
// we should catch the browser as it's closing and immediately fire these
const timerId = setTimeout(async () => {
let { device, tabs } = this.pendingClosedTabs.get(deviceId);
// send a push notification for this specific device
await this._sendCloseTabPush(device, tabs);
// Clear the timer
this.pushTimers.delete(deviceId);
// We also need to locally store the tabs we sent so the user doesn't
// see these anymore
this.pendingClosedTabs.delete(deviceId);
// This is used for tests only, to get around timer woes
Observers.notify("test:fxaccounts:commands:close-uri:sent");
}, pushDelay);
// Store the new timer with the device
this.pushTimers.set(deviceId, timerId);
}
/**
* @param {Device} target - Device object (typically returned by fxAccounts.getDevicesList()).
* @param {String[]} urls - array of urls that should be closed on the remote device
*/
async _sendCloseTabPush(target, urls) {
log.info(`Sending tab closures to ${target.id} device.`);
const flowID = this._fxai.telemetry.generateFlowID();
const encoder = new TextEncoder();
try {
const streamID = this._fxai.telemetry.generateFlowID();
const targetData = { flowID, streamID, urls };
const bytes = encoder.encode(JSON.stringify(targetData));
const encrypted = await this._encrypt(bytes, target);
// FxA expects an object as the payload, but we only have a single encrypted string; wrap it.
// If you add any plaintext items to this payload, please carefully consider the privacy implications
// of revealing that data to the FxA server.
const payload = { encrypted };
await this._commands.invoke(COMMAND_CLOSETAB, target, payload);
this._fxai.telemetry.recordEvent(
"command-sent",
COMMAND_CLOSETAB_TAIL,
this._fxai.telemetry.sanitizeDeviceId(target.id),
{ flowID, streamID }
);
} catch (error) {
// We should also show the user there was some kind've error
log.error("Error while invoking a send tab command.", error);
}
}
// Returns true if the target device is compatible with FxA Commands Send tab.
isDeviceCompatible(device) {
let pref = Services.prefs.getBoolPref(
"identity.fxaccounts.commands.remoteTabManagement.enabled",
false
);
return (
pref &&
device.availableCommands &&
device.availableCommands[COMMAND_CLOSETAB]
);
}
// Handle incoming remote tab payload, called by FxAccountsCommands.
async handleTabClose(senderID, { encrypted }, reason) {
const bytes = await this._decrypt(encrypted);
const decoder = new TextDecoder("utf8");
const data = JSON.parse(decoder.decode(bytes));
// urls is an array of strings
const { flowID, streamID, urls } = data;
this._fxai.telemetry.recordEvent(
"command-received",
COMMAND_CLOSETAB_TAIL,
this._fxai.telemetry.sanitizeDeviceId(senderID),
{ flowID, streamID, reason }
);
return {
urls,
};
}
async _encrypt(bytes, device) {
let bundle = device.availableCommands[COMMAND_CLOSETAB];
if (!bundle) {
throw new Error(`Device ${device.id} does not have close tab keys.`);
}
const oldsyncKey = await this._fxai.keys.getKeyForScope(SCOPE_OLD_SYNC);
const json = JSON.parse(bundle);
const wrapper = new lazy.CryptoWrapper();
wrapper.deserialize({ payload: json });
const syncKeyBundle = lazy.BulkKeyBundle.fromJWK(oldsyncKey);
let { publicKey, authSecret } = await wrapper.decrypt(syncKeyBundle);
authSecret = urlsafeBase64Decode(authSecret);
publicKey = urlsafeBase64Decode(publicKey);
const { ciphertext: encrypted } = await lazy.PushCrypto.encrypt(
bytes,
publicKey,
authSecret
);
return urlsafeBase64Encode(encrypted);
}
async _getPersistedCloseTabKeys() {
const { device } = await this._fxai.getUserAccountData(["device"]);
return device && device.closeTabKeys;
}
async _decrypt(ciphertext) {
let { privateKey, publicKey, authSecret } =
await this._getPersistedCloseTabKeys();
publicKey = urlsafeBase64Decode(publicKey);
authSecret = urlsafeBase64Decode(authSecret);
ciphertext = new Uint8Array(urlsafeBase64Decode(ciphertext));
return lazy.PushCrypto.decrypt(
privateKey,
publicKey,
authSecret,
// The only Push encoding we support.
{ encoding: "aes128gcm" },
ciphertext
);
}
async _generateAndPersistCloseTabKeys() {
let [publicKey, privateKey] = await lazy.PushCrypto.generateKeys();
publicKey = urlsafeBase64Encode(publicKey);
let authSecret = lazy.PushCrypto.generateAuthenticationSecret();
authSecret = urlsafeBase64Encode(authSecret);
const closeTabKeys = {
publicKey,
privateKey,
authSecret,
};
await this._fxai.withCurrentAccountState(async state => {
const { device } = await state.getUserAccountData(["device"]);
await state.updateUserAccountData({
device: {
...device,
closeTabKeys,
},
});
});
return closeTabKeys;
}
async _getPersistedEncryptedCloseTabKey() {
const { encryptedCloseTabKeys } = await this._fxai.getUserAccountData([
"encryptedCloseTabKeys",
]);
return encryptedCloseTabKeys;
}
async _generateAndPersistEncryptedCloseTabKeys() {
let closeTabKeys = await this._getPersistedCloseTabKeys();
if (!closeTabKeys) {
log.info("Could not find closeTab keys, generating them");
closeTabKeys = await this._generateAndPersistCloseTabKeys();
}
// Strip the private key from the bundle to encrypt.
const keyToEncrypt = {
publicKey: closeTabKeys.publicKey,
authSecret: closeTabKeys.authSecret,
};
if (!(await this._fxai.keys.canGetKeyForScope(SCOPE_OLD_SYNC))) {
log.info("Can't fetch keys, so unable to determine closeTab keys");
return null;
}
let oldsyncKey;
try {
oldsyncKey = await this._fxai.keys.getKeyForScope(SCOPE_OLD_SYNC);
} catch (ex) {
log.warn(
"Failed to fetch keys, so unable to determine closeTab keys",
ex
);
return null;
}
const wrapper = new lazy.CryptoWrapper();
wrapper.cleartext = keyToEncrypt;
const keyBundle = lazy.BulkKeyBundle.fromJWK(oldsyncKey);
await wrapper.encrypt(keyBundle);
const encryptedCloseTabKeys = JSON.stringify({
// This is expected in hex, due to pre-JWK sync key ids :-(
kid: this._fxai.keys.kidAsHex(oldsyncKey),
IV: wrapper.IV,
hmac: wrapper.hmac,
ciphertext: wrapper.ciphertext,
});
await this._fxai.withCurrentAccountState(async state => {
await state.updateUserAccountData({
encryptedCloseTabKeys,
});
});
return encryptedCloseTabKeys;
}
async getEncryptedCloseTabKeys() {
let encryptedCloseTabKeys = await this._getPersistedEncryptedCloseTabKey();
const closeTabKeys = await this._getPersistedCloseTabKeys();
if (!encryptedCloseTabKeys || !closeTabKeys) {
log.info("Generating and persisting encrypted closeTab keys");
// `_generateAndPersistEncryptedCloseTabKeys` requires the sync key
// which cannot be accessed if the login manager is locked
// (i.e when the primary password is locked) or if the sync keys
// aren't accessible (account isn't verified)
// so this function could fail to retrieve the keys
// however, device registration will trigger when the account
// is verified, so it's OK
// Note that it's okay to persist those keys, because they are
// already persisted in plaintext and the encrypted bundle
// does not include the sync-key (the sync key is used to encrypt
// it though)
encryptedCloseTabKeys =
await this._generateAndPersistEncryptedCloseTabKeys();
}
return encryptedCloseTabKeys;
}
}
function urlsafeBase64Encode(buffer) {
return ChromeUtils.base64URLEncode(new Uint8Array(buffer), { pad: false });
}
function urlsafeBase64Decode(str) {
return ChromeUtils.base64URLDecode(str, { padding: "reject" });
}