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
npm ci
cd desktop
npm ci
cd /github/workspace
grunt desktop-linux

View File

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

View File

@ -25,16 +25,24 @@ module.exports = function (grunt) {
const dt = date.toISOString().replace(/T.*/, '');
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 skipSign = grunt.option('skip-sign');
const getCodeSignConfig = () =>
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 = {
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({
noop: { noop: {} },
clean: {
@ -127,18 +144,6 @@ module.exports = function (grunt) {
expand: true,
nonull: true
},
'desktop-update': {
cwd: 'tmp/desktop/keeweb-linux-x64/resources/',
src: 'app.asar',
dest: 'tmp/desktop/update/',
expand: true,
nonull: true
},
'desktop-update-helper': {
src: ['helper/darwin/KeeWebHelper', 'helper/win32/KeeWebHelper.exe'],
dest: 'tmp/desktop/update/',
nonull: true
},
'desktop-darwin-helper-x64': {
src: 'helper/darwin/KeeWebHelper',
dest: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Resources/',
@ -152,7 +157,7 @@ module.exports = function (grunt) {
options: { mode: '0755' }
},
'desktop-darwin-installer-helper-x64': {
cwd: 'package/osx/KeeWeb Installer.app',
cwd: 'tmp/desktop/KeeWeb Installer.app',
src: '**',
dest:
'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app/Contents/Installer/KeeWeb Installer.app',
@ -161,7 +166,7 @@ module.exports = function (grunt) {
options: { mode: true }
},
'desktop-darwin-installer-helper-arm64': {
cwd: 'package/osx/KeeWeb Installer.app',
cwd: 'tmp/desktop/KeeWeb Installer.app',
src: '**',
dest:
'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`,
dest: `dist/desktop/KeeWeb-${pkg.version}.linux.AppImage`,
nonull: true
},
'darwin-installer-icon': {
src: 'graphics/icon.icns',
dest: 'tmp/desktop/KeeWeb Installer.app/Contents/Resources/applet.icns',
nonull: true
}
},
eslint: {
@ -250,7 +260,8 @@ module.exports = function (grunt) {
desktop: ['desktop/**/*.js', '!desktop/node_modules/**'],
build: ['Gruntfile.js', 'grunt.*.js', 'build/**/*.js', 'webpack.config.js'],
plugins: ['plugins/**/*.js'],
util: ['util/**/*.js']
util: ['util/**/*.js'],
installer: ['package/osx/installer.js']
},
inline: {
app: {
@ -263,7 +274,7 @@ module.exports = function (grunt) {
algo: 'sha512',
expected: {
style: 1,
script: 2
script: 1
}
},
app: {
@ -283,20 +294,20 @@ module.exports = function (grunt) {
}
},
'string-replace': {
manifest: {
'update-manifest': {
options: {
replacements: [
{
pattern: '# YYYY-MM-DD:v0.0.0',
replacement: '# ' + dt + ':v' + pkg.version
pattern: /"version":\s*".*?"/,
replacement: `"version": "${pkg.version}"`
},
{
pattern: '# updmin:v0.0.0',
replacement: '# updmin:v' + minElectronVersionForUpdate
pattern: /"date":\s*".*?"/,
replacement: `"date": "${dt}"`
}
]
},
files: { 'dist/manifest.appcache': 'app/manifest.appcache' }
files: { 'dist/update.json': 'app/update.json' }
},
'service-worker': {
options: { replacements: [{ pattern: '0.0.0', replacement: pkg.version }] },
@ -437,26 +448,37 @@ module.exports = function (grunt) {
category: 'Utility'
},
rpm: {
// depends: ['libappindicator1', 'libgconf-2-4', 'gnome-keyring']
// depends: linuxDependencies
},
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: {
options: {
level: 6
},
'desktop-update': {
options: {
archive: 'dist/desktop/UpdateDesktop.zip',
comment: zipCommentPlaceholder
},
files: [{ cwd: 'tmp/desktop/update', src: '**', expand: true, nonull: true }]
},
'win32-x64': {
options: { archive: `dist/desktop/KeeWeb-${pkg.version}.win.x64.zip` },
files: [{ cwd: 'tmp/desktop/KeeWeb-win32-x64', src: '**', expand: true }]
@ -565,7 +587,7 @@ module.exports = function (grunt) {
pkgName: `KeeWeb-${pkg.version}.linux.x64.deb`,
targetDir: 'dist/desktop',
appName: 'KeeWeb',
depends: 'libappindicator1, libgconf-2-4, gnome-keyring',
depends: linuxDependencies.join(', '),
scripts: {
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': {
options: {
get identity() {
return getCodeSignConfig().identities.app;
},
hardenedRuntime: true,
entitlements: 'package/osx/entitlements.mac.plist',
'entitlements-inherit': 'package/osx/entitlements.mac.plist',
entitlements: 'package/osx/entitlements.plist',
'entitlements-inherit': 'package/osx/entitlements-inherit.plist',
'gatekeeper-assess': false
},
'desktop-x64': {
options: {
'provisioning-profile': './keys/keeweb.provisionprofile'
},
src: 'tmp/desktop/KeeWeb-darwin-x64/KeeWeb.app'
},
'desktop-arm64': {
options: {
'provisioning-profile': './keys/keeweb.provisionprofile'
},
src: 'tmp/desktop/KeeWeb-darwin-arm64/KeeWeb.app'
},
'installer': {
src: 'tmp/desktop/KeeWeb Installer.app'
}
},
notarize: {
@ -747,10 +749,7 @@ module.exports = function (grunt) {
sign: 'dist/desktop/Verify.sign.sha256'
},
files: {
'dist/desktop/Verify.sha256': [
'dist/desktop/KeeWeb-*',
'dist/desktop/UpdateDesktop.zip'
]
'dist/desktop/Verify.sha256': ['dist/desktop/KeeWeb-*']
}
}
},

View File

@ -1,6 +1,6 @@
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
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:
- 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)
- 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)

View File

@ -72,7 +72,6 @@
<link rel="manifest" href="manifest.json" />
<link rel="stylesheet" href="css/app.css?__inline=true" />
<script src="js/app.js?__inline=true"></script>
<script src="js/runtime.js?__inline=true"></script>
</head>
<body class="th-d">
<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 { FocusDetector } from 'comp/browser/focus-detector';
import { IdleTracker } from 'comp/browser/idle-tracker';
import { ThemeWatcher } from 'comp/browser/theme-watcher';
import { KeyHandler } from 'comp/browser/key-handler';
import { PopupNotifier } from 'comp/browser/popup-notifier';
import { Launcher } from 'comp/launcher';
@ -91,6 +92,8 @@ ready(() => {
KdbxwebInit.init();
FocusDetector.init();
AutoType.init();
ThemeWatcher.init();
SettingsManager.init();
window.kw = ExportApi;
return PluginManager.init().then(() => {
StartProfiler.milestone('initializing modules');
@ -111,13 +114,13 @@ ready(() => {
function loadRemoteConfig() {
return Promise.resolve()
.then(() => {
SettingsManager.setBySettings(appModel.settings);
SettingsManager.setBySettings();
const configParam = getConfigParam();
if (configParam) {
return appModel
.loadConfig(configParam)
.then(() => {
SettingsManager.setBySettings(appModel.settings);
SettingsManager.setBySettings();
})
.catch((e) => {
if (!appModel.settings.cacheConfigSettings) {

View File

@ -1,10 +1,15 @@
import { Launcher } from 'comp/launcher';
import { AppSettingsModel } from 'models/app-settings-model';
import { AutoTypeEmitter } from 'auto-type/auto-type-emitter';
const AutoTypeEmitterFactory = {
create(callback, windowId) {
if (Launcher && Launcher.autoTypeSupported) {
const { AutoTypeEmitter } = require('./emitter/auto-type-emitter-' +
Launcher.platform());
if (AppSettingsModel.useLegacyAutoType) {
const { AutoTypeEmitter } = require('./emitter/auto-type-emitter-' +
Launcher.platform());
return new AutoTypeEmitter(callback, windowId);
}
return new AutoTypeEmitter(callback, windowId);
}
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 { AppSettingsModel } from 'models/app-settings-model';
import { AutoTypeHelper } from 'auto-type/auto-type-helper';
const AutoTypeHelperFactory = {
create() {
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 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 { StringFormat } from 'util/formatting/string-format';
import { Logger } from 'util/logger';
import { AppSettingsModel } from 'models/app-settings-model';
const emitterLogger = new Logger(
'auto-type-emitter',
undefined,
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Warn
localStorage.debugAutoType ? Logger.Level.All : Logger.Level.Info
);
const AutoTypeRunner = function (ops) {
@ -239,7 +240,7 @@ AutoTypeRunner.prototype.tryParseCommand = function (op) {
// {VKEY 10} {VKEY 0x1F}
op.type = 'key';
op.value = parseInt(op.arg);
if (isNaN(op.value) || op.value <= 0) {
if (isNaN(op.value) || op.value < 0) {
throw 'Bad vkey: ' + op.arg;
}
return true;
@ -432,6 +433,8 @@ AutoTypeRunner.prototype.obfuscateOp = function (op) {
};
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.emitterState = {
callback,
@ -442,7 +445,7 @@ AutoTypeRunner.prototype.run = function (callback, windowId) {
activeMod: {},
finished: null
};
this.emitNext();
this.emitter.begin();
};
AutoTypeRunner.prototype.emitNext = function (err) {

View File

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

View File

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

View File

@ -58,6 +58,7 @@ const AppRightsChecker = {
runInstaller() {
Launcher.spawn({
cmd: this.AppPath + '/Contents/Installer/KeeWeb Installer.app/Contents/MacOS/applet',
args: ['--install'],
complete: () => {
this.needRunInstaller((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 { RuntimeInfo } from 'const/runtime-info';
import { Transport } from 'comp/browser/transport';
@ -15,10 +16,9 @@ const Updater = {
UpdateInterval: 1000 * 60 * 60 * 24,
MinUpdateTimeout: 500,
MinUpdateSize: 10000,
UpdateCheckFiles: ['app.asar'],
nextCheckTimeout: null,
updateCheckDate: new Date(0),
enabled: Launcher && Launcher.updaterEnabled(),
enabled: Launcher?.updaterEnabled(),
getAutoUpdateType() {
if (!this.enabled) {
@ -34,7 +34,7 @@ const Updater = {
updateInProgress() {
return (
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...');
Transport.httpGet({
url: Links.Manifest,
utf8: true,
success: (data) => {
url: Links.UpdateJson,
json: true,
success: (updateJson) => {
const dt = new Date();
const match = data.match(/#\s*(\d+\-\d+\-\d+):v([\d+\.\w]+)/);
logger.info('Update check: ' + (match ? match[0] : 'unknown'));
if (!match) {
logger.info('Update check: ' + (updateJson.version || 'unknown'));
if (!updateJson.version) {
const errMsg = 'No version info found';
UpdateModel.set({
status: 'error',
@ -114,16 +113,15 @@ const Updater = {
this.scheduleNextCheck();
return;
}
const updateMinVersionMatch = data.match(/#\s*updmin:v([\d+\.\w]+)/);
const prevLastVersion = UpdateModel.lastVersion;
UpdateModel.set({
status: 'ok',
lastCheckDate: dt,
lastSuccessCheckDate: dt,
lastVersionReleaseDate: new Date(match[1]),
lastVersion: match[2],
lastVersionReleaseDate: new Date(updateJson.date),
lastVersion: updateJson.version,
lastCheckError: null,
lastCheckUpdMin: updateMinVersionMatch ? updateMinVersionMatch[1] : null
lastCheckUpdMin: updateJson.minVersion || null
});
UpdateModel.save();
this.scheduleNextCheck();
@ -161,7 +159,7 @@ const Updater = {
canAutoUpdate() {
const minLauncherVersion = UpdateModel.lastCheckUpdMin;
if (minLauncherVersion) {
const cmp = SemVer.compareVersions(Launcher.version, minLauncherVersion);
const cmp = SemVer.compareVersions(RuntimeInfo.version, minLauncherVersion);
if (cmp < 0) {
UpdateModel.set({ updateStatus: 'ready', updateManual: true });
return false;
@ -182,28 +180,55 @@ const Updater = {
}
UpdateModel.set({ updateStatus: 'downloading', updateError: null });
logger.info('Downloading update', ver);
const updateAssetName = this.getUpdateAssetName(ver);
if (!updateAssetName) {
logger.error('Empty updater asset name for', Launcher.platform(), Launcher.arch());
return;
}
const updateUrlBasePath = Links.UpdateBasePath.replace('{ver}', ver);
const updateAssetUrl = updateUrlBasePath + updateAssetName;
Transport.httpGet({
url: Links.UpdateDesktop.replace('{ver}', ver),
file: 'KeeWeb-' + ver + '.zip',
cache: !startedByUser,
success: (filePath) => {
UpdateModel.set({ updateStatus: 'extracting' });
logger.info('Extracting update file', this.UpdateCheckFiles, filePath);
this.extractAppUpdate(filePath, (err) => {
if (err) {
logger.error('Error extracting update', err);
url: updateAssetUrl,
file: updateAssetName,
cleanupOldFiles: true,
cache: true,
success: (assetFilePath) => {
logger.info('Downloading update signatures');
Transport.httpGet({
url: updateUrlBasePath + 'Verify.sign.sha256',
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({
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) {
const expectedFiles = this.UpdateCheckFiles;
const appPath = Launcher.getUserDataPath();
const StreamZip = Launcher.req('node-stream-zip');
StreamZip.setFs(Launcher.req('original-fs'));
const zip = new StreamZip({ file: updateFile, storeEntries: true });
zip.on('error', cb);
zip.on('ready', () => {
const containsAll = expectedFiles.every((expFile) => {
const entry = zip.entry(expFile);
return entry && entry.isFile;
verifySignature(assetFilePath, assetName, callback) {
logger.info('Verifying update signature', assetName);
const fs = Launcher.req('fs');
const signaturesTxt = fs.readFileSync(assetFilePath + '.sign', 'utf8');
const assetSignatureLine = signaturesTxt
.split('\n')
.find((line) => line.endsWith(assetName));
if (!assetSignatureLine) {
logger.error('Signature not found for asset', assetName);
callback('Asset signature not found');
return;
}
const signature = kdbxweb.ByteUtils.hexToBytes(assetSignatureLine.split(' ')[0]);
const fileBytes = fs.readFileSync(assetFilePath);
SignatureVerifier.verify(fileBytes, signature)
.catch((e) => {
logger.error('Error verifying signature', e);
callback('Error verifying signature');
})
.then((valid) => {
logger.info(`Update asset signature is ${valid ? 'valid' : 'invalid'}`);
callback(undefined, valid);
});
if (!containsAll) {
return cb('Bad archive');
}
this.validateArchiveSignature(updateFile, zip)
.then(() => {
zip.extract(null, appPath, (err) => {
zip.close();
if (err) {
return cb(err);
}
Launcher.deleteFile(updateFile);
cb();
});
})
.catch((e) => {
return cb('Invalid archive: ' + e);
});
});
},
validateArchiveSignature(archivePath, zip) {
if (!zip.comment) {
return Promise.reject('No comment in ZIP');
getUpdateAssetName(ver) {
const platform = Launcher.platform();
const arch = Launcher.arch();
switch (platform) {
case 'win32':
switch (arch) {
case 'x64':
return `KeeWeb-${ver}.win.x64.exe`;
case 'ia32':
return `KeeWeb-${ver}.win.ia32.exe`;
case 'arm64':
return `KeeWeb-${ver}.win.arm64.exe`;
}
break;
case 'darwin':
switch (arch) {
case 'x64':
return `KeeWeb-${ver}.mac.x64.dmg`;
case 'arm64':
return `KeeWeb-${ver}.mac.arm64.dmg`;
}
break;
}
if (zip.comment.length !== 512) {
return Promise.reject('Bad comment length in ZIP: ' + zip.comment.length);
}
try {
const zipFileData = Launcher.req('fs').readFileSync(archivePath);
const dataToVerify = zipFileData.slice(0, zip.centralDirectory.headerOffset + 22);
const signature = window.Buffer.from(zip.comment, 'hex');
return SignatureVerifier.verify(dataToVerify, signature)
.catch(() => {
throw new Error('Error verifying signature');
})
.then((isValid) => {
if (!isValid) {
throw new Error('Invalid signature');
}
});
} catch (err) {
return Promise.reject(err.toString());
return undefined;
},
installAndRestart() {
if (!Launcher) {
return;
}
const updateAssetName = this.getUpdateAssetName(UpdateModel.lastVersion);
const updateFilePath = Transport.cacheFilePath(updateAssetName);
Launcher.requestRestartAndUpdate(updateFilePath);
}
};

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

View File

@ -17,6 +17,9 @@ const Launcher = {
platform() {
return process.platform;
},
arch() {
return process.arch;
},
electron() {
return this.req('electron');
},
@ -55,7 +58,15 @@ const Launcher = {
return this.joinPath(this.userDataPath, fileName || '');
},
getTempPath(fileName) {
return this.joinPath(this.remoteApp().getPath('temp'), fileName || '');
let tempPath = this.joinPath(this.remoteApp().getPath('temp'), 'KeeWeb');
const fs = this.req('fs');
if (!fs.existsSync(tempPath)) {
fs.mkdirSync(tempPath);
}
if (fileName) {
tempPath = this.joinPath(tempPath, fileName);
}
return tempPath;
},
getDocumentsPath(fileName) {
return this.joinPath(this.remoteApp().getPath('documents'), fileName || '');
@ -164,18 +175,18 @@ const Launcher = {
requestExit() {
const app = this.remoteApp();
app.setHookBeforeQuitEvent(false);
if (this.restartPending) {
app.restartApp();
if (this.pendingUpdateFile) {
app.restartAndUpdate(this.pendingUpdateFile);
} else {
app.quit();
}
},
requestRestart() {
this.restartPending = true;
requestRestartAndUpdate(updateFilePath) {
this.pendingUpdateFile = updateFilePath;
this.requestExit();
},
cancelRestart() {
this.restartPending = false;
this.pendingUpdateFile = undefined;
},
setClipboardText(text) {
return this.electron().clipboard.writeText(text);
@ -203,7 +214,7 @@ const Launcher = {
return process.platform !== 'linux';
},
updaterEnabled() {
return this.electron().remote.process.argv.indexOf('--disable-updater') === -1;
return process.platform !== 'linux';
},
getMainWindow() {
return this.remoteApp().getMainWindow();
@ -301,6 +312,18 @@ const Launcher = {
},
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);
});
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('start-profile', (data) => StartProfiler.reportAppProfile(data));
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 { Logger } from 'util/logger';
import { Launcher } from 'comp/launcher';
@ -8,11 +9,18 @@ let NativeModules;
if (Launcher) {
const logger = new Logger('native-module-connector');
let host;
let hostRunning = false;
let hostStartPromise;
let callId = 0;
let promises = {};
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 = {
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];
if (callback) {
const willBeCalledAgain = error && error.touchRequested;
@ -49,39 +57,39 @@ if (Launcher) {
NativeModules = {
startHost() {
if (host) {
return;
if (hostRunning) {
return Promise.resolve();
}
if (hostStartPromise) {
return hostStartPromise;
}
logger.debug('Starting native module host');
const path = Launcher.req('path');
const appContentRoot = Launcher.remoteApp().getAppContentRoot();
const mainModulePath = path.join(appContentRoot, 'native-module-host.js');
hostStartPromise = this.callNoWait('start').then(() => {
hostStartPromise = undefined;
hostRunning = true;
const { fork } = Launcher.req('child_process');
if (this.usbListenerRunning) {
return this.call('startUsbListener');
}
});
host = fork(mainModulePath);
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');
}
return hostStartPromise;
},
hostError(e) {
logger.error('Host error', e);
},
hostDisconnect() {
logger.error('Host disconnected');
},
hostExit(code, sig) {
logger.error(`Host exited with code ${code} and signal ${sig}`);
host = null;
hostRunning = false;
const err = new Error('Native module host crashed');
@ -121,56 +129,109 @@ if (Launcher) {
},
call(cmd, ...args) {
return new Promise((resolve, reject) => {
if (!host) {
try {
this.startHost();
} catch (e) {
return reject(e);
}
}
return this.startHost().then(() => this.callNoWait(cmd, ...args));
},
callNoWait(cmd, ...args) {
return new Promise((resolve, reject) => {
callId++;
if (callId === Number.MAX_SAFE_INTEGER) {
callId = 1;
}
// logger.debug('Call', cmd, args, callId);
promises[callId] = { cmd, resolve, reject };
host.send({ cmd, args, callId });
ipcRenderer.send('nativeModuleCall', { cmd, args, callId });
});
},
startUsbListener() {
this.call('start-usb');
this.call('startUsbListener');
this.usbListenerRunning = true;
},
stopUsbListener() {
this.usbListenerRunning = false;
if (host) {
this.call('stop-usb');
if (hostRunning) {
this.call('stopUsbListener');
}
},
getYubiKeys(config) {
return this.call('get-yubikeys', config);
return this.call('getYubiKeys', config);
},
yubiKeyChallengeResponse(yubiKey, challenge, slot, callback) {
ykChalRespCallbacks[callId] = callback;
return this.call('yk-chal-resp', yubiKey, challenge, slot, callId);
return this.call('yubiKeyChallengeResponse', yubiKey, challenge, slot, callId);
},
yubiKeyCancelChallengeResponse() {
if (host) {
this.call('yk-cancel-chal-resp');
if (hostRunning) {
this.call('yubiKeyCancelChallengeResponse');
}
},
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 };

View File

@ -1,6 +1,11 @@
import { Events } from 'framework/events';
import { Features } from 'util/features';
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 = {
neutralLocale: null,
@ -16,23 +21,65 @@ const SettingsManager = {
allThemes: {
dark: 'setGenThemeDark',
light: 'setGenThemeLight',
fb: 'setGenThemeFb',
db: 'setGenThemeDb',
sd: 'setGenThemeSd',
sl: 'setGenThemeSl',
fb: 'setGenThemeFb',
bl: 'setGenThemeBl',
db: 'setGenThemeDb',
lb: 'setGenThemeLb',
te: 'setGenThemeTe',
lt: 'setGenThemeLt',
dc: 'setGenThemeDc',
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: {},
setBySettings(settings) {
this.setTheme(settings.theme);
this.setFontSize(settings.fontSize);
const locale = settings.locale;
init() {
Events.on('dark-mode-changed', () => this.darkModeChanged());
},
setBySettings() {
this.setTheme(AppSettingsModel.theme);
this.setFontSize(AppSettingsModel.fontSize);
const locale = AppSettingsModel.locale;
try {
if (locale) {
this.setLocale(settings.locale);
this.setLocale(AppSettingsModel.locale);
} else {
this.setLocale(this.getBrowserLocale());
}
@ -55,18 +102,45 @@ const SettingsManager = {
document.body.classList.remove(cls);
}
}
if (AppSettingsModel.autoSwitchTheme) {
theme = this.selectDarkOrLightTheme(theme);
}
document.body.classList.add(this.getThemeClass(theme));
const metaThemeColor = document.head.querySelector('meta[name=theme-color]');
if (metaThemeColor) {
metaThemeColor.content = window.getComputedStyle(document.body).backgroundColor;
}
this.activeTheme = theme;
logger.debug('Theme changed', theme);
Events.emit('theme-applied');
},
getThemeClass(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) {
const defaultFontSize = Features.isMobile ? 14 : 12;
document.documentElement.style.fontSize = defaultFontSize + (fontSize || 0) * 2 + 'px';

View File

@ -1,5 +1,6 @@
const DefaultAppSettings = {
theme: null, // UI theme
autoSwitchTheme: false, // automatically switch between light and dark theme
locale: null, // user interface language
expandGroups: true, // show entries from all subgroups
listViewWidth: null, // width of the entry list representation
@ -12,6 +13,7 @@ const DefaultAppSettings = {
rememberKeyFiles: 'path', // remember keyfiles selected on the Open screen
idleMinutes: 15, // app lock timeout after inactivity, minutes
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
colorfulIcons: false, // use colorful custom icons instead of grayscale
useMarkdown: true, // use Markdown in Notes field
@ -36,6 +38,15 @@ const DefaultAppSettings = {
useGroupIconForEntries: false, // automatically use group icon when creating new entries
enableUsb: true, // enable interaction with USB devices
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
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',
LicenseApache: 'https://opensource.org/licenses/Apache-2.0',
LicenseLinkCCBY40: 'https://creativecommons.org/licenses/by/4.0/',
UpdateDesktop: 'https://github.com/keeweb/keeweb/releases/download/v{ver}/UpdateDesktop.zip',
UpdateBasePath: 'https://github.com/keeweb/keeweb/releases/download/v{ver}/',
ReleaseNotes: 'https://github.com/keeweb/keeweb/blob/master/release-notes.md#release-notes',
SelfHostedDropbox: 'https://github.com/keeweb/keeweb#self-hosting',
Manifest: 'https://app.keeweb.info/manifest.appcache',
UpdateJson: 'https://app.keeweb.info/update.json',
AutoType: 'https://github.com/keeweb/keeweb/wiki/Auto-Type',
AutoTypeMacOS: 'https://github.com/keeweb/keeweb/wiki/Auto-Type#macos',
Translation: 'https://keeweb.oneskyapp.com/',
Donation: 'https://opencollective.com/keeweb#support',
Plugins: 'https://plugins.keeweb.info',
PluginDevelopStart: 'https://github.com/keeweb/keeweb/wiki/Plugins',
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 };

View File

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

View File

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

View File

@ -26,6 +26,15 @@
"shiftKey": "shift",
"altKey": "alt",
"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",
"file": "file",
@ -301,6 +310,16 @@
"detRevealField": "Reveal",
"detHideField": "Hide",
"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",
"autoTypeModifiers": "Modifier keys",
@ -308,6 +327,7 @@
"autoTypeLink": "more...",
"autoTypeError": "Auto-type error",
"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",
"autoTypeErrorNotInstalled": "{} is not installed",
"autoTypeHeader": "Auto-Type: Select",
@ -367,19 +387,30 @@
"setGenExtractingUpdate": "Extracting update...",
"setGenCheckErr": "There was an error downloading new version",
"setGenNeverChecked": "Never checked for updates",
"setGenRestartToUpdate": "Restart the app to update",
"setGenRestartToUpdate": "Restart KeeWeb to update",
"setGenDownloadAndRestart": "Download update and restart",
"setGenAppearance": "Appearance",
"setGenTheme": "Theme",
"setGenThemeDefault": "Default",
"setGenThemeDark": "Dark",
"setGenThemeLight": "Light",
"setGenThemeFb": "Flat blue",
"setGenThemeBlue": "Flat blue",
"setGenThemeFb": "Dark blue",
"setGenThemeBl": "Light blue",
"setGenThemeBrown": "Brownie",
"setGenThemeDb": "Dark brown",
"setGenThemeLb": "Light brown",
"setGenThemeTerminal": "Terminal",
"setGenThemeTe": "Terminal",
"setGenThemeLt": "Terminal light",
"setGenThemeHighContrast": "High contrast",
"setGenThemeHc": "High contrast",
"setGenThemeDc": "Dark contrast",
"setGenThemeSol": "Solarized",
"setGenThemeSd": "Solarized dark",
"setGenThemeSl": "Solarized light",
"setGenMoreThemes": "More themes",
"setGenAutoSwitchTheme": "Automatically switch between light and dark theme when possible",
"setGenLocale": "Language",
"setGenLocOther": "other languages are available as plugins",
"setGenFontSize": "Font size",
@ -416,12 +447,14 @@
"setGenClearSeconds": "In {} seconds",
"setGenClearMinute": "In a minute",
"setGenMinInstead": "Minimize the app instead of close",
"setGenMinOnFieldCopy": "Minimize on field copy",
"setGenLock": "Auto lock",
"setGenLockMinimize": "When the app is minimized",
"setGenLockCopy": "On password copy",
"setGenLockAutoType": "On auto-type",
"setGenLockOrSleep": "When the computer is locked or put to sleep",
"setGenStorage": "Storage",
"setGenDisableOfflineStorage": "Don't cache loaded files in offline storage",
"setGenStorageLogout": "Log out",
"setGenShowAdvanced": "Show advanced settings",
"setGenDevTools": "Show dev tools",
@ -431,6 +464,23 @@
"setGenShowAppLogs": "Show app logs",
"setGenReloadApp": "Reload the app",
"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",
"setFileStorage": "This file is loaded from {}.",
@ -661,5 +711,7 @@
"yubiKeyTouchRequestedBody": "Please touch your YubiKey with serial number {}",
"yubiKeyDisabledErrorHeader": "USB is disabled",
"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",
"altKey": "Alt",
"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",
"file": "Datei",
"device": "Gerät",
@ -408,6 +417,11 @@
"setGenShowAppLogs": "App-Logs anzeigen",
"setGenReloadApp": "App neu laden",
"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",
"setFileStorage": "Diese Datei wird von {} geladen.",
"setFileIntl": "Diese Datei ist im internen App-Speicher abgelegt",

View File

@ -26,6 +26,15 @@
"shiftKey": "shift",
"altKey": "alt",
"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",
"file": "fichier",
"device": "appareil",
@ -87,7 +96,7 @@
"tagExists": "Ce tag existe déjà",
"tagExistsBody": "Un tag existe déjà avec ce nom. Merci de choisir un autre nom.",
"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",
"genPsCreate": "Nouveau préréglage",
"genPsDelete": "Supprimer préréglage",
@ -122,7 +131,7 @@
"listNoWebsite": "aucun site web",
"listNoUser": "aucun utilisateur",
"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.",
"listAddTemplateBody2": "Vous pouvez toujours retrouver vos modèles dans le groupe {}.",
"searchAddNew": "Ajouter Nouveau",
@ -237,7 +246,7 @@
"detDelToTrashBody": "L'entrée sera déplacée dans la corbeille.",
"detFieldCopied": "Copié",
"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",
"detClickToAddField": "cliquez pour ajouter un nouveau champ",
"detMenuAddNewField": "Ajouter nouveau champ",
@ -286,6 +295,12 @@
"detRevealField": "Révéler",
"detHideField": "Cacher",
"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",
"autoTypeModifiers": "Touches modificatrices",
"autoTypeKeys": "Clés",
@ -349,7 +364,7 @@
"setGenExtractingUpdate": "Décompression 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",
"setGenRestartToUpdate": "Redémarrer pour mettre à jour",
"setGenRestartToUpdate": "Redémarrer KeeWeb pour mettre à jour",
"setGenDownloadAndRestart": "Télécharger la mise à jour et redémarrer",
"setGenAppearance": "Apparence",
"setGenTheme": "Thème",
@ -424,6 +439,22 @@
"setGenShowAppLogs": "Voir les logs",
"setGenReloadApp": "Recharger l'application",
"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",
"setFileStorage": "Le fichier est ouvert de {}.",
"setFileIntl": "Le fichier est conservé dans le stockage interne de l'application",
@ -615,7 +646,7 @@
"gdriveSharedWithMe": "Partagé avec moi",
"webdavSaveMethod": "Méthode de sauvegarde",
"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",
"webdavStatReload": "Toujours recharger le fichier au lieu de se fier à l'entête HTTP \"Last-Modified\"",
"launcherSave": "Sauvegarder base des mots de passe",
@ -640,5 +671,6 @@
"yubiKeyTouchRequestedBody": "Merci de toucher votre YubiKey avec le numéro de série {}",
"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",
"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 { FileInfoCollection } from 'collections/file-info-collection';
import { RuntimeInfo } from 'const/runtime-info';
import { Launcher } from 'comp/launcher';
import { UsbListener } from 'comp/app/usb-listener';
import { NativeModules } from 'comp/launcher/native-modules';
import { Timeouts } from 'const/timeouts';
import { AppSettingsModel } from 'models/app-settings-model';
import { EntryModel } from 'models/entry-model';
@ -37,6 +37,7 @@ class AppModel {
isBeta = RuntimeInfo.beta;
advancedSearch = null;
attachedYubiKeysCount = 0;
memoryPasswordStorage = {};
constructor() {
Events.on('refresh', this.refresh.bind(this));
@ -525,7 +526,8 @@ class AppModel {
fileInfo &&
fileInfo.openDate &&
fileInfo.rev === params.rev &&
fileInfo.storage !== 'file'
fileInfo.storage !== 'file' &&
!this.settings.disableOfflineStorage
) {
logger.info('Open file from cache because it is latest');
this.openFileFromCache(
@ -546,7 +548,12 @@ class AppModel {
},
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);
} else {
logger.info('Open file from cache, will sync after load', params.storage);
@ -594,7 +601,7 @@ class AppModel {
logger.info('Load from storage');
storage.load(params.path, params.opts, (err, data, stat) => {
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);
this.openFileFromCache(params, callback, fileInfo);
} else {
@ -618,7 +625,8 @@ class AppModel {
!noCache &&
fileInfo &&
storage.name !== 'file' &&
(err || (stat && stat.rev === cacheRev))
(err || (stat && stat.rev === cacheRev)) &&
!this.settings.disableOfflineStorage
) {
logger.info(
'Open file from cache because ' + (err ? 'stat error' : 'it is latest'),
@ -663,10 +671,13 @@ class AppModel {
path: params.path,
keyFileName: params.keyFileName,
keyFilePath: params.keyFilePath,
backup: (fileInfo && fileInfo.backup) || null,
fingerprint: (fileInfo && fileInfo.fingerprint) || null,
backup: fileInfo?.backup || null,
chalResp: params.chalResp
});
if (params.encryptedPassword) {
file.encryptedPassword = fileInfo.encryptedPassword;
file.encryptedPasswordDate = fileInfo?.encryptedPasswordDate || new Date();
}
const openComplete = (err) => {
if (err) {
return callback(err);
@ -685,7 +696,7 @@ class AppModel {
if (fileInfo) {
file.syncDate = fileInfo.syncDate;
}
if (updateCacheOnSuccess) {
if (updateCacheOnSuccess && !this.settings.disableOfflineStorage) {
logger.info('Save loaded file to cache');
Storage.cache.save(file.id, null, params.fileData);
}
@ -755,7 +766,6 @@ class AppModel {
syncDate: file.syncDate || dt,
openDate: dt,
backup: file.backup,
fingerprint: file.fingerprint,
chalResp: file.chalResp
});
switch (this.settings.rememberKeyFiles) {
@ -771,6 +781,14 @@ class AppModel {
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.unshift(fileInfo);
this.fileInfos.save();
@ -808,14 +826,14 @@ class AppModel {
if (data && backup && backup.enabled && backup.pending) {
this.scheduleBackupFile(file, data);
}
if (params) {
this.saveFileFingerprint(file, params.password);
}
if (this.settings.yubiKeyAutoOpen) {
if (this.attachedYubiKeysCount > 0 && !this.files.some((f) => f.external)) {
this.tryOpenOtpDeviceInBackground();
}
}
if (this.settings.deviceOwnerAuth) {
this.saveEncryptedPassword(file, params);
}
}
fileClosed(file) {
@ -858,7 +876,7 @@ class AppModel {
path = Storage[storage].getPathForName(file.name);
}
const optionsForLogging = { ...options };
if (optionsForLogging && optionsForLogging.opts && optionsForLogging.opts.password) {
if (optionsForLogging.opts && optionsForLogging.opts.password) {
optionsForLogging.opts = { ...optionsForLogging.opts };
optionsForLogging.opts.password = '***';
}
@ -965,6 +983,10 @@ class AppModel {
logger.info('Updated sync date, saving modified file');
saveToCacheAndStorage();
} 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');
Storage.cache.save(fileInfo.id, null, data, (err) => {
if (err) {
@ -1027,6 +1049,9 @@ class AppModel {
} else if (!file.dirty) {
logger.info('Saving to storage, skip cache because not dirty');
saveToStorage(data);
} else if (this.settings.disableOfflineStorage) {
logger.info('Saving to storage because cache is disabled');
saveToStorage(data);
} else {
logger.info('Saving to cache');
Storage.cache.save(fileInfo.id, null, data, (err) => {
@ -1050,6 +1075,10 @@ class AppModel {
logger.info('File does not exist in storage, creating');
saveToCacheAndStorage();
} 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');
file.getData((data, 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() {
for (const fileInfo of this.fileInfos) {
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() {
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 };

View File

@ -339,7 +339,7 @@ class EntryModel extends Model {
if (val && !val.isProtected) {
// https://github.com/keeweb/keeweb/issues/910
// 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;
}
@ -627,6 +627,18 @@ class EntryModel extends Model {
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) {
const model = new EntryModel();
model.setEntry(entry, group, file);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import EventEmitter from 'events';
import { Logger } from 'util/logger';
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 logger = new Logger('storage-oauth-listener');
@ -23,9 +24,9 @@ const StorageOAuthListener = {
let resultHandled = false;
const server = http.createServer((req, resp) => {
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) {
this.stop();
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,
supportsTitleBarStyles() {
return this.isMac;
return isDesktop && (this.isMac || this.isWindows);
},
supportsCustomTitleBarAndDraggableWindow() {
return isDesktop && this.isMac;
},
renderCustomTitleBar() {
return isDesktop && this.isWindows;
},
hasUnicodeFlags() {
return this.isMac;

View File

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

View File

@ -139,8 +139,6 @@ kdbxweb.ProtectedValue.prototype.indexOfSelfInLower = function (targetLower) {
return firstCharIndex;
};
window.PV = kdbxweb.ProtectedValue;
kdbxweb.ProtectedValue.prototype.equals = function (other) {
if (!other) {
return false;
@ -176,3 +174,38 @@ kdbxweb.ProtectedValue.prototype.isFieldReference = function () {
});
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 { TagView } from 'views/tag-view';
import { ImportCsvView } from 'views/import-csv-view';
import { TitlebarView } from 'views/titlebar-view';
import template from 'templates/app.hbs';
class AppView extends View {
@ -45,6 +46,9 @@ class AppView extends View {
constructor(model) {
super(model);
this.titlebarStyle = this.model.settings.titlebarStyle;
this.views.menu = new MenuView(this.model.menu, { ownParent: true });
this.views.menuDrag = new DragView('x', { parent: '.app__menu-drag' });
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.details = new DetailsView(undefined, { ownParent: true });
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.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:locale', this.setLocale);
this.listenTo(this.model.settings, 'change:fontSize', this.setFontSize);
@ -120,6 +125,9 @@ class AppView extends View {
}
if (this.titlebarStyle !== 'default') {
document.body.classList.add('titlebar-' + this.titlebarStyle);
if (Features.renderCustomTitleBar()) {
document.body.classList.add('titlebar-custom');
}
}
if (Features.isMobile) {
document.body.classList.add('mobile');
@ -129,7 +137,8 @@ class AppView extends View {
render() {
super.render({
beta: this.model.isBeta,
titlebarStyle: this.titlebarStyle
titlebarStyle: this.titlebarStyle,
customTitlebar: Features.renderCustomTitleBar()
});
this.panelEl = this.$el.find('.app__panel:first');
this.views.listWrap.render();
@ -139,13 +148,14 @@ class AppView extends View {
this.views.list.render();
this.views.listDrag.render();
this.views.details.render();
this.views.titlebar?.render();
this.showLastOpenFile();
}
showOpenFile() {
this.hideContextMenu();
this.views.menu.hide();
this.views.menuDrag.hide();
this.views.menuDrag.$el.parent().hide();
this.views.listWrap.hide();
this.views.list.hide();
this.views.listDrag.hide();
@ -190,7 +200,7 @@ class AppView extends View {
showEntries() {
this.views.menu.show();
this.views.menuDrag.show();
this.views.menuDrag.$el.parent().show();
this.views.listWrap.show();
this.views.list.show();
this.views.listDrag.show();
@ -254,7 +264,7 @@ class AppView extends View {
showSettings(selectedMenuItem) {
this.model.menu.setMenu('settings');
this.views.menu.show();
this.views.menuDrag.show();
this.views.menuDrag.$el.parent().show();
this.views.listWrap.hide();
this.views.list.hide();
this.views.listDrag.hide();
@ -578,9 +588,13 @@ class AppView extends View {
complete: (res) => {
if (res === 'ignore') {
this.model.closeAllFiles();
complete(true);
if (complete) {
complete(true);
}
} 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;
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.page === page || !menuItem) {
@ -686,9 +704,7 @@ class AppView extends View {
this.views.open.toggleMore();
}
} else {
if (menuItem) {
this.model.menu.select({ item: menuItem });
}
this.model.menu.select({ item: menuItem });
}
} else {
this.showSettings();

View File

@ -232,7 +232,7 @@ class AutoTypeSelectView extends View {
this.highlightActive();
}
const view = new DropdownView();
const view = new DropdownView({ selectedOption: 0 });
this.listenTo(view, 'cancel', this.hideItemOptionsDropdown);
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 { DetailsAutoTypeView } from 'views/details/details-auto-type-view';
import { DetailsHistoryView } from 'views/details/details-history-view';
import { DetailsIssuesView } from 'views/details/details-issues-view';
import { DropdownView } from 'views/dropdown-view';
import { createDetailsFields } from 'views/details/details-fields';
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 emptyTemplate from 'templates/details/details-empty.hbs';
import groupTemplate from 'templates/details/details-group.hbs';
import { Launcher } from 'comp/launcher';
class DetailsView extends View {
parent = '.app__details';
@ -117,11 +119,15 @@ class DetailsView extends View {
super.render();
return;
}
const model = { deleted: this.appModel.filter.trash, ...this.model };
const model = {
deleted: this.appModel.filter.trash,
...this.model
};
this.template = template;
super.render(model);
this.setSelectedColor(this.model.color);
this.addFieldViews();
this.checkPasswordIssues();
this.createScroll({
root: this.$el.find('.details__body')[0],
scroller: this.$el.find('.scroller')[0],
@ -152,7 +158,7 @@ class DetailsView extends View {
fieldView.parent = views === fieldViews ? fieldsMainEl[0] : fieldsAsideEl[0];
fieldView.render();
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));
if (hideEmptyFields) {
const value = fieldView.model.value();
@ -479,13 +485,17 @@ class DetailsView extends View {
CopyPaste.createHiddenInput(fieldText);
}
const copyRes = CopyPaste.copy(fieldText);
this.fieldCopied({ source: editView, copyRes });
this.copyFieldValue({ source: editView, copyRes });
return true;
}
return false;
}
copyPasswordFromShortcut(e) {
if (!this.model) {
return;
}
if (this.model.external) {
this.copyOtp();
e.preventDefault();
@ -576,6 +586,9 @@ class DetailsView extends View {
} else if (fieldName) {
this.model.setField(fieldName, e.val);
}
if (fieldName === 'Password' && this.views.issues) {
this.views.issues.passwordChanged();
}
} else if (e.field === 'Tags') {
this.model.setTags(e.val);
this.appModel.updateTags();
@ -988,6 +1001,20 @@ class DetailsView extends View {
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);

View File

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

View File

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

View File

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

View File

@ -21,11 +21,14 @@ class IconSelectView extends View {
};
render() {
const customIcons = this.model.file.getCustomIcons();
const hasCustomIcons = Object.keys(customIcons).length > 0;
super.render({
sel: this.model.iconId,
icons: IconMap,
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')
.append(img);
this.downloadingFavicon = false;
const id = this.model.file.addCustomIcon(this.special.download.data);
this.emit('select', { id, custom: true });
};
img.onerror = (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-icon-search': 'advancedSearchClick',
'click .list__search-btn-menu': 'toggleMenu',
'click .list__search-icon-clear': 'clickClear',
'change .list__search-adv input[type=checkbox]': 'toggleAdvCheck'
};
@ -212,7 +213,9 @@ class ListSearchView extends View {
}
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) {
@ -428,6 +431,11 @@ class ListSearchView extends View {
fileListUpdated() {
this.render();
}
clickClear() {
this.inputEl.val('');
this.inputChange();
}
}
export { ListSearchView };

View File

@ -1,4 +1,5 @@
import { View } from 'framework/views/view';
import { Launcher } from 'comp/launcher';
import { Keys } from 'const/keys';
import template from 'templates/modal.hbs';
@ -10,6 +11,7 @@ class ModalView extends View {
events = {
'click .modal__buttons button': 'buttonClick',
'click .modal__link': 'linkClick',
'click': 'bodyClick'
};
@ -55,6 +57,13 @@ class ModalView extends View {
this.closeWithResult(result);
}
linkClick(e) {
if (Launcher) {
e.preventDefault();
Launcher.openLink(e.target.href);
}
}
bodyClick(e) {
if (typeof this.model.click === 'string' && !e.target.matches('button')) {
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 { omit } from 'util/fn';
import { GeneratorView } from 'views/generator-view';
import { NativeModules } from 'comp/launcher/native-modules';
import template from 'templates/open.hbs';
const logger = new Logger('open-view');
@ -57,12 +58,10 @@ class OpenView extends View {
};
params = null;
passwordInput = null;
busy = false;
currentSelectedIndex = -1;
encryptedPassword = null;
constructor(model) {
super(model);
@ -152,6 +151,7 @@ class OpenView extends View {
windowFocused() {
this.inputEl.focus();
this.checkIfEncryptedPasswordDateIsValid();
}
focusInput(focusOnMobile) {
@ -237,8 +237,10 @@ class OpenView extends View {
if (!this.params.keyFileData) {
this.params.keyFileName = null;
}
this.encryptedPassword = null;
this.displayOpenFile();
this.displayOpenKeyFile();
this.displayOpenDeviceOwnerAuth();
success = true;
break;
case 'xml':
@ -248,7 +250,9 @@ class OpenView extends View {
this.params.path = null;
this.params.storage = null;
this.params.rev = null;
this.encryptedPassword = null;
this.importDbWithXml();
this.displayOpenDeviceOwnerAuth();
success = true;
break;
case 'kdb':
@ -341,6 +345,15 @@ class OpenView extends View {
.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) {
this.reading = 'fileData';
this.processFile(file, (success) => {
@ -479,6 +492,10 @@ class OpenView extends View {
}
}
inputInput() {
this.displayOpenDeviceOwnerAuth();
}
toggleCapsLockWarning(on) {
this.$el.find('.open__pass-warning').toggleClass('invisible', !on);
}
@ -587,11 +604,12 @@ class OpenView extends View {
this.params.keyFileData = null;
this.params.opts = fileInfo.opts;
this.params.chalResp = fileInfo.chalResp;
this.setEncryptedPassword(fileInfo);
this.displayOpenFile();
this.displayOpenKeyFile();
this.displayOpenChalResp();
this.openFileWithFingerprint(fileInfo);
this.displayOpenDeviceOwnerAuth();
if (fileWasClicked) {
this.focusInput(true);
@ -608,7 +626,9 @@ class OpenView extends View {
this.params.name = path.match(/[^/\\]*$/)[0];
this.params.rev = null;
this.params.fileData = null;
this.encryptedPassword = null;
this.displayOpenFile();
this.displayOpenDeviceOwnerAuth();
if (keyFilePath) {
const parsed = Launcher.parsePath(keyFilePath);
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() {
if (!this.busy) {
this.closeConfig();
@ -662,15 +668,37 @@ class OpenView extends View {
this.inputEl.attr('disabled', 'disabled');
this.busy = true;
this.params.password = this.passwordInput.value;
this.afterPaint(() => {
this.model.openFile(this.params, (err) => this.openDbComplete(err));
});
if (this.encryptedPassword && !this.params.password.length) {
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) {
this.busy = 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) {
logger.error('Error opening file', err);
this.focusInput(true);
@ -822,7 +850,9 @@ class OpenView extends View {
this.params.name = UrlFormat.getDataFileName(file.name);
this.params.rev = file.rev;
this.params.fileData = null;
this.encryptedPassword = null;
this.displayOpenFile();
this.displayOpenDeviceOwnerAuth();
}
showConfig(storage) {
@ -918,7 +948,9 @@ class OpenView extends View {
this.params.name = UrlFormat.getDataFileName(req.path);
this.params.rev = stat.rev;
this.params.fileData = null;
this.encryptedPassword = null;
this.displayOpenFile();
this.displayOpenDeviceOwnerAuth();
}
}
@ -1078,6 +1110,37 @@ class OpenView extends View {
}
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 };

View File

@ -15,7 +15,8 @@ class SettingsAboutView extends View {
licenseLinkCCBY40: Links.LicenseLinkCCBY40,
repoLink: Links.Repo,
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 { SettingsLogsView } from 'views/settings/settings-logs-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';
class SettingsGeneralView extends View {
@ -24,6 +25,7 @@ class SettingsGeneralView extends View {
events = {
'click .settings__general-theme': 'changeTheme',
'click .settings__general-auto-switch-theme': 'changeAuthSwitchTheme',
'change .settings__general-locale': 'changeLocale',
'change .settings__general-font-size': 'changeFontSize',
'change .settings__general-expand': 'changeExpandGroups',
@ -34,6 +36,13 @@ class SettingsGeneralView extends View {
'change .settings__general-auto-save-interval': 'changeAutoSaveInterval',
'change .settings__general-remember-key-files': 'changeRememberKeyFiles',
'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-copy': 'changeLockOnCopy',
'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-field-label-dblclick-autotype':
'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',
'click .settings__general-update-btn': 'checkUpdate',
'click .settings__general-restart-btn': 'restartApp',
'click .settings__general-restart-btn': 'installUpdateAndRestart',
'click .settings__general-download-update-btn': 'downloadUpdate',
'click .settings__general-update-found-btn': 'installFoundUpdate',
'change .settings__general-disable-offline-storage': 'changeDisableOfflineStorage',
'change .settings__general-prv-check': 'changeStorageEnabled',
'click .settings__general-prv-logout': 'logoutFromStorage',
'click .settings__general-show-advanced': 'showAdvancedSettings',
@ -61,8 +74,8 @@ class SettingsGeneralView extends View {
constructor(model, options) {
super(model, options);
this.listenTo(UpdateModel, 'change:status', this.render);
this.listenTo(UpdateModel, 'change:updateStatus', this.render);
this.listenTo(UpdateModel, 'change', this.render);
this.listenTo(Events, 'theme-applied', this.render);
}
render() {
@ -72,7 +85,8 @@ class SettingsGeneralView extends View {
const storageProviders = this.getStorageProviders();
super.render({
themes: mapObject(SettingsManager.allThemes, (theme) => Locale[theme]),
themes: this.getAllThemes(),
autoSwitchTheme: AppSettingsModel.autoSwitchTheme,
activeTheme: SettingsManager.activeTheme,
locales: SettingsManager.allLocales,
activeLocale: SettingsManager.activeLocale,
@ -86,6 +100,7 @@ class SettingsGeneralView extends View {
autoSaveInterval: AppSettingsModel.autoSaveInterval,
idleMinutes: AppSettingsModel.idleMinutes,
minimizeOnClose: AppSettingsModel.minimizeOnClose,
minimizeOnFieldCopy: AppSettingsModel.minimizeOnFieldCopy,
devTools: Launcher && Launcher.devTools,
canAutoUpdate: Updater.enabled,
canAutoSaveOnClose: !!Launcher,
@ -93,6 +108,13 @@ class SettingsGeneralView extends View {
canDetectMinimize: !!Launcher,
canDetectOsSleep: Launcher && Launcher.canDetectOsSleep(),
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,
lockOnCopy: AppSettingsModel.lockOnCopy,
lockOnAutoType: AppSettingsModel.lockOnAutoType,
@ -113,10 +135,16 @@ class SettingsGeneralView extends View {
useGroupIconForEntries: AppSettingsModel.useGroupIconForEntries,
directAutotype: AppSettingsModel.directAutotype,
fieldLabelDblClickAutoType: AppSettingsModel.fieldLabelDblClickAutoType,
supportsTitleBarStyles: Launcher && Features.supportsTitleBarStyles(),
useLegacyAutoType: AppSettingsModel.useLegacyAutoType,
supportsTitleBarStyles: Features.supportsTitleBarStyles(),
supportsCustomTitleBarAndDraggableWindow: Features.supportsCustomTitleBarAndDraggableWindow(),
titlebarStyle: AppSettingsModel.titlebarStyle,
storageProviders,
showReloadApp: Features.isStandalone
showReloadApp: Features.isStandalone,
hasDeviceOwnerAuth: Features.isDesktop && Features.isMac,
deviceOwnerAuth: AppSettingsModel.deviceOwnerAuth,
deviceOwnerAuthTimeout: AppSettingsModel.deviceOwnerAuthTimeoutMinutes,
disableOfflineStorage: AppSettingsModel.disableOfflineStorage
});
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) {
const theme = e.target.closest('.settings__general-theme').dataset.theme;
if (theme === '...') {
this.goToPlugins();
} else {
AppSettingsModel.theme = theme;
this.render();
const changedInSettings = AppSettingsModel.theme !== theme;
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) {
const locale = e.target.value;
if (locale === '...') {
@ -283,6 +344,43 @@ class SettingsGeneralView extends View {
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) {
const lockOnMinimize = e.target.checked || false;
AppSettingsModel.lockOnMinimize = lockOnMinimize;
@ -324,13 +422,11 @@ class SettingsGeneralView extends View {
changeUseGroupIconForEntries(e) {
const useGroupIconForEntries = e.target.checked || false;
AppSettingsModel.useGroupIconForEntries = useGroupIconForEntries;
Events.emit('refresh');
}
changeDirectAutotype(e) {
const directAutotype = e.target.checked || false;
AppSettingsModel.directAutotype = directAutotype;
Events.emit('refresh');
}
changeFieldLabelDblClickAutoType(e) {
@ -339,9 +435,36 @@ class SettingsGeneralView extends View {
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) {
Launcher.requestRestart();
Updater.installAndRestart();
} else {
window.location.reload();
}
@ -353,7 +476,7 @@ class SettingsGeneralView extends View {
installFoundUpdate() {
Updater.update(true, () => {
Launcher.requestRestart();
Updater.installAndRestart();
});
}
@ -363,6 +486,14 @@ class SettingsGeneralView extends View {
Events.emit('refresh');
}
changeDisableOfflineStorage(e) {
const disableOfflineStorage = e.target.checked;
AppSettingsModel.disableOfflineStorage = disableOfflineStorage;
if (disableOfflineStorage) {
this.appModel.deleteAllCachedFiles();
}
}
changeStorageEnabled(e) {
const storage = Storage[$(e.target).data('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 & {
padding-top: $titlebar-padding-large;
}
.titlebar-custom.titlebar-hidden &,
.titlebar-custom.titlebar-hidden-inset & {
padding-top: $titlebar-custom-height;
}
.fullscreen .app & {
padding-top: 0;
}
@ -123,6 +127,7 @@
display: flex;
}
}
@include padding-if-titlebar;
}
&__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 {
display: flex;
align-items: stretch;

View File

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

View File

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

View File

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

View File

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

View File

@ -108,6 +108,18 @@
.open--opening & {
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 {
display: none;

View File

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

View File

@ -7,6 +7,7 @@
width: 100%;
user-select: none;
padding: $medium-padding;
@include padding-if-titlebar;
&__space {
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 {
padding-right: 1.7em;
&.input-search {
padding-left: 2.9em;
padding-right: 1.8em;
}
&::placeholder {

View File

@ -58,6 +58,7 @@ $fa-var-unlock: next-fa-glyph();
$fa-var-lock: next-fa-glyph();
$fa-var-check: 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-open: 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-hdd: 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-terminal: 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-usb-token: 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:
mix(map-get($t, medium-color), map-get($t, background-color), 14%),
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
);

View File

@ -66,9 +66,12 @@ $titlebar-padding-large: 40px;
// Animations
$base-duration: 150ms;
$fast-duration: 80ms;
$base-timing: ease;
$slow-transition-in: $base-duration * 2 ease-in;
$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-out: $slow-transition-out;
@ -81,3 +84,7 @@ $z-index-modal: 100000;
// Screen sizes
$tablet-width: 736px;
$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;
}
.fade-in {
animation: fade-in $fast-transition-in 0s;
}
.fade-out {
opacity: 0;
animation: fade-out $fast-transition-out 0s;
}
.rotate-90,
.fa.rotate-90:before {
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 {
0%,
1%,

View File

@ -7,7 +7,8 @@
flex-wrap: wrap;
user-select: none;
padding-bottom: $base-padding-h;
&--custom {
&--custom,
&--actions {
padding-top: $base-padding-h;
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/scroll';
@import 'common/tip';
@import 'common/titlebar';
@import 'areas/app';
@import 'areas/auto-type';
@ -38,3 +39,4 @@ $fa-font-path: '~font-awesome/fonts';
@import 'areas/open';
@import 'areas/settings';
@import 'areas/import-csv';
@import 'areas/titlebar';

View File

@ -4,9 +4,13 @@ $themes: ();
@import 'dark';
@import 'light';
@import 'dark-brown';
@import 'light-brown';
@import 'flat-blue';
@import 'light-blue';
@import 'terminal';
@import 'light-terminal';
@import 'high-contrast';
@import 'dark-contrast';
@import 'solarized-dark';
@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-on-secondary-item-color: #d6d6d6;
--selected-item-text-color: #f6f6f6;
--open-icon-color: var(--muted-color);
--open-icon-color: #565656;
.list__item--active .blue-color {
color: #7baeff;

View File

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

View File

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

View File

@ -1,6 +1,10 @@
<div class="app">
{{#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__menu"></div>
<div class="app__menu-drag"></div>

View File

@ -1,17 +1,19 @@
<div class="auto-type-hint">
<a href="{{link}}" class="auto-type-hint__link-details" target="_blank">{{res 'autoTypeLink'}}</a>
<div class="auto-type-hint__block">
<div>{{res 'autoTypeEntryFields'}}:</div>
<a>{TITLE}</a><a>{USERNAME}</a><a>{URL}</a><a>{PASSWORD}</a><a>{NOTES}</a><a>{GROUP}</a>
<a>{TOTP}</a><a>{S:Custom Field Name}</a>
</div>
<div class="auto-type-hint__block">
<div>{{res 'autoTypeModifiers'}}:</div>
<a>+ (shift)</a><a>% (alt)</a><a>^ ({{cmd}})</a>{{#if hasCtrl}}<a>^^ (ctrl)</a>{{/if}}
</div>
<div class="auto-type-hint__block">
<div>{{res 'autoTypeKeys'}}:</div>
<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 class="auto-type-hint__body">
<a href="{{link}}" class="auto-type-hint__link-details" target="_blank">{{res 'autoTypeLink'}}</a>
<div class="auto-type-hint__block">
<div>{{res 'autoTypeEntryFields'}}:</div>
<a>{TITLE}</a><a>{USERNAME}</a><a>{URL}</a><a>{PASSWORD}</a><a>{NOTES}</a><a>{GROUP}</a>
<a>{TOTP}</a><a>{S:Custom Field Name}</a>
</div>
<div class="auto-type-hint__block">
<div>{{res 'autoTypeModifiers'}}:</div>
<a>+ (shift)</a><a>% (alt)</a><a>^ ({{cmd}})</a>{{#if hasCtrl}}<a>^^ (ctrl)</a>{{/if}}
</div>
<div class="auto-type-hint__block">
<div>{{res 'autoTypeKeys'}}:</div>
<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>

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>
{{#unless readOnly}}
<div class="details__issues-container">
</div>
<div class="details__buttons">
{{#if deleted~}}
<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>
{{/each}}
</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/*" />
{{#if canDownloadFavicon}}
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-download"
data-val="special" data-special="download" title="{{res 'iconFavTitle'}}">
<span class="icon-select__icon icon-select__icon-btn icon-select__icon-download"
data-val="special" data-special="download" title="{{res 'iconFavTitle'}}">
<i class="fa fa-cloud-download-alt"></i>
</span>
{{/if}}
@ -16,6 +16,9 @@
data-val="special" data-special="select" title="{{res 'iconSelCustom'}}">
<i class="fa fa-ellipsis-h"></i>
</span>
</div>
{{#if hasCustomIcons}}
<div class="icon-select__items icon-select__items--custom">
{{#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}}"
data-val="{{ci}}">
@ -23,4 +26,5 @@
</span>
{{/each}}
</div>
{{/if}}
</div>

View File

@ -4,11 +4,14 @@
<i class="fa fa-bars"></i>
</div>
<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'}}">
<i class="fa fa-search"></i>
<i class="fa fa-caret-down"></i>
</div>
<div class="list__search-icon-clear">
<i class="fa fa-times-circle"></i>
</div>
</div>
<div class="list__search-btn-new {{#unless canCreate}}hide{{/unless}}" title="{{res 'searchAddNew'}}">
<i class="fa fa-plus"></i>

View File

@ -10,6 +10,7 @@
{{#unless @last}}<br/>{{/unless}}
{{/each}}
{{#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 checkbox}}
<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">
<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" />
<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>
<div class="open__settings">

View File

@ -37,11 +37,12 @@
{{#if isDesktop}}
<h3>Desktop modules</h3>
<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/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/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>
{{/if}}
@ -67,7 +68,7 @@
<h2>{{res 'setAboutLic'}}</h2>
<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
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,

View File

@ -66,6 +66,10 @@
</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>
<label for="settings__general-font-size">{{res 'setGenFontSize'}}:</label>
<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">
<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-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>
</div>
{{/if}}
@ -150,6 +156,11 @@
{{#if minimizeOnClose}}checked{{/if}} />
<label for="settings__general-minimize">{{res 'setGenMinInstead'}}</label>
</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 canAutoType}}
<div>
@ -172,6 +183,85 @@
id="settings__general-use-group-icon-for-entries" {{#if useGroupIconForEntries}}checked{{/if}} />
<label for="settings__general-use-group-icon-for-entries">{{res 'setGenUseGroupIconForEntries'}}</label>
</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>
<div>
@ -183,6 +273,8 @@
<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="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="1440" {{#ifeq idleMinutes 1440}}selected{{/ifeq}}>{{res 'setGenLockDay'}}</option>
</select>
@ -215,6 +307,12 @@
{{/if}}
<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|}}
<h4 class="settings__general-storage-header"><input
type="checkbox" id="settings__general-prv-check-{{prv.name}}" class="settings__general-prv-check"
@ -228,6 +326,13 @@
<h2 id="advanced">{{res 'advanced'}}</h2>
<a class="settings__general-show-advanced">{{res 'setGenShowAdvanced'}}</a>
<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}}
<button class="btn-silent settings__general-dev-tools-link">{{res 'setGenDevTools'}}</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