mirror of https://github.com/keeweb/keeweb.git
Merge branch 'develop' into master
This commit is contained in:
commit
31214b10e7
|
@ -2,7 +2,4 @@
|
|||
|
||||
cd /github/workspace
|
||||
npm ci
|
||||
cd desktop
|
||||
npm ci
|
||||
cd /github/workspace
|
||||
grunt desktop-linux
|
||||
|
|
|
@ -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:
|
||||
|
|
143
Gruntfile.js
143
Gruntfile.js
|
@ -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-*']
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
CACHE MANIFEST
|
||||
|
||||
# YYYY-MM-DD:v0.0.0
|
||||
# updmin:v0.0.0
|
||||
|
||||
NETWORK:
|
||||
*
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 };
|
|
@ -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;
|
||||
|
|
|
@ -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 };
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 };
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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 };
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export const KeeWebLogo =
|
||||
'data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PSIwIDAgMTAyNCAxMDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxkZWZzPjxyYWRpYWxHcmFkaWVudCBpZD0iYSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGN4PSI2NjMuMTMzIiBjeT0iMTM4LjYwMSIgcj0iODY0LjU2OCIgZng9IjY2My4xMzMiIGZ5PSIxMzguNjAxIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM5MmJmZjUiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMzMDQxYzgiLz48L3JhZGlhbEdyYWRpZW50PjwvZGVmcz48cGF0aCBkPSJNOTIzIDM1Ny42MjhjMC05Ljc4MiAwLTE5LjU2NC0uMDU2LTI5LjM0OC0uMDUtOC4yNC0uMTQ0LTE2LjQ4LS4zNjgtMjQuNzE2YTM1OC43MjQgMzU4LjcyNCAwIDAwLTQuNzM2LTUzLjgxMSAxODEuNDk3IDE4MS40OTcgMCAwMC0xNi44NTgtNTEuMTMzIDE3Mi4wNjYgMTcyLjA2NiAwIDAwLTc1LjIzNi03NS4yIDE4MS43MDIgMTgxLjcwMiAwIDAwLTUxLjE4OC0xNi44NiAzNTkuNjI2IDM1OS42MjYgMCAwMC01My44Mi00LjczYy04LjI0Mi0uMjMtMTYuNDg2LS4zMTgtMjQuNzM4LS4zNjgtOS43OC0uMDYyLTE5LjU2OC0uMDYyLTI5LjM1Ni0uMDYyTDU1MyAxMDFoLTg1bC0xMTEuNjM4LjRjLTkuOCAwLTE5LjYxNCAwLTI5LjQyLjA1NC04LjI2Mi4wNS0xNi41Mi4xNDQtMjQuNzc4LjM2OGEzNjAuODA4IDM2MC44MDggMCAwMC01My45NDggNC43NDIgMTgyLjI3MiAxODIuMjcyIDAgMDAtNTEuMjU4IDE2Ljg0OCAxNzIuMjMgMTcyLjIzIDAgMDAtNzUuMzg2IDc1LjE4OCAxODEuMjM4IDE4MS4yMzggMCAwMC0xNi45IDUxLjE2IDM1OC4zMTIgMzU4LjMxMiAwIDAwLTQuNzQyIDUzLjhjLS4yMjIgOC4yNC0uMzE4IDE2LjQ4LS4zNjggMjQuNzItLjA2IDkuNzg0LS41NjIgMjEuOTM2LS41NjIgMzEuNzJ2MTk2bC41MDggMTEyLjQyOGMwIDkuOCAwIDE5LjYuMDU0IDI5LjM4OC4wNSA4LjI1Mi4xNDYgMTYuNTAyLjM2OCAyNC43NWEzNTkuMDQ2IDM1OS4wNDYgMCAwMDQuNzQ2IDUzLjg4NSAxODEuNjY4IDE4MS42NjggMCAwMDE2Ljg5IDUxLjIwMSAxNzIuMzU2IDE3Mi4zNTYgMCAwMDc1LjM4MiA3NS4zMDIgMTgyLjEwNSAxODIuMTA1IDAgMDA1MS4yODggMTYuODggMzYwLjIwNyAzNjAuMjA3IDAgMDA1My45MjQgNC43MzZjOC4yNTguMjI0IDE2LjUxOC4zMTggMjQuNzguMzY4IDkuOC4wNiAxOS42MTIuMDU2IDI5LjQyLjA1NmgzMTAuMjg0YzkuNzg4IDAgMTkuNTc2IDAgMjkuMzY0LS4wNTQgOC4yNDQtLjA1IDE2LjQ4OC0uMTQ0IDI0LjczLS4zNjhhMzU5LjEgMzU5LjEgMCAwMDUzLjg0LTQuNzQgMTgxLjUxOCAxODEuNTE4IDAgMDA1MS4xNi0xNi44NzIgMTcyLjE5NSAxNzIuMTk1IDAgMDA3NS4yNC03NS4yOTggMTgxLjk1IDE4MS45NSAwIDAwMTYuODY2LTUxLjIzIDM1OS44MTUgMzU5LjgxNSAwIDAwNC43MzItNTMuODY0Yy4yMjQtOC4yNS4zMTgtMTYuNS4zNjgtMjQuNzUuMDYtOS44LjA1Ni0xOS42LjA1Ni0yOS4zODh2LTMxMC44IiBmaWxsPSJ1cmwoI2EpIi8+PHBhdGggZD0iTTk5IDEwMWg4MjR2ODI0SDk5eiIgZmlsbD0ibm9uZSIvPjxnIGZpbGw9IiNmZmYiPjxwYXRoIGQ9Ik0zNzkuMTk2IDgxMy4yNzNjLTUuOTM5LS4wMS0xMi41My0yLjUwMi0xOS4zMjYtNi4wNjctMTQuNDk3LTcuNjA1LTI0LjQzOS0xNy4yMi0xNi4xMzktMzMuMDQyTDU0NS4zOCAzODkuNzYxYTE1MTguMzEgMTUxOC4zMSAwIDAxLTUuNzU4LTIuOTk4Yy03Mi43MDktMzguMTQyLTEwNC45NjctOTQuNTM1LTc4Ljg1Mi0xNDQuMzE4IDMwLjAxOC01Ny4yMjMgMTIyLjk4NC02Ny42MDggMjA3LjY0Ny0yMy4xOTYgODQuNjYzIDQ0LjQxMyAxMjguOTYxIDEyNi44MDUgOTguOTQzIDE4NC4wMjgtMjUuODE1IDQ5LjIxMS05My4yMTcgNTMuNTk5LTE2MC45OSAxOC4wNDctMS44NjctLjk4LTQuODk2LTIuNTUzLTguMzI0LTQuMzNMMzk2LjIzMSA4MDEuNzA1Yy00LjQwOSA4LjQwNS0xMC4zMDQgMTEuNTc5LTE3LjAzNSAxMS41Njh6bS0zNy4xMjctODIuMzUybC0zLjgxNi0yLjAwMi0uODUtLjQ0NS01MS42NTItMjcuMDk2di0uMDAybC0xMi40NTEtNi41MzFhNC4zMSA0LjMxIDAgMDEtMS44MTUtNS44MmwxNi4zNDYtMzEuMTZhNC4zMTIgNC4zMTIgMCAwMTUuNzg5LTEuODMybC4wMzMuMDE1IDI0LjkwMyAxMy4wNjVhNC4zMTEgNC4zMTEgMCAwMDUuODItMS44MTdsMTYuMzQ4LTMxLjE2YTQuMzEgNC4zMSAwIDAwLTEuODE3LTUuODJsLTEyLjQ1MS02LjUzMy0xMi40NTEtNi41MzJhNC4zMSA0LjMxIDAgMDEtMS44MTUtNS44MmwxNi4zNDYtMzEuMTYyYTQuMzEgNC4zMSAwIDAxNS44Mi0xLjgxNGwxMi40NTMgNi41MzEgMzIuNDExIDE3LjAwMiAyMC4wODkgMTAuNTM5LjAyNi4wMTQgMy43OTMgMS45OS0xNy4xNjggMzIuNzI2LTEuMTgyIDIuMjUyYS4wMDguMDA4IDAgMDEtLjAwMi4wMDRsLTI0LjM1NyA0Ni40My0yLjQ3NyA0LjcyMS0xMy44NjkgMjYuNDM5LS4wMS4wMTgtMS45OTQgMy44ek00MjMuNjkgNTc1LjMzbC0zLjgxOC0yLjAwNC0xOC4xMzUtOS41MTQtMTQuMzE4LTcuNTEyYTQuMzEgNC4zMSAwIDAxLTEuODE3LTUuODJsMTYuMzQ4LTMxLjE2YTQuMzExIDQuMzExIDAgMDE1LjgyLTEuODE3bDE0LjMxOSA3LjUxMiAxOC4xMzYgOS41MTQuMDI2LjAxMyAzLjc5MyAxLjk5MS0yLjAwNCAzLjgxOC04LjE3MiAxNS41OC04LjE3NCAxNS41OC0uMDEuMDE2LTEuOTk0IDMuODAzem0yNjYuODEzLTE4NC40MjZjMTAuNzUzLjAyIDE5LjE2My00LjMxMiAyNC43NS0xNC45NjMgMTcuMDI4LTMyLjQ2LTE0LjQ3Mi04Mi41MzgtNzAuMzU2LTExMS44NTQtNTUuODg0LTI5LjMxNi0xMTQuOTkxLTI2Ljc2OC0xMzIuMDE5IDUuNjkyLTE3LjAyOCAzMi40NTkgMzAuMzM0IDUzLjc3MiA4Ni4yMTkgODMuMDg4IDM3LjU0NyAxOS42OTYgNjkuMzg4IDM3Ljk5NCA5MS40MDYgMzguMDM3eiIvPjxwYXRoIGQ9Ik00MjMuNjkxIDU3NS4zMjlsMTkuMDkxIDEwLjAxNSAyMC4zNTMtMzguNzk3LTE5LjA5MS0xMC4wMTV6TTM0Mi4wNyA3MzAuOTIybDE5LjA5MSAxMC4wMTUgNjEuMDU3LTExNi4zOTItMTkuMDkxLTEwLjAxNXoiIGZpbGwtb3BhY2l0eT0iLjIiLz48L2c+PC9zdmc+';
|
|
@ -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 };
|
||||
|
|
|
@ -16,7 +16,9 @@ const Timeouts = {
|
|||
ExternalDeviceReconnect: 3000,
|
||||
ExternalDeviceAfterReconnect: 1000,
|
||||
FieldLabelDoubleClick: 300,
|
||||
NativeModuleHostRestartTime: 3000
|
||||
NativeModuleHostRestartTime: 3000,
|
||||
FastAnimation: 100,
|
||||
AutoTypeCopyPaste: 300
|
||||
};
|
||||
|
||||
export { Timeouts };
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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 \"{}\""
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 \"{}\""
|
||||
}
|
|
@ -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 };
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 };
|
|
@ -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;
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 };
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 };
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
user-select: none;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
@include padding-if-titlebar;
|
||||
|
||||
> .scroller {
|
||||
flex: 1;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
user-select: none;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
@include padding-if-titlebar;
|
||||
|
||||
> .scroller {
|
||||
flex: 1;
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
@include padding-if-titlebar;
|
||||
|
||||
&__body {
|
||||
@include scrollbar-on-hover;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
width: 100%;
|
||||
user-select: none;
|
||||
padding: $medium-padding;
|
||||
@include padding-if-titlebar;
|
||||
|
||||
&__space {
|
||||
flex: 1;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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%,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
@mixin padding-if-titlebar {
|
||||
.titlebar-custom & {
|
||||
margin-top: $titlebar-custom-height;
|
||||
}
|
||||
}
|
|
@ -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';
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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">
|
||||
|
|
|
@ -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, © 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, © 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, © 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, © 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, © 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, © 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, © 2021 Antelle</span></li>
|
||||
</ul>
|
||||
{{/if}}
|
||||
|
||||
|
@ -67,7 +68,7 @@
|
|||
|
||||
<h2>{{res 'setAboutLic'}}</h2>
|
||||
<p>{{res 'setAboutLicComment'}}:</p>
|
||||
<p>Copyright © 2020 Antelle https://antelle.net</p>
|
||||
<p>Copyright © {{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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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
Loading…
Reference in New Issue