2020-06-03 18:07:50 +02:00
|
|
|
import KeychainServiceDriverBase from './KeychainServiceDriverBase';
|
2021-01-22 19:41:11 +02:00
|
|
|
import Setting from '../../models/Setting';
|
|
|
|
import BaseService from '../BaseService';
|
2024-08-08 20:53:43 +02:00
|
|
|
import Logger from '@joplin/utils/Logger';
|
|
|
|
|
|
|
|
const logger = Logger.create('KeychainService');
|
2020-06-03 18:07:50 +02:00
|
|
|
|
|
|
|
export default class KeychainService extends BaseService {
|
|
|
|
|
2024-08-08 20:53:43 +02:00
|
|
|
private drivers_: KeychainServiceDriverBase[];
|
|
|
|
private keysNeedingMigration_: Set<string>;
|
2020-11-12 21:13:28 +02:00
|
|
|
private static instance_: KeychainService;
|
2023-06-30 10:07:03 +02:00
|
|
|
private enabled_ = true;
|
2020-06-03 18:07:50 +02:00
|
|
|
|
2023-03-06 16:22:01 +02:00
|
|
|
public static instance(): KeychainService {
|
2020-06-03 18:07:50 +02:00
|
|
|
if (!this.instance_) this.instance_ = new KeychainService();
|
|
|
|
return this.instance_;
|
|
|
|
}
|
|
|
|
|
2024-08-08 20:53:43 +02:00
|
|
|
// The drivers list should be provided in order of preference, with the most preferred driver
|
|
|
|
// first. If not present in the first supported driver, the keychain service will attempt to
|
|
|
|
// migrate keys to it.
|
|
|
|
public async initialize(drivers: KeychainServiceDriverBase[]) {
|
|
|
|
if (drivers.some(driver => !driver.appId || !driver.clientId)) {
|
|
|
|
throw new Error('appId and clientId must be set on the KeychainServiceDriver');
|
|
|
|
}
|
|
|
|
|
|
|
|
this.drivers_ = [];
|
|
|
|
this.keysNeedingMigration_ = new Set();
|
|
|
|
for (const driver of drivers) {
|
|
|
|
if (await driver.supported()) {
|
|
|
|
this.drivers_.push(driver);
|
|
|
|
} else {
|
2024-08-12 16:34:36 +02:00
|
|
|
logger.info(`Driver unsupported:${driver.driverId}`);
|
2024-08-08 20:53:43 +02:00
|
|
|
}
|
|
|
|
}
|
2020-06-03 18:07:50 +02:00
|
|
|
}
|
|
|
|
|
2021-05-13 18:57:37 +02:00
|
|
|
// This is to programatically disable the keychain service, whether keychain
|
|
|
|
// is supported or not in the system (In other word, this be might "enabled"
|
|
|
|
// but nothing will be saved to the keychain if there isn't one).
|
2020-11-12 21:13:28 +02:00
|
|
|
public get enabled(): boolean {
|
2021-05-13 18:57:37 +02:00
|
|
|
if (!this.enabled_) return false;
|
|
|
|
|
|
|
|
// Otherwise we assume it's enabled if "keychain.supported" is either -1
|
|
|
|
// (undetermined) or 1 (working). We make it work for -1 too because the
|
|
|
|
// setPassword() and password() functions need to work to test if the
|
|
|
|
// keychain is supported (in detectIfKeychainSupported).
|
|
|
|
return Setting.value('keychain.supported') !== 0;
|
2020-10-20 17:16:18 +02:00
|
|
|
}
|
|
|
|
|
2020-11-12 21:13:28 +02:00
|
|
|
public set enabled(v: boolean) {
|
2020-10-20 17:16:18 +02:00
|
|
|
this.enabled_ = v;
|
|
|
|
}
|
|
|
|
|
2020-11-12 21:13:28 +02:00
|
|
|
public async setPassword(name: string, password: string): Promise<boolean> {
|
2020-10-20 17:16:18 +02:00
|
|
|
if (!this.enabled) return false;
|
|
|
|
|
2024-08-08 20:53:43 +02:00
|
|
|
// Optimization: Handles the case where the password doesn't need to change.
|
|
|
|
// TODO: Re-evaluate whether this optimization is necessary after refactoring the driver
|
|
|
|
// logic.
|
|
|
|
if (!this.keysNeedingMigration_.has(name) && await this.password(name) === password) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-06-03 18:07:50 +02:00
|
|
|
// Due to a bug in macOS, this may throw an exception "The user name or passphrase you entered is not correct."
|
|
|
|
// The fix is to open Keychain Access.app. Right-click on the login keychain and try locking it and then unlocking it again.
|
|
|
|
// https://github.com/atom/node-keytar/issues/76
|
2024-08-08 20:53:43 +02:00
|
|
|
let i = 0;
|
|
|
|
let didSet = false;
|
|
|
|
for (; i < this.drivers_.length && !didSet; i++) {
|
|
|
|
didSet = await this.drivers_[i].setPassword(name, password);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (didSet && this.keysNeedingMigration_.has(name)) {
|
|
|
|
logger.info(`Marking key ${name} as copied to new keychain backend...`);
|
|
|
|
|
|
|
|
// At this point, the key has been saved in drivers[i - 1].
|
|
|
|
//
|
|
|
|
// Deleting the key from the less-preferred drivers would complete the
|
|
|
|
// migration. However, to allow users to roll back to a previous Joplin
|
|
|
|
// version without data loss, avoid deleting old keys here.
|
|
|
|
|
|
|
|
this.keysNeedingMigration_.delete(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
return didSet;
|
2020-06-03 18:07:50 +02:00
|
|
|
}
|
|
|
|
|
2020-11-12 21:13:28 +02:00
|
|
|
public async password(name: string): Promise<string> {
|
2020-10-20 17:16:18 +02:00
|
|
|
if (!this.enabled) return null;
|
|
|
|
|
2024-08-08 20:53:43 +02:00
|
|
|
let foundInPreferredDriver = true;
|
|
|
|
let password: string|null = null;
|
|
|
|
for (const driver of this.drivers_) {
|
|
|
|
password = await driver.password(name);
|
|
|
|
if (password) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
foundInPreferredDriver = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (password && !foundInPreferredDriver) {
|
|
|
|
this.keysNeedingMigration_.add(name);
|
|
|
|
}
|
|
|
|
|
|
|
|
return password;
|
2020-06-03 18:07:50 +02:00
|
|
|
}
|
|
|
|
|
2020-11-12 21:13:28 +02:00
|
|
|
public async deletePassword(name: string): Promise<void> {
|
2020-10-20 17:16:18 +02:00
|
|
|
if (!this.enabled) return;
|
|
|
|
|
2024-08-08 20:53:43 +02:00
|
|
|
for (const driver of this.drivers_) {
|
|
|
|
await driver.deletePassword(name);
|
|
|
|
}
|
2020-06-03 18:07:50 +02:00
|
|
|
}
|
|
|
|
|
2020-10-20 17:16:18 +02:00
|
|
|
public async detectIfKeychainSupported() {
|
2020-06-03 18:07:50 +02:00
|
|
|
this.logger().info('KeychainService: checking if keychain supported');
|
|
|
|
|
2024-08-08 20:53:43 +02:00
|
|
|
const lastAvailableDrivers = Setting.value('keychain.lastAvailableDrivers');
|
|
|
|
const availableDriversChanged = (() => {
|
|
|
|
if (lastAvailableDrivers.length !== this.drivers_.length) return true;
|
|
|
|
return this.drivers_.some(driver => {
|
|
|
|
return !lastAvailableDrivers.includes(driver.driverId);
|
|
|
|
});
|
|
|
|
})();
|
|
|
|
|
|
|
|
const checkAlreadyDone = Setting.value('keychain.supported') >= 0;
|
|
|
|
if (checkAlreadyDone && !availableDriversChanged) {
|
2020-06-03 18:07:50 +02:00
|
|
|
this.logger().info('KeychainService: check was already done - skipping. Supported:', Setting.value('keychain.supported'));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-08-08 20:53:43 +02:00
|
|
|
if (availableDriversChanged) {
|
|
|
|
// Reset supported -- this allows the test .setPassword to work.
|
|
|
|
Setting.setValue('keychain.supported', -1);
|
|
|
|
}
|
|
|
|
|
2020-06-03 18:07:50 +02:00
|
|
|
const passwordIsSet = await this.setPassword('zz_testingkeychain', 'mytest');
|
|
|
|
|
|
|
|
if (!passwordIsSet) {
|
|
|
|
this.logger().info('KeychainService: could not set test password - keychain support will be disabled');
|
|
|
|
Setting.setValue('keychain.supported', 0);
|
|
|
|
} else {
|
|
|
|
const result = await this.password('zz_testingkeychain');
|
|
|
|
await this.deletePassword('zz_testingkeychain');
|
|
|
|
this.logger().info('KeychainService: tried to set and get password. Result was:', result);
|
|
|
|
Setting.setValue('keychain.supported', result === 'mytest' ? 1 : 0);
|
|
|
|
}
|
2024-08-08 20:53:43 +02:00
|
|
|
Setting.setValue('keychain.lastAvailableDrivers', this.drivers_.map(driver => driver.driverId));
|
2020-06-03 18:07:50 +02:00
|
|
|
}
|
|
|
|
}
|