diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index dde4efb6..8b3d42b4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -95,11 +95,6 @@ jobs: with: name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm path: dist/desktop/KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm - - name: Upload update artifact - uses: actions/upload-artifact@v1 - with: - name: UpdateDesktop.zip - path: dist/desktop/UpdateDesktop.zip darwin: runs-on: macos-latest @@ -124,9 +119,6 @@ jobs: path: dist - name: Install npm modules run: npm ci - - name: Install desktop npm modules - working-directory: desktop - run: npm ci - name: Install grunt run: sudo npm i -g grunt-cli - name: Write secrets @@ -179,9 +171,6 @@ jobs: path: dist - name: Install npm modules run: npm ci - - name: Install desktop npm modules - working-directory: desktop - run: npm ci - name: Install grunt run: npm i -g grunt-cli - name: Write secrets @@ -328,11 +317,6 @@ jobs: with: name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip path: assets - - name: Download update artifact - uses: actions/download-artifact@v1 - with: - name: UpdateDesktop.zip - path: assets - name: Zip html working-directory: html run: zip -vr ../assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip . @@ -505,15 +489,6 @@ jobs: asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip asset_name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip asset_content_type: application/octet-stream - - name: Upload update asset - uses: actions/upload-release-asset@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - upload_url: ${{ steps.create_release.outputs.upload_url }} - asset_path: assets/UpdateDesktop.zip - asset_name: UpdateDesktop.zip - asset_content_type: application/octet-stream - name: Upload verify.sign asset uses: actions/upload-release-asset@v1 env: diff --git a/Gruntfile.js b/Gruntfile.js index 43953b07..7546a418 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -138,18 +138,6 @@ module.exports = function (grunt) { expand: true, nonull: true }, - 'desktop-update': { - cwd: 'tmp/desktop/keeweb-linux-x64/resources/', - src: 'app.asar', - dest: 'tmp/desktop/update/', - expand: true, - nonull: true - }, - 'desktop-update-helper': { - src: ['helper/darwin/KeeWebHelper', 'helper/win32/KeeWebHelper.exe'], - dest: 'tmp/desktop/update/', - nonull: true - }, 'desktop-darwin-helper-x64': { src: 'helper/darwin/KeeWebHelper', dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/', @@ -461,13 +449,6 @@ module.exports = function (grunt) { options: { level: 6 }, - 'desktop-update': { - options: { - archive: 'dist/desktop/UpdateDesktop.zip', - comment: zipCommentPlaceholder - }, - files: [{ cwd: 'tmp/desktop/update', src: '**', expand: true, nonull: true }] - }, 'win32-x64': { options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.x64.zip` }, files: [{ cwd: 'tmp/desktop/KeeWeb-win32-x64', src: '**', expand: true }] @@ -599,35 +580,6 @@ module.exports = function (grunt) { ] } }, - 'sign-archive': { - 'desktop-update': { - options: { - file: 'dist/desktop/UpdateDesktop.zip', - signature: zipCommentPlaceholder - } - } - }, - 'sign-desktop-files': { - 'desktop-update': { - options: { - path: 'tmp/desktop/update' - } - } - }, - 'validate-desktop-update': { - desktop: { - options: { - file: 'dist/desktop/UpdateDesktop.zip', - expected: [ - 'app.asar', - 'helper/darwin/KeeWebHelper', - 'helper/win32/KeeWebHelper.exe' - ], - expectedCount: 7, - publicKey: 'app/resources/public-key.pem' - } - } - }, 'osx-sign': { options: { get identity() { @@ -758,10 +710,7 @@ module.exports = function (grunt) { sign: 'dist/desktop/Verify.sign.sha256' }, files: { - 'dist/desktop/Verify.sha256': [ - 'dist/desktop/KeeWeb-*', - 'dist/desktop/UpdateDesktop.zip' - ] + 'dist/desktop/Verify.sha256': ['dist/desktop/KeeWeb-*'] } } }, diff --git a/app/scripts/comp/app/updater.js b/app/scripts/comp/app/updater.js index b72d657d..fb16ac3c 100644 --- a/app/scripts/comp/app/updater.js +++ b/app/scripts/comp/app/updater.js @@ -1,3 +1,4 @@ +import kdbxweb from 'kdbxweb'; import { Events } from 'framework/events'; import { RuntimeInfo } from 'const/runtime-info'; import { Transport } from 'comp/browser/transport'; @@ -15,7 +16,6 @@ const Updater = { UpdateInterval: 1000 * 60 * 60 * 24, MinUpdateTimeout: 500, MinUpdateSize: 10000, - UpdateCheckFiles: ['app.asar'], nextCheckTimeout: null, updateCheckDate: new Date(0), enabled: Launcher && Launcher.updaterEnabled(), @@ -34,7 +34,7 @@ const Updater = { updateInProgress() { return ( UpdateModel.status === 'checking' || - ['downloading', 'extracting'].indexOf(UpdateModel.updateStatus) >= 0 + ['downloading', 'extracting', 'updating'].indexOf(UpdateModel.updateStatus) >= 0 ); }, @@ -180,28 +180,61 @@ const Updater = { } UpdateModel.set({ updateStatus: 'downloading', updateError: null }); logger.info('Downloading update', ver); + const updateAssetName = this.getUpdateAssetName(ver); + if (!updateAssetName) { + logger.error('Empty updater asset name for', Launcher.platform(), Launcher.arch()); + return; + } + const updateUrlBasePath = Links.UpdateBasePath.replace('{ver}', ver); + const updateAssetUrl = updateUrlBasePath + updateAssetName; + const useCache = !startedByUser; Transport.httpGet({ - url: Links.UpdateDesktop.replace('{ver}', ver), - file: 'KeeWeb-' + ver + '.zip', - cache: !startedByUser, - success: (filePath) => { - UpdateModel.set({ updateStatus: 'extracting' }); - logger.info('Extracting update file', this.UpdateCheckFiles, filePath); - this.extractAppUpdate(filePath, (err) => { - if (err) { - logger.error('Error extracting update', err); + url: updateAssetUrl, + file: updateAssetName, + cleanupOldFiles: true, + cache: useCache, + success: (assetFilePath) => { + logger.info('Downloading update signatures'); + Transport.httpGet({ + url: updateUrlBasePath + 'Verify.sign.sha256', + text: true, + file: updateAssetName + '.sign', + cleanupOldFiles: true, + cache: useCache, + success: (assetFileSignaturePath) => { + this.verifySignature(assetFilePath, updateAssetName, (err, valid) => { + if (err) { + UpdateModel.set({ + updateStatus: 'error', + updateError: 'Error verifying update signature' + }); + return; + } + if (!valid) { + UpdateModel.set({ + updateStatus: 'error', + updateError: 'Invalid update signature' + }); + Launcher.deleteFile(assetFilePath); + Launcher.deleteFile(assetFileSignaturePath); + return; + } + logger.info('Update is ready', assetFilePath); + UpdateModel.set({ updateStatus: 'ready', updateError: null }); + if (!startedByUser) { + Events.emit('update-app'); + } + if (typeof successCallback === 'function') { + successCallback(); + } + }); + }, + error(e) { + logger.error('Error downloading update signatures', e); UpdateModel.set({ updateStatus: 'error', - updateError: 'Error extracting update' + updateError: 'Error downloading update signatures' }); - } else { - UpdateModel.set({ updateStatus: 'ready', updateError: null }); - if (!startedByUser) { - Events.emit('update-app'); - } - if (typeof successCallback === 'function') { - successCallback(); - } } }); }, @@ -215,61 +248,64 @@ const Updater = { }); }, - extractAppUpdate(updateFile, cb) { - const expectedFiles = this.UpdateCheckFiles; - const appPath = Launcher.getUserDataPath(); - const StreamZip = Launcher.req('node-stream-zip'); - StreamZip.setFs(Launcher.req('original-fs')); - const zip = new StreamZip({ file: updateFile, storeEntries: true }); - zip.on('error', cb); - zip.on('ready', () => { - const containsAll = expectedFiles.every((expFile) => { - const entry = zip.entry(expFile); - return entry && entry.isFile; + verifySignature(assetFilePath, assetName, callback) { + logger.info('Verifying update signature', assetName); + const fs = Launcher.req('fs'); + const signaturesTxt = fs.readFileSync(assetFilePath + '.sign', 'utf8'); + const assetSignatureLine = signaturesTxt + .split('\n') + .find((line) => line.endsWith(assetName)); + if (!assetSignatureLine) { + logger.error('Signature not found for asset', assetName); + callback('Asset signature not found'); + return; + } + const signature = kdbxweb.ByteUtils.hexToBytes(assetSignatureLine.split(' ')[0]); + const fileBytes = fs.readFileSync(assetFilePath); + SignatureVerifier.verify(fileBytes, signature) + .catch((e) => { + logger.error('Error verifying signature', e); + callback('Error verifying signature'); + }) + .then((valid) => { + logger.info(`Update asset signature is ${valid ? 'valid' : 'invalid'}`); + callback(undefined, valid); }); - if (!containsAll) { - return cb('Bad archive'); - } - this.validateArchiveSignature(updateFile, zip) - .then(() => { - zip.extract(null, appPath, (err) => { - zip.close(); - if (err) { - return cb(err); - } - Launcher.deleteFile(updateFile); - cb(); - }); - }) - .catch((e) => { - return cb('Invalid archive: ' + e); - }); - }); }, - validateArchiveSignature(archivePath, zip) { - if (!zip.comment) { - return Promise.reject('No comment in ZIP'); + getUpdateAssetName(ver) { + const platform = Launcher.platform(); + const arch = Launcher.arch(); + switch (platform) { + case 'win32': + switch (arch) { + case 'x64': + return `KeeWeb-${ver}.win.x64.exe`; + case 'ia32': + return `KeeWeb-${ver}.win.ia32.exe`; + case 'arm64': + return `KeeWeb-${ver}.win.arm64.exe`; + } + break; + case 'darwin': + switch (arch) { + case 'x64': + return `KeeWeb-${ver}.mac.x64.dmg`; + case 'arm64': + return `KeeWeb-${ver}.mac.arm64.dmg`; + } + break; } - if (zip.comment.length !== 512) { - return Promise.reject('Bad comment length in ZIP: ' + zip.comment.length); - } - try { - const zipFileData = Launcher.req('fs').readFileSync(archivePath); - const dataToVerify = zipFileData.slice(0, zip.centralDirectory.headerOffset + 22); - const signature = window.Buffer.from(zip.comment, 'hex'); - return SignatureVerifier.verify(dataToVerify, signature) - .catch(() => { - throw new Error('Error verifying signature'); - }) - .then((isValid) => { - if (!isValid) { - throw new Error('Invalid signature'); - } - }); - } catch (err) { - return Promise.reject(err.toString()); + return undefined; + }, + + installAndRestart() { + if (!Launcher) { + return; } + const updateAssetName = this.getUpdateAssetName(UpdateModel.lastVersion); + const updateFilePath = Transport.cacheFilePath(updateAssetName); + Launcher.requestRestartAndUpdate(updateFilePath); } }; diff --git a/app/scripts/comp/browser/transport.js b/app/scripts/comp/browser/transport.js index b2c6c6f2..316db958 100644 --- a/app/scripts/comp/browser/transport.js +++ b/app/scripts/comp/browser/transport.js @@ -1,15 +1,33 @@ import { Launcher } from 'comp/launcher'; import { Logger } from 'util/logger'; import { noop } from 'util/fn'; +import { StringFormat } from 'util/formatting/string-format'; const logger = new Logger('transport'); const Transport = { + cacheFilePath(fileName) { + return Launcher.getTempPath(fileName); + }, + httpGet(config) { let tmpFile; const fs = Launcher.req('fs'); if (config.file) { - tmpFile = Launcher.getTempPath(config.file); + const baseTempPath = Launcher.getTempPath(); + if (config.cleanupOldFiles) { + const allFiles = fs.readdirSync(baseTempPath); + for (const file of allFiles) { + if ( + file !== config.file && + StringFormat.replaceVersion(file, '0') === + StringFormat.replaceVersion(config.file, '0') + ) { + fs.unlinkSync(Launcher.joinPath(baseTempPath, file)); + } + } + } + tmpFile = Launcher.joinPath(baseTempPath, config.file); if (fs.existsSync(tmpFile)) { try { if (config.cache && fs.statSync(tmpFile).size > 0) { @@ -62,8 +80,10 @@ const Transport = { }); res.on('end', () => { data = window.Buffer.concat(data); - if (config.json) { + if (config.text || config.json) { data = data.toString('utf8'); + } + if (config.json) { try { data = JSON.parse(data); } catch (e) { diff --git a/app/scripts/comp/launcher/launcher-electron.js b/app/scripts/comp/launcher/launcher-electron.js index 06d44067..2a5a0d91 100644 --- a/app/scripts/comp/launcher/launcher-electron.js +++ b/app/scripts/comp/launcher/launcher-electron.js @@ -17,6 +17,9 @@ const Launcher = { platform() { return process.platform; }, + arch() { + return process.arch; + }, electron() { return this.req('electron'); }, @@ -55,7 +58,15 @@ const Launcher = { return this.joinPath(this.userDataPath, fileName || ''); }, getTempPath(fileName) { - return this.joinPath(this.remoteApp().getPath('temp'), fileName || ''); + let tempPath = this.joinPath(this.remoteApp().getPath('temp'), 'KeeWeb'); + const fs = this.req('fs'); + if (!fs.existsSync(tempPath)) { + fs.mkdirSync(tempPath); + } + if (fileName) { + tempPath = this.joinPath(tempPath, fileName); + } + return tempPath; }, getDocumentsPath(fileName) { return this.joinPath(this.remoteApp().getPath('documents'), fileName || ''); @@ -164,18 +175,18 @@ const Launcher = { requestExit() { const app = this.remoteApp(); app.setHookBeforeQuitEvent(false); - if (this.restartPending) { - app.restartApp(); + if (this.pendingUpdateFile) { + app.restartAndUpdate(this.pendingUpdateFile); } else { app.quit(); } }, - requestRestart() { - this.restartPending = true; + requestRestartAndUpdate(updateFilePath) { + this.pendingUpdateFile = updateFilePath; this.requestExit(); }, cancelRestart() { - this.restartPending = false; + this.pendingUpdateFile = undefined; }, setClipboardText(text) { return this.electron().clipboard.writeText(text); diff --git a/app/scripts/const/links.js b/app/scripts/const/links.js index 52388746..069044b6 100644 --- a/app/scripts/const/links.js +++ b/app/scripts/const/links.js @@ -7,7 +7,7 @@ const Links = { License: 'https://github.com/keeweb/keeweb/blob/master/LICENSE', LicenseApache: 'https://opensource.org/licenses/Apache-2.0', LicenseLinkCCBY40: 'https://creativecommons.org/licenses/by/4.0/', - UpdateDesktop: 'https://github.com/keeweb/keeweb/releases/download/v{ver}/UpdateDesktop.zip', + UpdateBasePath: 'https://github.com/keeweb/keeweb/releases/download/v{ver}/', ReleaseNotes: 'https://github.com/keeweb/keeweb/blob/master/release-notes.md#release-notes', SelfHostedDropbox: 'https://github.com/keeweb/keeweb#self-hosting', UpdateJson: 'https://app.keeweb.info/update.json', diff --git a/app/scripts/locales/base.json b/app/scripts/locales/base.json index a0c490b4..9dee0c53 100644 --- a/app/scripts/locales/base.json +++ b/app/scripts/locales/base.json @@ -373,7 +373,7 @@ "setGenExtractingUpdate": "Extracting update...", "setGenCheckErr": "There was an error downloading new version", "setGenNeverChecked": "Never checked for updates", - "setGenRestartToUpdate": "Restart the app to update", + "setGenRestartToUpdate": "Restart KeeWeb to update", "setGenDownloadAndRestart": "Download update and restart", "setGenAppearance": "Appearance", "setGenTheme": "Theme", diff --git a/app/scripts/util/formatting/string-format.js b/app/scripts/util/formatting/string-format.js index 4264a7a3..0e8a9fd8 100644 --- a/app/scripts/util/formatting/string-format.js +++ b/app/scripts/util/formatting/string-format.js @@ -29,6 +29,10 @@ const StringFormat = { pascalCase(str) { return this.capFirst(str.replace(this.camelCaseRegex, (match) => match[1].toUpperCase())); + }, + + replaceVersion(str, replacement) { + return str.replace(/\d+\.\d+\.\d+/g, replacement); } }; diff --git a/app/scripts/views/settings/settings-general-view.js b/app/scripts/views/settings/settings-general-view.js index 00f57ee2..1b8e38c5 100644 --- a/app/scripts/views/settings/settings-general-view.js +++ b/app/scripts/views/settings/settings-general-view.js @@ -55,7 +55,7 @@ class SettingsGeneralView extends View { 'changeFieldLabelDblClickAutoType', 'change .settings__general-titlebar-style': 'changeTitlebarStyle', 'click .settings__general-update-btn': 'checkUpdate', - 'click .settings__general-restart-btn': 'restartApp', + 'click .settings__general-restart-btn': 'installUpdateAndRestart', 'click .settings__general-download-update-btn': 'downloadUpdate', 'click .settings__general-update-found-btn': 'installFoundUpdate', 'change .settings__general-prv-check': 'changeStorageEnabled', @@ -421,9 +421,9 @@ class SettingsGeneralView extends View { Events.emit('refresh'); } - restartApp() { + installUpdateAndRestart() { if (Launcher) { - Launcher.requestRestart(); + Updater.installAndRestart(); } else { window.location.reload(); } @@ -435,7 +435,7 @@ class SettingsGeneralView extends View { installFoundUpdate() { Updater.update(true, () => { - Launcher.requestRestart(); + Updater.installAndRestart(); }); } diff --git a/app/templates/settings/settings-about.hbs b/app/templates/settings/settings-about.hbs index b9da9208..8ccb1234 100644 --- a/app/templates/settings/settings-about.hbs +++ b/app/templates/settings/settings-about.hbs @@ -37,7 +37,6 @@ {{#if isDesktop}}

Desktop modules