Bug 1752609 - Adds autorecovery from outdated sendtab keys. r=markh

Differential Revision: https://phabricator.services.mozilla.com/D139455
This commit is contained in:
Tarik Eshaq 2022-04-19 17:04:42 +00:00
Родитель df70b3afd2
Коммит b022bfa53d
6 изменённых файлов: 131 добавлений и 91 удалений

Просмотреть файл

@ -372,17 +372,11 @@ class SendTab {
}
async _decrypt(ciphertext) {
let sendTabKeys = await this._getPersistedSendTabKeys();
if (!sendTabKeys) {
// If we lost the user's send tab keys for any reason,
// `generateAndPersistEncryptedSendTabKeys` will regenerate the send tab keys,
// persist them, then persist an encrypted bundle of the keys.
// It should be impossible for us to hit this for new devices
// this was added to recover users who hit Bug 1752609
await this._generateAndPersistEncryptedSendTabKey();
sendTabKeys = await this._getPersistedSendTabKeys();
}
let { privateKey, publicKey, authSecret } = sendTabKeys;
let {
privateKey,
publicKey,
authSecret,
} = await this._getPersistedSendTabKeys();
publicKey = urlsafeBase64Decode(publicKey);
authSecret = urlsafeBase64Decode(authSecret);
ciphertext = new Uint8Array(urlsafeBase64Decode(ciphertext));
@ -468,7 +462,8 @@ class SendTab {
async getEncryptedSendTabKeys() {
let encryptedSendTabKeys = await this._getPersistedEncryptedSendTabKey();
if (!encryptedSendTabKeys) {
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

Просмотреть файл

@ -243,24 +243,8 @@ class FxAccountsDevice {
log.info(
`Got new device list: ${devices.map(d => d.id).join(", ")}`
);
// Check if our push registration previously succeeded and is still
// good (although background device registration means it's possible
// we'll be fetching the device list before we've actually
// registered ourself!)
// (For a missing subscription we check for an explicit 'null' -
// both to help tests and as a safety valve - missing might mean
// "no push available" for self-hosters or similar?)
const ourDevice = devices.find(device => device.isCurrentDevice);
if (
ourDevice &&
(ourDevice.pushCallback === null || ourDevice.pushEndpointExpired)
) {
log.warn(`Our push endpoint needs resubscription`);
await this._fxai.fxaPushService.unsubscribe();
await this._registerOrUpdateDevice(currentState, accountData);
// and there's a reasonable chance there are commands waiting.
await this._fxai.commands.pollDeviceCommands();
}
await this._refreshRemoteDevice(currentState, accountData, devices);
return devices;
}
);
@ -279,6 +263,33 @@ class FxAccountsDevice {
return this._fetchAndCacheDeviceListPromise;
}
async _refreshRemoteDevice(currentState, accountData, remoteDevices) {
// Check if our push registration previously succeeded and is still
// good (although background device registration means it's possible
// we'll be fetching the device list before we've actually
// registered ourself!)
// (For a missing subscription we check for an explicit 'null' -
// both to help tests and as a safety valve - missing might mean
// "no push available" for self-hosters or similar?)
const ourDevice = remoteDevices.find(device => device.isCurrentDevice);
if (
ourDevice &&
(ourDevice.pushCallback === null || ourDevice.pushEndpointExpired)
) {
log.warn(`Our push endpoint needs resubscription`);
await this._fxai.fxaPushService.unsubscribe();
await this._registerOrUpdateDevice(currentState, accountData);
// and there's a reasonable chance there are commands waiting.
await this._fxai.commands.pollDeviceCommands();
} else if (
ourDevice &&
(await this._checkRemoteCommandsUpdateNeeded(ourDevice.availableCommands))
) {
log.warn(`Our commands need to be updated on the server`);
await this._registerOrUpdateDevice(currentState, accountData);
}
}
async updateDeviceRegistration() {
return this._withCurrentAccountState(async currentState => {
const signedInUser = await currentState.getUserAccountData([
@ -355,6 +366,35 @@ class FxAccountsDevice {
);
}
async _checkRemoteCommandsUpdateNeeded(remoteAvailableCommands) {
if (!remoteAvailableCommands) {
return true;
}
const remoteAvailableCommandsKeys = Object.keys(
remoteAvailableCommands
).sort();
const localAvailableCommands = await this._fxai.commands.availableCommands();
const localAvailableCommandsKeys = Object.keys(
localAvailableCommands
).sort();
if (
!CommonUtils.arrayEqual(
localAvailableCommandsKeys,
remoteAvailableCommandsKeys
)
) {
return true;
}
for (const key of localAvailableCommandsKeys) {
if (remoteAvailableCommands[key] !== localAvailableCommands[key]) {
return true;
}
}
return false;
}
async _updateDeviceRegistrationIfNecessary(currentState) {
let data = await currentState.getUserAccountData([
"sessionToken",

Просмотреть файл

@ -211,6 +211,7 @@ function MockFxAccounts(credentials = null) {
observerPreloads: [],
device: {
_registerOrUpdateDevice() {},
_checkRemoteCommandsUpdateNeeded: async () => false,
},
profile: {
getProfile() {
@ -245,10 +246,12 @@ async function MakeFxAccounts({ internal = {}, credentials } = {}) {
if (internal.device) {
if (!internal.device._registerOrUpdateDevice) {
internal.device._registerOrUpdateDevice = () => Promise.resolve();
internal.device._checkRemoteCommandsUpdateNeeded = async () => false;
}
} else {
internal.device = {
_registerOrUpdateDevice() {},
_checkRemoteCommandsUpdateNeeded: async () => false,
};
}
if (!internal.observerPreloads) {

Просмотреть файл

@ -162,6 +162,7 @@ async function MockFxAccounts(credentials, device = {}) {
},
device: {
DEVICE_REGISTRATION_VERSION,
_checkRemoteCommandsUpdateNeeded: async () => false,
},
VERIFICATION_POLL_TIMEOUT_INITIAL: 1,
});
@ -817,6 +818,7 @@ add_task(async function test_refreshDeviceList() {
fxaPushService: null,
};
let device = new FxAccountsDevice(fxai);
device._checkRemoteCommandsUpdateNeeded = async () => false;
Assert.equal(
device.recentDeviceList,
@ -908,6 +910,56 @@ add_task(async function test_refreshDeviceList() {
);
});
add_task(async function test_checking_remote_availableCommands_mismatch() {
const credentials = getTestUser("baz");
credentials.verified = true;
const fxa = await MockFxAccounts(credentials);
fxa.device._checkRemoteCommandsUpdateNeeded =
FxAccountsDevice.prototype._checkRemoteCommandsUpdateNeeded;
fxa.commands.availableCommands = async () => {
return {
"https://identity.mozilla.com/cmd/open-uri": "local-keys",
};
};
const ourDevice = {
isCurrentDevice: true,
availableCommands: {
"https://identity.mozilla.com/cmd/open-uri": "remote-keys",
},
};
Assert.ok(
await fxa.device._checkRemoteCommandsUpdateNeeded(
ourDevice.availableCommands
)
);
});
add_task(async function test_checking_remote_availableCommands_match() {
const credentials = getTestUser("baz");
credentials.verified = true;
const fxa = await MockFxAccounts(credentials);
fxa.device._checkRemoteCommandsUpdateNeeded =
FxAccountsDevice.prototype._checkRemoteCommandsUpdateNeeded;
fxa.commands.availableCommands = async () => {
return {
"https://identity.mozilla.com/cmd/open-uri": "local-keys",
};
};
const ourDevice = {
isCurrentDevice: true,
availableCommands: {
"https://identity.mozilla.com/cmd/open-uri": "local-keys",
},
};
Assert.ok(
!(await fxa.device._checkRemoteCommandsUpdateNeeded(
ourDevice.availableCommands
))
);
});
function expandHex(two_hex) {
// Return a 64-character hex string, encoding 32 identical bytes.
let eight_hex = two_hex + two_hex + two_hex + two_hex;

Просмотреть файл

@ -639,9 +639,9 @@ add_task(async function test_send_tab_keys_regenerated_if_lost() {
const accountState = {
data: {
// Since the device object has no
// sendTabKeys, when we _decrypt,
// we will attempt to regenerate the
// keys.
// sendTabKeys, it will recover
// when we attempt to get the
// encryptedSendTabKeys
device: {
lastCommandIndex: 10,
},
@ -664,39 +664,12 @@ add_task(async function test_send_tab_keys_regenerated_if_lost() {
},
telemetry: new TelemetryMock(),
};
const sendTab = new SendTab(commands, fxAccounts);
sendTab._encrypt = (bytes, device) => {
return bytes;
};
let generateEncryptedKeysCalled = false;
sendTab._generateAndPersistEncryptedSendTabKey = async () => {
generateEncryptedKeysCalled = true;
};
sendTab._fxai = fxAccounts;
const tab = { title: "tab title", url: "http://example.com" };
const to = [{ id: "devid", name: "The Device" }];
const reason = "push";
await sendTab.send(to, tab);
Assert.equal(commands._invokes.length, 1);
for (let { cmd, device, payload } of commands._invokes) {
Assert.equal(cmd, COMMAND_SENDTAB);
sendTab._fxai = fxAccounts;
try {
await sendTab.handle(device.id, payload, reason);
} catch {
// The `handle` function will throw an error
// since we are not mocking the `_decrypt`
// function. This is intentional, since
// we want to capture that `_decrypt` will
// call `_generateEncryptedSendTabKeys` if
// it fails to retrieve the keys.
// Receiving a send tab is covered
// in the above test_sendtab_receive test.
}
}
await sendTab.getEncryptedSendTabKeys();
Assert.ok(generateEncryptedKeysCalled);
});
@ -712,8 +685,10 @@ add_task(async function test_send_tab_keys_are_not_regenerated_if_not_lost() {
const accountState = {
data: {
// Since the device object has
// sendTabKeys, when we _decrypt,
// we will not try to regenerate them
// sendTabKeys, it will not try
// to regenerate them
// when we attempt to get the
// encryptedSendTabKeys
device: {
lastCommandIndex: 10,
sendTabKeys: "keys",
@ -737,38 +712,11 @@ add_task(async function test_send_tab_keys_are_not_regenerated_if_not_lost() {
},
telemetry: new TelemetryMock(),
};
const sendTab = new SendTab(commands, fxAccounts);
sendTab._encrypt = (bytes, device) => {
return bytes;
};
let generateEncryptedKeysCalled = false;
sendTab._generateAndPersistEncryptedSendTabKey = async () => {
generateEncryptedKeysCalled = true;
};
sendTab._fxai = fxAccounts;
const tab = { title: "tab title", url: "http://example.com" };
const to = [{ id: "devid", name: "The Device" }];
const reason = "push";
await sendTab.send(to, tab);
Assert.equal(commands._invokes.length, 1);
for (let { cmd, device, payload } of commands._invokes) {
Assert.equal(cmd, COMMAND_SENDTAB);
sendTab._fxai = fxAccounts;
try {
await sendTab.handle(device.id, payload, reason);
} catch {
// The `handle` function will throw an error
// since we are not mocking the `_decrypt`
// function. This is intentional, since
// we want to capture that `_decrypt` will
// not call `_generateEncryptedSendTabKeys` if
// it succeeds to retrieve the keys.
// Receiving a send tab is covered
// in the above test_sendtab_receive test.
}
}
await sendTab.getEncryptedSendTabKeys();
Assert.ok(!generateEncryptedKeysCalled);
});

Просмотреть файл

@ -13,6 +13,8 @@ const { PREF_ACCOUNT_ROOT } = ChromeUtils.import(
_("Misc tests for FxAccounts.device");
fxAccounts.device._checkRemoteCommandsUpdateNeeded = async () => false;
add_test(function test_default_device_name() {
// Note that head_helpers overrides getDefaultLocalName - this test is
// really just to ensure the actual implementation is sane - we can't