Tell the user to set up syncing again if credentials can't be decrypted
Some checks failed
CI / Detect changes (push) Has been cancelled
CI / Utilities Tests (push) Has been cancelled
CI / Build, Upload (push) Has been cancelled
CI / Test () (push) Has been cancelled
CI / Test (macOS NFS) (push) Has been cancelled

If the keystore's secret is replaced -- say, after a keyring reset --
the stored value won't be able to be decrypted again, so tell them to
try logging in again instead of trying to fix their keyring.

https://forums.zotero.org/discussion/133603/

(cherry picked from commit fc17dcd24a)
This commit is contained in:
Dan Stillman 2026-09-04 17:56:34 -04:00
parent 5f4a8252c4
commit a5b4b4d20d
3 changed files with 51 additions and 2 deletions

View file

@ -101,6 +101,22 @@ Zotero.OSKeyStore = {
);
},
// Returns whether the store can actually be used. asyncSecretAvailable() isn't enough,
// since a locked or inaccessible store can still report a secret.
_usable: async function () {
let mod = this._load();
if (!mod) {
return false;
}
try {
await mod.encrypt('test');
}
catch {
return false;
}
return true;
},
// Mozilla's OSKeyStore reports every failure as a canceled unlock prompt, whatever the
// cause, so probe the native store to record what actually went wrong and return an error
// with a message that can be shown to the user
@ -164,7 +180,13 @@ Zotero.OSKeyStore = {
binaryStr = await mod.decrypt(value.slice(this._prefix.length));
}
catch (e) {
throw await this._error(e, 'os-keystore-read-failed');
// A read can fail because the store is unusable or because the store is working
// and the stored value just can't be decrypted with the current key. Those need
// different advice, since fixing the store won't bring the value back.
let stringName = (await this._usable())
? 'os-keystore-read-unrecoverable'
: 'os-keystore-read-failed';
throw await this._error(e, stringName);
}
return new TextDecoder().decode(
Uint8Array.from(binaryStr, char => char.charCodeAt(0))

View file

@ -1151,6 +1151,13 @@ os-keystore-read-failed =
*[other] { -app-name } couldn’t access your { -os-name } keyring to read your saved credentials. Make sure a keyring service such as GNOME Keyring or KWallet is running and try again.
}
os-keystore-read-unrecoverable =
{ PLATFORM() ->
[macos] { -app-name } couldn’t read your saved credentials from the { -os-name } Keychain.
[windows] { -app-name } couldn’t read your saved credentials from { -os-name } Credential Manager.
*[other] { -app-name } couldn’t read your saved credentials from your { -os-name } keyring.
} You’ll need to set up syncing again in the { -app-name } settings.
os-keystore-save-unencrypted = { -app-name } can save your credentials unencrypted instead. Anyone with access to your { -app-name } profile folder would then be able to read them.
os-keystore-save-unencrypted-button = Save Anyway

View file

@ -266,12 +266,32 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
this.skip();
}
await controller.setPassword("password");
var stub = sinon.stub(Zotero.OSKeyStore._module, "decrypt")
var decryptStub = sinon.stub(Zotero.OSKeyStore._module, "decrypt")
.rejects(new Error("User canceled OS unlock entry"));
var encryptStub = sinon.stub(Zotero.OSKeyStore._module, "encrypt")
.rejects(new Error("User canceled OS unlock entry"));
try {
let e = await getPromiseError(controller.getPassword());
assert.equal(e.message, Zotero.getString('os-keystore-read-failed'));
}
finally {
decryptStub.restore();
encryptStub.restore();
}
})
it("should tell the user to reenter a password the keystore can't decrypt", async function () {
if (!Zotero.OSKeyStore.available) {
this.skip();
}
await controller.setPassword("password");
var stub = sinon.stub(Zotero.OSKeyStore._module, "decrypt")
.rejects(new Error("User canceled OS unlock entry"));
try {
let e = await getPromiseError(controller.getPassword());
assert.equal(e.message, Zotero.getString('os-keystore-read-unrecoverable'));
}
finally {
stub.restore();
}