index.js
'use strict'
const electron = require('electron')
const app = electron.app
const BrowserWindow = electron.BrowserWindow
const ipc = electron.ipcMain
const Tray = electron.Tray
const shell = electron.shell
const dialog = electron.dialog
const OAuth = require('./src/OAuth')
const util = require('./src/util')
const vault = require('./src/vault')
const MasterPass = require('./src/MasterPass')
const MasterPassKey = require('./src/_MasterPassKey')
const sync = require('./src/sync')
const init = require('./init')
const synker = require('./src/synker')
const moment = require('moment')
// const Vault_cl = require('./src/Vault_cl')
const Positioner = require('electron-positioner')
const _ = require('lodash')
const logger = require('./script/logger')
// change exec path
logger.info(`AppPath: ${app.getAppPath()}`)
logger.info(`__dirname: ${__dirname}`)
process.chdir(app.getAppPath())
logger.info(`Changed cwd to: ${process.cwd()}`)
// require('dotenv').config()
// App init
// app.dock.setIcon('res/app-icons/CryptoSync256.png')
// enable remote debugging
// app.commandLine.appendSwitch('remote-debugging-port', '8315')
// app.commandLine.appendSwitch('host-rules', 'MAP * 127.0.0.1')
// adds debug features like hotkeys for triggering dev tools and reload
require('electron-debug')()
// MasterPassKey is protected (private var) and only exist in Main memory
// MasterPassKey is a derived key of the actual user MasterPass
global.gAuth = {}
global.accounts = {}
global.creds = {}
global.state = {}
global.files = {}
global.stats = {}
global.paths = {
home: `${app.getPath('home')}/CryptoSync`,
crypted: `${app.getPath('home')}/CryptoSync/.crypto`,
mdb: `${app.getPath('userData')}/mdb`,
userData: app.getPath('userData'),
vault: `${app.getPath('home')}/CryptoSync/vault.crypto`
}
logger.verbose(require('util').inspect(global.paths, { depth: null }))
global.settings = {
user: {
},
default: {
keyLength: '128',
algorithm: 'CTR',
randomness: 'Pseudo',
MPkeyLength: '256',
shares: 's2n3',
autostart: 'true',
offlineEnc: 'true'
}
}
global.views = {
main: `file://${__dirname}/static/index.html`,
masterpassprompt: `file://${__dirname}/static/masterpassprompt.html`,
setup: `file://${__dirname}/static/setup.html`,
menubar: `file://${__dirname}/static/menubar.html`,
errorprompt: `file://${__dirname}/static/errorprompt.html`,
settings: `file://${__dirname}/static/settings.html`,
vault: `file://${__dirname}/static/vault.html`
}
// prevent the following from being garbage collected
let Menubar
let exit = false
/**
* Promises (global)
**/
/**
* Window constructors
**/
function Cryptobar (callback) {
function click (e, bounds) {
if (e.altKey || e.shiftKey || e.ctrlKey || e.metaKey) {
return hideWindow()
}
if (win && win.isVisible()) {
return hideWindow()
}
// double click sometimes returns `undefined`
bounds = bounds || cachedBounds
cachedBounds = bounds
showWindow(cachedBounds)
}
function showWindow (trayPos) {
// Default the window to the right if `trayPos` bounds are undefined or null.
let noBoundsPosition = null
if ((trayPos === undefined || trayPos.x === 0) && winPosition.substr(0, 4) === 'tray') {
noBoundsPosition = (process.platform === 'win32') ? 'bottomRight' : 'topRight'
}
let position = positioner.calculate(noBoundsPosition || winPosition, trayPos)
win.setPosition(position.x, position.y)
webContents.send('updateMoments')
win.show()
return
}
function hideWindow () {
if (!win) {
return
}
// emit hide
win.hide()
// emitt after-hide
}
let win = new BrowserWindow({
width: 500, // 290
height: 312,
frame: false,
show: false
// resizable: false
})
app.dock.hide()
let cachedBounds
const winPosition = (process.platform === 'win32') ? 'trayBottomCenter' : 'trayCenter'
const positioner = new Positioner(win)
// TODO: Change icon based on mode (dark || light) on OSX and set default to light
Menubar = new Tray('static/images/mb/trayic_light.png')
Menubar.on('click', click)
.on('double-click', click)
win.on('blur', hideWindow)
win.loadURL(global.views.menubar)
win.openDevTools()
const webContents = win.webContents
// Event listeners
sync.event.on('put', (file) => {
logger.verbose(`PUT EVENT RECEIVED for ${file.name}`)
webContents.send('synced', {
name: file.name,
fileType: file.fullFileExtension,
type: 'gdrive',
lastMoment: file.lastSynced,
synced: 'Uploaded'
})
})
sync.event.on('crypted', (file) => {
logger.verbose(`PUT EVENT RECEIVED for ${file.name}`)
webContents.send('synced', {
name: file.name,
fileType: file.fullFileExtension,
type: 'gdrive',
lastMoment: file.lastCrypted,
synced: 'Encrypted'
})
})
sync.event.on('statusChange', (status) => {
logger.verbose(`statusChange: status changed to ${status}`)
webContents.send('statusChange', status)
})
ipc.on('openSyncFolder', function (event) {
logger.verbose('IPCMAIN: openSyncFolder event emitted')
shell.showItemInFolder(global.paths.vault)
})
ipc.on('quitApp', function (event) {
logger.verbose('IPCMAIN: quitApp event emitted Calling app.quit()...')
app.quit()
})
ipc.on('openSettings', function (event) {
logger.verbose('IPCMAIN: openSettings event emitted')
Settings(function (result) {})
})
ipc.on('openVault', function (event) {
logger.verbose('IPCMAIN: openVault event emitted')
VaultUI(null)
})
win.on('closed', function () {
logger.verbose('win.closed event emitted for Menubar.')
win = null
callback()
})
}
function VaultUI (callback) {
let win = new BrowserWindow({
width: 700,
height: 400,
center: true,
titleBarStyle: 'hidden-inset'
})
win.loadURL(global.views.vault)
win.openDevTools()
win.on('closed', function () {
logger.verbose('win.closed event emitted for VaultUI.')
win = null
if (callback) callback()
})
}
function Setup (callback) {
let win = new BrowserWindow({
width: 640,
height: 420,
center: true,
show: true,
titleBarStyle: 'hidden-inset'
// width: 580,
// height: 420
// resizable: false,
})
let setupComplete = false
let webContents = win.webContents
win.loadURL(global.views.setup)
win.openDevTools()
ipc.on('initAuth', function (event, type) {
logger.verbose('IPCMAIN: initAuth emitted. Creating Auth...')
global.gAuth = new OAuth(type)
global.mdb.onlyGetValue('gdrive-token').then((token) => {
global.gAuth.authorize(token, function (authUrl) {
if (authUrl) {
logger.info(`Loading authUrl... ${authUrl}`)
win.loadURL(authUrl, {
'extraHeaders': 'pragma: no-cache\n'
})
} else {
logger.warn('As already exists, loading masterpass...')
win.loadURL(`${global.views.setup}?nav_to=masterpass`)
}
})
})
})
win.on('unresponsive', function (event) {
logger.verbose('Setup UNRESPONSIVE')
})
webContents.on('did-navigate', function (event, url) {
logger.verbose(`IPCMAIN: did-navigate emitted URL: ${url}`)
const regex = /^http:\/\/localhost\/\?(error|code)/g
if (regex.test(url)) {
logger.info('localhost URL matches')
win.loadURL(`${global.views.setup}?nav_to=auth`)
// logger.verbose('MAIN: url matched, sending to RENDER...')
let err = util.getParam('error', url)
// if error then callback URL is http://localhost/?error=access_denied#
// if sucess then callback URL is http://localhost/?code=2bybyu3b2bhbr
if (!err) {
let auth_code = util.getParam('code', url)
logger.verbose(`IPCMAIN: Got the auth_code, ${auth_code}`)
logger.verbose('IPCMAIN: Calling callback with the code...')
global.gAuth.getToken(auth_code) // Get auth token from auth code
// store auth token in mdb
.then((token) => {
global.gAuth.oauth2Client.credentials = token
return global.mdb.storeToken(token)
})
.then(() => {
return init.drive(global.gAuth)
})
.then(() => {
return sync.getAccountInfo()
})
.then((res) => {
return sync.getPhoto(res)
})
.then((param) => {
return sync.setAccountInfo(param, global.gAuth)
})
.then((email) => {
return sync.getAllFiles(email)
})
.then((trees) => {
return init.syncGlobals(trees)
})
.catch(function (error) {
logger.error(`PROMISE ERR: ${error.stack}`)
})
webContents.on('did-finish-load', function () {
webContents.send('authResult', null)
})
} else {
webContents.on('did-finish-load', function () {
webContents.send('authResult', err)
})
}
}
})
webContents.on('will-navigate', function (event, url) {
logger.verbose(`IPCMAIN: will-navigate emitted URL: ${url}`)
})
ipc.on('setMasterPass', function (event, masterpass) {
logger.verbose('IPCMAIN: setMasterPass emitted Setting Masterpass...')
MasterPass.set(masterpass, function (err, mpkey) {
global.MasterPassKey = new MasterPassKey(mpkey)
global.mdb.saveGlobalObj('creds')
.catch((err) => {
throw err
})
webContents.send('setMasterPassResult', err)
})
})
ipc.on('done', function (event, masterpass) {
logger.info('IPCMAIN: done emitted setup complete. Closing this window and opening menubar...')
setupComplete = true
vault.init(global.MasterPassKey.get())
.then(() => {
return win.close()
})
.then(() => {
return app.quit()
})
.catch((err) => {
logger.error(`vault.init ERR: ${err.stack}`)
throw (err)
})
})
win.on('closed', function () {
logger.verbose('IPCMAIN: win.closed event emitted for setupWindow.')
win = null
if (setupComplete) {
callback(null)
} else {
callback(new Error('Setup did not finish successfully'))
}
})
}
function addAccountPrompt (callback) {
let win = new BrowserWindow({
width: 580,
height: 420,
center: true,
show: true,
titleBarStyle: 'hidden-inset'
// width: 400,
// height: 460
// resizable: false,
})
let webContents = win.webContents
win.loadURL(global.views.setup)
win.openDevTools()
ipc.on('initAuth', function (event, type) {
logger.verbose('IPCMAIN: initAuth emitted. Creating Auth...')
global.gAuth = new OAuth(type)
global.mdb.onlyGetValue('gdrive-token').then((token) => {
global.gAuth.authorize(token, function (authUrl) {
if (authUrl) {
logger.verbose(`Loading authUrl... ${authUrl}`)
win.loadURL(authUrl, {
'extraHeaders': 'pragma: no-cache\n'
})
} else {
logger.verbose('As already exists, loading masterpass...')
win.loadURL(`${global.views.setup}?nav_to=masterpass`)
}
})
})
})
win.on('unresponsive', function (event) {
logger.verbose('addAccountPrompt UNRESPONSIVE')
})
webContents.on('did-navigate', function (event, url) {
logger.verbose(`IPCMAIN: did-navigate emitted URL: ${url}`)
const regex = /^http:\/\/localhost/g
if (regex.test(url)) {
logger.verbose('localhost URL matches')
win.loadURL(`${global.views.setup}?nav_to=auth`)
// logger.verbose('MAIN: url matched, sending to RENDER...')
let err = util.getParam('error', url)
// if error then callback URL is http://localhost/?error=access_denied#
// if sucess then callback URL is http://localhost/?code=2bybyu3b2bhbr
if (!err) {
let auth_code = util.getParam('code', url)
logger.verbose(`IPCMAIN: Got the auth_code, ${auth_code}`)
logger.verbose('IPCMAIN: Calling callback with the code...')
} else {
callback(err)
}
}
})
webContents.on('will-navigate', function (event, url) {
logger.verbose(`IPCMAIN: will-navigate emitted URL: ${url}`)
})
win.on('closed', function () {
logger.verbose('IPCMAIN: win.closed event emitted for setupWindow.')
win = null
callback('ERROR: Cancelled the account adding flow')
})
}
function Settings (callback) {
let win = new BrowserWindow({
width: 800,
height: 600,
center: true
})
win.loadURL(global.views.settings)
let webContents = win.webContents
win.openDevTools()
// TODO: close app after pass has been reset and vault has been re-encrypted
ipc.on('resetMasterPass', function (event, type) {
event.preventDefault()
logger.verbose('IPCMAIN: resetMasterPass emitted. Creating MasterPassPrompt...')
MasterPass.Prompt(true)
.then((MPKset) => {
webContents.send('resetMasterPassResult', MPKset)
return
})
.catch((err) => {
webContents.send('resetMasterPassResult', MPKset)
logger.error(`resetMasterPass err: ${err}`)
})
})
ipc.on('removeAccount', function (event, account) {
logger.verbose(`IPCMAIN: removeAccount emitted. Creating removing ${account}...`)
// TODO: IMPLEMENT ACCOUNT REMOVAL ROUTINE
// if (_.unset(global.accounts, account)) {
// deleted
// reload window to update
// win.loadURL(global.views.settings)
// TODO: decide whether to do setup is all accounts removed
// if (Object.keys(global.accounts).length === 0) {
// // Create Setup
//
// } else {
//
// }
// } else {
// not deleted
// }
})
win.on('closed', function () {
logger.verbose('win.closed event emitted for Settings.')
win = null
callback()
})
}
// TODO: replace with dialog.showErrorBox(title, content) for native dialog?
function ErrorPrompt (err, callback) {
let win = new BrowserWindow({
width: 240,
height: 120,
center: true,
titleBarStyle: 'hidden-inset',
show: true
})
let webContents = win.webContents
let res
win.loadURL(global.views.errorprompt)
logger.info(`ERROR PROMPT: the error is ${err}`)
webContents.on('did-finish-load', function () {
webContents.send('error', err)
})
ipc.on('response', function (event, response) {
logger.verbose('ERROR PROMPT: Got user response')
res = response
win.close()
})
win.on('closed', function (response) {
logger.verbose('win.closed event emitted for ErrPromptWindow.')
win = null
if (callback) {
if (response) {
callback(res)
} else {
callback(null)
}
}
})
}
/**
* Functions
**/
/**
* Event handlers
**/
// Check for connection status
ipc.on('online-status-changed', function (event, status) {
logger.verbose(`APP: online-status-changed event emitted changed to ${status}`)
})
app.on('window-all-closed', () => {
logger.verbose('APP: window-all-closed event emitted')
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('quit', () => {
logger.info('APP: quit event emitted')
})
app.on('will-quit', (event) => {
if (!exit) {
event.preventDefault()
logger.info(`APP.ON('will-quit'): will-quit event emitted`)
logger.verbose(`platform is ${process.platform}`)
// TODO: global.accounts[Object.keys(global.accounts)[0]].oauth.oauth2Client.credentials = global.gAuth.credentials
global.stats.endTime = moment().format()
Promise.all([
global.mdb.saveGlobalObj('accounts'),
global.mdb.saveGlobalObj('state'),
global.mdb.saveGlobalObj('settings'),
global.mdb.saveGlobalObj('files'),
global.mdb.saveGlobalObj('stats')
]).then(function () {
if (global.MasterPassKey !== undefined && !_.isEmpty(global.vault)) {
logger.info(`DEFAULT EXIT. global.MasterPassKey and global.vault not empty. Calling crypto.encryptObj...`)
logger.verbose(`Encrypting using MasterPass = ${global.MasterPassKey.get().toString('hex')}, viv = ${global.creds.viv.toString('hex')}`)
vault.encrypt(global.MasterPassKey.get())
.then((tag) => {
logger.verbose(`crypto.encryptObj invoked...`)
logger.info(`Encrypted successfully with tag = ${tag.toString('hex')}, saving auth tag and closing mdb...`)
global.creds.authTag = tag
global.mdb.saveGlobalObj('creds').then(() => {
global.mdb.close()
logger.info('Closed vault and mdb (called mdb.close()).')
exit = true
app.quit()
}).catch((err) => {
logger.error(`Error while saving global.creds before quit: ${err.stack}`)
})
})
.catch((err) => {
logger.error(err.stack)
throw err
})
} else {
logger.info(`NORMAL EXIT. global.MasterPassKey / global.vault empty. Just closing mdb (global.mdb.close())...`)
global.mdb.close()
exit = true
app.quit()
}
}, function (reason) {
logger.error(`PROMISE ERR: ${reason}`)
}).catch(function (error) {
logger.error(`PROMISE ERR: ${error.stack}`)
})
} else {
return
}
})
app.on('activate', function (win) {
logger.verbose('activate event emitted')
})
/**
* Main
**/
app.on('ready', function () {
// Check synchronously whether paths exist
let mainRun = ((util.checkDirectorySync(global.paths.mdb)) && (util.checkFileSync(global.paths.vault)))
// If the MDB or vault does not exist, run setup
// otherwise run main
if (mainRun) {
// Run main
logger.info('Main run. Creating Menubar...')
init.main() // Initialise (open mdb and get creds)
.then(() => {
return MasterPass.Prompt() // Obtain MP, derive MPK and set globally
})
.then(() => {
return vault.decrypt(global.MasterPassKey.get()) // Decrypt vault with MPK
})
.then(() => {
// restore global state from mdb
return Promise.all([
global.mdb.restoreGlobalObj('accounts'),
global.mdb.restoreGlobalObj('state'),
global.mdb.restoreGlobalObj('settings'),
global.mdb.restoreGlobalObj('stats'),
global.mdb.restoreGlobalObj('files')
])
})
.then(() => {
// Initialise Google Drive client
return init.drive(global.accounts[Object.keys(global.accounts)[0]].oauth.oauth2Client, true)
})
.then(() => {
// Set initial stats
return init.stats()
})
.then(() => {
// Initial sync worker
return synker.init()
})
.then(() => {
// TODO: start sync daemon
// Start menubar
return Cryptobar(function (result) {
logger.info(`Cryptobar results: ${result}`)
})
})
.catch(function (error) {
// Catch any fatal errors and exit
logger.error(`PROMISE ERR: ${error.stack}`)
// dialog.showErrorBox('Oops, we encountered a problem...', error.message)
app.quit()
})
} else {
// Run Setup
logger.info('Setup run. Creating Setup wizard...')
init.setup()
.then(() => {
return new Promise(function (resolve, reject) {
Setup(function (err) {
if (err) {
logger.error(err)
reject(err)
} else {
logger.info('MAIN Setup successfully completed. quitting...')
resolve()
}
})
})
})
.catch(function (error) {
logger.error(`PROMISE ERR: ${error.stack}`)
// dialog.showErrorBox('Oops, we encountered a problem...', error.message)
app.quit()
})
}
})
exports.MasterPassPrompt = function (reset, callback) {
let tries = 0
let gotMP = false
let error = null
let win = new BrowserWindow({
width: 300, // 300
height: 435,
center: true,
titleBarStyle: 'hidden-inset'
// resizable: false,
})
let webContents = win.webContents
if (reset) {
win.loadURL(`${global.views.masterpassprompt}?nav_to=reset`)
} else {
win.loadURL(global.views.masterpassprompt)
}
// win.openDevTools()
ipc.on('checkMasterPass', function (event, masterpass) {
logger.verbose('IPCMAIN: checkMasterPass emitted. Checking MasterPass...')
MasterPass.check(masterpass, function (err, match, mpkey) {
if (err) {
// send error
webContents.send('checkMasterPassResult', err)
error = err
win.close()
}
if (match) {
logger.info('IPCMAIN: PASSWORD MATCHES!')
// Now derive masterpasskey and set it (temporarily)
global.MasterPassKey = new MasterPassKey(mpkey)
webContents.send('checkMasterPassResult', {
err: null,
match: match
})
gotMP = true
setTimeout(function () {
win.close()
}, 1000)
} else {
logger.warn('IPCMAIN: PASSWORD DOES NOT MATCH!')
webContents.send('checkMasterPassResult', {
err: null,
match: match
})
if (++tries >= 3) {
error = new Error('Limit of three tries exceeded')
win.close()
}
}
})
})
ipc.on('setMasterPass', function (event, masterpass) {
logger.verbose('IPCMAIN: setMasterPass emitted Setting Masterpass...')
MasterPass.set(masterpass, function (err, mpkey) {
if (!err) {
global.MasterPassKey = new MasterPassKey(mpkey)
// TODO: test this
vault.init(global.MasterPassKey.get())
.then((value) => {
return global.mdb.saveGlobalObj('creds')
})
.catch((err) => {
error = err
win.close()
})
gotMP = true
webContents.send('setMasterPassResult', null)
} else {
webContents.send('setMasterPassResult', err)
error = err
win.close()
}
})
})
win.on('closed', function () {
logger.info('win.closed event emitted for MasterPassPrompt')
callback(error, gotMP)
win = null
})
return win
}