Report the real cause of an OS keystore failure

Every failure shows "User canceled OS unlock entry" no matter what went
wrong. Test the store for the actual state, and give callers a message
describing what couldn't be accessed.

(cherry picked from commit ace9fa0227)
This commit is contained in:
Dan Stillman 2026-08-31 11:55:29 -04:00
parent 799c05c598
commit cc408d0a72
3 changed files with 64 additions and 2 deletions

View file

@ -101,6 +101,32 @@ Zotero.OSKeyStore = {
);
},
// 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
_error: async function (e, stringName) {
let detail;
// Make sure the label from Mozilla's module still exists
let label = this._module && this._module.STORE_LABEL;
if (typeof label != 'string' || !label) {
detail = "store label unavailable";
}
else {
try {
let keyStore = Cc["@mozilla.org/security/oskeystore;1"]
.getService(Ci.nsIOSKeyStore);
detail = (await keyStore.asyncSecretAvailable(label))
? "secret is available"
: "no secret stored";
}
catch (probeError) {
detail = "key store unusable: " + probeError;
}
}
Zotero.debug(`OS key store failure (${detail}): ${e}`, 1);
return new Zotero.Error(Zotero.getString(stringName), 0, { keyStoreError: e });
},
// Returns prefixed ciphertext. Throws if OSKeyStore is unavailable so we
// don't silently store plaintext when a caller expects encryption.
encrypt: async function (plaintext) {
@ -108,7 +134,13 @@ Zotero.OSKeyStore = {
if (!mod) {
throw new Error("OSKeyStore unavailable");
}
let ciphertext = await mod.encrypt(plaintext);
let ciphertext;
try {
ciphertext = await mod.encrypt(plaintext);
}
catch (e) {
throw await this._error(e, 'os-keystore-save-failed');
}
return this._prefix + ciphertext;
},
@ -127,7 +159,13 @@ Zotero.OSKeyStore = {
// OSKeyStore.encrypt() encodes the string as UTF-8 before encrypting, but
// OSKeyStore.decrypt() returns the decrypted bytes as a binary string, so
// decode it here
let binaryStr = await mod.decrypt(value.slice(this._prefix.length));
let binaryStr;
try {
binaryStr = await mod.decrypt(value.slice(this._prefix.length));
}
catch (e) {
throw await this._error(e, 'os-keystore-read-failed');
}
return new TextDecoder().decode(
Uint8Array.from(binaryStr, char => char.charCodeAt(0))
);

View file

@ -1142,6 +1142,13 @@ os-keystore-save-failed =
*[other] { -app-name } couldnt access your { -os-name } keyring to securely save your credentials. Make sure a keyring service such as GNOME Keyring or KWallet is running and try again.
}
os-keystore-read-failed =
{ PLATFORM() ->
[macos] { -app-name } couldnt access the { -os-name } Keychain to read your saved credentials. Make sure your Keychain is accessible and try again.
[windows] { -app-name } couldnt use { -os-name } Credential Manager to read your saved credentials. Try again or restart { -app-name }.
*[other] { -app-name } couldnt 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-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

@ -261,6 +261,23 @@ describe("Zotero.Sync.Storage.Mode.WebDAV", function () {
})
it("should report a keystore read failure with a usable message", 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-failed'));
}
finally {
stub.restore();
}
})
it("shouldn't store a password that would read back as encrypted", async function () {
var encryptStub = sinon.stub(Zotero.OSKeyStore, "encrypt")
.rejects(new Error("User canceled OS unlock entry"));