mirror of https://github.com/keeweb/keeweb
secure enclave support for #679
parent
4e8648fca0
commit
d22ddd99f6
|
@ -1,3 +1,4 @@
|
|||
import kdbxweb from 'kdbxweb';
|
||||
import { Events } from 'framework/events';
|
||||
import { Logger } from 'util/logger';
|
||||
import { Launcher } from 'comp/launcher';
|
||||
|
@ -68,8 +69,6 @@ if (Launcher) {
|
|||
host.on('error', (e) => this.hostError(e));
|
||||
host.on('exit', (code, sig) => this.hostExit(code, sig));
|
||||
|
||||
this.call('init', Launcher.remoteApp().getAppMainRoot());
|
||||
|
||||
if (this.usbListenerRunning) {
|
||||
this.call('start-usb');
|
||||
}
|
||||
|
@ -140,6 +139,36 @@ if (Launcher) {
|
|||
});
|
||||
},
|
||||
|
||||
makeXoredValue(val) {
|
||||
const data = Buffer.from(val);
|
||||
const random = Buffer.from(kdbxweb.Random.getBytes(data.length));
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] ^= random[i];
|
||||
}
|
||||
|
||||
const result = { data: [...data], random: [...random] };
|
||||
|
||||
data.fill(0);
|
||||
random.fill(0);
|
||||
|
||||
return result;
|
||||
},
|
||||
|
||||
readXoredValue(val) {
|
||||
const data = Buffer.from(val.data);
|
||||
const random = Buffer.from(val.random);
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] ^= random[i];
|
||||
}
|
||||
|
||||
val.data.fill(0);
|
||||
val.random.fill(0);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
startUsbListener() {
|
||||
this.call('start-usb');
|
||||
this.usbListenerRunning = true;
|
||||
|
@ -169,8 +198,18 @@ if (Launcher) {
|
|||
|
||||
argon2(password, salt, options) {
|
||||
return this.call('argon2', password, salt, options);
|
||||
},
|
||||
|
||||
hardwareCrypt: async (value, encrypt, touchIdPrompt) => {
|
||||
// let enc = await NativeModules.hardwareCrypt(NativeModules.makeXoredValue('hello'), true);
|
||||
// let dec = await NativeModules.hardwareCrypt(enc, false, 'decrypt');
|
||||
// NativeModules.readXoredValue(dec).toString('utf8');
|
||||
const { ipcRenderer } = Launcher.electron();
|
||||
return await ipcRenderer.invoke('hardware-crypt', value, encrypt, touchIdPrompt);
|
||||
}
|
||||
};
|
||||
|
||||
global.NativeModules = NativeModules;
|
||||
}
|
||||
|
||||
export { NativeModules };
|
||||
|
|
|
@ -34,8 +34,8 @@ const KdbxwebInit = {
|
|||
hash(args) {
|
||||
const ts = logger.ts();
|
||||
|
||||
const password = makeXoredValue(args.password);
|
||||
const salt = makeXoredValue(args.salt);
|
||||
const password = NativeModules.makeXoredValue(args.password);
|
||||
const salt = NativeModules.makeXoredValue(args.salt);
|
||||
|
||||
return NativeModules.argon2(password, salt, {
|
||||
type: args.type,
|
||||
|
@ -52,7 +52,7 @@ const KdbxwebInit = {
|
|||
|
||||
logger.debug('Argon2 hash calculated', logger.ts(ts));
|
||||
|
||||
return readXoredValue(res);
|
||||
return NativeModules.readXoredValue(res);
|
||||
})
|
||||
.catch((err) => {
|
||||
password.data.fill(0);
|
||||
|
@ -61,36 +61,6 @@ const KdbxwebInit = {
|
|||
logger.error('Argon2 error', err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
function makeXoredValue(val) {
|
||||
const data = Buffer.from(val);
|
||||
const random = Buffer.from(kdbxweb.Random.getBytes(data.length));
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] ^= random[i];
|
||||
}
|
||||
|
||||
const result = { data: [...data], random: [...random] };
|
||||
|
||||
data.fill(0);
|
||||
random.fill(0);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function readXoredValue(val) {
|
||||
const data = Buffer.from(val.data);
|
||||
const random = Buffer.from(val.random);
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] ^= random[i];
|
||||
}
|
||||
|
||||
val.data.fill(0);
|
||||
val.random.fill(0);
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
};
|
||||
return Promise.resolve(this.runtimeModule);
|
||||
|
|
|
@ -110,6 +110,7 @@ main.on('ready', () => {
|
|||
settingsPromise
|
||||
.then(() => {
|
||||
createMainWindow();
|
||||
setupIpcHandlers();
|
||||
setGlobalShortcuts(appSettings);
|
||||
subscribePowerEvents();
|
||||
hookRequestHeaders();
|
||||
|
@ -947,6 +948,11 @@ function httpRequest(config, log, onLoad) {
|
|||
req.end();
|
||||
}
|
||||
|
||||
function setupIpcHandlers() {
|
||||
const { setupIpcHandlers } = require('./scripts/ipc');
|
||||
setupIpcHandlers();
|
||||
}
|
||||
|
||||
function exitAndStartUpdate() {
|
||||
if (pendingUpdateFilePath) {
|
||||
const { installUpdate } = require('./scripts/update-installer');
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
let appMainRoot;
|
||||
const nativeModules = {};
|
||||
const { readXoredValue, makeXoredValue } = require('./scripts/util/byte-utils');
|
||||
const { reqNative } = require('./scripts/util/req-native');
|
||||
|
||||
const YubiKeyVendorIds = [0x1050];
|
||||
const attachedYubiKeys = [];
|
||||
|
@ -12,10 +8,6 @@ let usbListenerRunning = false;
|
|||
startListener();
|
||||
|
||||
const messageHandlers = {
|
||||
init(root) {
|
||||
appMainRoot = root;
|
||||
},
|
||||
|
||||
'start-usb'() {
|
||||
if (usbListenerRunning) {
|
||||
return;
|
||||
|
@ -101,15 +93,7 @@ const messageHandlers = {
|
|||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
const xoredRes = makeXoredValue(res);
|
||||
res.fill(0);
|
||||
|
||||
resolve(xoredRes);
|
||||
|
||||
setTimeout(() => {
|
||||
xoredRes.data.fill(0);
|
||||
xoredRes.random.fill(0);
|
||||
}, 0);
|
||||
resolve(makeXoredValue(res));
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
|
@ -119,15 +103,6 @@ const messageHandlers = {
|
|||
}
|
||||
};
|
||||
|
||||
const moduleInit = {
|
||||
usb(binding) {
|
||||
Object.keys(EventEmitter.prototype).forEach((key) => {
|
||||
binding[key] = EventEmitter.prototype[key];
|
||||
});
|
||||
return binding;
|
||||
}
|
||||
};
|
||||
|
||||
function isYubiKey(device) {
|
||||
return YubiKeyVendorIds.includes(device.deviceDescriptor.idVendor);
|
||||
}
|
||||
|
@ -159,54 +134,6 @@ function reportYubiKeys() {
|
|||
callback('yubikeys', attachedYubiKeys.length);
|
||||
}
|
||||
|
||||
function reqNative(mod) {
|
||||
if (nativeModules[mod]) {
|
||||
return nativeModules[mod];
|
||||
}
|
||||
|
||||
const fileName = `${mod}-${process.platform}-${process.arch}.node`;
|
||||
|
||||
const modulePath = `../node_modules/@keeweb/keeweb-native-modules/${fileName}`;
|
||||
const fullPath = path.join(appMainRoot, modulePath);
|
||||
|
||||
let binding = require(fullPath);
|
||||
|
||||
if (moduleInit[mod]) {
|
||||
binding = moduleInit[mod](binding);
|
||||
}
|
||||
|
||||
nativeModules[mod] = binding;
|
||||
return binding;
|
||||
}
|
||||
|
||||
function readXoredValue(val) {
|
||||
const data = Buffer.from(val.data);
|
||||
const random = Buffer.from(val.random);
|
||||
|
||||
val.data.fill(0);
|
||||
val.random.fill(0);
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] ^= random[i];
|
||||
}
|
||||
|
||||
random.fill(0);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function makeXoredValue(val) {
|
||||
const data = Buffer.from(val);
|
||||
const random = crypto.randomBytes(data.length);
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] ^= random[i];
|
||||
}
|
||||
const result = { data: [...data], random: [...random] };
|
||||
data.fill(0);
|
||||
random.fill(0);
|
||||
return result;
|
||||
}
|
||||
|
||||
function startListener() {
|
||||
process.on('message', ({ callId, cmd, args }) => {
|
||||
Promise.resolve()
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
const { readXoredValue, makeXoredValue } = require('../util/byte-utils');
|
||||
const { reqNative } = require('../util/req-native');
|
||||
|
||||
module.exports.hardwareCrypt = async function hardwareCrypt(e, value, encrypt, touchIdPrompt) {
|
||||
if (process.platform !== 'darwin') {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
// This is a native module, but why is it here and not in native-module-host?
|
||||
// It's because native-module-host is started as a fork, and macOS thinks it doesn't have necessary entitlements,
|
||||
// so any attempts to use secure enclave API fails with an error that there's no necessary entitlement.
|
||||
|
||||
const secureEnclave = reqNative('secure-enclave');
|
||||
const keyTag = 'net.antelle.keeweb.encryption-key';
|
||||
|
||||
const data = readXoredValue(value);
|
||||
|
||||
await checkKey();
|
||||
let res;
|
||||
if (encrypt) {
|
||||
res = await secureEnclave.encrypt({ keyTag, data });
|
||||
} else {
|
||||
res = await secureEnclave.decrypt({ keyTag, data, touchIdPrompt });
|
||||
}
|
||||
|
||||
data.fill(0);
|
||||
|
||||
return makeXoredValue(res);
|
||||
|
||||
async function checkKey() {
|
||||
if (checkKey.done) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await secureEnclave.createKeyPair({ keyTag });
|
||||
checkKey.done = true;
|
||||
} catch (e) {
|
||||
if (!e.keyExists) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
const { ipcMain } = require('electron');
|
||||
const { hardwareCrypt } = require('./ipc-handlers/hardware-crypt');
|
||||
|
||||
module.exports.setupIpcHandlers = () => {
|
||||
ipcMain.handle('hardware-crypt', hardwareCrypt);
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
const crypto = require('crypto');
|
||||
|
||||
module.exports = {
|
||||
readXoredValue: function readXoredValue(val) {
|
||||
const data = Buffer.from(val.data);
|
||||
const random = Buffer.from(val.random);
|
||||
|
||||
val.data.fill(0);
|
||||
val.random.fill(0);
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] ^= random[i];
|
||||
}
|
||||
|
||||
random.fill(0);
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
makeXoredValue: function makeXoredValue(val) {
|
||||
const data = Buffer.from(val);
|
||||
const random = crypto.randomBytes(data.length);
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
data[i] ^= random[i];
|
||||
}
|
||||
const result = { data: [...data], random: [...random] };
|
||||
data.fill(0);
|
||||
random.fill(0);
|
||||
|
||||
val.fill(0);
|
||||
|
||||
setTimeout(() => {
|
||||
result.data.fill(0);
|
||||
result.random.fill(0);
|
||||
}, 0);
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
const { EventEmitter } = require('events');
|
||||
const nativeModules = {};
|
||||
|
||||
const moduleInit = {
|
||||
usb(binding) {
|
||||
Object.keys(EventEmitter.prototype).forEach((key) => {
|
||||
binding[key] = EventEmitter.prototype[key];
|
||||
});
|
||||
return binding;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.reqNative = function reqNative(mod) {
|
||||
if (nativeModules[mod]) {
|
||||
return nativeModules[mod];
|
||||
}
|
||||
|
||||
const fileName = `${mod}-${process.platform}-${process.arch}.node`;
|
||||
const modulePath = `@keeweb/keeweb-native-modules/${fileName}`;
|
||||
|
||||
let binding = require(modulePath);
|
||||
|
||||
if (moduleInit[mod]) {
|
||||
binding = moduleInit[mod](binding);
|
||||
}
|
||||
|
||||
nativeModules[mod] = binding;
|
||||
return binding;
|
||||
};
|
|
@ -1,6 +1,7 @@
|
|||
Release notes
|
||||
-------------
|
||||
##### v1.17.0 (TBD)
|
||||
`+` opening files with Touch ID on macOS
|
||||
`+` password quality warnings
|
||||
`+` "Have I Been Pwned" service integration (opt-in)
|
||||
`+` automatically switching between dark and light theme
|
||||
|
|
Loading…
Reference in New Issue