Merge branch 'develop' into master

This commit is contained in:
antelle 2021-03-07 14:23:29 +01:00
commit 31214b10e7
No known key found for this signature in database
GPG Key ID: 63C9777AAB7C563C
155 changed files with 27531 additions and 5012 deletions

View File

@ -2,7 +2,4 @@
cd /github/workspace cd /github/workspace
npm ci npm ci
cd desktop
npm ci
cd /github/workspace
grunt desktop-linux grunt desktop-linux

View File

@ -24,6 +24,11 @@ jobs:
run: npm test run: npm test
- name: Grunt - name: Grunt
run: grunt run: grunt
- name: Upload artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
path: dist
- name: Write secrets - name: Write secrets
env: env:
VIRUS_TOTAL: ${{ secrets.VIRUS_TOTAL }} VIRUS_TOTAL: ${{ secrets.VIRUS_TOTAL }}
@ -33,11 +38,6 @@ jobs:
- name: Check on VirusTotal - name: Check on VirusTotal
run: grunt virustotal run: grunt virustotal
if: ${{ github.repository == 'keeweb/keeweb' }} if: ${{ github.repository == 'keeweb/keeweb' }}
- name: Upload artifact
uses: actions/upload-artifact@v1
with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.html
path: dist
linux: linux:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -95,11 +95,6 @@ jobs:
with: with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm name: KeeWeb-${{ steps.get_tag.outputs.tag }}.linux.x86_64.rpm
path: dist/desktop/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: darwin:
runs-on: macos-latest runs-on: macos-latest
@ -124,9 +119,6 @@ jobs:
path: dist path: dist
- name: Install npm modules - name: Install npm modules
run: npm ci run: npm ci
- name: Install desktop npm modules
working-directory: desktop
run: npm ci
- name: Install grunt - name: Install grunt
run: sudo npm i -g grunt-cli run: sudo npm i -g grunt-cli
- name: Write secrets - name: Write secrets
@ -134,10 +126,12 @@ jobs:
CODESIGN: ${{ secrets.CODESIGN }} CODESIGN: ${{ secrets.CODESIGN }}
APPLE_DEPLOY_PASSWORD: ${{ secrets.APPLE_DEPLOY_PASSWORD }} APPLE_DEPLOY_PASSWORD: ${{ secrets.APPLE_DEPLOY_PASSWORD }}
APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }} APPLE_ID_USERNAME: ${{ secrets.APPLE_ID_USERNAME }}
APPLE_PROVISIONING_PROFILE: ${{ secrets.APPLE_PROVISIONING_PROFILE }}
run: | run: |
mkdir keys mkdir keys
echo "$CODESIGN" > keys/codesign.json echo "$CODESIGN" > keys/codesign.json
xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "$APPLE_ID_USERNAME" -p "$APPLE_DEPLOY_PASSWORD" xcrun altool --store-password-in-keychain-item "AC_PASSWORD" -u "$APPLE_ID_USERNAME" -p "$APPLE_DEPLOY_PASSWORD"
echo "$APPLE_PROVISIONING_PROFILE" | base64 -d > keys/keeweb.provisionprofile
- name: Import certificates - name: Import certificates
uses: keeweb/import-codesign-certs@v1 uses: keeweb/import-codesign-certs@v1
with: with:
@ -179,9 +173,6 @@ jobs:
path: dist path: dist
- name: Install npm modules - name: Install npm modules
run: npm ci run: npm ci
- name: Install desktop npm modules
working-directory: desktop
run: npm ci
- name: Install grunt - name: Install grunt
run: npm i -g grunt-cli run: npm i -g grunt-cli
- name: Write secrets - name: Write secrets
@ -328,11 +319,6 @@ jobs:
with: with:
name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip name: KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
path: assets path: assets
- name: Download update artifact
uses: actions/download-artifact@v1
with:
name: UpdateDesktop.zip
path: assets
- name: Zip html - name: Zip html
working-directory: html working-directory: html
run: zip -vr ../assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip . run: zip -vr ../assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.html.zip .
@ -505,15 +491,6 @@ jobs:
asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip asset_path: assets/KeeWeb-${{ steps.get_tag.outputs.tag }}.win.arm64.zip
asset_name: 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 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 - name: Upload verify.sign asset
uses: actions/upload-release-asset@v1 uses: actions/upload-release-asset@v1
env: env:

View File

