From 251b763ea921ad810c899deba23a8b58477d6b1e Mon Sep 17 00:00:00 2001 From: antelle Date: Wed, 16 Dec 2020 18:53:15 +0100 Subject: [PATCH 1/9] fix #1656: false positive report on VirusTotal --- app/scripts/plugins/theme-vars.js | 4 +++- release-notes.md | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/scripts/plugins/theme-vars.js b/app/scripts/plugins/theme-vars.js index 677ddce8..ca4662dc 100644 --- a/app/scripts/plugins/theme-vars.js +++ b/app/scripts/plugins/theme-vars.js @@ -5,6 +5,8 @@ import ThemeDefaults from '!!raw-loader!../../styles/themes/_theme-defaults.scss const ThemeVars = { themeDefaults: null, + newLineRegEx: /[\n\s]+/g, // don't inline it, see #1656 + themeVarsRegEx: /([\w\-]+):([^:]+),(\$)?/g, init() { if (this.themeDefaults) { @@ -24,7 +26,7 @@ const ThemeVars = { apply(cssStyle) { this.init(); - const matches = ThemeVarsScss.replace(/[\n\s]+/g, '').matchAll(/([\w\-]+):([^:]+),(\$)?/g); + const matches = ThemeVarsScss.replace(this.newLineRegEx, '').matchAll(this.themeVarsRegEx); for (let [, name, def, last] of matches) { if (last && def.endsWith(')')) { // definitions are written like this: diff --git a/release-notes.md b/release-notes.md index c142a1f9..8d71cece 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,5 +1,8 @@ Release notes ------------- +##### v1.16.4 (2020-12-16) +`-` fix #1656: false positive report on VirusTotal + ##### v1.16.3 (2020-12-10) `-` fix #1650: keyfiles stored in the app can't be used From 27d5fc4cbade15da691f108e0c493064b71dd767 Mon Sep 17 00:00:00 2001 From: antelle Date: Wed, 16 Dec 2020 20:15:14 +0100 Subject: [PATCH 2/9] running VirusTotal checks on CI, see #1656 --- .github/workflows/build.yaml | 8 +++ Gruntfile.js | 9 +++ build/tasks/grunt-virustotal.js | 97 +++++++++++++++++++++++++++++++++ package-lock.json | 18 ++++-- package.json | 2 + 5 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 build/tasks/grunt-virustotal.js diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 22a3de5b..6c569b42 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -24,6 +24,14 @@ jobs: run: npm test - name: Grunt run: grunt + - name: Write secrets + env: + VIRUS_TOTAL: ${{ secrets.VIRUS_TOTAL }} + run: | + mkdir keys + echo "$VIRUS_TOTAL" > keys/virus-total.json + - name: Check on VirusTotal + run: grunt virustotal - name: Upload artifact uses: actions/upload-artifact@v1 with: diff --git a/Gruntfile.js b/Gruntfile.js index ec5a2f7d..6a799414 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -759,6 +759,15 @@ module.exports = function (grunt) { headless: true }, default: 'test/runner.html' + }, + virustotal: { + options: { + timeout: 10 * 60 * 1000, + get apiKey() { + return require('./keys/virus-total.json').apiKey; + } + }, + html: 'dist/index.html' } }); }; diff --git a/build/tasks/grunt-virustotal.js b/build/tasks/grunt-virustotal.js new file mode 100644 index 00000000..255f5fa3 --- /dev/null +++ b/build/tasks/grunt-virustotal.js @@ -0,0 +1,97 @@ +module.exports = function (grunt) { + grunt.registerMultiTask('virustotal', 'Checks if a file has issues on VirusTotal', function () { + const done = this.async(); + const opt = this.options(); + + const path = require('path'); + const fs = require('fs'); + const fetch = require('node-fetch'); + const FormData = require('form-data'); + + Promise.all( + this.files[0].src.map((file) => + checkFile(opt, file).catch((err) => { + grunt.warn('VirusTotal check failed: ' + err); + }) + ) + ).then(done); + + async function checkFile(opt, file) { + grunt.log.writeln(`Uploading to VirusTotal: ${file}...`); + + const timeStarted = Date.now(); + + const { apiKey, timeout = 60 * 1000 } = opt; + const interval = 5000; + + const headers = { 'x-apikey': apiKey }; + + const fileData = fs.readFileSync(file); + const fileName = path.basename(file); + + const form = new FormData(); + form.append('file', fileData, fileName); + const fileUploadResp = await fetch('https://www.virustotal.com/api/v3/files', { + method: 'POST', + headers, + body: form + }); + const fileUploadRespData = await fileUploadResp.json(); + if (fileUploadRespData.error) { + const errStr = JSON.stringify(fileUploadRespData.error); + throw new Error(`File upload error: ${errStr}`); + } + + const id = fileUploadRespData.data.id; + if (!id) { + throw new Error('File upload error: empty id'); + } + + grunt.log.writeln(`Uploaded ${file} to VirusTotal, id: ${id}`); + + let elapsed; + do { + const checkResp = await fetch(`https://www.virustotal.com/api/v3/analyses/${id}`, { + headers + }); + const checkRespData = await checkResp.json(); + if (checkRespData.error) { + const errStr = JSON.stringify(checkRespData.error); + throw new Error(`File check error: ${errStr}`); + } + const { attributes } = checkRespData.data; + if (attributes.status === 'completed') { + const { stats } = attributes; + if (stats.malicious > 0) { + throw new Error( + `File ${file} reported as malicious ${stats.malicious} time(s)` + ); + } + if (stats.suspicious > 0) { + throw new Error( + `File ${file} reported as malicious ${stats.suspicious} time(s)` + ); + } + const statsStr = Object.entries(stats) + .map(([k, v]) => `${k}=${v}`) + .join(', '); + grunt.log.writeln(`VirusTotal check OK: ${file}, stats:`, statsStr); + return; + } + + elapsed = Date.now() - timeStarted; + grunt.log.writeln( + `VirusTotal check status: ${attributes.status}, elapsed ${elapsed}ms` + ); + + await wait(interval); + } while (elapsed < timeout); + + throw new Error(`Timed out after ${timeout}ms`); + } + + function wait(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + }); +}; diff --git a/package-lock.json b/package-lock.json index 0f3be11d..82c02c7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6829,12 +6829,12 @@ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, @@ -14294,6 +14294,16 @@ "uuid": "^3.3.2" }, "dependencies": { + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", diff --git a/package.json b/package.json index 11631099..065e0c3c 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "eslint-plugin-promise": "4.2.1", "eslint-plugin-standard": "4.1.0", "exports-loader": "1.1.1", + "form-data": "^3.0.0", "fs-extra": "^9.0.1", "grunt": "1.3.0", "grunt-chmod": "^1.1.1", @@ -73,6 +74,7 @@ "mini-css-extract-plugin": "^1.3.1", "mocha": "^8.2.1", "morphdom": "^2.6.1", + "node-fetch": "^2.6.1", "node-sass": "^5.0.0", "node-stream-zip": "1.12.0", "normalize.css": "8.0.1", From 9f2a09cc973de10ed7c4586690a399b82fab425a Mon Sep 17 00:00:00 2001 From: antelle Date: Wed, 16 Dec 2020 20:32:48 +0100 Subject: [PATCH 3/9] optional oauth secret --- app/scripts/storage/storage-base.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/scripts/storage/storage-base.js b/app/scripts/storage/storage-base.js index c54ccb5b..49870f85 100644 --- a/app/scripts/storage/storage-base.js +++ b/app/scripts/storage/storage-base.js @@ -397,7 +397,7 @@ class StorageBase { skipAuth: true, data: UrlFormat.buildFormData({ 'client_id': config.clientId, - 'client_secret': config.clientSecret, + ...(config.clientSecret ? { 'client_secret': config.clientSecret } : null), 'grant_type': 'authorization_code', 'code': result.code, 'redirect_uri': session.redirectUri, @@ -430,7 +430,7 @@ class StorageBase { skipAuth: true, data: UrlFormat.buildFormData({ 'client_id': config.clientId, - 'client_secret': config.clientSecret, + ...(config.clientSecret ? { 'client_secret': config.clientSecret } : null), 'grant_type': 'refresh_token', 'refresh_token': refreshToken }), From 938f2be487c8e993a9d8efba75f64faa5303097f Mon Sep 17 00:00:00 2001 From: antelle Date: Wed, 16 Dec 2020 21:12:48 +0100 Subject: [PATCH 4/9] passing sha to VirusTotal --- Gruntfile.js | 1 + build/tasks/grunt-virustotal.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 6a799414..2fdc180b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -762,6 +762,7 @@ module.exports = function (grunt) { }, virustotal: { options: { + prefix: `keeweb.v${pkg.version}-${sha}.`, timeout: 10 * 60 * 1000, get apiKey() { return require('./keys/virus-total.json').apiKey; diff --git a/build/tasks/grunt-virustotal.js b/build/tasks/grunt-virustotal.js index 255f5fa3..94b5af97 100644 --- a/build/tasks/grunt-virustotal.js +++ b/build/tasks/grunt-virustotal.js @@ -21,13 +21,13 @@ module.exports = function (grunt) { const timeStarted = Date.now(); - const { apiKey, timeout = 60 * 1000 } = opt; + const { apiKey, prefix, timeout = 60 * 1000 } = opt; const interval = 5000; const headers = { 'x-apikey': apiKey }; const fileData = fs.readFileSync(file); - const fileName = path.basename(file); + const fileName = (prefix || '') + path.basename(file); const form = new FormData(); form.append('file', fileData, fileName); From 3c0693fdd9dacd0f18c1ce220b350806c5a60767 Mon Sep 17 00:00:00 2001 From: antelle Date: Wed, 16 Dec 2020 21:14:39 +0100 Subject: [PATCH 5/9] skip VirusTotal checks in ci-sandbox --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6c569b42..12a3fab9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -32,6 +32,7 @@ jobs: echo "$VIRUS_TOTAL" > keys/virus-total.json - name: Check on VirusTotal run: grunt virustotal + if: ${{ github.repository == 'keeweb/keeweb' }} - name: Upload artifact uses: actions/upload-artifact@v1 with: From b733b43f8b975402d6be41cd4692be7687610040 Mon Sep 17 00:00:00 2001 From: antelle Date: Wed, 16 Dec 2020 21:15:27 +0100 Subject: [PATCH 6/9] release notes --- release-notes.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/release-notes.md b/release-notes.md index 8d71cece..cf6e4814 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,7 +1,8 @@ Release notes ------------- -##### v1.16.4 (2020-12-16) +##### v1.16.4 (2020-12-17) `-` fix #1656: false positive report on VirusTotal +`+` fix #1629: possibility to use OneDrive as SPA ##### v1.16.3 (2020-12-10) `-` fix #1650: keyfiles stored in the app can't be used From ccfd5126bc59221b450c678ab5e28fba8ea0a207 Mon Sep 17 00:00:00 2001 From: antelle Date: Wed, 16 Dec 2020 21:15:40 +0100 Subject: [PATCH 7/9] release notes --- release-notes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release-notes.md b/release-notes.md index cf6e4814..5e8f322c 100644 --- a/release-notes.md +++ b/release-notes.md @@ -2,7 +2,7 @@ Release notes ------------- ##### v1.16.4 (2020-12-17) `-` fix #1656: false positive report on VirusTotal -`+` fix #1629: possibility to use OneDrive as SPA +`+` #1629: possibility to use OneDrive as SPA ##### v1.16.3 (2020-12-10) `-` fix #1650: keyfiles stored in the app can't be used From b4b6ef32069036a5974134d926857b8825397740 Mon Sep 17 00:00:00 2001 From: antelle Date: Thu, 17 Dec 2020 19:25:36 +0100 Subject: [PATCH 8/9] calling oauth authorize again if the refresh token is expired --- app/scripts/storage/storage-base.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/scripts/storage/storage-base.js b/app/scripts/storage/storage-base.js index 49870f85..55ee23ff 100644 --- a/app/scripts/storage/storage-base.js +++ b/app/scripts/storage/storage-base.js @@ -447,9 +447,12 @@ class StorageBase { if (xhr.status === 400) { delete this.runtimeData[this.name + 'OAuthToken']; this._oauthToken = null; + this.logger.error('Error exchanging refresh token, trying to authorize again'); + this._oauthAuthorize(callback); + } else { + this.logger.error('Error exchanging refresh token', err); + callback?.('Error exchanging refresh token'); } - this.logger.error('Error exchanging refresh token', err); - callback?.('Error exchanging refresh token'); } }); } From cc4aa6fc1a69585ca27093f8c4383c0733bb91a0 Mon Sep 17 00:00:00 2001 From: antelle Date: Thu, 17 Dec 2020 19:28:45 +0100 Subject: [PATCH 9/9] bump version --- desktop/package-lock.json | 2 +- desktop/package.json | 2 +- package-lock.json | 2 +- package.json | 2 +- util/bump-version.js | 2 ++ 5 files changed, 6 insertions(+), 4 deletions(-) mode change 100644 => 100755 util/bump-version.js diff --git a/desktop/package-lock.json b/desktop/package-lock.json index f1245004..73beea56 100644 --- a/desktop/package-lock.json +++ b/desktop/package-lock.json @@ -1,6 +1,6 @@ { "name": "KeeWeb", - "version": "1.16.3", + "version": "1.16.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/desktop/package.json b/desktop/package.json index 4d070012..416b3c72 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "KeeWeb", - "version": "1.16.3", + "version": "1.16.4", "description": "Free cross-platform password manager compatible with KeePass", "main": "main.js", "homepage": "https://keeweb.info", diff --git a/package-lock.json b/package-lock.json index 82c02c7d..0b73f14d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "keeweb", - "version": "1.16.3", + "version": "1.16.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 065e0c3c..584756da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "keeweb", - "version": "1.16.3", + "version": "1.16.4", "description": "Free cross-platform password manager compatible with KeePass", "main": "Gruntfile.js", "private": true, diff --git a/util/bump-version.js b/util/bump-version.js old mode 100644 new mode 100755 index 0b6422bb..c4181e22 --- a/util/bump-version.js +++ b/util/bump-version.js @@ -1,3 +1,5 @@ +#!/usr/bin/env node + /* eslint-disable no-console */ const fs = require('fs');