1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-11-06 09:19:22 +02:00

Desktop: Resolves #2773: Add support for system keychain to save sensitive settings (#3207)

This commit is contained in:
Laurent Cozic
2020-06-03 17:07:50 +01:00
committed by GitHub
parent 5082181c49
commit 26ce102113
28 changed files with 661 additions and 40 deletions

View File

@@ -0,0 +1,21 @@
import KeychainService from './keychain/KeychainService';
const Setting = require('lib/models/Setting');
const { uuid } = require('lib/uuid.js');
// This function takes care of initialising both the keychain service and settings.
//
// Loading the settings became more complicated with the keychain integration. This is because
// the settings needs a keychain service, and the keychain service needs a clientId, which
// is set dynamically and saved to the settings.
// In other words, it's not possible to load the settings without the KS service and it's not
// possible to initialise the KS service without the settings.
// The solution is to fetch just the client ID directly from the database.
export async function loadKeychainServiceAndSettings(KeychainServiceDriver:any) {
const clientIdSetting = await Setting.loadOne('clientId');
const clientId = clientIdSetting ? clientIdSetting.value : uuid.create();
KeychainService.instance().initialize(new KeychainServiceDriver(Setting.value('appId'), clientId));
Setting.setKeychainService(KeychainService.instance());
await Setting.load();
if (!clientIdSetting) Setting.setValue('clientId', clientId);
await KeychainService.instance().detectIfKeychainSupported();
}

View File

@@ -0,0 +1,56 @@
import KeychainServiceDriverBase from './KeychainServiceDriverBase';
const Setting = require('lib/models/Setting');
const BaseService = require('lib/services/BaseService');
export default class KeychainService extends BaseService {
private driver:KeychainServiceDriverBase;
private static instance_:KeychainService;
static instance():KeychainService {
if (!this.instance_) this.instance_ = new KeychainService();
return this.instance_;
}
initialize(driver:KeychainServiceDriverBase) {
if (!driver.appId || !driver.clientId) throw new Error('appId and clientId must be set on the KeychainServiceDriver');
this.driver = driver;
}
async setPassword(name:string, password:string):Promise<boolean> {
// 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
return this.driver.setPassword(name, password);
}
async password(name:string):Promise<string> {
return this.driver.password(name);
}
async deletePassword(name:string):Promise<void> {
await this.driver.deletePassword(name);
}
async detectIfKeychainSupported() {
this.logger().info('KeychainService: checking if keychain supported');
if (Setting.value('keychain.supported') >= 0) {
this.logger().info('KeychainService: check was already done - skipping. Supported:', Setting.value('keychain.supported'));
return;
}
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);
}
}
}

View File

@@ -0,0 +1,17 @@
import KeychainServiceDriverBase from './KeychainServiceDriverBase';
export default class KeychainServiceDriver extends KeychainServiceDriverBase {
async setPassword(/* name:string, password:string*/):Promise<boolean> {
return false;
}
async password(/* name:string*/):Promise<string> {
return null;
}
async deletePassword(/* name:string*/):Promise<void> {
}
}

View File

@@ -0,0 +1,17 @@
import KeychainServiceDriverBase from './KeychainServiceDriverBase';
export default class KeychainServiceDriver extends KeychainServiceDriverBase {
async setPassword(/* name:string, password:string*/):Promise<boolean> {
return false;
}
async password(/* name:string*/):Promise<string> {
return null;
}
async deletePassword(/* name:string*/):Promise<void> {
}
}

View File

@@ -0,0 +1,44 @@
import KeychainServiceDriverBase from './KeychainServiceDriverBase';
const { shim } = require('lib/shim.js');
// keytar throws an error when system keychain is not present;
// even when keytar itself is installed.
// try/catch to ensure system keychain is present and no error is thrown.
// For now, keychain support is disabled on Linux because when keytar is loaded
// it seems to cause the following error when loading Sharp:
//
// Something went wrong installing the "sharp" module
// /lib/x86_64-linux-gnu/libz.so.1: version `ZLIB_1.2.9' not found (required by /home/travis/build/laurent22/joplin/CliClient/node_modules/sharp/build/Release/../../vendor/lib/libpng16.so.16)
//
// See: https://travis-ci.org/github/laurent22/joplin/jobs/686222036
//
// Also disabled in portable mode obviously.
let keytar:any;
try {
keytar = shim.isLinux() || shim.isPortable() ? null : require('keytar');
} catch (error) {
console.error('Cannot load keytar - keychain support will be disabled', error);
keytar = null;
}
export default class KeychainServiceDriver extends KeychainServiceDriverBase {
async setPassword(name:string, password:string):Promise<boolean> {
if (!keytar) return false;
await keytar.setPassword(`${this.appId}.${name}`, `${this.clientId}@joplin`, password);
return true;
}
async password(name:string):Promise<string> {
if (!keytar) return null;
return keytar.getPassword(`${this.appId}.${name}`, `${this.clientId}@joplin`);
}
async deletePassword(name:string):Promise<void> {
if (!keytar) return;
await keytar.deletePassword(`${this.appId}.${name}`, `${this.clientId}@joplin`);
}
}

View File

@@ -0,0 +1,25 @@
abstract class KeychainServiceDriverBase {
private appId_:string;
private clientId_:string;
constructor(appId:string, clientId:string) {
this.appId_ = appId;
this.clientId_ = clientId;
}
get appId():string {
return this.appId_;
}
get clientId():string {
return this.clientId_;
}
abstract async setPassword(name:string, password:string):Promise<boolean>;
abstract async password(name:string):Promise<string>;
abstract async deletePassword(name:string):Promise<void>;
}
export default KeychainServiceDriverBase;