@ -25,16 +25,24 @@ module.exports = function (grunt) {
const dt = date.toISOString().replace(/T.*/, ''); const dt = date.toISOString().replace(/T.*/, '');
const year = date.getFullYear(); const year = date.getFullYear();
const minElectronVersionForUpdate = '11.0.3';
const zipCommentPlaceholderPart = 'zip_comment_placeholder_that_will_be_replaced_with_hash';
const zipCommentPlaceholder =
zipCommentPlaceholderPart + '.'.repeat(512 - zipCommentPlaceholderPart.length);
const electronVersion = pkg.dependencies.electron.replace(/^\D/, ''); const electronVersion = pkg.dependencies.electron.replace(/^\D/, '');
const skipSign = grunt.option('skip-sign'); const skipSign = grunt.option('skip-sign');
const getCodeSignConfig = () => const getCodeSignConfig = () =>
skipSign ? { identities: {} } : require('./keys/codesign.json'); skipSign ? { identities: {} } : require('./keys/codesign.json');
const sha = execSync('git rev-parse --short HEAD').toString('utf8').trim(); let sha = grunt.option('commit-sha');
if (!sha) {
try {
sha = execSync('git rev-parse --short HEAD').toString('utf8').trim();
} catch (e) {
grunt.warn(
"Cannot get commit sha from git. It's recommended to build KeeWeb from a git repo " +
'because commit sha is displayed in the UI, however if you would like to build from a folder, ' +
'you can override what will be displayed in the UI with --commit-sha=xxx.'
);
}
}
grunt.log.writeln(`Building KeeWeb v${pkg.version} (${sha})`);
const webpackOptions = { const webpackOptions = {
date, date,
@ -68,6 +76,15 @@ module.exports = function (grunt) {
] ]
}); });
const linuxDependencies = [
'libappindicator1',
'libgconf-2-4',
'gnome-keyring',
'libxtst6',
'libx11-6',
'libatspi2.0-0'
];
grunt.initConfig({ grunt.initConfig({
noop: { noop: {} }, noop: { noop: {} },
clean: { clean: {
@ -127,18 +144,6 @@ module.exports = function (grunt) {
expand: true, expand: true,
nonull: 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': { 'desktop-darwin-helper-x64': {
src: 'helper/darwin/KeeWebHelper', src: 'helper/darwin/KeeWebHelper',
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/', dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
@ -152,7 +157,7 @@ module.exports = function (grunt) {
options: { mode: '0755' } options: { mode: '0755' }
}, },
'desktop-darwin-installer-helper-x64': { 'desktop-darwin-installer-helper-x64': {
cwd: 'package/osx/KeeWeb Installer.app', cwd: 'tmp/desktop/KeeWeb Installer.app',
src: '**', src: '**',
dest: dest:
'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app', 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
@ -161,7 +166,7 @@ module.exports = function (grunt) {
options: { mode: true } options: { mode: true }
}, },
'desktop-darwin-installer-helper-arm64': { 'desktop-darwin-installer-helper-arm64': {
cwd: 'package/osx/KeeWeb Installer.app', cwd: 'tmp/desktop/KeeWeb Installer.app',
src: '**', src: '**',
dest: dest:
'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app', 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
@ -243,6 +248,11 @@ module.exports = function (grunt) {
src: `tmp/desktop/electron-builder/keeweb-${pkg.version}.AppImage`, src: `tmp/desktop/electron-builder/keeweb-${pkg.version}.AppImage`,
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.AppImage`, dest: `dist/desktop/KeeWeb-${pkg.version}.linux.AppImage`,
nonull: true nonull: true
},
'darwin-installer-icon': {
src: 'graphics/icon.icns',
dest: 'tmp/desktop/KeeWeb Installer.app/Contents/Resources/applet.icns',
nonull: true
} }
}, },
eslint: { eslint: {
@ -250,7 +260,8 @@ module.exports = function (grunt) {
desktop: ['desktop/**/*.js', '!desktop/node_modules/**'], desktop: ['desktop/**/*.js', '!desktop/node_modules/**'],
build: ['Gruntfile.js', 'grunt.*.js', 'build/**/*.js', 'webpack.config.js'], build: ['Gruntfile.js', 'grunt.*.js', 'build/**/*.js', 'webpack.config.js'],
plugins: ['plugins/**/*.js'], plugins: ['plugins/**/*.js'],
util: ['util/**/*.js'] util: ['util/**/*.js'],
installer: ['package/osx/installer.js']
}, },
inline: { inline: {
app: { app: {
@ -263,7 +274,7 @@ module.exports = function (grunt) {
algo: 'sha512', algo: 'sha512',
expected: { expected: {
style: 1, style: 1,
script: 2 script: 1
} }
}, },
app: { app: {
@ -283,20 +294,20 @@ module.exports = function (grunt) {
} }
}, },
'string-replace': { 'string-replace': {
manifest: { 'update-manifest': {
options: { options: {
replacements: [ replacements: [
{ {
pattern: '# YYYY-MM-DD:v0.0.0', pattern: /"version":\s*".*?"/,
replacement: '# ' + dt + ':v' + pkg.version replacement: `"version": "${pkg.version}"`
}, },
{ {
pattern: '# updmin:v0.0.0', pattern: /"date":\s*".*?"/,
replacement: '# updmin:v' + minElectronVersionForUpdate replacement: `"date": "${dt}"`
} }
] ]
}, },
files: { 'dist/manifest.appcache': 'app/manifest.appcache' } files: { 'dist/update.json': 'app/update.json' }
}, },
'service-worker': { 'service-worker': {
options: { replacements: [{ pattern: '0.0.0', replacement: pkg.version }] }, options: { replacements: [{ pattern: '0.0.0', replacement: pkg.version }] },
@ -437,26 +448,37 @@ module.exports = function (grunt) {
category: 'Utility' category: 'Utility'
}, },
rpm: { rpm: {
// depends: ['libappindicator1', 'libgconf-2-4', 'gnome-keyring'] // depends: linuxDependencies
}, },
snap: { snap: {
stagePackages: ['libappindicator1', 'libgconf-2-4', 'gnome-keyring'] stagePackages: linuxDependencies
} }
} }
} }
} }
}, },
'electron-patch': {
'win32-x64': 'tmp/desktop/KeeWeb-win32-x64/KeeWeb.exe',
'win32-ia32': 'tmp/desktop/KeeWeb-win32-ia32/KeeWeb.exe',
'win32-arm64': 'tmp/desktop/KeeWeb-win32-arm64/KeeWeb.exe',
'darwin-x64': 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app',
'darwin-arm64': 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app',
'linux': 'tmp/desktop/KeeWeb-linux-x64/keeweb'
},
osacompile: {
options: {
language: 'JavaScript'
},
installer: {
files: {
'tmp/desktop/KeeWeb Installer.app': 'package/osx/installer.js'
}
}
},
compress: { compress: {
options: { options: {
level: 6 level: 6
}, },
'desktop-update': {
options: {
archive: 'dist/desktop/UpdateDesktop.zip',
comment: zipCommentPlaceholder
},
files: [{ cwd: 'tmp/desktop/update', src: '**', expand: true, nonull: true }]
},
'win32-x64': { 'win32-x64': {
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.x64.zip` }, options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.x64.zip` },
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-x64', src: '**', expand: true }] files: [{ cwd: 'tmp/desktop/KeeWeb-win32-x64', src: '**', expand: true }]
@ -565,7 +587,7 @@ module.exports = function (grunt) {
pkgName: `KeeWeb-${pkg.version}.linux.x64.deb`, pkgName: `KeeWeb-${pkg.version}.linux.x64.deb`,
targetDir: 'dist/desktop', targetDir: 'dist/desktop',
appName: 'KeeWeb', appName: 'KeeWeb',
depends: 'libappindicator1, libgconf-2-4, gnome-keyring', depends: linuxDependencies.join(', '),
scripts: { scripts: {
postinst: 'package/deb/scripts/postinst' postinst: 'package/deb/scripts/postinst'
} }
@ -588,50 +610,30 @@ 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': { 'osx-sign': {
options: { options: {
get identity() { get identity() {
return getCodeSignConfig().identities.app; return getCodeSignConfig().identities.app;
}, },
hardenedRuntime: true, hardenedRuntime: true,
entitlements: 'package/osx/entitlements.mac.plist', entitlements: 'package/osx/entitlements.plist',
'entitlements-inherit': 'package/osx/entitlements.mac.plist', 'entitlements-inherit': 'package/osx/entitlements-inherit.plist',
'gatekeeper-assess': false 'gatekeeper-assess': false
}, },
'desktop-x64': { 'desktop-x64': {
options: {
'provisioning-profile': './keys/keeweb.provisionprofile'
},
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app' src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app'
}, },
'desktop-arm64': { 'desktop-arm64': {
options: {
'provisioning-profile': './keys/keeweb.provisionprofile'
},
src: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app' src: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app'
},
'installer': {
src: 'tmp/desktop/KeeWeb Installer.app'
} }
}, },
notarize: { notarize: {
@ -747,10 +749,7 @@ module.exports = function (grunt) {
sign: 'dist/desktop/Verify.sign.sha256' sign: 'dist/desktop/Verify.sign.sha256'
}, },
files: { files: {
'dist/desktop/Verify.sha256': [ 'dist/desktop/Verify.sha256': ['dist/desktop/KeeWeb-*']
'dist/desktop/KeeWeb-*',
'dist/desktop/UpdateDesktop.zip'
]
} }
} }
}, },

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020 Antelle https://antelle.net Copyright (c) 2021 Antelle https://antelle.net
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -90,7 +90,7 @@ Please note: donation does not imply any type of service contract.
Notable contributions to KeeWeb: Notable contributions to KeeWeb:
- Florian Reuschel ([@Loilo](https://github.com/Loilo)): [German translation](http://keeweb.oneskyapp.com/collaboration/translate/project/project/173183/language/550) - Florian Reuschel ([@Loilo](https://github.com/Loilo)): [German translation](https://keeweb.oneskyapp.com/collaboration/translate/project/project/173183/language/550)
- Dennis Ploeger ([@dploeger](https://github.com/dploeger)): [auto-type improvements](https://github.com/keeweb/keeweb/pulls?q=is%3Apr+is%3Aclosed+author%3Adploeger) - Dennis Ploeger ([@dploeger](https://github.com/dploeger)): [auto-type improvements](https://github.com/keeweb/keeweb/pulls?q=is%3Apr+is%3Aclosed+author%3Adploeger)
- Hackmanit ([hackmanit.de](https://www.hackmanit.de)): [penetration test](https://www.hackmanit.de/en/blog-en/104-pro-bono-penetration-test-keeweb) - Hackmanit ([hackmanit.de](https://www.hackmanit.de)): [penetration test](https://www.hackmanit.de/en/blog-en/104-pro-bono-penetration-test-keeweb)
- Peter Bittner ([@bittner](https://github.com/bittner)): [Wikipedia article](https://en.wikipedia.org/wiki/KeeWeb) - Peter Bittner ([@bittner](https://github.com/bittner)): [Wikipedia article](https://en.wikipedia.org/wiki/KeeWeb)

View File

@ -72,7 +72,6 @@
<link rel="manifest" href="manifest.json" /> <link rel="manifest" href="manifest.json" />
<link rel="stylesheet" href="css/app.css?__inline=true" /> <link rel="stylesheet" href="css/app.css?__inline=true" />
<script src="js/app.js?__inline=true"></script> <script src="js/app.js?__inline=true"></script>
<script src="js/runtime.js?__inline=true"></script>
</head> </head>
<body class="th-d"> <body class="th-d">
<noscript> <noscript>

View File

@ -1,7 +0,0 @@
CACHE MANIFEST
# YYYY-MM-DD:v0.0.0
# updmin:v0.0.0
NETWORK:
*

View File

@ -9,6 +9,7 @@ import { UsbListener } from 'comp/app/usb-listener';
import { FeatureTester } from 'comp/browser/feature-tester'; import { FeatureTester } from 'comp/browser/feature-tester';
import { FocusDetector } from 'comp/browser/focus-detector'; import { FocusDetector } from 'comp/browser/focus-detector';
import { IdleTracker } from 'comp/browser/idle-tracker'; import { IdleTracker } from 'comp/browser/idle-tracker';
import { ThemeWatcher } from 'comp/browser/theme-watcher';
import { KeyHandler } from 'comp/browser/key-handler'; import { KeyHandler } from 'comp/browser/key-handler';
import { PopupNotifier } from 'comp/browser/popup-notifier'; import { PopupNotifier } from 'comp/browser/popup-notifier';
import { Launcher } from 'comp/launcher'; import { Launcher } from 'comp/launcher';
@ -91,6 +92,8 @@ ready(() => {
KdbxwebInit.init(); KdbxwebInit.init();
FocusDetector.init(); FocusDetector.init();
AutoType.init(); AutoType.init();
ThemeWatcher.init();
SettingsManager.init();
window.kw = ExportApi; window.kw = ExportApi;
return PluginManager.init().then(() => { return PluginManager.init().then(() => {
StartProfiler.milestone('initializing modules'); StartProfiler.milestone('initializing modules');
@ -111,13 +114,13 @@ ready(() => {
function loadRemoteConfig() { function loadRemoteConfig() {
return Promise.resolve() return Promise.resolve()
.then(() => { .then(() => {
SettingsManager.setBySettings(appModel.settings); SettingsManager.setBySettings();
const configParam = getConfigParam(); const configParam = getConfigParam();
if (configParam) { if (configParam) {
return appModel return appModel
.loadConfig(configParam) .loadConfig(configParam)
.then(() => { .then(() => {
SettingsManager.setBySettings(appModel.settings); SettingsManager.setBySettings();
}) })
.catch((e) => { .catch((e) => {
if (!appModel.settings.cacheConfigSettings) { if (!appModel.settings.cacheConfigSettings) {

View File

@ -1,10 +1,15 @@
import { Launcher } from 'comp/launcher'; import { Launcher } from 'comp/launcher';
import { AppSettingsModel } from 'models/app-settings-model';
import { AutoTypeEmitter } from 'auto-type/auto-type-emitter';
const AutoTypeEmitterFactory = { const AutoTypeEmitterFactory = {
create(callback, windowId) { create(callback, windowId) {
if (Launcher && Launcher.autoTypeSupported) { if (Launcher && Launcher.autoTypeSupported) {
const { AutoTypeEmitter } = require('./emitter/auto-type-emitter-' + if (AppSettingsModel.useLegacyAutoType) {
Launcher.platform()); const { AutoTypeEmitter } = require('./emitter/auto-type-emitter-' +
Launcher.platform());
return new AutoTypeEmitter(callback, windowId);
}
return new AutoTypeEmitter(callback, windowId); return new AutoTypeEmitter(callback, windowId);
} }
return null; return null;

View File

@ -0,0 +1,160 @@
import { Features } from 'util/features';
import { NativeModules } from 'comp/launcher/native-modules';
import { Logger } from 'util/logger';
import { Launcher } from 'comp/launcher';
import { Timeouts } from 'const/timeouts';
const logger = new Logger('auto-type-emitter');
const KeyMap = {
tab: 'Tab',
enter: 'Return',
space: 'Space',
up: 'UpArrow',
down: 'DownArrow',
left: 'LeftArrow',
right: 'RightArrow',
home: 'Home',
end: 'End',
pgup: 'PageUp',
pgdn: 'PageDown',
ins: 'Insert',
del: 'ForwardDelete',
bs: 'BackwardDelete',
esc: 'Escape',
win: 'Meta',
rwin: 'RightMeta',
f1: 'F1',
f2: 'F2',
f3: 'F3',
f4: 'F4',
f5: 'F5',
f6: 'F6',
f7: 'F7',
f8: 'F8',
f9: 'F9',
f10: 'F10',
f11: 'F11',
f12: 'F12',
f13: 'F13',
f14: 'F14',
f15: 'F15',
f16: 'F16',
add: 'KeypadPlus',
subtract: 'KeypadMinus',
multiply: 'KeypadMultiply',
divide: 'KeypadDivide',
n0: 'D0',
n1: 'D1',
n2: 'D2',
n3: 'D3',
n4: 'D4',
n5: 'D5',
n6: 'D6',
n7: 'D7',
n8: 'D8',
n9: 'D9'
};
const ModMap = {
'^': Features.isMac ? 'Command' : 'Ctrl',
'+': 'Shift',
'%': 'Alt',
'^^': 'Ctrl'
};
class AutoTypeEmitter {
constructor(callback) {
this.callback = callback;
this.mod = {};
}
begin() {
this.withCallback(NativeModules.kbdEnsureModifierNotPressed());
}
setMod(mod, enabled) {
const nativeMod = ModMap[mod];
if (!nativeMod) {
return this.callback(`Bad modifier: ${mod}`);
}
NativeModules.kbdKeyMoveWithModifier(!!enabled, [nativeMod]).catch((e) => {
logger.error('Error moving modifier', mod, enabled ? 'down' : 'up', e);
});
if (enabled) {
this.mod[nativeMod] = true;
} else {
delete this.mod[nativeMod];
}
}
text(str) {
if (!str) {
return this.withCallback(Promise.resolve());
}
const mods = Object.keys(this.mod);
if (mods.length) {
this.withCallback(NativeModules.kbdTextAsKeys(str, mods));
} else {
this.withCallback(NativeModules.kbdText(str));
}
}
key(key) {
const mods = Object.keys(this.mod);
if (typeof key === 'number') {
this.withCallback(NativeModules.kbdKeyPressWithCharacter(0, key, mods));
} else {
if (!KeyMap[key]) {
return this.callback('Bad key: ' + key);
}
const code = KeyMap[key];
this.withCallback(NativeModules.kbdKeyPress(code, mods));
}
}
copyPaste(text) {
setTimeout(() => {
Launcher.setClipboardText(text);
setTimeout(() => {
this.withCallback(NativeModules.kbdShortcut('V'));
}, Timeouts.AutoTypeCopyPaste);
}, Timeouts.AutoTypeCopyPaste);
}
wait(time) {
setTimeout(() => this.withCallback(Promise.resolve()), time);
}
waitComplete() {
this.withCallback(Promise.resolve());
}
setDelay() {
this.callback('Not implemented');
}
withCallback(promise) {
promise
.then(() => {
try {
this.callback();
} catch (err) {
logger.error('Callback error', err);
}
})
.catch((err) => {
const keyPressFailed = err.message === 'Key press failed';
if (keyPressFailed) {
err.keyPressFailed = true;
}
try {
this.callback(err);
} catch (err) {
logger.error('Callback error', err);
}
});
}
}
export { AutoTypeEmitter };

View File

@ -1,9 +1,15 @@
import { Launcher } from 'comp/launcher'; import { Launcher } from 'comp/launcher';
import { AppSettingsModel } from 'models/app-settings-model';
import { AutoTypeHelper } from 'auto-type/auto-type-helper';
const AutoTypeHelperFactory = { const AutoTypeHelperFactory = {
create() { create() {
if (Launcher && Launcher.autoTypeSupported) { if (Launcher && Launcher.autoTypeSupported) {
const { AutoTypeHelper } = require('./helper/auto-type-helper-' + Launcher.platform()); if (AppSettingsModel.useLegacyAutoType) {
const { AutoTypeHelper } = require('./helper/auto-type-helper-' +
Launcher.platform());
return new AutoTypeHelper();
}
return new AutoTypeHelper(); return new AutoTypeHelper();
} }
return null; return null;

View File

@ -0,0 +1,16 @@
import { NativeModules } from 'comp/launcher/native-modules';
class AutoTypeHelper {
getActiveWindowInfo(callback) {
NativeModules.kbdGetActiveWindow({
getWindowTitle: true,
getBrowserUrl: true
})
.then((win) => {
callback(undefined, win);
})
.catch((err) => callback(err));
}
}
export { AutoTypeHelper };

View File

@ -2,11 +2,12 @@ import { AutoTypeEmitterFactory } from 'auto-type/auto-type-emitter-factory';
import { AutoTypeObfuscator } from 'auto-type/auto-type-obfuscator'; import { AutoTypeObfuscator } from 'auto-type/auto-type-obfuscator';
import { StringFormat } from 'util/formatting/string-format'; import { StringFormat } from 'util/formatting/string-format';
import { Logger } from 'util/logger'; import { Logger } from 'util/logger';
import { AppSettingsModel } from 'models/app-settings-model';
const emitterLogger = new Logger( const emitterLogger = new Logger(
'auto-type-emitter', 'auto-type-emitter',
undefined, undefined,
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Warn localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Info
); );
const AutoTypeRunner = function (ops) { const AutoTypeRunner = function (ops) {
@ -239,7 +240,7 @@ AutoTypeRunner.prototype.tryParseCommand = function (op) {
// {VKEY 10} {VKEY 0x1F} // {VKEY 10} {VKEY 0x1F}
op.type = 'key'; op.type = 'key';
op.value = parseInt(op.arg); op.value = parseInt(op.arg);
if (isNaN(op.value) || op.value <= 0) { if (isNaN(op.value) || op.value < 0) {
throw 'Bad vkey: ' + op.arg; throw 'Bad vkey: ' + op.arg;
} }
return true; return true;
@ -432,6 +433,8 @@ AutoTypeRunner.prototype.obfuscateOp = function (op) {
}; };
AutoTypeRunner.prototype.run = function (callback, windowId) { AutoTypeRunner.prototype.run = function (callback, windowId) {
const emitterType = AppSettingsModel.useLegacyAutoType ? 'legacy' : 'native';
emitterLogger.info(`Using ${emitterType} auto-type emitter`);
this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this), windowId); this.emitter = AutoTypeEmitterFactory.create(this.emitNext.bind(this), windowId);
this.emitterState = { this.emitterState = {
callback, callback,
@ -442,7 +445,7 @@ AutoTypeRunner.prototype.run = function (callback, windowId) {
activeMod: {}, activeMod: {},
finished: null finished: null
}; };
this.emitNext(); this.emitter.begin();
}; };
AutoTypeRunner.prototype.emitNext = function (err) { AutoTypeRunner.prototype.emitNext = function (err) {

View File

@ -65,6 +65,10 @@ const AutoTypeEmitter = function (callback) {
this.pendingScript = []; this.pendingScript = [];
}; };
AutoTypeEmitter.prototype.begin = function () {
this.callback();
};
AutoTypeEmitter.prototype.setMod = function (mod, enabled) { AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
if (enabled) { if (enabled) {
this.mod[ModMap[mod]] = true; this.mod[ModMap[mod]] = true;

View File

@ -68,6 +68,10 @@ const AutoTypeEmitter = function (callback, windowId) {
} }
}; };
AutoTypeEmitter.prototype.begin = function () {
this.callback();
};
AutoTypeEmitter.prototype.setMod = function (mod, enabled) { AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
if (enabled) { if (enabled) {
this.mod[ModMap[mod]] = true; this.mod[ModMap[mod]] = true;

View File

@ -65,6 +65,10 @@ const AutoTypeEmitter = function (callback) {
this.pendingScript = []; this.pendingScript = [];
}; };
AutoTypeEmitter.prototype.begin = function () {
this.callback();
};
AutoTypeEmitter.prototype.setMod = function (mod, enabled) { AutoTypeEmitter.prototype.setMod = function (mod, enabled) {
if (enabled) { if (enabled) {
this.mod[ModMap[mod]] = true; this.mod[ModMap[mod]] = true;

View File

@ -3,19 +3,20 @@ import { AutoTypeFilter } from 'auto-type/auto-type-filter';
import { AutoTypeHelperFactory } from 'auto-type/auto-type-helper-factory'; import { AutoTypeHelperFactory } from 'auto-type/auto-type-helper-factory';
import { AutoTypeParser } from 'auto-type/auto-type-parser'; import { AutoTypeParser } from 'auto-type/auto-type-parser';
import { Launcher } from 'comp/launcher'; import { Launcher } from 'comp/launcher';
import { Features } from 'util/features';
import { Alerts } from 'comp/ui/alerts'; import { Alerts } from 'comp/ui/alerts';
import { Timeouts } from 'const/timeouts'; import { Timeouts } from 'const/timeouts';
import { AppSettingsModel } from 'models/app-settings-model'; import { AppSettingsModel } from 'models/app-settings-model';
import { AppModel } from 'models/app-model'; import { AppModel } from 'models/app-model';
import { Locale } from 'util/locale'; import { Locale } from 'util/locale';
import { Logger } from 'util/logger'; import { Logger } from 'util/logger';
import { Links } from 'const/links';
import { AutoTypeSelectView } from 'views/auto-type/auto-type-select-view'; import { AutoTypeSelectView } from 'views/auto-type/auto-type-select-view';
const logger = new Logger('auto-type'); const logger = new Logger('auto-type');
const clearTextAutoTypeLog = !!localStorage.debugAutoType; const clearTextAutoTypeLog = !!localStorage.debugAutoType;
const AutoType = { const AutoType = {
helper: AutoTypeHelperFactory.create(),
enabled: !!(Launcher && Launcher.autoTypeSupported), enabled: !!(Launcher && Launcher.autoTypeSupported),
supportsEventsWithWindowId: !!(Launcher && Launcher.platform() === 'linux'), supportsEventsWithWindowId: !!(Launcher && Launcher.platform() === 'linux'),
selectEntryView: false, selectEntryView: false,
@ -64,9 +65,16 @@ const AutoType = {
runAndHandleResult(result, windowId) { runAndHandleResult(result, windowId) {
this.run(result, windowId, (err) => { this.run(result, windowId, (err) => {
if (err) { if (err) {
let body = Locale.autoTypeErrorGeneric.replace('{}', err.message || err.toString());
let link;
if (err.keyPressFailed && Features.isMac) {
body = Locale.autoTypeErrorAccessibilityMacOS;
link = Links.AutoTypeMacOS;
}
Alerts.error({ Alerts.error({
header: Locale.autoTypeError, header: Locale.autoTypeError,
body: Locale.autoTypeErrorGeneric.replace('{}', err.toString()) body,
link
}); });
} }
}); });
@ -160,8 +168,10 @@ const AutoType = {
}, },
getActiveWindowInfo(callback) { getActiveWindowInfo(callback) {
logger.debug('Getting window info'); const helperType = AppSettingsModel.useLegacyAutoType ? 'legacy' : 'native';
return this.helper.getActiveWindowInfo((err, windowInfo) => { logger.debug(`Getting window info using ${helperType} helper`);
const helper = AutoTypeHelperFactory.create();
return helper.getActiveWindowInfo((err, windowInfo) => {
if (err) { if (err) {
logger.error('Error getting window info', err); logger.error('Error getting window info', err);
} else { } else {

View File

@ -58,6 +58,7 @@ const AppRightsChecker = {
runInstaller() { runInstaller() {
Launcher.spawn({ Launcher.spawn({
cmd: this.AppPath + '/Contents/Installer/KeeWeb Installer.app/Contents/MacOS/applet', cmd: this.AppPath + '/Contents/Installer/KeeWeb Installer.app/Contents/MacOS/applet',
args: ['--install'],
complete: () => { complete: () => {
this.needRunInstaller((needRun) => { this.needRunInstaller((needRun) => {
if (this.alert && !needRun) { if (this.alert && !needRun) {

View File

@ -0,0 +1,37 @@
import kdbxweb from 'kdbxweb';
import { Logger } from 'util/logger';
const logger = new Logger('online-password-checker');
const exposedPasswords = {};
function checkIfPasswordIsExposedOnline(password) {
if (!password || !password.isProtected || !password.byteLength) {
return false;
}
const saltedValue = password.saltedValue();
const cached = exposedPasswords[saltedValue];
if (cached !== undefined) {
return cached;
}
const passwordBytes = password.getBinary();
return crypto.subtle
.digest({ name: 'SHA-1' }, passwordBytes)
.then((sha1) => {
kdbxweb.ByteUtils.zeroBuffer(passwordBytes);
sha1 = kdbxweb.ByteUtils.bytesToHex(sha1).toUpperCase();
const shaFirst = sha1.substr(0, 5);
return fetch(`https://api.pwnedpasswords.com/range/${shaFirst}`)
.then((response) => response.text())
.then((response) => {
const isPresent = response.includes(sha1.substr(5));
exposedPasswords[saltedValue] = isPresent;
return isPresent;
});
})
.catch((e) => {
logger.error('Error checking password online', e);
});
}
export { checkIfPasswordIsExposedOnline };

View File

@ -1,3 +1,4 @@
import kdbxweb from 'kdbxweb';
import { Events } from 'framework/events'; import { Events } from 'framework/events';
import { RuntimeInfo } from 'const/runtime-info'; import { RuntimeInfo } from 'const/runtime-info';
import { Transport } from 'comp/browser/transport'; import { Transport } from 'comp/browser/transport';
@ -15,10 +16,9 @@ const Updater = {
UpdateInterval: 1000 * 60 * 60 * 24, UpdateInterval: 1000 * 60 * 60 * 24,
MinUpdateTimeout: 500, MinUpdateTimeout: 500,
MinUpdateSize: 10000, MinUpdateSize: 10000,
UpdateCheckFiles: ['app.asar'],
nextCheckTimeout: null, nextCheckTimeout: null,
updateCheckDate: new Date(0), updateCheckDate: new Date(0),
enabled: Launcher && Launcher.updaterEnabled(), enabled: Launcher?.updaterEnabled(),
getAutoUpdateType() { getAutoUpdateType() {
if (!this.enabled) { if (!this.enabled) {
@ -34,7 +34,7 @@ const Updater = {
updateInProgress() { updateInProgress() {
return ( return (
UpdateModel.status === 'checking' || UpdateModel.status === 'checking' ||
['downloading', 'extracting'].indexOf(UpdateModel.updateStatus) >= 0 ['downloading', 'extracting', 'updating'].indexOf(UpdateModel.updateStatus) >= 0
); );
}, },
@ -97,13 +97,12 @@ const Updater = {
} }
logger.info('Checking for update...'); logger.info('Checking for update...');
Transport.httpGet({ Transport.httpGet({
url: Links.Manifest, url: Links.UpdateJson,
utf8: true, json: true,
success: (data) => { success: (updateJson) => {
const dt = new Date(); const dt = new Date();
const match = data.match(/#\s*(\d+\-\d+\-\d+):v([\d+\.\w]+)/); logger.info('Update check: ' + (updateJson.version || 'unknown'));
logger.info('Update check: ' + (match ? match[0] : 'unknown')); if (!updateJson.version) {
if (!match) {
const errMsg = 'No version info found'; const errMsg = 'No version info found';
UpdateModel.set({ UpdateModel.set({
status: 'error', status: 'error',
@ -114,16 +113,15 @@ const Updater = {
this.scheduleNextCheck(); this.scheduleNextCheck();
return; return;
} }
const updateMinVersionMatch = data.match(/#\s*updmin:v([\d+\.\w]+)/);
const prevLastVersion = UpdateModel.lastVersion; const prevLastVersion = UpdateModel.lastVersion;
UpdateModel.set({ UpdateModel.set({
status: 'ok', status: 'ok',
lastCheckDate: dt, lastCheckDate: dt,
lastSuccessCheckDate: dt, lastSuccessCheckDate: dt,
lastVersionReleaseDate: new Date(match[1]), lastVersionReleaseDate: new Date(updateJson.date),
lastVersion: match[2], lastVersion: updateJson.version,
lastCheckError: null, lastCheckError: null,
lastCheckUpdMin: updateMinVersionMatch ? updateMinVersionMatch[1] : null lastCheckUpdMin: updateJson.minVersion || null
}); });
UpdateModel.save(); UpdateModel.save();
this.scheduleNextCheck(); this.scheduleNextCheck();
@ -161,7 +159,7 @@ const Updater = {
canAutoUpdate() { canAutoUpdate() {
const minLauncherVersion = UpdateModel.lastCheckUpdMin; const minLauncherVersion = UpdateModel.lastCheckUpdMin;
if (minLauncherVersion) { if (minLauncherVersion) {
const cmp = SemVer.compareVersions(Launcher.version, minLauncherVersion); const cmp = SemVer.compareVersions(RuntimeInfo.version, minLauncherVersion);
if (cmp < 0) { if (cmp < 0) {
UpdateModel.set({ updateStatus: 'ready', updateManual: true }); UpdateModel.set({ updateStatus: 'ready', updateManual: true });
return false; return false;
@ -182,28 +180,55 @@ const Updater = {
} }
UpdateModel.set({ updateStatus: 'downloading', updateError: null }); UpdateModel.set({ updateStatus: 'downloading', updateError: null });
logger.info('Downloading update', ver); 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;
Transport.httpGet({ Transport.httpGet({
url: Links.UpdateDesktop.replace('{ver}', ver), url: updateAssetUrl,
file: 'KeeWeb-' + ver + '.zip', file: updateAssetName,
cache: !startedByUser, cleanupOldFiles: true,
success: (filePath) => { cache: true,
UpdateModel.set({ updateStatus: 'extracting' }); success: (assetFilePath) => {
logger.info('Extracting update file', this.UpdateCheckFiles, filePath); logger.info('Downloading update signatures');
this.extractAppUpdate(filePath, (err) => { Transport.httpGet({
if (err) { url: updateUrlBasePath + 'Verify.sign.sha256',
logger.error('Error extracting update', err); text: true,
file: updateAssetName + '.sign',
cleanupOldFiles: true,
cache: true,
success: (assetFileSignaturePath) => {
this.verifySignature(assetFilePath, updateAssetName, (err, valid) => {
if (err || !valid) {
UpdateModel.set({
updateStatus: 'error',
updateError: err
? 'Error verifying update signature'
: '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({ UpdateModel.set({
updateStatus: 'error', 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();
}
} }
}); });
}, },
@ -217,61 +242,64 @@ const Updater = {
}); });
}, },
extractAppUpdate(updateFile, cb) { verifySignature(assetFilePath, assetName, callback) {
const expectedFiles = this.UpdateCheckFiles; logger.info('Verifying update signature', assetName);
const appPath = Launcher.getUserDataPath(); const fs = Launcher.req('fs');
const StreamZip = Launcher.req('node-stream-zip'); const signaturesTxt = fs.readFileSync(assetFilePath + '.sign', 'utf8');
StreamZip.setFs(Launcher.req('original-fs')); const assetSignatureLine = signaturesTxt
const zip = new StreamZip({ file: updateFile, storeEntries: true }); .split('\n')
zip.on('error', cb); .find((line) => line.endsWith(assetName));
zip.on('ready', () => { if (!assetSignatureLine) {
const containsAll = expectedFiles.every((expFile) => { logger.error('Signature not found for asset', assetName);
const entry = zip.entry(expFile); callback('Asset signature not found');
return entry && entry.isFile; 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) { getUpdateAssetName(ver) {
if (!zip.comment) { const platform = Launcher.platform();
return Promise.reject('No comment in ZIP'); 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 undefined;
return Promise.reject('Bad comment length in ZIP: ' + zip.comment.length); },
}
try { installAndRestart() {
const zipFileData = Launcher.req('fs').readFileSync(archivePath); if (!Launcher) {
const dataToVerify = zipFileData.slice(0, zip.centralDirectory.headerOffset + 22); return;
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());
} }
const updateAssetName = this.getUpdateAssetName(UpdateModel.lastVersion);
const updateFilePath = Transport.cacheFilePath(updateAssetName);
Launcher.requestRestartAndUpdate(updateFilePath);
} }
}; };

View File

@ -0,0 +1,17 @@
import { Events } from 'framework/events';
const ThemeWatcher = {
dark: false,
init() {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', (e) => {
const dark = e.matches;
this.dark = dark;
Events.emit('dark-mode-changed', { dark });
});
this.dark = mediaQuery.matches;
}
};
export { ThemeWatcher };

View File

@ -1,15 +1,33 @@
import { Launcher } from 'comp/launcher'; import { Launcher } from 'comp/launcher';
import { Logger } from 'util/logger'; import { Logger } from 'util/logger';
import { noop } from 'util/fn'; import { noop } from 'util/fn';
import { StringFormat } from 'util/formatting/string-format';
const logger = new Logger('transport'); const logger = new Logger('transport');
const Transport = { const Transport = {
cacheFilePath(fileName) {
return Launcher.getTempPath(fileName);
},
httpGet(config) { httpGet(config) {
let tmpFile; let tmpFile;
const fs = Launcher.req('fs'); const fs = Launcher.req('fs');
if (config.file) { 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)) { if (fs.existsSync(tmpFile)) {
try { try {
if (config.cache && fs.statSync(tmpFile).size > 0) { if (config.cache && fs.statSync(tmpFile).size > 0) {
@ -62,9 +80,16 @@ const Transport = {
}); });
res.on('end', () => { res.on('end', () => {
data = window.Buffer.concat(data); data = window.Buffer.concat(data);
if (config.utf8) { if (config.text || config.json) {
data = data.toString('utf8'); data = data.toString('utf8');
} }
if (config.json) {
try {
data = JSON.parse(data);
} catch (e) {
config.error('Error parsing JSON: ' + e.message);
}
}
config.success(data); config.success(data);
}); });
} }

View File

@ -17,6 +17,9 @@ const Launcher = {
platform() { platform() {
return process.platform; return process.platform;
}, },
arch() {
return process.arch;
},
electron() { electron() {
return this.req('electron'); return this.req('electron');
}, },
@ -55,7 +58,15 @@ const Launcher = {
return this.joinPath(this.userDataPath, fileName || ''); return this.joinPath(this.userDataPath, fileName || '');
}, },
getTempPath(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) { getDocumentsPath(fileName) {
return this.joinPath(this.remoteApp().getPath('documents'), fileName || ''); return this.joinPath(this.remoteApp().getPath('documents'), fileName || '');
@ -164,18 +175,18 @@ const Launcher = {
requestExit() { requestExit() {
const app = this.remoteApp(); const app = this.remoteApp();
app.setHookBeforeQuitEvent(false); app.setHookBeforeQuitEvent(false);
if (this.restartPending) { if (this.pendingUpdateFile) {
app.restartApp(); app.restartAndUpdate(this.pendingUpdateFile);
} else { } else {
app.quit(); app.quit();
} }
}, },
requestRestart() { requestRestartAndUpdate(updateFilePath) {
this.restartPending = true; this.pendingUpdateFile = updateFilePath;
this.requestExit(); this.requestExit();
}, },
cancelRestart() { cancelRestart() {
this.restartPending = false; this.pendingUpdateFile = undefined;
}, },
setClipboardText(text) { setClipboardText(text) {
return this.electron().clipboard.writeText(text); return this.electron().clipboard.writeText(text);
@ -203,7 +214,7 @@ const Launcher = {
return process.platform !== 'linux'; return process.platform !== 'linux';
}, },
updaterEnabled() { updaterEnabled() {
return this.electron().remote.process.argv.indexOf('--disable-updater') === -1; return process.platform !== 'linux';
}, },
getMainWindow() { getMainWindow() {
return this.remoteApp().getMainWindow(); return this.remoteApp().getMainWindow();
@ -301,6 +312,18 @@ const Launcher = {
}, },
setGlobalShortcuts(appSettings) { setGlobalShortcuts(appSettings) {
this.remoteApp().setGlobalShortcuts(appSettings); this.remoteApp().setGlobalShortcuts(appSettings);
},
minimizeMainWindow() {
this.getMainWindow().minimize();
},
maximizeMainWindow() {
this.getMainWindow().maximize();
},
restoreMainWindow() {
this.getMainWindow().restore();
},
mainWindowMaximized() {
return this.getMainWindow().isMaximized();
} }
}; };
@ -308,6 +331,8 @@ Events.on('launcher-exit-request', () => {
setTimeout(() => Launcher.exit(), 0); setTimeout(() => Launcher.exit(), 0);
}); });
Events.on('launcher-minimize', () => setTimeout(() => Events.emit('app-minimized'), 0)); Events.on('launcher-minimize', () => setTimeout(() => Events.emit('app-minimized'), 0));
Events.on('launcher-maximize', () => setTimeout(() => Events.emit('app-maximized'), 0));
Events.on('launcher-unmaximize', () => setTimeout(() => Events.emit('app-unmaximized'), 0));
Events.on('launcher-started-minimized', () => setTimeout(() => Launcher.minimizeApp(), 0)); Events.on('launcher-started-minimized', () => setTimeout(() => Launcher.minimizeApp(), 0));
Events.on('start-profile', (data) => StartProfiler.reportAppProfile(data)); Events.on('start-profile', (data) => StartProfiler.reportAppProfile(data));
Events.on('log', (e) => new Logger(e.category || 'remote-app')[e.method || 'info'](e.message)); Events.on('log', (e) => new Logger(e.category || 'remote-app')[e.method || 'info'](e.message));

View File

@ -1,3 +1,4 @@
import kdbxweb from 'kdbxweb';
import { Events } from 'framework/events'; import { Events } from 'framework/events';
import { Logger } from 'util/logger'; import { Logger } from 'util/logger';
import { Launcher } from 'comp/launcher'; import { Launcher } from 'comp/launcher';
@ -8,11 +9,18 @@ let NativeModules;
if (Launcher) { if (Launcher) {
const logger = new Logger('native-module-connector'); const logger = new Logger('native-module-connector');
let host; let hostRunning = false;
let hostStartPromise;
let callId = 0; let callId = 0;
let promises = {}; let promises = {};
let ykChalRespCallbacks = {}; let ykChalRespCallbacks = {};
const { ipcRenderer } = Launcher.electron();
ipcRenderer.on('nativeModuleCallback', (e, msg) => NativeModules.hostCallback(msg));
ipcRenderer.on('nativeModuleHostError', (e, err) => NativeModules.hostError(err));
ipcRenderer.on('nativeModuleHostExit', (e, { code, sig }) => NativeModules.hostExit(code, sig));
ipcRenderer.on('nativeModuleHostDisconnect', () => NativeModules.hostDisconnect());
const handlers = { const handlers = {
yubikeys(numYubiKeys) { yubikeys(numYubiKeys) {
Events.emit('native-modules-yubikeys', { numYubiKeys }); Events.emit('native-modules-yubikeys', { numYubiKeys });
@ -35,7 +43,7 @@ if (Launcher) {
} }
}, },
'yk-chal-resp-result'({ callbackId, error, result }) { yubiKeyChallengeResponseResult({ callbackId, error, result }) {
const callback = ykChalRespCallbacks[callbackId]; const callback = ykChalRespCallbacks[callbackId];
if (callback) { if (callback) {
const willBeCalledAgain = error && error.touchRequested; const willBeCalledAgain = error && error.touchRequested;
@ -49,39 +57,39 @@ if (Launcher) {
NativeModules = { NativeModules = {
startHost() { startHost() {
if (host) { if (hostRunning) {
return; return Promise.resolve();
}
if (hostStartPromise) {
return hostStartPromise;
} }
logger.debug('Starting native module host'); logger.debug('Starting native module host');
const path = Launcher.req('path'); hostStartPromise = this.callNoWait('start').then(() => {
const appContentRoot = Launcher.remoteApp().getAppContentRoot(); hostStartPromise = undefined;
const mainModulePath = path.join(appContentRoot, 'native-module-host.js'); hostRunning = true;
const { fork } = Launcher.req('child_process'); if (this.usbListenerRunning) {
return this.call('startUsbListener');
}
});
host = fork(mainModulePath); return hostStartPromise;
host.on('message', (message) => this.hostCallback(message));
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');
}
}, },
hostError(e) { hostError(e) {
logger.error('Host error', e); logger.error('Host error', e);
}, },
hostDisconnect() {
logger.error('Host disconnected');
},
hostExit(code, sig) { hostExit(code, sig) {
logger.error(`Host exited with code ${code} and signal ${sig}`); logger.error(`Host exited with code ${code} and signal ${sig}`);
host = null;
hostRunning = false;
const err = new Error('Native module host crashed'); const err = new Error('Native module host crashed');
@ -121,56 +129,109 @@ if (Launcher) {
}, },
call(cmd, ...args) { call(cmd, ...args) {
return new Promise((resolve, reject) => { return this.startHost().then(() => this.callNoWait(cmd, ...args));
if (!host) { },
try {
this.startHost();
} catch (e) {
return reject(e);
}
}
callNoWait(cmd, ...args) {
return new Promise((resolve, reject) => {
callId++; callId++;
if (callId === Number.MAX_SAFE_INTEGER) { if (callId === Number.MAX_SAFE_INTEGER) {
callId = 1; callId = 1;
} }
// logger.debug('Call', cmd, args, callId); // logger.debug('Call', cmd, args, callId);
promises[callId] = { cmd, resolve, reject }; promises[callId] = { cmd, resolve, reject };
host.send({ cmd, args, callId });
ipcRenderer.send('nativeModuleCall', { cmd, args, callId });
}); });
}, },
startUsbListener() { startUsbListener() {
this.call('start-usb'); this.call('startUsbListener');
this.usbListenerRunning = true; this.usbListenerRunning = true;
}, },
stopUsbListener() { stopUsbListener() {
this.usbListenerRunning = false; this.usbListenerRunning = false;
if (host) { if (hostRunning) {
this.call('stop-usb'); this.call('stopUsbListener');
} }
}, },
getYubiKeys(config) { getYubiKeys(config) {
return this.call('get-yubikeys', config); return this.call('getYubiKeys', config);
}, },
yubiKeyChallengeResponse(yubiKey, challenge, slot, callback) { yubiKeyChallengeResponse(yubiKey, challenge, slot, callback) {
ykChalRespCallbacks[callId] = callback; ykChalRespCallbacks[callId] = callback;
return this.call('yk-chal-resp', yubiKey, challenge, slot, callId); return this.call('yubiKeyChallengeResponse', yubiKey, challenge, slot, callId);
}, },
yubiKeyCancelChallengeResponse() { yubiKeyCancelChallengeResponse() {
if (host) { if (hostRunning) {
this.call('yk-cancel-chal-resp'); this.call('yubiKeyCancelChallengeResponse');
} }
}, },
argon2(password, salt, options) { argon2(password, salt, options) {
return this.call('argon2', password, salt, options); return this.call('argon2', password, salt, options);
},
hardwareEncrypt: async (value) => {
const { data, salt } = await ipcRenderer.invoke('hardwareEncrypt', value.dataAndSalt());
return new kdbxweb.ProtectedValue(data, salt);
},
hardwareDecrypt: async (value, touchIdPrompt) => {
const { data, salt } = await ipcRenderer.invoke(
'hardwareDecrypt',
value.dataAndSalt(),
touchIdPrompt
);
return new kdbxweb.ProtectedValue(data, salt);
},
kbdGetActiveWindow(options) {
return this.call('kbdGetActiveWindow', options);
},
kbdGetActivePid() {
return this.call('kbdGetActivePid');
},
kbdShowWindow(win) {
return this.call('kbdShowWindow', win);
},
kbdText(str) {
return this.call('kbdText', str);
},
kbdTextAsKeys(str, mods) {
return this.call('kbdTextAsKeys', str, mods);
},
kbdKeyPress(code, modifiers) {
return this.call('kbdKeyPress', code, modifiers);
},
kbdShortcut(code, modifiers) {
return this.call('kbdShortcut', code, modifiers);
},
kbdKeyMoveWithModifier(down, modifiers) {
return this.call('kbdKeyMoveWithModifier', down, modifiers);
},
kbdKeyPressWithCharacter(character, code, modifiers) {
return this.call('kbdKeyPressWithCharacter', character, code, modifiers);
},
kbdEnsureModifierNotPressed() {
return this.call('kbdEnsureModifierNotPressed');
} }
}; };
global.NativeModules = NativeModules;
} }
export { NativeModules }; export { NativeModules };

View File

@ -1,6 +1,11 @@
import { Events } from 'framework/events'; import { Events } from 'framework/events';
import { Features } from 'util/features'; import { Features } from 'util/features';
import { Locale } from 'util/locale'; import { Locale } from 'util/locale';
import { ThemeWatcher } from 'comp/browser/theme-watcher';
import { AppSettingsModel } from 'models/app-settings-model';
import { Logger } from 'util/logger';
const logger = new Logger('settings-manager');
const SettingsManager = { const SettingsManager = {
neutralLocale: null, neutralLocale: null,
@ -16,23 +21,65 @@ const SettingsManager = {
allThemes: { allThemes: {
dark: 'setGenThemeDark', dark: 'setGenThemeDark',
light: 'setGenThemeLight', light: 'setGenThemeLight',
fb: 'setGenThemeFb',
db: 'setGenThemeDb',
sd: 'setGenThemeSd', sd: 'setGenThemeSd',
sl: 'setGenThemeSl', sl: 'setGenThemeSl',
fb: 'setGenThemeFb',
bl: 'setGenThemeBl',
db: 'setGenThemeDb',
lb: 'setGenThemeLb',
te: 'setGenThemeTe', te: 'setGenThemeTe',
lt: 'setGenThemeLt',
dc: 'setGenThemeDc',
hc: 'setGenThemeHc' hc: 'setGenThemeHc'
}, },
// changing something here? don't forget about desktop/app.js
autoSwitchedThemes: [
{
name: 'setGenThemeDefault',
dark: 'dark',
light: 'light'
},
{
name: 'setGenThemeSol',
dark: 'sd',
light: 'sl'
},
{
name: 'setGenThemeBlue',
dark: 'fb',
light: 'bl'
},
{
name: 'setGenThemeBrown',
dark: 'db',
light: 'lb'
},
{
name: 'setGenThemeTerminal',
dark: 'te',
light: 'lt'
},
{
name: 'setGenThemeHighContrast',
dark: 'dc',
light: 'hc'
}
],
customLocales: {}, customLocales: {},
setBySettings(settings) { init() {
this.setTheme(settings.theme); Events.on('dark-mode-changed', () => this.darkModeChanged());
this.setFontSize(settings.fontSize); },
const locale = settings.locale;
setBySettings() {
this.setTheme(AppSettingsModel.theme);
this.setFontSize(AppSettingsModel.fontSize);
const locale = AppSettingsModel.locale;
try { try {
if (locale) { if (locale) {
this.setLocale(settings.locale); this.setLocale(AppSettingsModel.locale);
} else { } else {
this.setLocale(this.getBrowserLocale()); this.setLocale(this.getBrowserLocale());
} }
@ -55,18 +102,45 @@ const SettingsManager = {
document.body.classList.remove(cls); document.body.classList.remove(cls);
} }
} }
if (AppSettingsModel.autoSwitchTheme) {
theme = this.selectDarkOrLightTheme(theme);
}
document.body.classList.add(this.getThemeClass(theme)); document.body.classList.add(this.getThemeClass(theme));
const metaThemeColor = document.head.querySelector('meta[name=theme-color]'); const metaThemeColor = document.head.querySelector('meta[name=theme-color]');
if (metaThemeColor) { if (metaThemeColor) {
metaThemeColor.content = window.getComputedStyle(document.body).backgroundColor; metaThemeColor.content = window.getComputedStyle(document.body).backgroundColor;
} }
this.activeTheme = theme; this.activeTheme = theme;
logger.debug('Theme changed', theme);
Events.emit('theme-applied');
}, },
getThemeClass(theme) { getThemeClass(theme) {
return 'th-' + theme; return 'th-' + theme;
}, },
selectDarkOrLightTheme(theme) {
for (const config of this.autoSwitchedThemes) {
if (config.light === theme || config.dark === theme) {
return ThemeWatcher.dark ? config.dark : config.light;
}
}
return theme;
},
darkModeChanged() {
if (AppSettingsModel.autoSwitchTheme) {
for (const config of this.autoSwitchedThemes) {
if (config.light === this.activeTheme || config.dark === this.activeTheme) {
const newTheme = ThemeWatcher.dark ? config.dark : config.light;
logger.debug('Setting theme triggered by system settings change', newTheme);
this.setTheme(newTheme);
break;
}
}
}
},
setFontSize(fontSize) { setFontSize(fontSize) {
const defaultFontSize = Features.isMobile ? 14 : 12; const defaultFontSize = Features.isMobile ? 14 : 12;
document.documentElement.style.fontSize = defaultFontSize + (fontSize || 0) * 2 + 'px'; document.documentElement.style.fontSize = defaultFontSize + (fontSize || 0) * 2 + 'px';

View File

@ -1,5 +1,6 @@
const DefaultAppSettings = { const DefaultAppSettings = {
theme: null, // UI theme theme: null, // UI theme
autoSwitchTheme: false, // automatically switch between light and dark theme
locale: null, // user interface language locale: null, // user interface language
expandGroups: true, // show entries from all subgroups expandGroups: true, // show entries from all subgroups
listViewWidth: null, // width of the entry list representation listViewWidth: null, // width of the entry list representation
@ -12,6 +13,7 @@ const DefaultAppSettings = {
rememberKeyFiles: 'path', // remember keyfiles selected on the Open screen rememberKeyFiles: 'path', // remember keyfiles selected on the Open screen
idleMinutes: 15, // app lock timeout after inactivity, minutes idleMinutes: 15, // app lock timeout after inactivity, minutes
minimizeOnClose: false, // minimise the app instead of closing minimizeOnClose: false, // minimise the app instead of closing
minimizeOnFieldCopy: false, // minimise the app on copy
tableView: false, // view entries as a table instead of list tableView: false, // view entries as a table instead of list
colorfulIcons: false, // use colorful custom icons instead of grayscale colorfulIcons: false, // use colorful custom icons instead of grayscale
useMarkdown: true, // use Markdown in Notes field useMarkdown: true, // use Markdown in Notes field
@ -36,6 +38,15 @@ const DefaultAppSettings = {
useGroupIconForEntries: false, // automatically use group icon when creating new entries useGroupIconForEntries: false, // automatically use group icon when creating new entries
enableUsb: true, // enable interaction with USB devices enableUsb: true, // enable interaction with USB devices
fieldLabelDblClickAutoType: false, // trigger auto-type by doubleclicking field label fieldLabelDblClickAutoType: false, // trigger auto-type by doubleclicking field label
auditPasswords: true, // enable password audit
auditPasswordEntropy: true, // show warnings for weak passwords
excludePinsFromAudit: true, // exclude PIN codes from audit
checkPasswordsOnHIBP: false, // check passwords on Have I Been Pwned
auditPasswordAge: 0, // show warnings about old passwords, number of years, 0 = disabled
useLegacyAutoType: false, // use legacy auto-type engine (will be removed in future versions)
deviceOwnerAuth: null, // Touch ID: null / 'memory' / 'file'
deviceOwnerAuthTimeoutMinutes: 0, // how often master password is required with Touch ID
disableOfflineStorage: false, // don't cache loaded files in offline storage
yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey yubiKeyShowIcon: true, // show an icon to open OTP codes from YubiKey
yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files yubiKeyAutoOpen: false, // auto-load one-time codes when there are open files

View File

@ -0,0 +1,2 @@
export const KeeWebLogo =
'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxkZWZzPjxyYWRpYWxHcmFkaWVudCBpZD0iYSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN4PSI2NjMuMTMzIiBjeT0iMTM4LjYwMSIgcj0iODY0LjU2OCIgZng9IjY2My4xMzMiIGZ5PSIxMzguNjAxIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM5MmJmZjUiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMDQxYzgiLz48L3JhZGlhbEdyYWRpZW50PjwvZGVmcz48cGF0aCBkPSJNOTIzIDM1Ny42MjhjMC05Ljc4MiAwLTE5LjU2NC0uMDU2LTI5LjM0OC0uMDUtOC4yNC0uMTQ0LTE2LjQ4LS4zNjgtMjQuNzE2YTM1OC43MjQgMzU4LjcyNCAwIDAwLTQuNzM2LTUzLjgxMSAxODEuNDk3IDE4MS40OTcgMCAwMC0xNi44NTgtNTEuMTMzIDE3Mi4wNjYgMTcyLjA2NiAwIDAwLTc1LjIzNi03NS4yIDE4MS43MDIgMTgxLjcwMiAwIDAwLTUxLjE4OC0xNi44NiAzNTkuNjI2IDM1OS42MjYgMCAwMC01My44Mi00LjczYy04LjI0Mi0uMjMtMTYuNDg2LS4zMTgtMjQuNzM4LS4zNjgtOS43OC0uMDYyLTE5LjU2OC0uMDYyLTI5LjM1Ni0uMDYyTDU1MyAxMDFoLTg1bC0xMTEuNjM4LjRjLTkuOCAwLTE5LjYxNCAwLTI5LjQyLjA1NC04LjI2Mi4wNS0xNi41Mi4xNDQtMjQuNzc4LjM2OGEzNjAuODA4IDM2MC44MDggMCAwMC01My45NDggNC43NDIgMTgyLjI3MiAxODIuMjcyIDAgMDAtNTEuMjU4IDE2Ljg0OCAxNzIuMjMgMTcyLjIzIDAgMDAtNzUuMzg2IDc1LjE4OCAxODEuMjM4IDE4MS4yMzggMCAwMC0xNi45IDUxLjE2IDM1OC4zMTIgMzU4LjMxMiAwIDAwLTQuNzQyIDUzLjhjLS4yMjIgOC4yNC0uMzE4IDE2LjQ4LS4zNjggMjQuNzItLjA2IDkuNzg0LS41NjIgMjEuOTM2LS41NjIgMzEuNzJ2MTk2bC41MDggMTEyLjQyOGMwIDkuOCAwIDE5LjYuMDU0IDI5LjM4OC4wNSA4LjI1Mi4xNDYgMTYuNTAyLjM2OCAyNC43NWEzNTkuMDQ2IDM1OS4wNDYgMCAwMDQuNzQ2IDUzLjg4NSAxODEuNjY4IDE4MS42NjggMCAwMDE2Ljg5IDUxLjIwMSAxNzIuMzU2IDE3Mi4zNTYgMCAwMDc1LjM4MiA3NS4zMDIgMTgyLjEwNSAxODIuMTA1IDAgMDA1MS4yODggMTYuODggMzYwLjIwNyAzNjAuMjA3IDAgMDA1My45MjQgNC43MzZjOC4yNTguMjI0IDE2LjUxOC4zMTggMjQuNzguMzY4IDkuOC4wNiAxOS42MTIuMDU2IDI5LjQyLjA1NmgzMTAuMjg0YzkuNzg4IDAgMTkuNTc2IDAgMjkuMzY0LS4wNTQgOC4yNDQtLjA1IDE2LjQ4OC0uMTQ0IDI0LjczLS4zNjhhMzU5LjEgMzU5LjEgMCAwMDUzLjg0LTQuNzQgMTgxLjUxOCAxODEuNTE4IDAgMDA1MS4xNi0xNi44NzIgMTcyLjE5NSAxNzIuMTk1IDAgMDA3NS4yNC03NS4yOTggMTgxLjk1IDE4MS45NSAwIDAwMTYuODY2LTUxLjIzIDM1OS44MTUgMzU5LjgxNSAwIDAwNC43MzItNTMuODY0Yy4yMjQtOC4yNS4zMTgtMTYuNS4zNjgtMjQuNzUuMDYtOS44LjA1Ni0xOS42LjA1Ni0yOS4zODh2LTMxMC44IiBmaWxsPSJ1cmwoI2EpIi8+PHBhdGggZD0iTTk5IDEwMWg4MjR2ODI0SDk5eiIgZmlsbD0ibm9uZSIvPjxnIGZpbGw9IiNmZmYiPjxwYXRoIGQ9Ik0zNzkuMTk2IDgxMy4yNzNjLTUuOTM5LS4wMS0xMi41My0yLjUwMi0xOS4zMjYtNi4wNjctMTQuNDk3LTcuNjA1LTI0LjQzOS0xNy4yMi0xNi4xMzktMzMuMDQyTDU0NS4zOCAzODkuNzYxYTE1MTguMzEgMTUxOC4zMSAwIDAxLTUuNzU4LTIuOTk4Yy03Mi43MDktMzguMTQyLTEwNC45NjctOTQuNTM1LTc4Ljg1Mi0xNDQuMzE4IDMwLjAxOC01Ny4yMjMgMTIyLjk4NC02Ny42MDggMjA3LjY0Ny0yMy4xOTYgODQuNjYzIDQ0LjQxMyAxMjguOTYxIDEyNi44MDUgOTguOTQzIDE4NC4wMjgtMjUuODE1IDQ5LjIxMS05My4yMTcgNTMuNTk5LTE2MC45OSAxOC4wNDctMS44NjctLjk4LTQuODk2LTIuNTUzLTguMzI0LTQuMzNMMzk2LjIzMSA4MDEuNzA1Yy00LjQwOSA4LjQwNS0xMC4zMDQgMTEuNTc5LTE3LjAzNSAxMS41Njh6bS0zNy4xMjctODIuMzUybC0zLjgxNi0yLjAwMi0uODUtLjQ0NS01MS42NTItMjcuMDk2di0uMDAybC0xMi40NTEtNi41MzFhNC4zMSA0LjMxIDAgMDEtMS44MTUtNS44MmwxNi4zNDYtMzEuMTZhNC4zMTIgNC4zMTIgMCAwMTUuNzg5LTEuODMybC4wMzMuMDE1IDI0LjkwMyAxMy4wNjVhNC4zMTEgNC4zMTEgMCAwMDUuODItMS44MTdsMTYuMzQ4LTMxLjE2YTQuMzEgNC4zMSAwIDAwLTEuODE3LTUuODJsLTEyLjQ1MS02LjUzMy0xMi40NTEtNi41MzJhNC4zMSA0LjMxIDAgMDEtMS44MTUtNS44MmwxNi4zNDYtMzEuMTYyYTQuMzEgNC4zMSAwIDAxNS44Mi0xLjgxNGwxMi40NTMgNi41MzEgMzIuNDExIDE3LjAwMiAyMC4wODkgMTAuNTM5LjAyNi4wMTQgMy43OTMgMS45OS0xNy4xNjggMzIuNzI2LTEuMTgyIDIuMjUyYS4wMDguMDA4IDAgMDEtLjAwMi4wMDRsLTI0LjM1NyA0Ni40My0yLjQ3NyA0LjcyMS0xMy44NjkgMjYuNDM5LS4wMS4wMTgtMS45OTQgMy44ek00MjMuNjkgNTc1LjMzbC0zLjgxOC0yLjAwNC0xOC4xMzUtOS41MTQtMTQuMzE4LTcuNTEyYTQuMzEgNC4zMSAwIDAxLTEuODE3LTUuODJsMTYuMzQ4LTMxLjE2YTQuMzExIDQuMzExIDAgMDE1LjgyLTEuODE3bDE0LjMxOSA3LjUxMiAxOC4xMzYgOS41MTQuMDI2LjAxMyAzLjc5MyAxLjk5MS0yLjAwNCAzLjgxOC04LjE3MiAxNS41OC04LjE3NCAxNS41OC0uMDEuMDE2LTEuOTk0IDMuODAzem0yNjYuODEzLTE4NC40MjZjMTAuNzUzLjAyIDE5LjE2My00LjMxMiAyNC43NS0xNC45NjMgMTcuMDI4LTMyLjQ2LTE0LjQ3Mi04Mi41MzgtNzAuMzU2LTExMS44NTQtNTUuODg0LTI5LjMxNi0xMTQuOTkxLTI2Ljc2OC0xMzIuMDE5IDUuNjkyLTE3LjAyOCAzMi40NTkgMzAuMzM0IDUzLjc3MiA4Ni4yMTkgODMuMDg4IDM3LjU0NyAxOS42OTYgNjkuMzg4IDM3Ljk5NCA5MS40MDYgMzguMDM3eiIvPjxwYXRoIGQ9Ik00MjMuNjkxIDU3NS4zMjlsMTkuMDkxIDEwLjAxNSAyMC4zNTMtMzguNzk3LTE5LjA5MS0xMC4wMTV6TTM0Mi4wNyA3MzAuOTIybDE5LjA5MSAxMC4wMTUgNjEuMDU3LTExNi4zOTItMTkuMDkxLTEwLjAxNXoiIGZpbGwtb3BhY2l0eT0iLjIiLz48L2c+PC9zdmc+';

View File

@ -7,17 +7,20 @@ const Links = {
License: 'https://github.com/keeweb/keeweb/blob/master/LICENSE', License: 'https://github.com/keeweb/keeweb/blob/master/LICENSE',
LicenseApache: 'https://opensource.org/licenses/Apache-2.0', LicenseApache: 'https://opensource.org/licenses/Apache-2.0',
LicenseLinkCCBY40: 'https://creativecommons.org/licenses/by/4.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', ReleaseNotes: 'https://github.com/keeweb/keeweb/blob/master/release-notes.md#release-notes',
SelfHostedDropbox: 'https://github.com/keeweb/keeweb#self-hosting', SelfHostedDropbox: 'https://github.com/keeweb/keeweb#self-hosting',
Manifest: 'https://app.keeweb.info/manifest.appcache', UpdateJson: 'https://app.keeweb.info/update.json',
AutoType: 'https://github.com/keeweb/keeweb/wiki/Auto-Type', AutoType: 'https://github.com/keeweb/keeweb/wiki/Auto-Type',
AutoTypeMacOS: 'https://github.com/keeweb/keeweb/wiki/Auto-Type#macos',
Translation: 'https://keeweb.oneskyapp.com/', Translation: 'https://keeweb.oneskyapp.com/',
Donation: 'https://opencollective.com/keeweb#support', Donation: 'https://opencollective.com/keeweb#support',
Plugins: 'https://plugins.keeweb.info', Plugins: 'https://plugins.keeweb.info',
PluginDevelopStart: 'https://github.com/keeweb/keeweb/wiki/Plugins', PluginDevelopStart: 'https://github.com/keeweb/keeweb/wiki/Plugins',
YubiKeyManual: 'https://github.com/keeweb/keeweb/wiki/YubiKey', YubiKeyManual: 'https://github.com/keeweb/keeweb/wiki/YubiKey',
YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation' YubiKeyManagerInstall: 'https://github.com/Yubico/yubikey-manager#installation',
HaveIBeenPwned: 'https://haveibeenpwned.com',
HaveIBeenPwnedPrivacy: 'https://haveibeenpwned.com/Passwords'
}; };
export { Links }; export { Links };

View File

@ -16,7 +16,9 @@ const Timeouts = {
ExternalDeviceReconnect: 3000, ExternalDeviceReconnect: 3000,
ExternalDeviceAfterReconnect: 1000, ExternalDeviceAfterReconnect: 1000,
FieldLabelDoubleClick: 300, FieldLabelDoubleClick: 300,
NativeModuleHostRestartTime: 3000 NativeModuleHostRestartTime: 3000,
FastAnimation: 100,
AutoTypeCopyPaste: 300
}; };
export { Timeouts }; export { Timeouts };

View File

@ -12,10 +12,14 @@ Handlebars.registerHelper('res', function (key, options) {
return value; return value;
}); });
Handlebars.registerHelper('Res', (key) => { Handlebars.registerHelper('Res', (key, options) => {
let value = Locale[key]; let value = Locale[key];
if (value) { if (value) {
value = value[0].toUpperCase() + value.substr(1); value = value[0].toUpperCase() + value.substr(1);
const ix = value.indexOf('{}');
if (ix >= 0) {
value = value.replace('{}', options.fn(this));
}
} }
return value; return value;
}); });

View File

@ -26,6 +26,15 @@
"shiftKey": "shift", "shiftKey": "shift",
"altKey": "alt", "altKey": "alt",
"error": "error", "error": "error",
"oneMinute": "one minute",
"minutes": "{} minutes",
"oneHour": "one hour",
"hours": "{} hours",
"oneDay": "one day",
"days": "{} days",
"oneWeek": "one week",
"oneMonth": "one month",
"oneYear": "one year",
"cache": "cache", "cache": "cache",
"file": "file", "file": "file",
@ -301,6 +310,16 @@
"detRevealField": "Reveal", "detRevealField": "Reveal",
"detHideField": "Hide", "detHideField": "Hide",
"detAutoTypeField": "Auto type", "detAutoTypeField": "Auto type",
"detIssuesHideTooltip": "Hide this warning",
"detIssueWeakPassword": "The password is weak, it's recommended to change it.",
"detIssuePoorPassword": "The password is very weak, it's strongly recommended to change it.",
"detIssuePwnedPassword": "This password has been exposed in a data breach according to {}, it's recommended to change it.",
"detIssuePasswordCheckError": "There was an error checking password strength online.",
"detIssueOldPassword": "The password is old.",
"detIssueCloseAlertHeader": "Hide password issues",
"detIssueCloseAlertBody": "There are different ways you can hide this warning:",
"detIssueCloseAlertEntry": "Don't show for this entry",
"detIssueCloseAlertSettings": "Adjust global settings",
"autoTypeEntryFields": "Entry fields", "autoTypeEntryFields": "Entry fields",
"autoTypeModifiers": "Modifier keys", "autoTypeModifiers": "Modifier keys",
@ -308,6 +327,7 @@
"autoTypeLink": "more...", "autoTypeLink": "more...",
"autoTypeError": "Auto-type error", "autoTypeError": "Auto-type error",
"autoTypeErrorGeneric": "There was an error performing auto-type: {}", "autoTypeErrorGeneric": "There was an error performing auto-type: {}",
"autoTypeErrorAccessibilityMacOS": "We tried to send keystrokes to the application, but it doesn't seem to work. This may happen because of missing permissions, click here to read more about it:",
"autoTypeErrorGlobal": "To use a system-wide shortcut, please focus the app where you want to type your password", "autoTypeErrorGlobal": "To use a system-wide shortcut, please focus the app where you want to type your password",
"autoTypeErrorNotInstalled": "{} is not installed", "autoTypeErrorNotInstalled": "{} is not installed",
"autoTypeHeader": "Auto-Type: Select", "autoTypeHeader": "Auto-Type: Select",
@ -367,19 +387,30 @@
"setGenExtractingUpdate": "Extracting update...", "setGenExtractingUpdate": "Extracting update...",
"setGenCheckErr": "There was an error downloading new version", "setGenCheckErr": "There was an error downloading new version",
"setGenNeverChecked": "Never checked for updates", "setGenNeverChecked": "Never checked for updates",
"setGenRestartToUpdate": "Restart the app to update", "setGenRestartToUpdate": "Restart KeeWeb to update",
"setGenDownloadAndRestart": "Download update and restart", "setGenDownloadAndRestart": "Download update and restart",
"setGenAppearance": "Appearance", "setGenAppearance": "Appearance",
"setGenTheme": "Theme", "setGenTheme": "Theme",
"setGenThemeDefault": "Default",
"setGenThemeDark": "Dark", "setGenThemeDark": "Dark",
"setGenThemeLight": "Light", "setGenThemeLight": "Light",
"setGenThemeFb": "Flat blue", "setGenThemeBlue": "Flat blue",
"setGenThemeFb": "Dark blue",
"setGenThemeBl": "Light blue",
"setGenThemeBrown": "Brownie",
"setGenThemeDb": "Dark brown", "setGenThemeDb": "Dark brown",
"setGenThemeLb": "Light brown",
"setGenThemeTerminal": "Terminal",
"setGenThemeTe": "Terminal", "setGenThemeTe": "Terminal",
"setGenThemeLt": "Terminal light",
"setGenThemeHighContrast": "High contrast",
"setGenThemeHc": "High contrast", "setGenThemeHc": "High contrast",
"setGenThemeDc": "Dark contrast",
"setGenThemeSol": "Solarized",
"setGenThemeSd": "Solarized dark", "setGenThemeSd": "Solarized dark",
"setGenThemeSl": "Solarized light", "setGenThemeSl": "Solarized light",
"setGenMoreThemes": "More themes", "setGenMoreThemes": "More themes",
"setGenAutoSwitchTheme": "Automatically switch between light and dark theme when possible",
"setGenLocale": "Language", "setGenLocale": "Language",
"setGenLocOther": "other languages are available as plugins", "setGenLocOther": "other languages are available as plugins",
"setGenFontSize": "Font size", "setGenFontSize": "Font size",
@ -416,12 +447,14 @@
"setGenClearSeconds": "In {} seconds", "setGenClearSeconds": "In {} seconds",
"setGenClearMinute": "In a minute", "setGenClearMinute": "In a minute",
"setGenMinInstead": "Minimize the app instead of close", "setGenMinInstead": "Minimize the app instead of close",
"setGenMinOnFieldCopy": "Minimize on field copy",
"setGenLock": "Auto lock", "setGenLock": "Auto lock",
"setGenLockMinimize": "When the app is minimized", "setGenLockMinimize": "When the app is minimized",
"setGenLockCopy": "On password copy", "setGenLockCopy": "On password copy",
"setGenLockAutoType": "On auto-type", "setGenLockAutoType": "On auto-type",
"setGenLockOrSleep": "When the computer is locked or put to sleep", "setGenLockOrSleep": "When the computer is locked or put to sleep",
"setGenStorage": "Storage", "setGenStorage": "Storage",
"setGenDisableOfflineStorage": "Don't cache loaded files in offline storage",
"setGenStorageLogout": "Log out", "setGenStorageLogout": "Log out",
"setGenShowAdvanced": "Show advanced settings", "setGenShowAdvanced": "Show advanced settings",
"setGenDevTools": "Show dev tools", "setGenDevTools": "Show dev tools",
@ -431,6 +464,23 @@
"setGenShowAppLogs": "Show app logs", "setGenShowAppLogs": "Show app logs",
"setGenReloadApp": "Reload the app", "setGenReloadApp": "Reload the app",
"setGenFieldLabelDblClickAutoType": "Auto-type on double-clicking field labels", "setGenFieldLabelDblClickAutoType": "Auto-type on double-clicking field labels",
"setGenUseLegacyAutoType": "Use legacy auto-type (if you have issues)",
"setGenTouchId": "Touch ID",
"setGenTouchIdDisabled": "Don't use Touch ID",
"setGenTouchIdMemory": "Unlock with Touch ID only while KeeWeb is running",
"setGenTouchIdFile": "Always use Touch ID instead of master password",
"setGenTouchIdPass": "Require master password after",
"setGenAudit": "Audit",
"setGenAuditPasswords": "Show warnings about password strength",
"setGenAuditPasswordEntropy": "Check password length and randomness",
"setGenExcludePinsFromAudit": "Never check short numeric PIN codes, such as 123456",
"setGenCheckPasswordsOnHIBP": "Check passwords using an online service {}",
"setGenHelpHIBP": "KeeWeb can check if your passwords have been previously exposed in a data breach using an online service. Your password cannot be recovered based on data sent online, however the number of passwords checked this way may be exposed. More about your privacy when using this service can be found {}. If this option is enabled, KeeWeb will automatically check your passwords there.",
"setGenHelpHIBPLink": "here",
"setGenAuditPasswordAge": "Old passwords",
"setGenAuditPasswordAgeOff": "Don't show warnings about old passwords",
"setGenAuditPasswordAgeOneYear": "Show warnings for passwords older than one year",
"setGenAuditPasswordAgeYears": "Show warnings for passwords older than {} years",
"setFilePath": "File path", "setFilePath": "File path",
"setFileStorage": "This file is loaded from {}.", "setFileStorage": "This file is loaded from {}.",
@ -661,5 +711,7 @@
"yubiKeyTouchRequestedBody": "Please touch your YubiKey with serial number {}", "yubiKeyTouchRequestedBody": "Please touch your YubiKey with serial number {}",
"yubiKeyDisabledErrorHeader": "USB is disabled", "yubiKeyDisabledErrorHeader": "USB is disabled",
"yubiKeyDisabledErrorBody": "YubiKey is required to open this file, please enable USB devices in settings.", "yubiKeyDisabledErrorBody": "YubiKey is required to open this file, please enable USB devices in settings.",
"yubiKeyErrorWithCode": "YubiKey error, code {}." "yubiKeyErrorWithCode": "YubiKey error, code {}.",
"bioOpenAuthPrompt": "open \"{}\""
} }

View File

@ -26,6 +26,15 @@
"shiftKey": "Umschalt", "shiftKey": "Umschalt",
"altKey": "Alt", "altKey": "Alt",
"error": "Fehler", "error": "Fehler",
"oneMinute": "Eine Minute",
"minutes": "{} Minuten",
"oneHour": "Eine Stunde",
"hours": "{} Stunden",
"oneDay": "Ein Tag",
"days": "{} Tage",
"oneWeek": "Eine Woche",
"oneMonth": "Ein Monat",
"oneYear": "Ein Jahr",
"cache": "Cache", "cache": "Cache",
"file": "Datei", "file": "Datei",
"device": "Gerät", "device": "Gerät",
@ -408,6 +417,11 @@
"setGenShowAppLogs": "App-Logs anzeigen", "setGenShowAppLogs": "App-Logs anzeigen",
"setGenReloadApp": "App neu laden", "setGenReloadApp": "App neu laden",
"setGenFieldLabelDblClickAutoType": "Auto-Type durch Anklicken von Beschriftungen aktivieren", "setGenFieldLabelDblClickAutoType": "Auto-Type durch Anklicken von Beschriftungen aktivieren",
"setGenTouchId": "Fingerabdruck",
"setGenTouchIdDisabled": "Fingerabdruck nicht benutzen",
"setGenTouchIdMemory": "Nur mit Fingerabdruck entsperren, wenn KeeWeb aktiv ist",
"setGenTouchIdFile": "Benutze immer den Fingerabdruck anstatt das Master-Passwort",
"setGenTouchIdPass": "Benötige Master-Passwort nach",
"setFilePath": "Dateipfad", "setFilePath": "Dateipfad",
"setFileStorage": "Diese Datei wird von {} geladen.", "setFileStorage": "Diese Datei wird von {} geladen.",
"setFileIntl": "Diese Datei ist im internen App-Speicher abgelegt", "setFileIntl": "Diese Datei ist im internen App-Speicher abgelegt",

View File

@ -26,6 +26,15 @@
"shiftKey": "shift", "shiftKey": "shift",
"altKey": "alt", "altKey": "alt",
"error": "erreur", "error": "erreur",
"oneMinute": "une minute",
"minutes": "{} minutes",
"oneHour": "une heure",
"hours": "{} heures",
"oneDay": "un jour",
"days": "{} jours",
"oneWeek": "une semaine",
"oneMonth": "un mois",
"oneYear": "une année",
"cache": "cache", "cache": "cache",
"file": "fichier", "file": "fichier",
"device": "appareil", "device": "appareil",
@ -87,7 +96,7 @@
"tagExists": "Ce tag existe déjà", "tagExists": "Ce tag existe déjà",
"tagExistsBody": "Un tag existe déjà avec ce nom. Merci de choisir un autre nom.", "tagExistsBody": "Un tag existe déjà avec ce nom. Merci de choisir un autre nom.",
"tagBadName": "Nom invalide", "tagBadName": "Nom invalide",
"tagBadNameBody": "Un nom de tag ne peut pas contenir les caractères {}. Merci de les supprimer.", "tagBadNameBody": "Tag name cannot contain characters {}. Please remove them.",
"genPsTitle": "Préréglages du Générateur", "genPsTitle": "Préréglages du Générateur",
"genPsCreate": "Nouveau préréglage", "genPsCreate": "Nouveau préréglage",
"genPsDelete": "Supprimer préréglage", "genPsDelete": "Supprimer préréglage",
@ -122,7 +131,7 @@
"listNoWebsite": "aucun site web", "listNoWebsite": "aucun site web",
"listNoUser": "aucun utilisateur", "listNoUser": "aucun utilisateur",
"listNoAttachments": "aucune pièce-jointe", "listNoAttachments": "aucune pièce-jointe",
"listAddTemplateHeader": "Templates", "listAddTemplateHeader": "Modèles",
"listAddTemplateBody1": "Les modèles (templates) vous permettent de créer des nouvelles entrées en un clic. Ajouter quelque chose à l'entrée du modèle et ensuite cliquer de nouveau sur {} pour utiliser ce modèle.", "listAddTemplateBody1": "Les modèles (templates) vous permettent de créer des nouvelles entrées en un clic. Ajouter quelque chose à l'entrée du modèle et ensuite cliquer de nouveau sur {} pour utiliser ce modèle.",
"listAddTemplateBody2": "Vous pouvez toujours retrouver vos modèles dans le groupe {}.", "listAddTemplateBody2": "Vous pouvez toujours retrouver vos modèles dans le groupe {}.",
"searchAddNew": "Ajouter Nouveau", "searchAddNew": "Ajouter Nouveau",
@ -237,7 +246,7 @@
"detDelToTrashBody": "L'entrée sera déplacée dans la corbeille.", "detDelToTrashBody": "L'entrée sera déplacée dans la corbeille.",
"detFieldCopied": "Copié", "detFieldCopied": "Copié",
"detFieldCopiedTime": "Copié pendant {} secondes", "detFieldCopiedTime": "Copié pendant {} secondes",
"detCopyHint": "Vous pouvez copier la valeur du champ en cliquant sur son titre", "detCopyHint": "You can copy field value by clicking its title",
"detMore": "plus", "detMore": "plus",
"detClickToAddField": "cliquez pour ajouter un nouveau champ", "detClickToAddField": "cliquez pour ajouter un nouveau champ",
"detMenuAddNewField": "Ajouter nouveau champ", "detMenuAddNewField": "Ajouter nouveau champ",
@ -286,6 +295,12 @@
"detRevealField": "Révéler", "detRevealField": "Révéler",
"detHideField": "Cacher", "detHideField": "Cacher",
"detAutoTypeField": "Saisie auto", "detAutoTypeField": "Saisie auto",
"detIssuesHideTooltip": "Cacher cet avertissement",
"detIssueWeakPassword": "Ce mot de passe est faible, nous vous recommandons de le changer",
"detIssuePoorPassword": "Ce mot de passe est très faible, nous vous recommandons très fort de le changer",
"detIssuePwnedPassword": "Ce mot de passe a été exposé à une faille selon {}, il est recommandé de le changer",
"detIssuePasswordCheckError": "Une erreur est survenue en vérifiant la force du mot de passe en ligne",
"detIssueOldPassword": "Ce mot de passe est vieux",
"autoTypeEntryFields": "Champs", "autoTypeEntryFields": "Champs",
"autoTypeModifiers": "Touches modificatrices", "autoTypeModifiers": "Touches modificatrices",
"autoTypeKeys": "Clés", "autoTypeKeys": "Clés",
@ -349,7 +364,7 @@
"setGenExtractingUpdate": "Décompression de la mise à jour...", "setGenExtractingUpdate": "Décompression de la mise à jour...",
"setGenCheckErr": "Une erreur est intervenue durant le téléchargement de la mise à jour", "setGenCheckErr": "Une erreur est intervenue durant le téléchargement de la mise à jour",
"setGenNeverChecked": "Ne jamais vérifier les mises à jour", "setGenNeverChecked": "Ne jamais vérifier les mises à jour",
"setGenRestartToUpdate": "Redémarrer pour mettre à jour", "setGenRestartToUpdate": "Redémarrer KeeWeb pour mettre à jour",
"setGenDownloadAndRestart": "Télécharger la mise à jour et redémarrer", "setGenDownloadAndRestart": "Télécharger la mise à jour et redémarrer",
"setGenAppearance": "Apparence", "setGenAppearance": "Apparence",
"setGenTheme": "Thème", "setGenTheme": "Thème",
@ -424,6 +439,22 @@
"setGenShowAppLogs": "Voir les logs", "setGenShowAppLogs": "Voir les logs",
"setGenReloadApp": "Recharger l'application", "setGenReloadApp": "Recharger l'application",
"setGenFieldLabelDblClickAutoType": "Remplissage auto par double clic sur les noms de champ", "setGenFieldLabelDblClickAutoType": "Remplissage auto par double clic sur les noms de champ",
"setGenTouchId": "Touch ID",
"setGenTouchIdDisabled": "Ne pas utiliser Touch ID",
"setGenTouchIdMemory": "Dévérouiller avec Touch ID uniquement quand Keeweb est lancé",
"setGenTouchIdFile": "Toujours utiliser Touch ID à la place du mot de passe",
"setGenTouchIdPass": "Exige le mot de passe après",
"setGenAudit": "Audit",
"setGenAuditPasswords": "Voir des avertissements sur la force du mot de passe",
"setGenAuditPasswordEntropy": "Vérifier la longueur et l'aléas du mot de passe",
"setGenExcludePinsFromAudit": "Ne jamais vérifier les codes PIN courts, comme 123456",
"setGenCheckPasswordsOnHIBP": "Vérifier les mots de passe en utilisant un service en ligne {}",
"setGenHelpHIBP": "KeeWeb peut vérifier si vos mots de passes ont été précédemment exposés à une faille de sécurité en utilisant un service en ligne. Votre mot de passe ne peut pas être récupéré à partir de données envoyées en ligne, cependant le nombre de mots de passes vérifiés peut être exposé. Plus d'informations sur votre sécurité en utilisant ce service peuvent être trouvées {}. Si cette option est activée, KeeWeb vérifiera automatiquement vos mots de passes là.",
"setGenHelpHIBPLink": "ici",
"setGenAuditPasswordAge": "Vieux mots de passe",
"setGenAuditPasswordAgeOff": "Ne pas afficher d'avertissements à propos des vieux mots de passe",
"setGenAuditPasswordAgeOneYear": "Afficher des avertissements pour les mots de passe plus vieux qu'un an",
"setGenAuditPasswordAgeYears": "Afficher des avertissement pour les mots de passe plus vieux que {} ans",
"setFilePath": "Chemin", "setFilePath": "Chemin",
"setFileStorage": "Le fichier est ouvert de {}.", "setFileStorage": "Le fichier est ouvert de {}.",
"setFileIntl": "Le fichier est conservé dans le stockage interne de l'application", "setFileIntl": "Le fichier est conservé dans le stockage interne de l'application",
@ -615,7 +646,7 @@
"gdriveSharedWithMe": "Partagé avec moi", "gdriveSharedWithMe": "Partagé avec moi",
"webdavSaveMethod": "Méthode de sauvegarde", "webdavSaveMethod": "Méthode de sauvegarde",
"webdavSaveMove": "Envoyer un fichier temporaire et le déplacer", "webdavSaveMove": "Envoyer un fichier temporaire et le déplacer",
"webdavSavePut": "Ecraser le fichier kdbx avec PUT", "webdavSavePut": "Écraser le fichier kdbx avec PUT",
"webdavNoLastModified": "L'entête HTTP \"Last-Modified\" est absent", "webdavNoLastModified": "L'entête HTTP \"Last-Modified\" est absent",
"webdavStatReload": "Toujours recharger le fichier au lieu de se fier à l'entête HTTP \"Last-Modified\"", "webdavStatReload": "Toujours recharger le fichier au lieu de se fier à l'entête HTTP \"Last-Modified\"",
"launcherSave": "Sauvegarder base des mots de passe", "launcherSave": "Sauvegarder base des mots de passe",
@ -640,5 +671,6 @@
"yubiKeyTouchRequestedBody": "Merci de toucher votre YubiKey avec le numéro de série {}", "yubiKeyTouchRequestedBody": "Merci de toucher votre YubiKey avec le numéro de série {}",
"yubiKeyDisabledErrorHeader": "L'USB est désactivé", "yubiKeyDisabledErrorHeader": "L'USB est désactivé",
"yubiKeyDisabledErrorBody": "Yubikey est nécessaire pour ouvrir ce fichier, merci d'activer les appareils USB dans les paramètres", "yubiKeyDisabledErrorBody": "Yubikey est nécessaire pour ouvrir ce fichier, merci d'activer les appareils USB dans les paramètres",
"yubiKeyErrorWithCode": "Erreur Yubikey code {}." "yubiKeyErrorWithCode": "Erreur Yubikey code {}.",
"bioOpenAuthPrompt": "ouvrir \"{}\""
} }

View File

@ -4,8 +4,8 @@ import { SearchResultCollection } from 'collections/search-result-collection';
import { FileCollection } from 'collections/file-collection'; import { FileCollection } from 'collections/file-collection';
import { FileInfoCollection } from 'collections/file-info-collection'; import { FileInfoCollection } from 'collections/file-info-collection';
import { RuntimeInfo } from 'const/runtime-info'; import { RuntimeInfo } from 'const/runtime-info';
import { Launcher } from 'comp/launcher';
import { UsbListener } from 'comp/app/usb-listener'; import { UsbListener } from 'comp/app/usb-listener';
import { NativeModules } from 'comp/launcher/native-modules';
import { Timeouts } from 'const/timeouts'; import { Timeouts } from 'const/timeouts';
import { AppSettingsModel } from 'models/app-settings-model'; import { AppSettingsModel } from 'models/app-settings-model';
import { EntryModel } from 'models/entry-model'; import { EntryModel } from 'models/entry-model';
@ -37,6 +37,7 @@ class AppModel {
isBeta = RuntimeInfo.beta; isBeta = RuntimeInfo.beta;
advancedSearch = null; advancedSearch = null;
attachedYubiKeysCount = 0; attachedYubiKeysCount = 0;
memoryPasswordStorage = {};
constructor() { constructor() {
Events.on('refresh', this.refresh.bind(this)); Events.on('refresh', this.refresh.bind(this));
@ -525,7 +526,8 @@ class AppModel {
fileInfo && fileInfo &&
fileInfo.openDate && fileInfo.openDate &&
fileInfo.rev === params.rev && fileInfo.rev === params.rev &&
fileInfo.storage !== 'file' fileInfo.storage !== 'file' &&
!this.settings.disableOfflineStorage
) { ) {
logger.info('Open file from cache because it is latest'); logger.info('Open file from cache because it is latest');
this.openFileFromCache( this.openFileFromCache(
@ -546,7 +548,12 @@ class AppModel {
}, },
fileInfo fileInfo
); );
} else if (!fileInfo || !fileInfo.openDate || params.storage === 'file') { } else if (
!fileInfo ||
!fileInfo.openDate ||
params.storage === 'file' ||
this.settings.disableOfflineStorage
) {
this.openFileFromStorage(params, callback, fileInfo, logger); this.openFileFromStorage(params, callback, fileInfo, logger);
} else { } else {
logger.info('Open file from cache, will sync after load', params.storage); logger.info('Open file from cache, will sync after load', params.storage);
@ -594,7 +601,7 @@ class AppModel {
logger.info('Load from storage'); logger.info('Load from storage');
storage.load(params.path, params.opts, (err, data, stat) => { storage.load(params.path, params.opts, (err, data, stat) => {
if (err) { if (err) {
if (fileInfo && fileInfo.openDate) { if (fileInfo && fileInfo.openDate && !this.settings.disableOfflineStorage) {
logger.info('Open file from cache because of storage load error', err); logger.info('Open file from cache because of storage load error', err);
this.openFileFromCache(params, callback, fileInfo); this.openFileFromCache(params, callback, fileInfo);
} else { } else {
@ -618,7 +625,8 @@ class AppModel {
!noCache && !noCache &&
fileInfo && fileInfo &&
storage.name !== 'file' && storage.name !== 'file' &&
(err || (stat && stat.rev === cacheRev)) (err || (stat && stat.rev === cacheRev)) &&
!this.settings.disableOfflineStorage
) { ) {
logger.info( logger.info(
'Open file from cache because ' + (err ? 'stat error' : 'it is latest'), 'Open file from cache because ' + (err ? 'stat error' : 'it is latest'),
@ -663,10 +671,13 @@ class AppModel {
path: params.path, path: params.path,
keyFileName: params.keyFileName, keyFileName: params.keyFileName,
keyFilePath: params.keyFilePath, keyFilePath: params.keyFilePath,
backup: (fileInfo && fileInfo.backup) || null, backup: fileInfo?.backup || null,
fingerprint: (fileInfo && fileInfo.fingerprint) || null,
chalResp: params.chalResp chalResp: params.chalResp
}); });
if (params.encryptedPassword) {
file.encryptedPassword = fileInfo.encryptedPassword;
file.encryptedPasswordDate = fileInfo?.encryptedPasswordDate || new Date();
}
const openComplete = (err) => { const openComplete = (err) => {
if (err) { if (err) {
return callback(err); return callback(err);
@ -685,7 +696,7 @@ class AppModel {
if (fileInfo) { if (fileInfo) {
file.syncDate = fileInfo.syncDate; file.syncDate = fileInfo.syncDate;
} }
if (updateCacheOnSuccess) { if (updateCacheOnSuccess && !this.settings.disableOfflineStorage) {
logger.info('Save loaded file to cache'); logger.info('Save loaded file to cache');
Storage.cache.save(file.id, null, params.fileData); Storage.cache.save(file.id, null, params.fileData);
} }
@ -755,7 +766,6 @@ class AppModel {
syncDate: file.syncDate || dt, syncDate: file.syncDate || dt,
openDate: dt, openDate: dt,
backup: file.backup, backup: file.backup,
fingerprint: file.fingerprint,
chalResp: file.chalResp chalResp: file.chalResp
}); });
switch (this.settings.rememberKeyFiles) { switch (this.settings.rememberKeyFiles) {
@ -771,6 +781,14 @@ class AppModel {
keyFilePath: file.keyFilePath || null keyFilePath: file.keyFilePath || null
}); });
} }
if (this.settings.deviceOwnerAuth === 'file' && file.encryptedPassword) {
const maxDate = new Date(file.encryptedPasswordDate);
maxDate.setMinutes(maxDate.getMinutes() + this.settings.deviceOwnerAuthTimeoutMinutes);
if (maxDate > new Date()) {
fileInfo.encryptedPassword = file.encryptedPassword;
fileInfo.encryptedPasswordDate = file.encryptedPasswordDate;
}
}
this.fileInfos.remove(file.id); this.fileInfos.remove(file.id);
this.fileInfos.unshift(fileInfo); this.fileInfos.unshift(fileInfo);
this.fileInfos.save(); this.fileInfos.save();
@ -808,14 +826,14 @@ class AppModel {
if (data && backup && backup.enabled && backup.pending) { if (data && backup && backup.enabled && backup.pending) {
this.scheduleBackupFile(file, data); this.scheduleBackupFile(file, data);
} }
if (params) {
this.saveFileFingerprint(file, params.password);
}
if (this.settings.yubiKeyAutoOpen) { if (this.settings.yubiKeyAutoOpen) {
if (this.attachedYubiKeysCount > 0 && !this.files.some((f) => f.external)) { if (this.attachedYubiKeysCount > 0 && !this.files.some((f) => f.external)) {
this.tryOpenOtpDeviceInBackground(); this.tryOpenOtpDeviceInBackground();
} }
} }
if (this.settings.deviceOwnerAuth) {
this.saveEncryptedPassword(file, params);
}
} }
fileClosed(file) { fileClosed(file) {
@ -858,7 +876,7 @@ class AppModel {
path = Storage[storage].getPathForName(file.name); path = Storage[storage].getPathForName(file.name);
} }
const optionsForLogging = { ...options }; const optionsForLogging = { ...options };
if (optionsForLogging && optionsForLogging.opts && optionsForLogging.opts.password) { if (optionsForLogging.opts && optionsForLogging.opts.password) {
optionsForLogging.opts = { ...optionsForLogging.opts }; optionsForLogging.opts = { ...optionsForLogging.opts };
optionsForLogging.opts.password = '***'; optionsForLogging.opts.password = '***';
} }
@ -965,6 +983,10 @@ class AppModel {
logger.info('Updated sync date, saving modified file'); logger.info('Updated sync date, saving modified file');
saveToCacheAndStorage(); saveToCacheAndStorage();
} else if (file.dirty) { } else if (file.dirty) {
if (this.settings.disableOfflineStorage) {
logger.info('File is dirty and cache is disabled');
return complete(err);
}
logger.info('Saving not modified dirty file to cache'); logger.info('Saving not modified dirty file to cache');
Storage.cache.save(fileInfo.id, null, data, (err) => { Storage.cache.save(fileInfo.id, null, data, (err) => {
if (err) { if (err) {
@ -1027,6 +1049,9 @@ class AppModel {
} else if (!file.dirty) { } else if (!file.dirty) {
logger.info('Saving to storage, skip cache because not dirty'); logger.info('Saving to storage, skip cache because not dirty');
saveToStorage(data); saveToStorage(data);
} else if (this.settings.disableOfflineStorage) {
logger.info('Saving to storage because cache is disabled');
saveToStorage(data);
} else { } else {
logger.info('Saving to cache'); logger.info('Saving to cache');
Storage.cache.save(fileInfo.id, null, data, (err) => { Storage.cache.save(fileInfo.id, null, data, (err) => {
@ -1050,6 +1075,10 @@ class AppModel {
logger.info('File does not exist in storage, creating'); logger.info('File does not exist in storage, creating');
saveToCacheAndStorage(); saveToCacheAndStorage();
} else if (file.dirty) { } else if (file.dirty) {
if (this.settings.disableOfflineStorage) {
logger.info('Stat error, dirty, cache is disabled', err || 'no error');
return complete(err);
}
logger.info('Stat error, dirty, save to cache', err || 'no error'); logger.info('Stat error, dirty, save to cache', err || 'no error');
file.getData((data, e) => { file.getData((data, e) => {
if (e) { if (e) {
@ -1087,6 +1116,14 @@ class AppModel {
} }
} }
deleteAllCachedFiles() {
for (const fileInfo of this.fileInfos) {
if (fileInfo.storage && !fileInfo.modified) {
Storage.cache.remove(fileInfo.id);
}
}
}
clearStoredKeyFiles() { clearStoredKeyFiles() {
for (const fileInfo of this.fileInfos) { for (const fileInfo of this.fileInfos) {
fileInfo.set({ fileInfo.set({
@ -1224,18 +1261,6 @@ class AppModel {
} }
} }
saveFileFingerprint(file, password) {
if (Launcher && Launcher.fingerprints && !file.fingerprint) {
const fileInfo = this.fileInfos.get(file.id);
Launcher.fingerprints.register(file.id, password, (token) => {
if (token) {
fileInfo.fingerprint = token;
this.fileInfos.save();
}
});
}
}
usbDevicesChanged() { usbDevicesChanged() {
const attachedYubiKeysCount = this.attachedYubiKeysCount; const attachedYubiKeysCount = this.attachedYubiKeysCount;
@ -1286,6 +1311,97 @@ class AppModel {
} }
} }
} }
saveEncryptedPassword(file, params) {
if (!this.settings.deviceOwnerAuth || params.encryptedPassword) {
return;
}
NativeModules.hardwareEncrypt(params.password)
.then((encryptedPassword) => {
encryptedPassword = encryptedPassword.toBase64();
const fileInfo = this.fileInfos.get(file.id);
const encryptedPasswordDate = new Date();
file.encryptedPassword = encryptedPassword;
file.encryptedPasswordDate = encryptedPasswordDate;
if (this.settings.deviceOwnerAuth === 'file') {
fileInfo.encryptedPassword = encryptedPassword;
fileInfo.encryptedPasswordDate = encryptedPasswordDate;
this.fileInfos.save();
} else if (this.settings.deviceOwnerAuth === 'memory') {
this.memoryPasswordStorage[file.id] = {
value: encryptedPassword,
date: encryptedPasswordDate
};
}
})
.catch((e) => {
file.encryptedPassword = null;
file.encryptedPasswordDate = null;
delete this.memoryPasswordStorage[file.id];
this.appLogger.error('Error encrypting password', e);
});
}
getMemoryPassword(fileId) {
return this.memoryPasswordStorage[fileId];
}
checkEncryptedPasswordsStorage() {
if (this.settings.deviceOwnerAuth === 'file') {
let changed = false;
for (const fileInfo of this.fileInfos) {
if (this.memoryPasswordStorage[fileInfo.id]) {
fileInfo.encryptedPassword = this.memoryPasswordStorage[fileInfo.id].value;
fileInfo.encryptedPasswordDate = this.memoryPasswordStorage[fileInfo.id].date;
changed = true;
}
}
if (changed) {
this.fileInfos.save();
}
for (const file of this.files) {
if (this.memoryPasswordStorage[file.id]) {
file.encryptedPassword = this.memoryPasswordStorage[file.id].value;
file.encryptedPasswordDate = this.memoryPasswordStorage[file.id].date;
}
}
} else if (this.settings.deviceOwnerAuth === 'memory') {
let changed = false;
for (const fileInfo of this.fileInfos) {
if (fileInfo.encryptedPassword) {
this.memoryPasswordStorage[fileInfo.id] = {
value: fileInfo.encryptedPassword,
date: fileInfo.encryptedPasswordDate
};
fileInfo.encryptedPassword = null;
fileInfo.encryptedPasswordDate = null;
changed = true;
}
}
if (changed) {
this.fileInfos.save();
}
} else {
let changed = false;
for (const fileInfo of this.fileInfos) {
if (fileInfo.encryptedPassword) {
fileInfo.encryptedPassword = null;
fileInfo.encryptedPasswordDate = null;
changed = true;
}
}
if (changed) {
this.fileInfos.save();
}
for (const file of this.files) {
if (file.encryptedPassword) {
file.encryptedPassword = null;
file.encryptedPasswordDate = null;
}
}
this.memoryPasswordStorage = {};
}
}
} }
export { AppModel }; export { AppModel };

View File

@ -339,7 +339,7 @@ class EntryModel extends Model {
if (val && !val.isProtected) { if (val && !val.isProtected) {
// https://github.com/keeweb/keeweb/issues/910 // https://github.com/keeweb/keeweb/issues/910
// eslint-disable-next-line no-control-regex // eslint-disable-next-line no-control-regex
val = val.replace(/[\x00-\x09\x0B-\x0C\x0E-\x1F\uFFF0-\uFFFF]/g, ''); val = val.replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\uFFF0-\uFFFF]/g, '');
} }
return val; return val;
} }
@ -627,6 +627,18 @@ class EntryModel extends Model {
return KdbxToHtml.entryToHtml(this.file.db, this.entry); return KdbxToHtml.entryToHtml(this.file.db, this.entry);
} }
canCheckPasswordIssues() {
return !this.entry.customData?.IgnorePwIssues;
}
setIgnorePasswordIssues() {
if (!this.entry.customData) {
this.entry.customData = {};
}
this.entry.customData.IgnorePwIssues = '1';
this._entryModified();
}
static fromEntry(entry, group, file) { static fromEntry(entry, group, file) {
const model = new EntryModel(); const model = new EntryModel();
model.setEntry(entry, group, file); model.setEntry(entry, group, file);

View File

@ -16,8 +16,10 @@ const DefaultProperties = {
keyFilePath: null, keyFilePath: null,
opts: null, opts: null,
backup: null, backup: null,
fingerprint: null, fingerprint: null, // obsolete
chalResp: null chalResp: null,
encryptedPassword: null,
encryptedPasswordDate: null
}; };
class FileInfoModel extends Model { class FileInfoModel extends Model {

View File

@ -487,9 +487,11 @@ class FileModel extends Model {
syncError: error syncError: error
}); });
const shouldResetFingerprint = this.passwordChanged && this.fingerprint; if (!error && this.passwordChanged && this.encryptedPassword) {
if (shouldResetFingerprint && !error) { this.set({
this.fingerprint = null; encryptedPassword: null,
encryptedPasswordDate: null
});
} }
if (!this.open) { if (!this.open) {
@ -759,10 +761,12 @@ FileModel.defineModelProperties({
keyEncryptionRounds: null, keyEncryptionRounds: null,
kdfName: null, kdfName: null,
kdfParameters: null, kdfParameters: null,
fingerprint: null, fingerprint: null, // obsolete
oldPasswordHash: null, oldPasswordHash: null,
oldKeyFileHash: null, oldKeyFileHash: null,
oldKeyChangeDate: null oldKeyChangeDate: null,
encryptedPassword: null,
encryptedPasswordDate: null
}); });
export { FileModel }; export { FileModel };

View File

@ -77,36 +77,37 @@ class MenuModel extends Model {
locTitle: 'setGenAppearance', locTitle: 'setGenAppearance',
icon: '0', icon: '0',
page: 'general', page: 'general',
section: 'appearance', section: 'appearance'
active: true
}, },
{ {
locTitle: 'setGenFunction', locTitle: 'setGenFunction',
icon: '0', icon: '0',
page: 'general', page: 'general',
section: 'function', section: 'function'
active: true },
{
locTitle: 'setGenAudit',
icon: '0',
page: 'general',
section: 'audit'
}, },
{ {
locTitle: 'setGenLock', locTitle: 'setGenLock',
icon: '0', icon: '0',
page: 'general', page: 'general',
section: 'lock', section: 'lock'
active: true
}, },
{ {
locTitle: 'setGenStorage', locTitle: 'setGenStorage',
icon: '0', icon: '0',
page: 'general', page: 'general',
section: 'storage', section: 'storage'
active: true
}, },
{ {
locTitle: 'advanced', locTitle: 'advanced',
icon: '0', icon: '0',
page: 'general', page: 'general',
section: 'advanced', section: 'advanced'
active: true
} }
]); ]);
this.shortcutsSection = new MenuSectionModel([ this.shortcutsSection = new MenuSectionModel([

View File

@ -27,7 +27,7 @@ class MenuSectionModel extends Model {
removeByFile(file) { removeByFile(file) {
const items = this.items; const items = this.items;
items.find((item) => { items.find((item) => {
if (item.file === file || item.file === file) { if (item.file === file) {
items.remove(item); items.remove(item);
return true; return true;
} }
@ -39,7 +39,7 @@ class MenuSectionModel extends Model {
replaceByFile(file, newItem) { replaceByFile(file, newItem) {
const items = this.items; const items = this.items;
items.find((item, ix) => { items.find((item, ix) => {
if (item.file === file || item.file === file) { if (item.file === file) {
items[ix] = newItem; items[ix] = newItem;
return true; return true;
} }

View File

@ -209,6 +209,10 @@ class StorageWebDav extends StorageBase {
.replace(/[^/]*$/, movePath); .replace(/[^/]*$/, movePath);
} }
} }
// prevent double encoding, see #1729
const encodedMovePath = /%[A-Z0-9]{2}/.test(movePath)
? movePath
: encodeURI(movePath);
this._request( this._request(
{ {
...saveOpts, ...saveOpts,
@ -217,7 +221,7 @@ class StorageWebDav extends StorageBase {
path: tmpPath, path: tmpPath,
nostat: true, nostat: true,
headers: { headers: {
Destination: encodeURI(movePath), Destination: encodedMovePath,
'Overwrite': 'T' 'Overwrite': 'T'
} }
}, },

View File

@ -1,7 +1,8 @@
import EventEmitter from 'events'; import EventEmitter from 'events';
import { Logger } from 'util/logger'; import { Logger } from 'util/logger';
import { Launcher } from 'comp/launcher'; import { Launcher } from 'comp/launcher';
import { Locale } from 'util/locale'; import { KeeWebLogo } from 'const/inline-images';
import oauthPageTemplate from 'templates/oauth/complete.hbs';
const DefaultPort = 48149; const DefaultPort = 48149;
const logger = new Logger('storage-oauth-listener'); const logger = new Logger('storage-oauth-listener');
@ -23,9 +24,9 @@ const StorageOAuthListener = {
let resultHandled = false; let resultHandled = false;
const server = http.createServer((req, resp) => { const server = http.createServer((req, resp) => {
resp.writeHead(200, 'OK', { resp.writeHead(200, 'OK', {
'Content-Type': 'text/plain; charset=UTF-8' 'Content-Type': 'text/html; charset=UTF-8'
}); });
resp.end(Locale.appBrowserAuthComplete); resp.end(oauthPageTemplate({ logoSrc: KeeWebLogo }));
if (!resultHandled) { if (!resultHandled) {
this.stop(); this.stop();
this.handleResult(req.url, listener); this.handleResult(req.url, listener);

View File

@ -0,0 +1,85 @@
/**
* Password strength level estimation according to OWASP password recommendations and entropy
* https://auth0.com/docs/connections/database/password-strength
*/
const PasswordStrengthLevel = {
None: 0,
Low: 1,
Good: 2
};
const charClasses = new Uint8Array(128);
for (let i = 48 /* '0' */; i <= 57 /* '9' */; i++) {
charClasses[i] = 1;
}
for (let i = 97 /* 'a' */; i <= 122 /* 'z' */; i++) {
charClasses[i] = 2;
}
for (let i = 65 /* 'A' */; i <= 90 /* 'Z' */; i++) {
charClasses[i] = 3;
}
const symbolsPerCharClass = new Uint8Array([
95 /* ASCII symbols */,
10 /* digits */,
26 /* lowercase letters */,
26 /* uppercase letters */
]);
function passwordStrength(password) {
if (!password || !password.isProtected) {
throw new TypeError('Bad password type');
}
if (!password.byteLength) {
return { level: PasswordStrengthLevel.None, length: 0 };
}
let length = 0;
const countByClass = [0, 0, 0, 0];
let isSingleChar = true;
let prevCharCode = -1;
password.forEachChar((charCode) => {
const charClass = charCode < charClasses.length ? charClasses[charCode] : 0;
countByClass[charClass]++;
length++;
if (isSingleChar) {
if (charCode !== prevCharCode) {
if (prevCharCode === -1) {
prevCharCode = charCode;
} else {
isSingleChar = false;
}
}
}
});
const onlyDigits = countByClass[1] === length;
if (length < 6) {
return { level: PasswordStrengthLevel.None, length, onlyDigits };
}
if (isSingleChar) {
return { level: PasswordStrengthLevel.None, length, onlyDigits };
}
if (length < 8) {
return { level: PasswordStrengthLevel.Low, length, onlyDigits };
}
let alphabetSize = 0;
for (let i = 0; i < countByClass.length; i++) {
if (countByClass[i] > 0) {
alphabetSize += symbolsPerCharClass[i];
}
}
const entropy = Math.log2(Math.pow(alphabetSize, length));
const level = entropy < 60 ? PasswordStrengthLevel.Low : PasswordStrengthLevel.Good;
return { length, level, onlyDigits };
}
export { PasswordStrengthLevel, passwordStrength };

View File

@ -18,7 +18,13 @@ const Features = {
isLocal: location.origin.indexOf('localhost') >= 0, isLocal: location.origin.indexOf('localhost') >= 0,
supportsTitleBarStyles() { supportsTitleBarStyles() {
return this.isMac; return isDesktop && (this.isMac || this.isWindows);
},
supportsCustomTitleBarAndDraggableWindow() {
return isDesktop && this.isMac;
},
renderCustomTitleBar() {
return isDesktop && this.isWindows;
}, },
hasUnicodeFlags() { hasUnicodeFlags() {
return this.isMac; return this.isMac;

View File

@ -75,3 +75,7 @@ export function isEqual(a, b) {
} }
return false; return false;
} }
export function minmax(val, min, max) {
return Math.min(max, Math.max(min, val));
}

View File

@ -29,6 +29,10 @@ const StringFormat = {
pascalCase(str) { pascalCase(str) {
return this.capFirst(str.replace(this.camelCaseRegex, (match) => match[1].toUpperCase())); return this.capFirst(str.replace(this.camelCaseRegex, (match) => match[1].toUpperCase()));
},
replaceVersion(str, replacement) {
return str.replace(/\d+\.\d+\.\d+/g, replacement);
} }
}; };

View File

@ -34,8 +34,8 @@ const KdbxwebInit = {
hash(args) { hash(args) {
const ts = logger.ts(); const ts = logger.ts();
const password = makeXoredValue(args.password); const password = kdbxweb.ProtectedValue.fromBinary(args.password).dataAndSalt();
const salt = makeXoredValue(args.salt); const salt = kdbxweb.ProtectedValue.fromBinary(args.salt).dataAndSalt();
return NativeModules.argon2(password, salt, { return NativeModules.argon2(password, salt, {
type: args.type, type: args.type,
@ -52,7 +52,8 @@ const KdbxwebInit = {
logger.debug('Argon2 hash calculated', logger.ts(ts)); logger.debug('Argon2 hash calculated', logger.ts(ts));
return readXoredValue(res); res = new kdbxweb.ProtectedValue(res.data, res.salt);
return res.getBinary();
}) })
.catch((err) => { .catch((err) => {
password.data.fill(0); password.data.fill(0);
@ -61,36 +62,6 @@ const KdbxwebInit = {
logger.error('Argon2 error', err); logger.error('Argon2 error', err);
throw 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); return Promise.resolve(this.runtimeModule);

View File

@ -139,8 +139,6 @@ kdbxweb.ProtectedValue.prototype.indexOfSelfInLower = function (targetLower) {
return firstCharIndex; return firstCharIndex;
}; };
window.PV = kdbxweb.ProtectedValue;
kdbxweb.ProtectedValue.prototype.equals = function (other) { kdbxweb.ProtectedValue.prototype.equals = function (other) {
if (!other) { if (!other) {
return false; return false;
@ -176,3 +174,38 @@ kdbxweb.ProtectedValue.prototype.isFieldReference = function () {
}); });
return true; return true;
}; };
const RandomSalt = kdbxweb.Random.getBytes(128);
kdbxweb.ProtectedValue.prototype.saltedValue = function () {
if (!this.byteLength) {
return 0;
}
const value = this._value;
const salt = this._salt;
let salted = '';
for (let i = 0, len = value.length; i < len; i++) {
const byte = value[i] ^ salt[i];
salted += String.fromCharCode(byte ^ RandomSalt[i % RandomSalt.length]);
}
return salted;
};
kdbxweb.ProtectedValue.prototype.dataAndSalt = function () {
return {
data: [...this._value],
salt: [...this._salt]
};
};
kdbxweb.ProtectedValue.prototype.toBase64 = function () {
const binary = this.getBinary();
const base64 = kdbxweb.ByteUtils.bytesToBase64(binary);
kdbxweb.ByteUtils.zeroBuffer(binary);
return base64;
};
kdbxweb.ProtectedValue.fromBase64 = function (base64) {
const bytes = kdbxweb.ByteUtils.base64ToBytes(base64);
return kdbxweb.ProtectedValue.fromBinary(bytes);
};

View File

@ -25,6 +25,7 @@ import { OpenView } from 'views/open-view';
import { SettingsView } from 'views/settings/settings-view'; import { SettingsView } from 'views/settings/settings-view';
import { TagView } from 'views/tag-view'; import { TagView } from 'views/tag-view';
import { ImportCsvView } from 'views/import-csv-view'; import { ImportCsvView } from 'views/import-csv-view';
import { TitlebarView } from 'views/titlebar-view';
import template from 'templates/app.hbs'; import template from 'templates/app.hbs';
class AppView extends View { class AppView extends View {
@ -45,6 +46,9 @@ class AppView extends View {
constructor(model) { constructor(model) {
super(model); super(model);
this.titlebarStyle = this.model.settings.titlebarStyle;
this.views.menu = new MenuView(this.model.menu, { ownParent: true }); this.views.menu = new MenuView(this.model.menu, { ownParent: true });
this.views.menuDrag = new DragView('x', { parent: '.app__menu-drag' }); this.views.menuDrag = new DragView('x', { parent: '.app__menu-drag' });
this.views.footer = new FooterView(this.model, { ownParent: true }); this.views.footer = new FooterView(this.model, { ownParent: true });
@ -54,12 +58,13 @@ class AppView extends View {
this.views.list.dragView = this.views.listDrag; this.views.list.dragView = this.views.listDrag;
this.views.details = new DetailsView(undefined, { ownParent: true }); this.views.details = new DetailsView(undefined, { ownParent: true });
this.views.details.appModel = this.model; this.views.details.appModel = this.model;
if (this.titlebarStyle !== 'default' && Features.renderCustomTitleBar()) {
this.views.titlebar = new TitlebarView(this.model);
}
this.views.menu.listenDrag(this.views.menuDrag); this.views.menu.listenDrag(this.views.menuDrag);
this.views.list.listenDrag(this.views.listDrag); this.views.list.listenDrag(this.views.listDrag);
this.titlebarStyle = this.model.settings.titlebarStyle;
this.listenTo(this.model.settings, 'change:theme', this.setTheme); this.listenTo(this.model.settings, 'change:theme', this.setTheme);
this.listenTo(this.model.settings, 'change:locale', this.setLocale); this.listenTo(this.model.settings, 'change:locale', this.setLocale);
this.listenTo(this.model.settings, 'change:fontSize', this.setFontSize); this.listenTo(this.model.settings, 'change:fontSize', this.setFontSize);
@ -120,6 +125,9 @@ class AppView extends View {
} }
if (this.titlebarStyle !== 'default') { if (this.titlebarStyle !== 'default') {
document.body.classList.add('titlebar-' + this.titlebarStyle); document.body.classList.add('titlebar-' + this.titlebarStyle);
if (Features.renderCustomTitleBar()) {
document.body.classList.add('titlebar-custom');
}
} }
if (Features.isMobile) { if (Features.isMobile) {
document.body.classList.add('mobile'); document.body.classList.add('mobile');
@ -129,7 +137,8 @@ class AppView extends View {
render() { render() {
super.render({ super.render({
beta: this.model.isBeta, beta: this.model.isBeta,
titlebarStyle: this.titlebarStyle titlebarStyle: this.titlebarStyle,
customTitlebar: Features.renderCustomTitleBar()
}); });
this.panelEl = this.$el.find('.app__panel:first'); this.panelEl = this.$el.find('.app__panel:first');
this.views.listWrap.render(); this.views.listWrap.render();
@ -139,13 +148,14 @@ class AppView extends View {
this.views.list.render(); this.views.list.render();
this.views.listDrag.render(); this.views.listDrag.render();
this.views.details.render(); this.views.details.render();
this.views.titlebar?.render();
this.showLastOpenFile(); this.showLastOpenFile();
} }
showOpenFile() { showOpenFile() {
this.hideContextMenu(); this.hideContextMenu();
this.views.menu.hide(); this.views.menu.hide();
this.views.menuDrag.hide(); this.views.menuDrag.$el.parent().hide();
this.views.listWrap.hide(); this.views.listWrap.hide();
this.views.list.hide(); this.views.list.hide();
this.views.listDrag.hide(); this.views.listDrag.hide();
@ -190,7 +200,7 @@ class AppView extends View {
showEntries() { showEntries() {
this.views.menu.show(); this.views.menu.show();
this.views.menuDrag.show(); this.views.menuDrag.$el.parent().show();
this.views.listWrap.show(); this.views.listWrap.show();
this.views.list.show(); this.views.list.show();
this.views.listDrag.show(); this.views.listDrag.show();
@ -254,7 +264,7 @@ class AppView extends View {
showSettings(selectedMenuItem) { showSettings(selectedMenuItem) {
this.model.menu.setMenu('settings'); this.model.menu.setMenu('settings');
this.views.menu.show(); this.views.menu.show();
this.views.menuDrag.show(); this.views.menuDrag.$el.parent().show();
this.views.listWrap.hide(); this.views.listWrap.hide();
this.views.list.hide(); this.views.list.hide();
this.views.listDrag.hide(); this.views.listDrag.hide();
@ -578,9 +588,13 @@ class AppView extends View {
complete: (res) => { complete: (res) => {
if (res === 'ignore') { if (res === 'ignore') {
this.model.closeAllFiles(); this.model.closeAllFiles();
complete(true); if (complete) {
complete(true);
}
} else { } else {
complete(false); if (complete) {
complete(false);
}
} }
} }
}); });
@ -672,10 +686,14 @@ class AppView extends View {
} }
} }
toggleSettings(page) { toggleSettings(page, section) {
let menuItem = page ? this.model.menu[page + 'Section'] : null; let menuItem = page ? this.model.menu[page + 'Section'] : null;
if (menuItem) { if (menuItem) {
menuItem = menuItem.items[0]; if (section) {
menuItem = menuItem.items.find((it) => it.section === section) || menuItem.items[0];
} else {
menuItem = menuItem.items[0];
}
} }
if (this.views.settings) { if (this.views.settings) {
if (this.views.settings.page === page || !menuItem) { if (this.views.settings.page === page || !menuItem) {
@ -686,9 +704,7 @@ class AppView extends View {
this.views.open.toggleMore(); this.views.open.toggleMore();
} }
} else { } else {
if (menuItem) { this.model.menu.select({ item: menuItem });
this.model.menu.select({ item: menuItem });
}
} }
} else { } else {
this.showSettings(); this.showSettings();

View File

@ -232,7 +232,7 @@ class AutoTypeSelectView extends View {
this.highlightActive(); this.highlightActive();
} }
const view = new DropdownView(); const view = new DropdownView({ selectedOption: 0 });
this.listenTo(view, 'cancel', this.hideItemOptionsDropdown); this.listenTo(view, 'cancel', this.hideItemOptionsDropdown);
this.listenTo(view, 'select', this.itemOptionsDropdownSelect); this.listenTo(view, 'select', this.itemOptionsDropdownSelect);

View File

@ -0,0 +1,160 @@
import { View } from 'framework/views/view';
import { Events } from 'framework/events';
import template from 'templates/details/details-issues.hbs';
import { Alerts } from 'comp/ui/alerts';
import { Timeouts } from 'const/timeouts';
import { Locale } from 'util/locale';
import { passwordStrength, PasswordStrengthLevel } from 'util/data/password-strength';
import { AppSettingsModel } from 'models/app-settings-model';
import { Links } from 'const/links';
import { checkIfPasswordIsExposedOnline } from 'comp/app/online-password-checker';
class DetailsIssuesView extends View {
parent = '.details__issues-container';
template = template;
events = {
'click .details__issues-close-btn': 'closeIssuesClick'
};
passwordIssue = null;
constructor(model) {
super(model);
this.listenTo(AppSettingsModel, 'change', this.settingsChanged);
if (AppSettingsModel.auditPasswords) {
this.checkPasswordIssues();
}
}
render(options) {
if (!AppSettingsModel.auditPasswords) {
super.render();
return;
}
super.render({
hibpLink: Links.HaveIBeenPwned,
passwordIssue: this.passwordIssue,
fadeIn: options?.fadeIn
});
}
settingsChanged() {
if (AppSettingsModel.auditPasswords) {
this.checkPasswordIssues();
}
this.render();
}
passwordChanged() {
const oldPasswordIssue = this.passwordIssue;
this.checkPasswordIssues();
if (oldPasswordIssue !== this.passwordIssue) {
const fadeIn = !oldPasswordIssue;
if (this.passwordIssue) {
this.render({ fadeIn });
} else {
this.el.classList.add('fade-out');
setTimeout(() => this.render(), Timeouts.FastAnimation);
}
}
}
checkPasswordIssues() {
if (!this.model.canCheckPasswordIssues()) {
this.passwordIssue = null;
return;
}
const { password } = this.model;
if (!password || !password.isProtected || !password.byteLength) {
this.passwordIssue = null;
return;
}
const auditEntropy = AppSettingsModel.auditPasswordEntropy;
const strength = passwordStrength(password);
if (AppSettingsModel.excludePinsFromAudit && strength.onlyDigits && strength.length <= 6) {
this.passwordIssue = null;
} else if (auditEntropy && strength.level < PasswordStrengthLevel.Low) {
this.passwordIssue = 'poor';
} else if (auditEntropy && strength.level < PasswordStrengthLevel.Good) {
this.passwordIssue = 'weak';
} else if (AppSettingsModel.auditPasswordAge && this.isOld()) {
this.passwordIssue = 'old';
} else {
this.passwordIssue = null;
this.checkOnHIBP();
}
}
isOld() {
if (!this.model.updated) {
return false;
}
const dt = new Date(this.model.updated);
dt.setFullYear(dt.getFullYear() + AppSettingsModel.auditPasswordAge);
return dt < Date.now();
}
checkOnHIBP() {
if (!AppSettingsModel.checkPasswordsOnHIBP) {
return;
}
const isExposed = checkIfPasswordIsExposedOnline(this.model.password);
if (typeof isExposed === 'boolean') {
this.passwordIssue = isExposed ? 'pwned' : null;
} else {
const iconEl = this.el?.querySelector('.details__issues-icon');
iconEl?.classList.add('details__issues-icon--loading');
isExposed.then((isExposed) => {
if (isExposed) {
this.passwordIssue = 'pwned';
} else if (isExposed === false) {
if (this.passwordIssue === 'pwned') {
this.passwordIssue = null;
}
} else {
this.passwordIssue = iconEl ? 'error' : null;
}
this.render();
});
}
}
closeIssuesClick() {
Alerts.alert({
header: Locale.detIssueCloseAlertHeader,
body: Locale.detIssueCloseAlertBody,
icon: 'exclamation-triangle',
buttons: [
{ result: 'entry', title: Locale.detIssueCloseAlertEntry, silent: true },
{ result: 'settings', title: Locale.detIssueCloseAlertSettings, silent: true },
Alerts.buttons.cancel
],
esc: '',
click: '',
success: (result) => {
switch (result) {
case 'entry':
this.disableAuditForEntry();
break;
case 'settings':
this.openAuditSettings();
break;
}
}
});
}
disableAuditForEntry() {
this.model.setIgnorePasswordIssues();
this.checkPasswordIssues();
this.render();
}
openAuditSettings() {
Events.emit('toggle-settings', 'general', 'audit');
}
}
export { DetailsIssuesView };

View File

@ -20,6 +20,7 @@ import { DetailsAddFieldView } from 'views/details/details-add-field-view';
import { DetailsAttachmentView } from 'views/details/details-attachment-view'; import { DetailsAttachmentView } from 'views/details/details-attachment-view';
import { DetailsAutoTypeView } from 'views/details/details-auto-type-view'; import { DetailsAutoTypeView } from 'views/details/details-auto-type-view';
import { DetailsHistoryView } from 'views/details/details-history-view'; import { DetailsHistoryView } from 'views/details/details-history-view';
import { DetailsIssuesView } from 'views/details/details-issues-view';
import { DropdownView } from 'views/dropdown-view'; import { DropdownView } from 'views/dropdown-view';
import { createDetailsFields } from 'views/details/details-fields'; import { createDetailsFields } from 'views/details/details-fields';
import { FieldViewCustom } from 'views/fields/field-view-custom'; import { FieldViewCustom } from 'views/fields/field-view-custom';
@ -28,6 +29,7 @@ import { isEqual } from 'util/fn';
import template from 'templates/details/details.hbs'; import template from 'templates/details/details.hbs';
import emptyTemplate from 'templates/details/details-empty.hbs'; import emptyTemplate from 'templates/details/details-empty.hbs';
import groupTemplate from 'templates/details/details-group.hbs'; import groupTemplate from 'templates/details/details-group.hbs';
import { Launcher } from 'comp/launcher';
class DetailsView extends View { class DetailsView extends View {
parent = '.app__details'; parent = '.app__details';
@ -117,11 +119,15 @@ class DetailsView extends View {
super.render(); super.render();
return; return;
} }
const model = { deleted: this.appModel.filter.trash, ...this.model }; const model = {
deleted: this.appModel.filter.trash,
...this.model
};
this.template = template; this.template = template;
super.render(model); super.render(model);
this.setSelectedColor(this.model.color); this.setSelectedColor(this.model.color);
this.addFieldViews(); this.addFieldViews();
this.checkPasswordIssues();
this.createScroll({ this.createScroll({
root: this.$el.find('.details__body')[0], root: this.$el.find('.details__body')[0],
scroller: this.$el.find('.scroller')[0], scroller: this.$el.find('.scroller')[0],
@ -152,7 +158,7 @@ class DetailsView extends View {
fieldView.parent = views === fieldViews ? fieldsMainEl[0] : fieldsAsideEl[0]; fieldView.parent = views === fieldViews ? fieldsMainEl[0] : fieldsAsideEl[0];
fieldView.render(); fieldView.render();
fieldView.on('change', this.fieldChanged.bind(this)); fieldView.on('change', this.fieldChanged.bind(this));
fieldView.on('copy', this.fieldCopied.bind(this)); fieldView.on('copy', (e) => this.copyFieldValue(e));
fieldView.on('autotype', (e) => this.autoType(e.source.model.sequence)); fieldView.on('autotype', (e) => this.autoType(e.source.model.sequence));
if (hideEmptyFields) { if (hideEmptyFields) {
const value = fieldView.model.value(); const value = fieldView.model.value();
@ -479,13 +485,17 @@ class DetailsView extends View {
CopyPaste.createHiddenInput(fieldText); CopyPaste.createHiddenInput(fieldText);
} }
const copyRes = CopyPaste.copy(fieldText); const copyRes = CopyPaste.copy(fieldText);
this.fieldCopied({ source: editView, copyRes }); this.copyFieldValue({ source: editView, copyRes });
return true; return true;
} }
return false; return false;
} }
copyPasswordFromShortcut(e) { copyPasswordFromShortcut(e) {
if (!this.model) {
return;
}
if (this.model.external) { if (this.model.external) {
this.copyOtp(); this.copyOtp();
e.preventDefault(); e.preventDefault();
@ -576,6 +586,9 @@ class DetailsView extends View {
} else if (fieldName) { } else if (fieldName) {
this.model.setField(fieldName, e.val); this.model.setField(fieldName, e.val);
} }
if (fieldName === 'Password' && this.views.issues) {
this.views.issues.passwordChanged();
}
} else if (e.field === 'Tags') { } else if (e.field === 'Tags') {
this.model.setTags(e.val); this.model.setTags(e.val);
this.appModel.updateTags(); this.appModel.updateTags();
@ -988,6 +1001,20 @@ class DetailsView extends View {
Events.emit('auto-type', { entry, sequence }); Events.emit('auto-type', { entry, sequence });
} }
} }
checkPasswordIssues() {
if (!this.model.readOnly) {
this.views.issues = new DetailsIssuesView(this.model);
this.views.issues.render();
}
}
copyFieldValue(e) {
this.fieldCopied(e);
if (AppSettingsModel.minimizeOnFieldCopy) {
Launcher.minimizeApp();
}
}
} }
Object.assign(DetailsView.prototype, Scrollable); Object.assign(DetailsView.prototype, Scrollable);

View File

@ -31,6 +31,8 @@ class DropdownView extends View {
this.once('remove', () => { this.once('remove', () => {
$('body').off('click contextmenu keydown', this.bodyClick); $('body').off('click contextmenu keydown', this.bodyClick);
}); });
this.selectedOption = model?.selectedOption;
} }
render(config) { render(config) {
@ -47,6 +49,9 @@ class DropdownView extends View {
top = Math.max(0, bodyRect.bottom - ownRect.height); top = Math.max(0, bodyRect.bottom - ownRect.height);
} }
this.$el.css({ top, left }); this.$el.css({ top, left });
if (typeof this.selectedOption === 'number') {
this.renderSelectedOption();
}
} }
bodyClick(e) { bodyClick(e) {

View File

@ -64,7 +64,7 @@ class FieldViewDate extends FieldViewText {
this.picker = null; this.picker = null;
} }
newVal = new Date(newVal); newVal = new Date(newVal);
if (!newVal || isNaN(newVal.getTime())) { if (isNaN(newVal.getTime())) {
newVal = null; newVal = null;
} }
super.endEdit(newVal, extra); super.endEdit(newVal, extra);

View File

@ -104,7 +104,7 @@ class FieldViewOtp extends FieldViewText {
this.resetOtp(); this.resetOtp();
return; return;
} }
this.otpValue = pass || ''; this.otpValue = pass;
this.otpTimeLeft = timeLeft || 0; this.otpTimeLeft = timeLeft || 0;
this.otpValidUntil = Date.now() + timeLeft; this.otpValidUntil = Date.now() + timeLeft;
if (!this.editing) { if (!this.editing) {

View File

@ -21,11 +21,14 @@ class IconSelectView extends View {
}; };
render() { render() {
const customIcons = this.model.file.getCustomIcons();
const hasCustomIcons = Object.keys(customIcons).length > 0;
super.render({ super.render({
sel: this.model.iconId, sel: this.model.iconId,
icons: IconMap, icons: IconMap,
canDownloadFavicon: !!this.model.url, canDownloadFavicon: !!this.model.url,
customIcons: this.model.file.getCustomIcons() customIcons,
hasCustomIcons
}); });
} }
@ -70,6 +73,9 @@ class IconSelectView extends View {
.addClass('icon-select__icon--custom-selected') .addClass('icon-select__icon--custom-selected')
.append(img); .append(img);
this.downloadingFavicon = false; this.downloadingFavicon = false;
const id = this.model.file.addCustomIcon(this.special.download.data);
this.emit('select', { id, custom: true });
}; };
img.onerror = (e) => { img.onerror = (e) => {
logger.error('Favicon download error: ' + url, e); logger.error('Favicon download error: ' + url, e);

View File

@ -24,6 +24,7 @@ class ListSearchView extends View {
'click .list__search-btn-sort': 'sortOptionsClick', 'click .list__search-btn-sort': 'sortOptionsClick',
'click .list__search-icon-search': 'advancedSearchClick', 'click .list__search-icon-search': 'advancedSearchClick',
'click .list__search-btn-menu': 'toggleMenu', 'click .list__search-btn-menu': 'toggleMenu',
'click .list__search-icon-clear': 'clickClear',
'change .list__search-adv input[type=checkbox]': 'toggleAdvCheck' 'change .list__search-adv input[type=checkbox]': 'toggleAdvCheck'
}; };
@ -212,7 +213,9 @@ class ListSearchView extends View {
} }
inputChange() { inputChange() {
Events.emit('add-filter', { text: this.inputEl.val() }); const text = this.inputEl.val();
this.inputEl[0].parentElement.classList.toggle('list__search-field-wrap--text', text);
Events.emit('add-filter', { text });
} }
inputFocus(e) { inputFocus(e) {
@ -428,6 +431,11 @@ class ListSearchView extends View {
fileListUpdated() { fileListUpdated() {
this.render(); this.render();
} }
clickClear() {
this.inputEl.val('');
this.inputChange();
}
} }
export { ListSearchView }; export { ListSearchView };

View File

@ -1,4 +1,5 @@
import { View } from 'framework/views/view'; import { View } from 'framework/views/view';
import { Launcher } from 'comp/launcher';
import { Keys } from 'const/keys'; import { Keys } from 'const/keys';
import template from 'templates/modal.hbs'; import template from 'templates/modal.hbs';
@ -10,6 +11,7 @@ class ModalView extends View {
events = { events = {
'click .modal__buttons button': 'buttonClick', 'click .modal__buttons button': 'buttonClick',
'click .modal__link': 'linkClick',
'click': 'bodyClick' 'click': 'bodyClick'
}; };
@ -55,6 +57,13 @@ class ModalView extends View {
this.closeWithResult(result); this.closeWithResult(result);
} }
linkClick(e) {
if (Launcher) {
e.preventDefault();
Launcher.openLink(e.target.href);
}
}
bodyClick(e) { bodyClick(e) {
if (typeof this.model.click === 'string' && !e.target.matches('button')) { if (typeof this.model.click === 'string' && !e.target.matches('button')) {
this.closeWithResult(this.model.click); this.closeWithResult(this.model.click);

View File

@ -22,6 +22,7 @@ import { StorageFileListView } from 'views/storage-file-list-view';
import { OpenChalRespView } from 'views/open-chal-resp-view'; import { OpenChalRespView } from 'views/open-chal-resp-view';
import { omit } from 'util/fn'; import { omit } from 'util/fn';
import { GeneratorView } from 'views/generator-view'; import { GeneratorView } from 'views/generator-view';
import { NativeModules } from 'comp/launcher/native-modules';
import template from 'templates/open.hbs'; import template from 'templates/open.hbs';
const logger = new Logger('open-view'); const logger = new Logger('open-view');
@ -57,12 +58,10 @@ class OpenView extends View {
}; };
params = null; params = null;
passwordInput = null; passwordInput = null;
busy = false; busy = false;
currentSelectedIndex = -1; currentSelectedIndex = -1;
encryptedPassword = null;
constructor(model) { constructor(model) {
super(model); super(model);
@ -152,6 +151,7 @@ class OpenView extends View {
windowFocused() { windowFocused() {
this.inputEl.focus(); this.inputEl.focus();
this.checkIfEncryptedPasswordDateIsValid();
} }
focusInput(focusOnMobile) { focusInput(focusOnMobile) {
@ -237,8 +237,10 @@ class OpenView extends View {
if (!this.params.keyFileData) { if (!this.params.keyFileData) {
this.params.keyFileName = null; this.params.keyFileName = null;
} }
this.encryptedPassword = null;
this.displayOpenFile(); this.displayOpenFile();
this.displayOpenKeyFile(); this.displayOpenKeyFile();
this.displayOpenDeviceOwnerAuth();
success = true; success = true;
break; break;
case 'xml': case 'xml':
@ -248,7 +250,9 @@ class OpenView extends View {
this.params.path = null; this.params.path = null;
this.params.storage = null; this.params.storage = null;
this.params.rev = null; this.params.rev = null;
this.encryptedPassword = null;
this.importDbWithXml(); this.importDbWithXml();
this.displayOpenDeviceOwnerAuth();
success = true; success = true;
break; break;
case 'kdb': case 'kdb':
@ -341,6 +345,15 @@ class OpenView extends View {
.toggleClass('open__settings-yubikey--active', !!this.params.chalResp); .toggleClass('open__settings-yubikey--active', !!this.params.chalResp);
} }
displayOpenDeviceOwnerAuth() {
const available = !!this.encryptedPassword;
const passEmpty = !this.passwordInput.length;
const canUseEncryptedPassword = available && passEmpty;
this.el
.querySelector('.open__pass-enter-btn')
.classList.toggle('open__pass-enter-btn--touch-id', canUseEncryptedPassword);
}
setFile(file, keyFile, fileReadyCallback) { setFile(file, keyFile, fileReadyCallback) {
this.reading = 'fileData'; this.reading = 'fileData';
this.processFile(file, (success) => { this.processFile(file, (success) => {
@ -479,6 +492,10 @@ class OpenView extends View {
} }
} }
inputInput() {
this.displayOpenDeviceOwnerAuth();
}
toggleCapsLockWarning(on) { toggleCapsLockWarning(on) {
this.$el.find('.open__pass-warning').toggleClass('invisible', !on); this.$el.find('.open__pass-warning').toggleClass('invisible', !on);
} }
@ -587,11 +604,12 @@ class OpenView extends View {
this.params.keyFileData = null; this.params.keyFileData = null;
this.params.opts = fileInfo.opts; this.params.opts = fileInfo.opts;
this.params.chalResp = fileInfo.chalResp; this.params.chalResp = fileInfo.chalResp;
this.setEncryptedPassword(fileInfo);
this.displayOpenFile(); this.displayOpenFile();
this.displayOpenKeyFile(); this.displayOpenKeyFile();
this.displayOpenChalResp(); this.displayOpenChalResp();
this.displayOpenDeviceOwnerAuth();
this.openFileWithFingerprint(fileInfo);
if (fileWasClicked) { if (fileWasClicked) {
this.focusInput(true); this.focusInput(true);
@ -608,7 +626,9 @@ class OpenView extends View {
this.params.name = path.match(/[^/\\]*$/)[0]; this.params.name = path.match(/[^/\\]*$/)[0];
this.params.rev = null; this.params.rev = null;
this.params.fileData = null; this.params.fileData = null;
this.encryptedPassword = null;
this.displayOpenFile(); this.displayOpenFile();
this.displayOpenDeviceOwnerAuth();
if (keyFilePath) { if (keyFilePath) {
const parsed = Launcher.parsePath(keyFilePath); const parsed = Launcher.parsePath(keyFilePath);
this.params.keyFileName = parsed.file; this.params.keyFileName = parsed.file;
@ -618,20 +638,6 @@ class OpenView extends View {
} }
} }
openFileWithFingerprint(fileInfo) {
if (!fileInfo.fingerprint) {
return;
}
if (Launcher && Launcher.fingerprints) {
Launcher.fingerprints.auth(fileInfo.id, fileInfo.fingerprint, (password) => {
this.inputEl.val(password);
this.inputEl.trigger('input');
this.openDb();
});
}
}
createDemo() { createDemo() {
if (!this.busy) { if (!this.busy) {
this.closeConfig(); this.closeConfig();
@ -662,15 +668,37 @@ class OpenView extends View {
this.inputEl.attr('disabled', 'disabled'); this.inputEl.attr('disabled', 'disabled');
this.busy = true; this.busy = true;
this.params.password = this.passwordInput.value; this.params.password = this.passwordInput.value;
this.afterPaint(() => { if (this.encryptedPassword && !this.params.password.length) {
this.model.openFile(this.params, (err) => this.openDbComplete(err)); logger.debug('Encrypting password using hardware decryption');
}); const touchIdPrompt = Locale.bioOpenAuthPrompt.replace('{}', this.params.name);
const encryptedPassword = kdbxweb.ProtectedValue.fromBase64(
this.encryptedPassword.value
);
NativeModules.hardwareDecrypt(encryptedPassword, touchIdPrompt)
.then((password) => {
this.params.password = password;
this.params.encryptedPassword = this.encryptedPassword;
this.model.openFile(this.params, (err) => this.openDbComplete(err));
})
.catch((err) => {
if (err.message.includes('User refused')) {
err.userCanceled = true;
}
this.openDbComplete(err);
});
} else {
this.params.encryptedPassword = null;
this.afterPaint(() => {
this.model.openFile(this.params, (err) => this.openDbComplete(err));
});
}
} }
openDbComplete(err) { openDbComplete(err) {
this.busy = false; this.busy = false;
this.$el.toggleClass('open--opening', false); this.$el.toggleClass('open--opening', false);
this.inputEl.removeAttr('disabled').toggleClass('input--error', !!err); const showInputError = err && !err.userCanceled;
this.inputEl.removeAttr('disabled').toggleClass('input--error', !!showInputError);
if (err) { if (err) {
logger.error('Error opening file', err); logger.error('Error opening file', err);
this.focusInput(true); this.focusInput(true);
@ -822,7 +850,9 @@ class OpenView extends View {
this.params.name = UrlFormat.getDataFileName(file.name); this.params.name = UrlFormat.getDataFileName(file.name);
this.params.rev = file.rev; this.params.rev = file.rev;
this.params.fileData = null; this.params.fileData = null;
this.encryptedPassword = null;
this.displayOpenFile(); this.displayOpenFile();
this.displayOpenDeviceOwnerAuth();
} }
showConfig(storage) { showConfig(storage) {
@ -918,7 +948,9 @@ class OpenView extends View {
this.params.name = UrlFormat.getDataFileName(req.path); this.params.name = UrlFormat.getDataFileName(req.path);
this.params.rev = stat.rev; this.params.rev = stat.rev;
this.params.fileData = null; this.params.fileData = null;
this.encryptedPassword = null;
this.displayOpenFile(); this.displayOpenFile();
this.displayOpenDeviceOwnerAuth();
} }
} }
@ -1078,6 +1110,37 @@ class OpenView extends View {
} }
return undefined; return undefined;
} }
setEncryptedPassword(fileInfo) {
this.encryptedPassword = null;
if (!fileInfo.id) {
return;
}
switch (this.model.settings.deviceOwnerAuth) {
case 'memory':
this.encryptedPassword = this.model.getMemoryPassword(fileInfo.id);
break;
case 'file':
this.encryptedPassword = {
value: fileInfo.encryptedPassword,
date: fileInfo.encryptedPasswordDate
};
break;
}
this.checkIfEncryptedPasswordDateIsValid();
}
checkIfEncryptedPasswordDateIsValid() {
if (this.encryptedPassword) {
const maxDate = new Date(this.encryptedPassword.date);
maxDate.setMinutes(
maxDate.getMinutes() + this.model.settings.deviceOwnerAuthTimeoutMinutes
);
if (maxDate < new Date()) {
this.encryptedPassword = null;
}
}
}
} }
export { OpenView }; export { OpenView };

View File

@ -15,7 +15,8 @@ class SettingsAboutView extends View {
licenseLinkCCBY40: Links.LicenseLinkCCBY40, licenseLinkCCBY40: Links.LicenseLinkCCBY40,
repoLink: Links.Repo, repoLink: Links.Repo,
donationLink: Links.Donation, donationLink: Links.Donation,
isDesktop: Features.isDesktop isDesktop: Features.isDesktop,
year: new Date().getFullYear()
}); });
} }
} }

View File

@ -16,7 +16,8 @@ import { DateFormat } from 'comp/i18n/date-format';
import { Locale } from 'util/locale'; import { Locale } from 'util/locale';
import { SettingsLogsView } from 'views/settings/settings-logs-view'; import { SettingsLogsView } from 'views/settings/settings-logs-view';
import { SettingsPrvView } from 'views/settings/settings-prv-view'; import { SettingsPrvView } from 'views/settings/settings-prv-view';
import { mapObject } from 'util/fn'; import { mapObject, minmax } from 'util/fn';
import { ThemeWatcher } from 'comp/browser/theme-watcher';
import template from 'templates/settings/settings-general.hbs'; import template from 'templates/settings/settings-general.hbs';
class SettingsGeneralView extends View { class SettingsGeneralView extends View {
@ -24,6 +25,7 @@ class SettingsGeneralView extends View {
events = { events = {
'click .settings__general-theme': 'changeTheme', 'click .settings__general-theme': 'changeTheme',
'click .settings__general-auto-switch-theme': 'changeAuthSwitchTheme',
'change .settings__general-locale': 'changeLocale', 'change .settings__general-locale': 'changeLocale',
'change .settings__general-font-size': 'changeFontSize', 'change .settings__general-font-size': 'changeFontSize',
'change .settings__general-expand': 'changeExpandGroups', 'change .settings__general-expand': 'changeExpandGroups',
@ -34,6 +36,13 @@ class SettingsGeneralView extends View {
'change .settings__general-auto-save-interval': 'changeAutoSaveInterval', 'change .settings__general-auto-save-interval': 'changeAutoSaveInterval',
'change .settings__general-remember-key-files': 'changeRememberKeyFiles', 'change .settings__general-remember-key-files': 'changeRememberKeyFiles',
'change .settings__general-minimize': 'changeMinimize', 'change .settings__general-minimize': 'changeMinimize',
'change .settings__general-minimize-on-field-copy': 'changeMinimizeOnFieldCopy',
'change .settings__general-audit-passwords': 'changeAuditPasswords',
'change .settings__general-audit-password-entropy': 'changeAuditPasswordEntropy',
'change .settings__general-exclude-pins-from-audit': 'changeExcludePinsFromAudit',
'change .settings__general-check-passwords-on-hibp': 'changeCheckPasswordsOnHIBP',
'click .settings__general-toggle-help-hibp': 'clickToggleHelpHIBP',
'change .settings__general-audit-password-age': 'changeAuditPasswordAge',
'change .settings__general-lock-on-minimize': 'changeLockOnMinimize', 'change .settings__general-lock-on-minimize': 'changeLockOnMinimize',
'change .settings__general-lock-on-copy': 'changeLockOnCopy', 'change .settings__general-lock-on-copy': 'changeLockOnCopy',
'change .settings__general-lock-on-auto-type': 'changeLockOnAutoType', 'change .settings__general-lock-on-auto-type': 'changeLockOnAutoType',
@ -45,11 +54,15 @@ class SettingsGeneralView extends View {
'change .settings__general-direct-autotype': 'changeDirectAutotype', 'change .settings__general-direct-autotype': 'changeDirectAutotype',
'change .settings__general-field-label-dblclick-autotype': 'change .settings__general-field-label-dblclick-autotype':
'changeFieldLabelDblClickAutoType', 'changeFieldLabelDblClickAutoType',
'change .settings__general-use-legacy-autotype': 'changeUseLegacyAutoType',
'change .settings__general-device-owner-auth': 'changeDeviceOwnerAuth',
'change .settings__general-device-owner-auth-timeout': 'changeDeviceOwnerAuthTimeout',
'change .settings__general-titlebar-style': 'changeTitlebarStyle', 'change .settings__general-titlebar-style': 'changeTitlebarStyle',
'click .settings__general-update-btn': 'checkUpdate', '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-download-update-btn': 'downloadUpdate',
'click .settings__general-update-found-btn': 'installFoundUpdate', 'click .settings__general-update-found-btn': 'installFoundUpdate',
'change .settings__general-disable-offline-storage': 'changeDisableOfflineStorage',
'change .settings__general-prv-check': 'changeStorageEnabled', 'change .settings__general-prv-check': 'changeStorageEnabled',
'click .settings__general-prv-logout': 'logoutFromStorage', 'click .settings__general-prv-logout': 'logoutFromStorage',
'click .settings__general-show-advanced': 'showAdvancedSettings', 'click .settings__general-show-advanced': 'showAdvancedSettings',
@ -61,8 +74,8 @@ class SettingsGeneralView extends View {
constructor(model, options) { constructor(model, options) {
super(model, options); super(model, options);
this.listenTo(UpdateModel, 'change:status', this.render); this.listenTo(UpdateModel, 'change', this.render);
this.listenTo(UpdateModel, 'change:updateStatus', this.render); this.listenTo(Events, 'theme-applied', this.render);
} }
render() { render() {
@ -72,7 +85,8 @@ class SettingsGeneralView extends View {
const storageProviders = this.getStorageProviders(); const storageProviders = this.getStorageProviders();
super.render({ super.render({
themes: mapObject(SettingsManager.allThemes, (theme) => Locale[theme]), themes: this.getAllThemes(),
autoSwitchTheme: AppSettingsModel.autoSwitchTheme,
activeTheme: SettingsManager.activeTheme, activeTheme: SettingsManager.activeTheme,
locales: SettingsManager.allLocales, locales: SettingsManager.allLocales,
activeLocale: SettingsManager.activeLocale, activeLocale: SettingsManager.activeLocale,
@ -86,6 +100,7 @@ class SettingsGeneralView extends View {
autoSaveInterval: AppSettingsModel.autoSaveInterval, autoSaveInterval: AppSettingsModel.autoSaveInterval,
idleMinutes: AppSettingsModel.idleMinutes, idleMinutes: AppSettingsModel.idleMinutes,
minimizeOnClose: AppSettingsModel.minimizeOnClose, minimizeOnClose: AppSettingsModel.minimizeOnClose,
minimizeOnFieldCopy: AppSettingsModel.minimizeOnFieldCopy,
devTools: Launcher && Launcher.devTools, devTools: Launcher && Launcher.devTools,
canAutoUpdate: Updater.enabled, canAutoUpdate: Updater.enabled,
canAutoSaveOnClose: !!Launcher, canAutoSaveOnClose: !!Launcher,
@ -93,6 +108,13 @@ class SettingsGeneralView extends View {
canDetectMinimize: !!Launcher, canDetectMinimize: !!Launcher,
canDetectOsSleep: Launcher && Launcher.canDetectOsSleep(), canDetectOsSleep: Launcher && Launcher.canDetectOsSleep(),
canAutoType: AutoType.enabled, canAutoType: AutoType.enabled,
auditPasswords: AppSettingsModel.auditPasswords,
auditPasswordEntropy: AppSettingsModel.auditPasswordEntropy,
excludePinsFromAudit: AppSettingsModel.excludePinsFromAudit,
checkPasswordsOnHIBP: AppSettingsModel.checkPasswordsOnHIBP,
auditPasswordAge: AppSettingsModel.auditPasswordAge,
hibpLink: Links.HaveIBeenPwned,
hibpPrivacyLink: Links.HaveIBeenPwnedPrivacy,
lockOnMinimize: Launcher && AppSettingsModel.lockOnMinimize, lockOnMinimize: Launcher && AppSettingsModel.lockOnMinimize,
lockOnCopy: AppSettingsModel.lockOnCopy, lockOnCopy: AppSettingsModel.lockOnCopy,
lockOnAutoType: AppSettingsModel.lockOnAutoType, lockOnAutoType: AppSettingsModel.lockOnAutoType,
@ -113,10 +135,16 @@ class SettingsGeneralView extends View {
useGroupIconForEntries: AppSettingsModel.useGroupIconForEntries, useGroupIconForEntries: AppSettingsModel.useGroupIconForEntries,
directAutotype: AppSettingsModel.directAutotype, directAutotype: AppSettingsModel.directAutotype,
fieldLabelDblClickAutoType: AppSettingsModel.fieldLabelDblClickAutoType, fieldLabelDblClickAutoType: AppSettingsModel.fieldLabelDblClickAutoType,
supportsTitleBarStyles: Launcher && Features.supportsTitleBarStyles(), useLegacyAutoType: AppSettingsModel.useLegacyAutoType,
supportsTitleBarStyles: Features.supportsTitleBarStyles(),
supportsCustomTitleBarAndDraggableWindow: Features.supportsCustomTitleBarAndDraggableWindow(),
titlebarStyle: AppSettingsModel.titlebarStyle, titlebarStyle: AppSettingsModel.titlebarStyle,
storageProviders, storageProviders,
showReloadApp: Features.isStandalone showReloadApp: Features.isStandalone,
hasDeviceOwnerAuth: Features.isDesktop && Features.isMac,
deviceOwnerAuth: AppSettingsModel.deviceOwnerAuth,
deviceOwnerAuthTimeout: AppSettingsModel.deviceOwnerAuthTimeoutMinutes,
disableOfflineStorage: AppSettingsModel.disableOfflineStorage
}); });
this.renderProviderViews(storageProviders); this.renderProviderViews(storageProviders);
} }
@ -204,16 +232,49 @@ class SettingsGeneralView extends View {
})); }));
} }
getAllThemes() {
const { autoSwitchTheme } = AppSettingsModel;
if (autoSwitchTheme) {
const themes = {};
const ignoredThemes = {};
for (const config of SettingsManager.autoSwitchedThemes) {
ignoredThemes[config.dark] = true;
ignoredThemes[config.light] = true;
const activeTheme = ThemeWatcher.dark ? config.dark : config.light;
themes[activeTheme] = Locale[config.name];
}
for (const [th, name] of Object.entries(SettingsManager.allThemes)) {
if (!ignoredThemes[th]) {
themes[th] = Locale[name];
}
}
return themes;
} else {
return mapObject(SettingsManager.allThemes, (theme) => Locale[theme]);
}
}
changeTheme(e) { changeTheme(e) {
const theme = e.target.closest('.settings__general-theme').dataset.theme; const theme = e.target.closest('.settings__general-theme').dataset.theme;
if (theme === '...') { if (theme === '...') {
this.goToPlugins(); this.goToPlugins();
} else { } else {
AppSettingsModel.theme = theme; const changedInSettings = AppSettingsModel.theme !== theme;
this.render(); if (changedInSettings) {
AppSettingsModel.theme = theme;
} else {
SettingsManager.setTheme(theme);
}
} }
} }
changeAuthSwitchTheme(e) {
const autoSwitchTheme = e.target.checked;
AppSettingsModel.autoSwitchTheme = autoSwitchTheme;
SettingsManager.darkModeChanged();
this.render();
}
changeLocale(e) { changeLocale(e) {
const locale = e.target.value; const locale = e.target.value;
if (locale === '...') { if (locale === '...') {
@ -283,6 +344,43 @@ class SettingsGeneralView extends View {
AppSettingsModel.minimizeOnClose = minimizeOnClose; AppSettingsModel.minimizeOnClose = minimizeOnClose;
} }
changeMinimizeOnFieldCopy(e) {
const minimizeOnFieldCopy = e.target.checked || false;
AppSettingsModel.minimizeOnFieldCopy = minimizeOnFieldCopy;
}
changeAuditPasswords(e) {
const auditPasswords = e.target.checked || false;
AppSettingsModel.auditPasswords = auditPasswords;
}
changeAuditPasswordEntropy(e) {
const auditPasswordEntropy = e.target.checked || false;
AppSettingsModel.auditPasswordEntropy = auditPasswordEntropy;
}
changeExcludePinsFromAudit(e) {
const excludePinsFromAudit = e.target.checked || false;
AppSettingsModel.excludePinsFromAudit = excludePinsFromAudit;
}
changeCheckPasswordsOnHIBP(e) {
if (e.target.closest('a')) {
return;
}
const checkPasswordsOnHIBP = e.target.checked || false;
AppSettingsModel.checkPasswordsOnHIBP = checkPasswordsOnHIBP;
}
clickToggleHelpHIBP() {
this.el.querySelector('.settings__general-help-hibp').classList.toggle('hide');
}
changeAuditPasswordAge(e) {
const auditPasswordAge = e.target.value | 0;
AppSettingsModel.auditPasswordAge = auditPasswordAge;
}
changeLockOnMinimize(e) { changeLockOnMinimize(e) {
const lockOnMinimize = e.target.checked || false; const lockOnMinimize = e.target.checked || false;
AppSettingsModel.lockOnMinimize = lockOnMinimize; AppSettingsModel.lockOnMinimize = lockOnMinimize;
@ -324,13 +422,11 @@ class SettingsGeneralView extends View {
changeUseGroupIconForEntries(e) { changeUseGroupIconForEntries(e) {
const useGroupIconForEntries = e.target.checked || false; const useGroupIconForEntries = e.target.checked || false;
AppSettingsModel.useGroupIconForEntries = useGroupIconForEntries; AppSettingsModel.useGroupIconForEntries = useGroupIconForEntries;
Events.emit('refresh');
} }
changeDirectAutotype(e) { changeDirectAutotype(e) {
const directAutotype = e.target.checked || false; const directAutotype = e.target.checked || false;
AppSettingsModel.directAutotype = directAutotype; AppSettingsModel.directAutotype = directAutotype;
Events.emit('refresh');
} }
changeFieldLabelDblClickAutoType(e) { changeFieldLabelDblClickAutoType(e) {
@ -339,9 +435,36 @@ class SettingsGeneralView extends View {
Events.emit('refresh'); Events.emit('refresh');
} }
restartApp() { changeUseLegacyAutoType(e) {
const useLegacyAutoType = e.target.checked || false;
AppSettingsModel.useLegacyAutoType = useLegacyAutoType;
Events.emit('refresh');
}
changeDeviceOwnerAuth(e) {
const deviceOwnerAuth = e.target.value || null;
let deviceOwnerAuthTimeoutMinutes = AppSettingsModel.deviceOwnerAuthTimeoutMinutes | 0;
if (deviceOwnerAuth) {
const timeouts = { memory: [30, 10080], file: [30, 525600] };
const [tMin, tMax] = timeouts[deviceOwnerAuth] || [0, 0];
deviceOwnerAuthTimeoutMinutes = minmax(deviceOwnerAuthTimeoutMinutes, tMin, tMax);
}
AppSettingsModel.set({ deviceOwnerAuth, deviceOwnerAuthTimeoutMinutes });
this.render();
this.appModel.checkEncryptedPasswordsStorage();
}
changeDeviceOwnerAuthTimeout(e) {
const deviceOwnerAuthTimeout = e.target.value | 0;
AppSettingsModel.deviceOwnerAuthTimeoutMinutes = deviceOwnerAuthTimeout;
}
installUpdateAndRestart() {
if (Launcher) { if (Launcher) {
Launcher.requestRestart(); Updater.installAndRestart();
} else { } else {
window.location.reload(); window.location.reload();
} }
@ -353,7 +476,7 @@ class SettingsGeneralView extends View {
installFoundUpdate() { installFoundUpdate() {
Updater.update(true, () => { Updater.update(true, () => {
Launcher.requestRestart(); Updater.installAndRestart();
}); });
} }
@ -363,6 +486,14 @@ class SettingsGeneralView extends View {
Events.emit('refresh'); Events.emit('refresh');
} }
changeDisableOfflineStorage(e) {
const disableOfflineStorage = e.target.checked;
AppSettingsModel.disableOfflineStorage = disableOfflineStorage;
if (disableOfflineStorage) {
this.appModel.deleteAllCachedFiles();
}
}
changeStorageEnabled(e) { changeStorageEnabled(e) {
const storage = Storage[$(e.target).data('storage')]; const storage = Storage[$(e.target).data('storage')];
if (storage) { if (storage) {

View File

@ -0,0 +1,62 @@
import { View } from 'framework/views/view';
import { Events } from 'framework/events';
import { Launcher } from 'comp/launcher';
import { KeeWebLogo } from 'const/inline-images';
import template from 'templates/titlebar.hbs';
class TitlebarView extends View {
parent = '.app__titlebar';
template = template;
events = {
'click .titlebar__minimize': 'clickMinimize',
'click .titlebar__maximize': 'clickMaximize',
'click .titlebar__restore': 'clickRestore',
'click .titlebar__close': 'clickClose'
};
constructor() {
super();
this.maximized = Launcher.mainWindowMaximized();
this.listenTo(Events, 'app-maximized', this.appMaximized);
this.listenTo(Events, 'app-unmaximized', this.appUnmaximized);
}
render() {
super.render({
maximized: this.maximized,
iconSrc: KeeWebLogo
});
}
clickMinimize() {
Launcher.minimizeMainWindow();
}
clickMaximize() {
Launcher.maximizeMainWindow();
}
clickRestore() {
Launcher.restoreMainWindow();
}
clickClose() {
window.close();
}
appMaximized() {
this.maximized = true;
this.render();
}
appUnmaximized() {
this.maximized = false;
this.render();
}
}
export { TitlebarView };

View File

@ -45,6 +45,10 @@
.titlebar-hidden-inset & { .titlebar-hidden-inset & {
padding-top: $titlebar-padding-large; padding-top: $titlebar-padding-large;
} }
.titlebar-custom.titlebar-hidden &,
.titlebar-custom.titlebar-hidden-inset & {
padding-top: $titlebar-custom-height;
}
.fullscreen .app & { .fullscreen .app & {
padding-top: 0; padding-top: 0;
} }
@ -123,6 +127,7 @@
display: flex; display: flex;
} }
} }
@include padding-if-titlebar;
} }
&__panel { &__panel {

View File

@ -575,6 +575,50 @@
} }
} }
&__issues {
margin-top: $base-padding-v;
color: var(--text-contrast-error-color);
background-color: var(--error-color);
border-radius: var(--block-border-radius);
display: flex;
align-items: stretch;
flex-direction: row;
justify-content: flex-start;
&-body {
padding: $medium-padding-v 0;
flex-grow: 1;
> a {
color: var(--text-contrast-error-color);
}
}
&-icon {
padding: $medium-padding;
width: 1em;
&-spin {
display: none;
.details__issues-icon--loading & {
display: inline-block;
}
}
&-warning {
.details__issues-icon--loading & {
display: none;
}
}
}
&-close-btn {
padding: $medium-padding;
cursor: pointer;
align-self: flex-start;
opacity: 0.8;
transition: opacity $base-duration $base-timing;
&:hover {
opacity: 1;
}
}
}
&__buttons { &__buttons {
display: flex; display: flex;
align-items: stretch; align-items: stretch;

View File

@ -9,6 +9,7 @@
user-select: none; user-select: none;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@include padding-if-titlebar;
> .scroller { > .scroller {
flex: 1; flex: 1;

View File

@ -9,6 +9,7 @@
user-select: none; user-select: none;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@include padding-if-titlebar;
> .scroller { > .scroller {
flex: 1; flex: 1;

View File

@ -3,6 +3,8 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
position: relative;
@include padding-if-titlebar;
&__body { &__body {
@include scrollbar-on-hover; @include scrollbar-on-hover;

View File

@ -55,21 +55,30 @@
box-shadow: none !important; box-shadow: none !important;
border-radius: 0.6em !important; border-radius: 0.6em !important;
border: none !important; border: none !important;
padding-left: 0.4em;
background-color: var(--secondary-background-color) !important; background-color: var(--secondary-background-color) !important;
} }
} }
&-icon-search { &-icon-search,
&-icon-clear {
color: var(--muted-color); color: var(--muted-color);
position: absolute; position: absolute;
top: 0.5em; top: 0.53em;
right: 0.5em;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: var(--medium-color); color: var(--medium-color);
} }
@include mobile { @include mobile {
top: 0.5em; top: 0.6em;
}
}
&-icon-search {
left: 0.6em;
}
&-icon-clear {
right: 0.6em;
display: none;
.list__search-field-wrap--text & {
display: block;
} }
} }
&-btn-new, &-btn-new,
@ -98,6 +107,7 @@
align-items: stretch; align-items: stretch;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
padding: 0 $small-spacing;
&-text { &-text {
flex: 100%; flex: 100%;
padding: $base-padding-v 0; padding: $base-padding-v 0;

View File

@ -108,6 +108,18 @@
.open--opening & { .open--opening & {
display: none; display: none;
} }
&-icon-enter {
display: block;
.open__pass-enter-btn--touch-id & {
display: none;
}
}
&-icon-touch-id {
display: none;
.open__pass-enter-btn--touch-id & {
display: block;
}
}
} }
&-opening-icon { &-opening-icon {
display: none; display: none;

View File

@ -8,6 +8,7 @@
@include scrollbar-on-hover; @include scrollbar-on-hover;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@include padding-if-titlebar;
&__content { &__content {
margin: $medium-padding; margin: $medium-padding;
@ -171,10 +172,10 @@
} }
&__general-update-btn { &__general-update-btn {
width: 15em; width: 15em;
margin-right: $small-spacing;
} }
&__general-storage-header { &__general-storage-header {
margin-bottom: 0; margin-bottom: 0;
line-height: 1.3em;
} }
&__general-prv { &__general-prv {
margin-bottom: $base-padding-v; margin-bottom: $base-padding-v;

View File

@ -7,6 +7,7 @@
width: 100%; width: 100%;
user-select: none; user-select: none;
padding: $medium-padding; padding: $medium-padding;
@include padding-if-titlebar;
&__space { &__space {
flex: 1; flex: 1;

View File

@ -0,0 +1,38 @@
.titlebar {
font-size: 0;
.titlebar-custom & {
position: fixed;
top: 0;
left: 0;
width: 100vw;
display: flex;
height: $titlebar-custom-height;
}
&__logo {
@include size(30px);
padding: 6px;
pointer-events: none;
}
&__grow {
flex-grow: 1;
-webkit-app-region: drag;
}
> .fa {
font-size: 16px;
padding: 4px 16px;
height: $titlebar-custom-height;
box-sizing: border-box;
&:hover {
background: var(--titlebar-button-background-color);
}
&.fa-titlebar-close {
&:hover {
background: $titlebar-close-button-background-color;
}
}
}
}

View File

@ -97,8 +97,9 @@ input:not([type]) {
} }
} }
&.input-padding-right { &.input-search {
padding-right: 1.7em; padding-left: 2.9em;
padding-right: 1.8em;
} }
&::placeholder { &::placeholder {

View File

@ -58,6 +58,7 @@ $fa-var-unlock: next-fa-glyph();
$fa-var-lock: next-fa-glyph(); $fa-var-lock: next-fa-glyph();
$fa-var-check: next-fa-glyph(); $fa-var-check: next-fa-glyph();
$fa-var-times: next-fa-glyph(); $fa-var-times: next-fa-glyph();
$fa-var-times-circle: next-fa-glyph();
$fa-var-folder: next-fa-glyph(); $fa-var-folder: next-fa-glyph();
$fa-var-folder-open: next-fa-glyph(); $fa-var-folder-open: next-fa-glyph();
$fa-var-ban: next-fa-glyph(); $fa-var-ban: next-fa-glyph();
@ -123,6 +124,7 @@ $fa-var-inbox: next-fa-glyph();
$fa-var-save: next-fa-glyph(); $fa-var-save: next-fa-glyph();
$fa-var-hdd: next-fa-glyph(); $fa-var-hdd: next-fa-glyph();
$fa-var-dot-circle: next-fa-glyph(); $fa-var-dot-circle: next-fa-glyph();
$fa-var-user: next-fa-glyph();
$fa-var-user-lock: next-fa-glyph(); $fa-var-user-lock: next-fa-glyph();
$fa-var-terminal: next-fa-glyph(); $fa-var-terminal: next-fa-glyph();
$fa-var-print: next-fa-glyph(); $fa-var-print: next-fa-glyph();
@ -193,3 +195,8 @@ $fa-var-paint-brush: next-fa-glyph();
$fa-var-at: next-fa-glyph(); $fa-var-at: next-fa-glyph();
$fa-var-usb-token: next-fa-glyph(); $fa-var-usb-token: next-fa-glyph();
$fa-var-bell: next-fa-glyph(); $fa-var-bell: next-fa-glyph();
$fa-var-fingerprint: next-fa-glyph();
$fa-var-titlebar-close: next-fa-glyph();
$fa-var-titlebar-maximize: next-fa-glyph();
$fa-var-titlebar-minimize: next-fa-glyph();
$fa-var-titlebar-restore: next-fa-glyph();

View File

@ -69,7 +69,8 @@
selectable-on-secondary-item-color: selectable-on-secondary-item-color:
mix(map-get($t, medium-color), map-get($t, background-color), 14%), mix(map-get($t, medium-color), map-get($t, background-color), 14%),
clickable-on-secondary-color: clickable-on-secondary-color:
mix(map-get($t, medium-color), map-get($t, background-color), 75%) mix(map-get($t, medium-color), map-get($t, background-color), 75%),
titlebar-button-background-color: rgba(map-get($t, text-color), 0.085)
), ),
$t $t
); );

View File

@ -66,9 +66,12 @@ $titlebar-padding-large: 40px;
// Animations // Animations
$base-duration: 150ms; $base-duration: 150ms;
$fast-duration: 80ms;
$base-timing: ease; $base-timing: ease;
$slow-transition-in: $base-duration * 2 ease-in; $slow-transition-in: $base-duration * 2 ease-in;
$slow-transition-out: $base-duration ease-out; $slow-transition-out: $base-duration ease-out;
$fast-transition-in: $fast-duration ease-in;
$fast-transition-out: $fast-duration ease-out;
$tip-transition-in: 500ms $ease-in-expo; $tip-transition-in: 500ms $ease-in-expo;
$tip-transition-out: $slow-transition-out; $tip-transition-out: $slow-transition-out;
@ -81,3 +84,7 @@ $z-index-modal: 100000;
// Screen sizes // Screen sizes
$tablet-width: 736px; $tablet-width: 736px;
$mobile-width: 620px; $mobile-width: 620px;
// Title bar and window buttons
$titlebar-custom-height: 32px;
$titlebar-close-button-background-color: #d71525;

View File

@ -25,6 +25,15 @@
animation: shake 50s cubic-bezier(0.36, 0.07, 0.19, 0.97) 0s; animation: shake 50s cubic-bezier(0.36, 0.07, 0.19, 0.97) 0s;
} }
.fade-in {
animation: fade-in $fast-transition-in 0s;
}
.fade-out {
opacity: 0;
animation: fade-out $fast-transition-out 0s;
}
.rotate-90, .rotate-90,
.fa.rotate-90:before { .fa.rotate-90:before {
transform: rotate(90deg); transform: rotate(90deg);
@ -52,6 +61,24 @@
} }
} }
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes shake { @keyframes shake {
0%, 0%,
1%, 1%,

View File

@ -7,7 +7,8 @@
flex-wrap: wrap; flex-wrap: wrap;
user-select: none; user-select: none;
padding-bottom: $base-padding-h; padding-bottom: $base-padding-h;
&--custom { &--custom,
&--actions {
padding-top: $base-padding-h; padding-top: $base-padding-h;
border-top: 1px solid var(--light-border-color); border-top: 1px solid var(--light-border-color);
} }

View File

@ -0,0 +1,5 @@
@mixin padding-if-titlebar {
.titlebar-custom & {
margin-top: $titlebar-custom-height;
}
}

View File

@ -23,6 +23,7 @@ $fa-font-path: '~font-awesome/fonts';
@import 'common/modal'; @import 'common/modal';
@import 'common/scroll'; @import 'common/scroll';
@import 'common/tip'; @import 'common/tip';
@import 'common/titlebar';
@import 'areas/app'; @import 'areas/app';
@import 'areas/auto-type'; @import 'areas/auto-type';
@ -38,3 +39,4 @@ $fa-font-path: '~font-awesome/fonts';
@import 'areas/open'; @import 'areas/open';
@import 'areas/settings'; @import 'areas/settings';
@import 'areas/import-csv'; @import 'areas/import-csv';
@import 'areas/titlebar';

View File

@ -4,9 +4,13 @@ $themes: ();
@import 'dark'; @import 'dark';
@import 'light'; @import 'light';
@import 'dark-brown'; @import 'dark-brown';
@import 'light-brown';
@import 'flat-blue'; @import 'flat-blue';
@import 'light-blue';
@import 'terminal'; @import 'terminal';
@import 'light-terminal';
@import 'high-contrast'; @import 'high-contrast';
@import 'dark-contrast';
@import 'solarized-dark'; @import 'solarized-dark';
@import 'solarized-light'; @import 'solarized-light';

View File

@ -0,0 +1,45 @@
$themes: map-merge(
$themes,
(
dc:
map-merge(
$theme-defaults,
map-merge(
$light-colors,
(
background-color: #050505,
medium-color: #fafafa,
text-color: #fafafa,
action-color: #1e5db8,
error-color: #e74859,
mute-percent: 60%,
light-border-percent: 50%,
modal-opacity: 1
)
)
)
)
);
body.th-dc {
--selected-item-color: #1e5db8;
--selected-item-text-color: #fafafa;
.list__item--active .red-color {
color: #ff6d6b;
}
.list__item--active .orange-color {
color: #ffbb86;
}
.list__item--active .yellow-color {
}
.list__item--active .green-color {
color: #baff92;
}
.list__item--active .blue-color {
color: #c1d9ff;
}
.list__item--active .violet-color {
color: #ff93c5;
}
}

View File

@ -0,0 +1,25 @@
$themes: map-merge(
$themes,
(
bl:
map-merge(
$theme-defaults,
(
background-color: #e5e8ec,
medium-color: #383b48,
text-color: #282c34,
action-color: #528bff,
error-color: #c34034,
mute-percent: 50%
)
)
)
);
body.th-bl {
--open-icon-color: #525462;
.list__item--active .blue-color {
color: #98bfff;
}
}

View File

@ -0,0 +1,26 @@
$themes: map-merge(
$themes,
(
lb:
map-merge(
$theme-defaults,
(
background-color: #fcf0e8,
medium-color: #454040,
text-color: #342f2e,
action-color: #2c9957,
error-color: #fd6d67,
mute-percent: 50%
)
)
)
);
body.th-lb {
.list__item--active .blue-color {
color: #0051d2;
}
.list__item--active .green-color {
color: #77d644;
}
}

View File

@ -0,0 +1,26 @@
$themes: map-merge(
$themes,
(
lt:
map-merge(
$theme-defaults,
(
background-color: #eee,
medium-color: #444,
text-color: #222,
action-color: #13a453,
error-color: #c34034,
mute-percent: 50%
)
)
)
);
body.th-lt {
.list__item--active .green-color {
color: #7be045;
}
.list__item--active .blue-color {
color: #0750c5;
}
}

View File

@ -28,7 +28,7 @@ body.th-light {
--selected-item-color: #2366d9; --selected-item-color: #2366d9;
--selected-on-secondary-item-color: #d6d6d6; --selected-on-secondary-item-color: #d6d6d6;
--selected-item-text-color: #f6f6f6; --selected-item-text-color: #f6f6f6;
--open-icon-color: var(--muted-color); --open-icon-color: #565656;
.list__item--active .blue-color { .list__item--active .blue-color {
color: #7baeff; color: #7baeff;

View File

@ -6,6 +6,10 @@
padding: $base-padding; padding: $base-padding;
box-sizing: border-box; box-sizing: border-box;
overflow: hidden; overflow: hidden;
&__body {
height: 100%;
overflow-y: auto;
}
&__block { &__block {
margin-bottom: $base-padding-v; margin-bottom: $base-padding-v;
> a, > a,

View File

@ -1,6 +1,10 @@
.info-btn { .info-btn {
cursor: pointer; cursor: pointer;
color: var(--muted-color); color: var(--muted-color);
margin-left: $tiny-spacing;
position: relative;
top: 0.15em;
font-size: 1.1em;
&:hover { &:hover {
color: var(--text-color); color: var(--text-color);
} }

View File

@ -1,6 +1,10 @@
<div class="app"> <div class="app">
{{#if beta}}<div class="app__beta"><i class="fa fa-exclamation-triangle"></i> {{res 'appBeta'}}</div>{{/if}} {{#if beta}}<div class="app__beta"><i class="fa fa-exclamation-triangle"></i> {{res 'appBeta'}}</div>{{/if}}
{{#ifeq titlebarStyle 'hidden'}}<div class="app__titlebar-drag"></div>{{/ifeq}} {{#if customTitlebar}}
<div class="app__titlebar"></div>
{{else}}
{{#ifeq titlebarStyle 'hidden'}}<div class="app__titlebar-drag"></div>{{/ifeq}}
{{/if}}
<div class="app__body"> <div class="app__body">
<div class="app__menu"></div> <div class="app__menu"></div>
<div class="app__menu-drag"></div> <div class="app__menu-drag"></div>

View File

@ -1,17 +1,19 @@
<div class="auto-type-hint"> <div class="auto-type-hint">
<a href="{{link}}" class="auto-type-hint__link-details" target="_blank">{{res 'autoTypeLink'}}</a> <div class="auto-type-hint__body">
<div class="auto-type-hint__block"> <a href="{{link}}" class="auto-type-hint__link-details" target="_blank">{{res 'autoTypeLink'}}</a>
<div>{{res 'autoTypeEntryFields'}}:</div> <div class="auto-type-hint__block">
<a>{TITLE}</a><a>{USERNAME}</a><a>{URL}</a><a>{PASSWORD}</a><a>{NOTES}</a><a>{GROUP}</a> <div>{{res 'autoTypeEntryFields'}}:</div>
<a>{TOTP}</a><a>{S:Custom Field Name}</a> <a>{TITLE}</a><a>{USERNAME}</a><a>{URL}</a><a>{PASSWORD}</a><a>{NOTES}</a><a>{GROUP}</a>
</div> <a>{TOTP}</a><a>{S:Custom Field Name}</a>
<div class="auto-type-hint__block"> </div>
<div>{{res 'autoTypeModifiers'}}:</div> <div class="auto-type-hint__block">
<a>+ (shift)</a><a>% (alt)</a><a>^ ({{cmd}})</a>{{#if hasCtrl}}<a>^^ (ctrl)</a>{{/if}} <div>{{res 'autoTypeModifiers'}}:</div>
</div> <a>+ (shift)</a><a>% (alt)</a><a>^ ({{cmd}})</a>{{#if hasCtrl}}<a>^^ (ctrl)</a>{{/if}}
<div class="auto-type-hint__block"> </div>
<div>{{res 'autoTypeKeys'}}:</div> <div class="auto-type-hint__block">
<a>{TAB}</a><a>{ENTER}</a><a>{SPACE}</a><a>{UP}</a><a>{DOWN}</a><a>{LEFT}</a><a>{RIGHT}</a><a>{HOME}</a><a>{END}</a> <div>{{res 'autoTypeKeys'}}:</div>
<a>{+}</a><a>{%}</a><a>{^}</a><a>{~}</a><a>{(}</a><a>{)}</a><a>{[}</a><a>{]}</a><a>\{{}</a><a>{}}</a> <a>{TAB}</a><a>{ENTER}</a><a>{SPACE}</a><a>{UP}</a><a>{DOWN}</a><a>{LEFT}</a><a>{RIGHT}</a><a>{HOME}</a><a>{END}</a>
<a>{+}</a><a>{%}</a><a>{^}</a><a>{~}</a><a>{(}</a><a>{)}</a><a>{[}</a><a>{]}</a><a>\{{}</a><a>{}}</a>
</div>
</div> </div>
</div> </div>

View File

@ -0,0 +1,28 @@
{{#if passwordIssue}}
<div class="details__issues {{#if fadeIn}}fade-in{{/if}}">
<div class="details__issues-icon">
<i class="fa fa-exclamation-triangle details__issues-icon-warning"></i>
<i class="fa fa-spinner spin details__issues-icon-spin"></i>
</div>
<div class="details__issues-body">
{{#ifeq passwordIssue 'weak'}}
{{~res 'detIssueWeakPassword'~}}
{{else ifeq passwordIssue 'poor'}}
{{~res 'detIssuePoorPassword'~}}
{{else ifeq passwordIssue 'pwned'}}
{{~#res 'detIssuePwnedPassword'~}}
<a href="{{hibpLink}}" rel="noreferrer noopener" target="_blank">Have I Been Pwned</a>
{{~/res~}}
{{else ifeq passwordIssue 'old'}}
{{~res 'detIssueOldPassword'~}}
{{else ifeq passwordIssue 'error'}}
{{~res 'detIssuePasswordCheckError'~}}
{{/ifeq}}
</div>
<div class="details__issues-close-btn" title="{{res 'detIssuesHideTooltip'}}">
<i class="fa fa-times-circle"></i>
</div>
</div>
{{else}}
<div></div>
{{/if}}

View File

@ -43,6 +43,8 @@
<div class="scroller__bar-wrapper"><div class="scroller__bar"></div></div> <div class="scroller__bar-wrapper"><div class="scroller__bar"></div></div>
</div> </div>
{{#unless readOnly}} {{#unless readOnly}}
<div class="details__issues-container">
</div>
<div class="details__buttons"> <div class="details__buttons">
{{#if deleted~}} {{#if deleted~}}
<i class="details__buttons-trash-del fa fa-minus-circle" title="{{res 'detDelEntryPerm'}}" tip-placement="top"></i> <i class="details__buttons-trash-del fa fa-minus-circle" title="{{res 'detDelEntryPerm'}}" tip-placement="top"></i>

View File

@ -4,11 +4,11 @@
<i class="fa fa-{{icon}} icon-select__icon {{#ifeq ix ../sel}}icon-select__icon--active{{/ifeq}}" data-val="{{ix}}"></i> <i class="fa fa-{{icon}} icon-select__icon {{#ifeq ix ../sel}}icon-select__icon--active{{/ifeq}}" data-val="{{ix}}"></i>
{{/each}} {{/each}}
</div> </div>
<div class="icon-select__items icon-select__items--custom"> <div class="icon-select__items icon-select__items--actions">
<input type="file" class="icon-select__file-input hide-by-pos" accept="image/*" /> <input type="file" class="icon-select__file-input hide-by-pos" accept="image/*" />
{{#if canDownloadFavicon}} {{#if canDownloadFavicon}}
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-download" <span class="icon-select__icon icon-select__icon-btn icon-select__icon-download"
data-val="special" data-special="download" title="{{res 'iconFavTitle'}}"> data-val="special" data-special="download" title="{{res 'iconFavTitle'}}">
<i class="fa fa-cloud-download-alt"></i> <i class="fa fa-cloud-download-alt"></i>
</span> </span>
{{/if}} {{/if}}
@ -16,6 +16,9 @@
data-val="special" data-special="select" title="{{res 'iconSelCustom'}}"> data-val="special" data-special="select" title="{{res 'iconSelCustom'}}">
<i class="fa fa-ellipsis-h"></i> <i class="fa fa-ellipsis-h"></i>
</span> </span>
</div>
{{#if hasCustomIcons}}
<div class="icon-select__items icon-select__items--custom">
{{#each customIcons as |icon ci|}} {{#each customIcons as |icon ci|}}
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-custom {{#ifeq ci ../sel}}icon-select__icon--active{{/ifeq}}" <span class="icon-select__icon icon-select__icon-btn icon-select__icon-custom {{#ifeq ci ../sel}}icon-select__icon--active{{/ifeq}}"
data-val="{{ci}}"> data-val="{{ci}}">
@ -23,4 +26,5 @@
</span> </span>
{{/each}} {{/each}}
</div> </div>
{{/if}}
</div> </div>

View File

@ -4,11 +4,14 @@
<i class="fa fa-bars"></i> <i class="fa fa-bars"></i>
</div> </div>
<div class="list__search-field-wrap"> <div class="list__search-field-wrap">
<input type="text" class="list__search-field input-padding-right" autocomplete="off" spellcheck="false"> <input type="text" class="list__search-field input-search" autocomplete="off" spellcheck="false">
<div class="list__search-icon-search" title="{{res 'searchAdvTitle'}}"> <div class="list__search-icon-search" title="{{res 'searchAdvTitle'}}">
<i class="fa fa-search"></i> <i class="fa fa-search"></i>
<i class="fa fa-caret-down"></i> <i class="fa fa-caret-down"></i>
</div> </div>
<div class="list__search-icon-clear">
<i class="fa fa-times-circle"></i>
</div>
</div> </div>
<div class="list__search-btn-new {{#unless canCreate}}hide{{/unless}}" title="{{res 'searchAddNew'}}"> <div class="list__search-btn-new {{#unless canCreate}}hide{{/unless}}" title="{{res 'searchAddNew'}}">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>

View File

@ -10,6 +10,7 @@
{{#unless @last}}<br/>{{/unless}} {{#unless @last}}<br/>{{/unless}}
{{/each}} {{/each}}
{{#if pre}}<pre class="modal__pre">{{pre}}</pre>{{/if}} {{#if pre}}<pre class="modal__pre">{{pre}}</pre>{{/if}}
{{#if link}}<a href="{{link}}" class="modal__link" target="_blank">{{link}}</a>{{/if}}
{{#if hint}}<p class="muted-color">{{hint}}</p>{{/if}} {{#if hint}}<p class="muted-color">{{hint}}</p>{{/if}}
{{#if checkbox}} {{#if checkbox}}
<div class="modal__check-wrap"><input type="checkbox" id="modal__check" /><label for="modal__check">{{checkbox}}</label></div> <div class="modal__check-wrap"><input type="checkbox" id="modal__check" /><label for="modal__check">{{checkbox}}</label></div>

View File

@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>KeeWeb</title>
<meta charset="utf8" />
<meta name="viewport" content="width=device-width" />
<style>
body {
height: 100vh;
min-height: 100vh;
background: #ffffff;
color: #242424;
font-family: -apple-system, 'BlinkMacSystemFont', 'Helvetica Neue', 'Helvetica',
'Roboto', 'Arial', sans-serif;
font-size: 16px;
line-height: 1.6;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #1e1e1e;
color: #fcfcfc;
}
}
img {
width: 96px;
height: 96px;
}
h1 {
padding: 20px;
font-size: 32px;
}
</style>
</head>
<body>
<img
src="{{logoSrc}}"
alt="KeeWeb"
/>
<h1>{{res 'appBrowserAuthComplete'}}</h1>
</body>
</html>

View File

@ -80,7 +80,10 @@
<div class="open__pass-field-wrap"> <div class="open__pass-field-wrap">
<input class="open__pass-input" name="password" type="password" size="30" autocomplete="new-password" maxlength="1024" <input class="open__pass-input" name="password" type="password" size="30" autocomplete="new-password" maxlength="1024"
placeholder="{{#if canOpen}}{{res 'openClickToOpen'}}{{/if}}" readonly tabindex="23" /> placeholder="{{#if canOpen}}{{res 'openClickToOpen'}}{{/if}}" readonly tabindex="23" />
<div class="open__pass-enter-btn" tabindex="24"><i class="fa fa-level-down-alt rotate-90"></i></div> <div class="open__pass-enter-btn" tabindex="24">
<i class="fa fa-level-down-alt rotate-90 open__pass-enter-btn-icon-enter"></i>
<i class="fa fa-fingerprint open__pass-enter-btn-icon-touch-id"></i>
</div>
<div class="open__pass-opening-icon"><i class="fa fa-spinner spin"></i></div> <div class="open__pass-opening-icon"><i class="fa fa-spinner spin"></i></div>
</div> </div>
<div class="open__settings"> <div class="open__settings">

View File

@ -37,11 +37,12 @@
{{#if isDesktop}} {{#if isDesktop}}
<h3>Desktop modules</h3> <h3>Desktop modules</h3>
<ul> <ul>
<li><a href="https://github.com/antelle/node-stream-zip" target="_blank">node-stream-zip</a><span class="muted-color">, node.js library for fast reading of large ZIPs, &copy; 2015 Antelle</span></li>
<li><a href="https://github.com/ranisalt/node-argon2" target="_blank">node-argon2</a><span class="muted-color">, node.js bindings for Argon2 hashing algorithm, &copy; 2015 Ranieri Althoff</span></li> <li><a href="https://github.com/ranisalt/node-argon2" target="_blank">node-argon2</a><span class="muted-color">, node.js bindings for Argon2 hashing algorithm, &copy; 2015 Ranieri Althoff</span></li>
<li><a href="https://github.com/tessel/node-usb" target="_blank">node-usb</a><span class="muted-color">, improved USB library for Node.js, &copy; 2012 Nonolith Labs, LLC</span></li> <li><a href="https://github.com/tessel/node-usb" target="_blank">node-usb</a><span class="muted-color">, improved USB library for Node.js, &copy; 2012 Nonolith Labs, LLC</span></li>
<li><a href="https://github.com/atom/node-keytar" target="_blank">node-keytar</a><span class="muted-color">, native password node module, &copy; 2013 GitHub Inc.</span></li> <li><a href="https://github.com/atom/node-keytar" target="_blank">node-keytar</a><span class="muted-color">, native password node module, &copy; 2013 GitHub Inc.</span></li>
<li><a href="https://github.com/antelle/node-yubikey-chalresp" target="_blank">node-yubikey-chalresp</a><span class="muted-color">, YubiKey challenge-response API for node.js, &copy; 2020 Antelle</span></li> <li><a href="https://github.com/antelle/node-yubikey-chalresp" target="_blank">node-yubikey-chalresp</a><span class="muted-color">, YubiKey challenge-response API for node.js, &copy; 2020 Antelle</span></li>
<li><a href="https://github.com/antelle/node-secure-enclave" target="_blank">node-secure-enclave</a><span class="muted-color">, Secure Enclave module for node.js and Electron, &copy; 2020 Antelle</span></li>
<li><a href="https://github.com/antelle/node-keyboard-auto-type" target="_blank">keyboard-auto-type</a><span class="muted-color">, node.js bindings for keyboard-auto-type, &copy; 2021 Antelle</span></li>
</ul> </ul>
{{/if}} {{/if}}
@ -67,7 +68,7 @@
<h2>{{res 'setAboutLic'}}</h2> <h2>{{res 'setAboutLic'}}</h2>
<p>{{res 'setAboutLicComment'}}:</p> <p>{{res 'setAboutLicComment'}}:</p>
<p>Copyright &copy; 2020 Antelle https://antelle.net</p> <p>Copyright &copy; {{year}} Antelle https://antelle.net</p>
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated <p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the Software without restriction, including without limitation documentation files (the "Software"), to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,

View File

@ -66,6 +66,10 @@
</div> </div>
</div> </div>
</div> </div>
<div>
<input type="checkbox" class="settings__input input-base settings__general-auto-switch-theme" id="settings__general-auto-switch-theme" {{#if autoSwitchTheme}}checked{{/if}} />
<label for="settings__general-auto-switch-theme">{{res 'setGenAutoSwitchTheme'}}</label>
</div>
<div> <div>
<label for="settings__general-font-size">{{res 'setGenFontSize'}}:</label> <label for="settings__general-font-size">{{res 'setGenFontSize'}}:</label>
<select class="settings__general-font-size settings__select input-base" id="settings__general-font-size"> <select class="settings__general-font-size settings__select input-base" id="settings__general-font-size">
@ -80,7 +84,9 @@
<select class="settings__general-titlebar-style settings__select input-base" id="settings__general-titlebar-style"> <select class="settings__general-titlebar-style settings__select input-base" id="settings__general-titlebar-style">
<option value="default" {{#ifeq titlebarStyle 'default'}}selected{{/ifeq}}>{{res 'setGenTitlebarStyleDefault'}}</option> <option value="default" {{#ifeq titlebarStyle 'default'}}selected{{/ifeq}}>{{res 'setGenTitlebarStyleDefault'}}</option>
<option value="hidden" {{#ifeq titlebarStyle 'hidden'}}selected{{/ifeq}}>{{res 'setGenTitlebarStyleHidden'}}</option> <option value="hidden" {{#ifeq titlebarStyle 'hidden'}}selected{{/ifeq}}>{{res 'setGenTitlebarStyleHidden'}}</option>
<option value="hidden-inset" {{#ifeq titlebarStyle 'hidden-inset'}}selected{{/ifeq}}>{{res 'setGenTitlebarStyleHiddenInset'}}</option> {{#if supportsCustomTitleBarAndDraggableWindow}}
<option value="hidden-inset" {{#ifeq titlebarStyle 'hidden-inset'}}selected{{/ifeq}}>{{res 'setGenTitlebarStyleHiddenInset'}}</option>
{{/if}}
</select> </select>
</div> </div>
{{/if}} {{/if}}
@ -150,6 +156,11 @@
{{#if minimizeOnClose}}checked{{/if}} /> {{#if minimizeOnClose}}checked{{/if}} />
<label for="settings__general-minimize">{{res 'setGenMinInstead'}}</label> <label for="settings__general-minimize">{{res 'setGenMinInstead'}}</label>
</div> </div>
<div>
<input type="checkbox" class="settings__input input-base settings__general-minimize-on-field-copy" id="settings__general-minimize-on-field-copy"
{{#if minimizeOnFieldCopy}}checked{{/if}} />
<label for="settings__general-minimize-on-field-copy">{{res 'setGenMinOnFieldCopy'}}</label>
</div>
{{/if}} {{/if}}
{{#if canAutoType}} {{#if canAutoType}}
<div> <div>
@ -172,6 +183,85 @@
id="settings__general-use-group-icon-for-entries" {{#if useGroupIconForEntries}}checked{{/if}} /> id="settings__general-use-group-icon-for-entries" {{#if useGroupIconForEntries}}checked{{/if}} />
<label for="settings__general-use-group-icon-for-entries">{{res 'setGenUseGroupIconForEntries'}}</label> <label for="settings__general-use-group-icon-for-entries">{{res 'setGenUseGroupIconForEntries'}}</label>
</div> </div>
{{#if hasDeviceOwnerAuth}}
<div>
<label for="settings__general-device-owner-auth">{{res 'setGenTouchId'}}:</label>
<select class="settings__general-device-owner-auth settings__select input-base" id="settings__general-device-owner-auth">
<option value="" {{#unless deviceOwnerAuth}}selected{{/unless}}>{{res 'setGenTouchIdDisabled'}}</option>
<option value="memory" {{#ifeq deviceOwnerAuth 'memory'}}selected{{/ifeq}}>{{res 'setGenTouchIdMemory'}}</option>
<option value="file" {{#ifeq deviceOwnerAuth 'file'}}selected{{/ifeq}}>{{res 'setGenTouchIdFile'}}</option>
</select>
</div>
{{#if deviceOwnerAuth}}
<label for="settings__general-device-owner-auth-timeout">{{res 'setGenTouchIdPass'}}:</label>
<select class="settings__general-device-owner-auth-timeout settings__select input-base" id="settings__general-device-owner-auth-timeout">
<option value="1" {{#ifeq deviceOwnerAuthTimeout 1}}selected{{/ifeq}}>{{Res 'oneMinute'}}</option>
<option value="5" {{#ifeq deviceOwnerAuthTimeout 5}}selected{{/ifeq}}>{{#Res 'minutes'}}5{{/Res}}</option>
<option value="30" {{#ifeq deviceOwnerAuthTimeout 30}}selected{{/ifeq}}>{{#Res 'minutes'}}30{{/Res}}</option>
<option value="60" {{#ifeq deviceOwnerAuthTimeout 60}}selected{{/ifeq}}>{{Res 'oneHour'}}</option>
<option value="120" {{#ifeq deviceOwnerAuthTimeout 120}}selected{{/ifeq}}>{{#Res 'hours'}}2{{/Res}}</option>
<option value="480" {{#ifeq deviceOwnerAuthTimeout 480}}selected{{/ifeq}}>{{#Res 'hours'}}8{{/Res}}</option>
<option value="1440" {{#ifeq deviceOwnerAuthTimeout 1440}}selected{{/ifeq}}>{{Res 'oneDay'}}</option>
<option value="10080" {{#ifeq deviceOwnerAuthTimeout 10080}}selected{{/ifeq}}>{{Res 'oneWeek'}}</option>
{{#ifeq deviceOwnerAuth 'file'}}
<option value="43200" {{#ifeq deviceOwnerAuthTimeout 43200}}selected{{/ifeq}}>{{Res 'oneMonth'}}</option>
<option value="525600" {{#ifeq deviceOwnerAuthTimeout 525600}}selected{{/ifeq}}>{{Res 'oneYear'}}</option>
{{/ifeq}}
</select>
{{/if}}
{{/if}}
<h2 id="audit">{{res 'setGenAudit'}}</h2>
<div>
<input type="checkbox" class="settings__input input-base settings__general-audit-passwords"
id="settings__general-audit-passwords" {{#if auditPasswords}}checked{{/if}} />
<label for="settings__general-audit-passwords">{{res 'setGenAuditPasswords'}}</label>
</div>
<div>
<input type="checkbox" class="settings__input input-base settings__general-audit-password-entropy"
id="settings__general-audit-password-entropy" {{#if auditPasswordEntropy}}checked{{/if}} />
<label for="settings__general-audit-password-entropy">{{res 'setGenAuditPasswordEntropy'}}</label>
</div>
<div>
<input type="checkbox" class="settings__input input-base settings__general-exclude-pins-from-audit"
id="settings__general-exclude-pins-from-audit" {{#if excludePinsFromAudit}}checked{{/if}} />
<label for="settings__general-exclude-pins-from-audit">{{res 'setGenExcludePinsFromAudit'}}</label>
</div>
<div>
<input type="checkbox" class="settings__input input-base settings__general-check-passwords-on-hibp"
id="settings__general-check-passwords-on-hibp" {{#if checkPasswordsOnHIBP}}checked{{/if}} />
<label for="settings__general-check-passwords-on-hibp">
{{~#res 'setGenCheckPasswordsOnHIBP'~}}
<a href="{{hibpLink}}" rel="noreferrer noopener" target="_blank">Have I Been Pwned</a>
{{~/res~}}
</label>
<i class="fa fa-info-circle info-btn settings__general-toggle-help-hibp"></i>
<div class="settings__general-help-hibp hide">
{{~#res 'setGenHelpHIBP'~}}
<a href="{{hibpPrivacyLink}}" rel="noreferrer noopener" target="_blank">{{res 'setGenHelpHIBPLink'}}</a>
{{~/res~}}
</div>
</div>
<div>
<label for="settings__general-audit-password-age">{{res 'setGenAuditPasswordAge'}}:</label>
<select class="settings__select input-base settings__general-audit-password-age"
id="settings__general-audit-password-age">
<option value="0" {{#ifeq auditPasswordAge 0}}selected{{/ifeq}}>{{res 'setGenAuditPasswordAgeOff'}}</option>
<option value="1" {{#ifeq auditPasswordAge 1}}selected{{/ifeq}}>{{res 'setGenAuditPasswordAgeOneYear'}}</option>
<option value="2" {{#ifeq auditPasswordAge 2}}selected{{/ifeq}}>{{#res 'setGenAuditPasswordAgeYears'}}
2{{/res}}</option>
<option value="3" {{#ifeq auditPasswordAge 3}}selected{{/ifeq}}>{{#res 'setGenAuditPasswordAgeYears'}}
3{{/res}}</option>
<option value="5" {{#ifeq auditPasswordAge 5}}selected{{/ifeq}}>{{#res 'setGenAuditPasswordAgeYears'}}
5{{/res}}</option>
<option value="10" {{#ifeq auditPasswordAge 10}}selected{{/ifeq}}>{{#res 'setGenAuditPasswordAgeYears'}}
10{{/res}}</option>
</select>
</div>
<h2 id="lock">{{res 'setGenLock'}}</h2> <h2 id="lock">{{res 'setGenLock'}}</h2>
<div> <div>
@ -183,6 +273,8 @@
<option value="15" {{#ifeq idleMinutes 15}}selected{{/ifeq}}>{{#res 'setGenLockMinutes'}}15{{/res}}</option> <option value="15" {{#ifeq idleMinutes 15}}selected{{/ifeq}}>{{#res 'setGenLockMinutes'}}15{{/res}}</option>
<option value="30" {{#ifeq idleMinutes 30}}selected{{/ifeq}}>{{#res 'setGenLockMinutes'}}30{{/res}}</option> <option value="30" {{#ifeq idleMinutes 30}}selected{{/ifeq}}>{{#res 'setGenLockMinutes'}}30{{/res}}</option>
<option value="60" {{#ifeq idleMinutes 60}}selected{{/ifeq}}>{{res 'setGenLockHour'}}</option> <option value="60" {{#ifeq idleMinutes 60}}selected{{/ifeq}}>{{res 'setGenLockHour'}}</option>
<option value="180" {{#ifeq idleMinutes 180}}selected{{/ifeq}}>{{#res 'setGenLockHours'}}3{{/res}}</option>
<option value="360" {{#ifeq idleMinutes 360}}selected{{/ifeq}}>{{#res 'setGenLockHours'}}6{{/res}}</option>
<option value="720" {{#ifeq idleMinutes 720}}selected{{/ifeq}}>{{#res 'setGenLockHours'}}12{{/res}}</option> <option value="720" {{#ifeq idleMinutes 720}}selected{{/ifeq}}>{{#res 'setGenLockHours'}}12{{/res}}</option>
<option value="1440" {{#ifeq idleMinutes 1440}}selected{{/ifeq}}>{{res 'setGenLockDay'}}</option> <option value="1440" {{#ifeq idleMinutes 1440}}selected{{/ifeq}}>{{res 'setGenLockDay'}}</option>
</select> </select>
@ -215,6 +307,12 @@
{{/if}} {{/if}}
<h2 id="storage">{{res 'setGenStorage'}}</h2> <h2 id="storage">{{res 'setGenStorage'}}</h2>
<div>
<input type="checkbox" class="settings__input input-base settings__general-disable-offline-storage" id="settings__general-disable-offline-storage"
{{#if disableOfflineStorage}}checked{{/if}} />
<label for="settings__general-disable-offline-storage">{{res 'setGenDisableOfflineStorage'}}</label>
</div>
{{#each storageProviders as |prv|}} {{#each storageProviders as |prv|}}
<h4 class="settings__general-storage-header"><input <h4 class="settings__general-storage-header"><input
type="checkbox" id="settings__general-prv-check-{{prv.name}}" class="settings__general-prv-check" type="checkbox" id="settings__general-prv-check-{{prv.name}}" class="settings__general-prv-check"
@ -228,6 +326,13 @@
<h2 id="advanced">{{res 'advanced'}}</h2> <h2 id="advanced">{{res 'advanced'}}</h2>
<a class="settings__general-show-advanced">{{res 'setGenShowAdvanced'}}</a> <a class="settings__general-show-advanced">{{res 'setGenShowAdvanced'}}</a>
<div class="settings__general-advanced hide"> <div class="settings__general-advanced hide">
{{#if canAutoType}}
<div>
<input type="checkbox" class="settings__input input-base settings__general-use-legacy-autotype"
id="settings__general-use-legacy-autotype" {{#if useLegacyAutoType}}checked{{/if}} />
<label for="settings__general-use-legacy-autotype">{{res 'setGenUseLegacyAutoType'}}</label>
</div>
{{/if}}
{{#if devTools}} {{#if devTools}}
<button class="btn-silent settings__general-dev-tools-link">{{res 'setGenDevTools'}}</button> <button class="btn-silent settings__general-dev-tools-link">{{res 'setGenDevTools'}}</button>
<button class="btn-silent settings__general-try-beta-link">{{res 'setGenTryBeta'}}</button> <button class="btn-silent settings__general-try-beta-link">{{res 'setGenTryBeta'}}</button>

View File

@ -0,0 +1,15 @@
<div class="titlebar">
<div class="titlebar__icon">
<img src="{{iconSrc}}" alt="logo" class="titlebar__logo" />
</div>
<div class="titlebar__grow"></div>
<i class="fa fa-titlebar-minimize titlebar__minimize"></i>
{{#if maximized}}
<i class="fa fa-titlebar-restore titlebar__restore"></i>
{{else}}
<i class="fa fa-titlebar-maximize titlebar__maximize"></i>
{{/if}}
<i class="fa fa-titlebar-close titlebar__close"></i>
</div>

4
app/update.json Normal file
View File

@ -0,0 +1,4 @@
{
"version": "0.0.0",
"date": "0000-00-00"
}

Some files were not shown because too many files have changed in this diff Show More