diff --git a/chrome/content/zotero/xpcom/osKeyStore.js b/chrome/content/zotero/xpcom/osKeyStore.js index c46d8fb5cf..4c6bd94378 100644 --- a/chrome/content/zotero/xpcom/osKeyStore.js +++ b/chrome/content/zotero/xpcom/osKeyStore.js @@ -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)) diff --git a/chrome/locale/en-US/zotero/zotero.ftl b/chrome/locale/en-US/zotero/zotero.ftl index 875ecb08ae..021b58ae99 100644 --- a/chrome/locale/en-US/zotero/zotero.ftl +++ b/chrome/locale/en-US/zotero/zotero.ftl @@ -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 diff --git a/test/tests/webdavTest.js b/test/tests/webdavTest.js index 564ae405f8..2ed7dd8af4 100644 --- a/test/tests/webdavTest.js +++ b/test/tests/webdavTest.js @@ -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(); }