From 758dbccd01c55239f2b18b5c0b7bd12d35d1f7a8 Mon Sep 17 00:00:00 2001 From: Jarek Kowalski Date: Tue, 4 Feb 2020 23:08:07 -0800 Subject: [PATCH] kopia-ui: UI/API server security (TLS & base auth) This automatically launches Kopia CLI with new flags: `--tls-generate-cert` which generates in-memory TLS certs for the server and prints its fingerprint to stderr. `--random-password` which sets up random base auth password KopiaUI will then listen to the stderr messages and parse the cert fingerprint and password and automatically provide them in the browser it opens to show the UI. This ensures that any local process (or script in a browser, etc.) other than KopiaUI itself won't have access to the server API nor UI because they won't know the ephemeral random password and won't be able to intercept it due to TLS protection. --- app/public/electron.js | 30 ++++++++++++++++++++++++-- app/public/server.js | 49 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/app/public/electron.js b/app/public/electron.js index ce35ed673..aaba2ae7d 100644 --- a/app/public/electron.js +++ b/app/public/electron.js @@ -5,7 +5,7 @@ const config = require('electron-json-config'); const { resourcesPath, selectByOS } = require('./utils'); const { toggleLaunchAtStartup, willLaunchAtStartup, refreshWillLaunchAtStartup } = require('./auto-launch'); -const { stopServer, actuateServer } = require('./server'); +const { stopServer, actuateServer, getServerAddress, getServerCertSHA256, getServerPassword } = require('./server'); ipcMain.on('fetch-config', (event, arg) => { @@ -62,7 +62,7 @@ function showMainWindow() { autoHideMenuBar: true, }) - mainWindow.loadURL('http://localhost:51515/?ts=' + new Date().valueOf()); + mainWindow.loadURL(getServerAddress() + '/?ts=' + new Date().valueOf()); mainWindow.on('closed', function () { // forget the reference. @@ -86,6 +86,32 @@ app.on('will-quit', function () { stopServer(); }); +app.on('login', (event, webContents, request, authInfo, callback) => { + // intercept password prompts and automatically enter password that the server has printed for us. + const p = getServerPassword(); + if (p) { + event.preventDefault(); + console.log('automatically logging in...'); + callback('kopia', p); + } +}); + +app.on('certificate-error', (event, webContents, url, error, certificate, callback) => { + // intercept certificate errors and automatically trust the certificate the server has printed for us. + const expected = 'sha256/' + Buffer.from(getServerCertSHA256(), 'hex').toString('base64'); + if (certificate.fingerprint === expected) { + console.log('accepting server certificate.'); + + // On certificate error we disable default behaviour (stop loading the page) + // and we then say "it is all fine - true" to the callback + event.preventDefault(); + callback(true); + return; + } + + console.log('certificate error:', certificate.fingerprint, expected); +}); + // Ignore app.on('window-all-closed', function () {}) diff --git a/app/public/server.js b/app/public/server.js index a995246cb..111f9f706 100644 --- a/app/public/server.js +++ b/app/public/server.js @@ -6,6 +6,37 @@ const { defaultServerBinary } = require('./utils'); const { spawn } = require('child_process'); let serverProcess = null; +let serverCertSHA256 = ""; +let serverPassword = ""; +let serverAddress = ""; + +function detectServerParam(data) { + let lines = (data + '').split('\n'); + for (let i = 0; i < lines.length; i++) { + const p = lines[i].indexOf(": "); + if (p < 0) { + continue + } + + const key = lines[i].substring(0, p); + const value = lines[i].substring(p + 2); + switch (key) { + case "SERVER PASSWORD": + serverPassword = value; + break; + + case "SERVER CERT SHA256": + serverCertSHA256 = value; + break; + + case "SERVER ADDRESS": + serverAddress = value; + break; + } + } + + appendToLog(data); +} function startServer() { let kopiaPath = config.get('kopiaPath'); @@ -22,12 +53,16 @@ function startServer() { args.push('server', '--ui'); + args.push('--random-password') + args.push('--tls-generate-cert') + // args.push('--auto-shutdown=600s') + console.log(`spawning ${kopiaPath} ${args.join(' ')}`); serverProcess = spawn(kopiaPath, args); ipcMain.emit('server-status-updated'); serverProcess.stdout.on('data', appendToLog); - serverProcess.stderr.on('data', appendToLog); + serverProcess.stderr.on('data', detectServerParam); serverProcess.on('close', (code, signal) => { appendToLog(`child process exited with code ${code} and signal ${signal}`); @@ -81,5 +116,17 @@ module.exports = { } }, + getServerAddress() { + return serverAddress; + }, + + getServerCertSHA256() { + return serverCertSHA256; + }, + + getServerPassword() { + return serverPassword; + }, + stopServer: stopServer, } \ No newline at end of file